feat: add Terminal Dashboard components and state management

- Implement TerminalTabBar for session tab management with status indicators and alert badges.
- Create TerminalWorkbench to combine TerminalTabBar and TerminalInstance for terminal session display.
- Add localization support for terminal dashboard in English and Chinese.
- Develop TerminalDashboardPage for the main layout of the terminal dashboard with a three-column structure.
- Introduce Zustand stores for session management and issue/queue integration, handling session groups, terminal metadata, and alert management.
- Create a monitor web worker for off-main-thread output analysis, detecting errors and stalls in terminal sessions.
- Define TypeScript types for terminal dashboard state management and integration.
This commit is contained in:
catlog22
2026-02-14 20:54:05 +08:00
parent 4d22ae4b2f
commit e4b898f401
37 changed files with 2810 additions and 5438 deletions

View File

@@ -1,599 +0,0 @@
---
name: execute
description: Coordinate agent execution for workflow tasks with automatic session discovery, parallel task processing, and status tracking
argument-hint: "[-y|--yes] [--resume-session=\"session-id\"] [--with-commit]"
---
# Workflow Execute Command
## Overview
Orchestrates autonomous workflow execution through systematic task discovery, agent coordination, and progress tracking. **Executes entire workflow without user interruption** (except initial session selection if multiple active sessions exist), providing complete context to agents and ensuring proper flow control execution with comprehensive TodoWrite tracking.
**Resume Mode**: When called with `--resume-session` flag, skips discovery phase and directly enters TodoWrite generation and agent execution for the specified session.
## Usage
```bash
# Interactive mode (with confirmations)
/workflow:execute
/workflow:execute --resume-session="WFS-auth"
# Auto mode (skip confirmations, use defaults)
/workflow:execute --yes
/workflow:execute -y
/workflow:execute -y --resume-session="WFS-auth"
# With auto-commit (commit after each task completion)
/workflow:execute --with-commit
/workflow:execute -y --with-commit
/workflow:execute -y --with-commit --resume-session="WFS-auth"
```
## Auto Mode Defaults
When `--yes` or `-y` flag is used:
- **Session Selection**: Automatically selects the first (most recent) active session
- **Completion Choice**: Automatically completes session (runs `/workflow:session:complete --yes`)
When `--with-commit` flag is used:
- **Auto-Commit**: After each agent task completes, commit changes based on summary document
- **Commit Principle**: Minimal commits - only commit files modified by the completed task
- **Commit Message**: Generated from task summary with format: "feat/fix/refactor: {task-title} - {summary}"
**Flag Parsing**:
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
const withCommit = $ARGUMENTS.includes('--with-commit')
```
## Performance Optimization Strategy
**Lazy Loading**: Task JSONs read **on-demand** during execution, not upfront. TODO_LIST.md + IMPL_PLAN.md provide metadata for planning.
**Loading Strategy**:
- **TODO_LIST.md**: Read in Phase 3 (task metadata, status, dependencies for TodoWrite generation)
- **IMPL_PLAN.md**: Check existence in Phase 2 (normal mode), parse execution strategy in Phase 4A
- **Task JSONs**: Lazy loading - read only when task is about to execute (Phase 4B)
## Core Rules
**Complete entire workflow autonomously without user interruption, using TodoWrite for comprehensive progress tracking.**
**Execute all discovered pending tasks until workflow completion or blocking dependency.**
**User-choice completion: When all tasks finished, ask user to choose review or complete.**
**ONE AGENT = ONE TASK JSON: Each agent instance executes exactly one task JSON file - never batch multiple tasks into single agent execution.**
## Core Responsibilities
- **Session Discovery**: Identify and select active workflow sessions
- **Execution Strategy Parsing**: Extract execution model from IMPL_PLAN.md
- **TodoWrite Progress Tracking**: Maintain real-time execution status throughout entire workflow
- **Agent Orchestration**: Coordinate specialized agents with complete context
- **Status Synchronization**: Update task JSON files and workflow state
- **Autonomous Completion**: Continue execution until all tasks complete or reach blocking state
- **Session User-Choice Completion**: Ask user to choose review or complete when all tasks finished
## Execution Philosophy
- **Progress tracking**: Continuous TodoWrite updates throughout entire workflow execution
- **Autonomous completion**: Execute all tasks without user interruption until workflow complete
## Execution Process
```
Normal Mode:
Phase 1: Discovery
├─ Count active sessions
└─ Decision:
├─ count=0 → ERROR: No active sessions
├─ count=1 → Auto-select session → Phase 2
└─ count>1 → AskUserQuestion (max 4 options) → Phase 2
Phase 2: Planning Document Validation
├─ Check IMPL_PLAN.md exists
├─ Check TODO_LIST.md exists
└─ Validate .task/ contains IMPL-*.json files
Phase 3: TodoWrite Generation
├─ Update session status to "active" (Step 0)
├─ Parse TODO_LIST.md for task statuses
├─ Generate TodoWrite for entire workflow
└─ Prepare session context paths
Phase 4: Execution Strategy & Task Execution
├─ Step 4A: Parse execution strategy from IMPL_PLAN.md
└─ Step 4B: Execute tasks with lazy loading
└─ Loop:
├─ Get next in_progress task from TodoWrite
├─ Lazy load task JSON
├─ Launch agent with task context
├─ Mark task completed (update IMPL-*.json status)
│ # Quick fix: Update task status for ccw dashboard
│ # TS=$(date -Iseconds) && jq --arg ts "$TS" '.status="completed" | .status_history=(.status_history // [])+[{"from":"in_progress","to":"completed","changed_at":$ts}]' IMPL-X.json > tmp.json && mv tmp.json IMPL-X.json
├─ [with-commit] Commit changes based on summary (minimal principle)
│ # Read summary from .summaries/IMPL-X-summary.md
│ # Extract changed files from summary's "Files Modified" section
│ # Generate commit message: "feat/fix/refactor: {task-title} - {summary}"
│ # git add <changed-files> && git commit -m "<commit-message>"
└─ Advance to next task
Phase 5: Completion
├─ Update task statuses in JSON files
├─ Generate summaries
└─ AskUserQuestion: Choose next step
├─ "Enter Review" → /workflow:review
└─ "Complete Session" → /workflow:session:complete
Resume Mode (--resume-session):
├─ Skip Phase 1 & Phase 2
└─ Entry Point: Phase 3 (TodoWrite Generation)
├─ Update session status to "active" (if not already)
└─ Continue: Phase 4 → Phase 5
```
## Execution Lifecycle
### Phase 1: Discovery
**Applies to**: Normal mode only (skipped in resume mode)
**Purpose**: Find and select active workflow session with user confirmation when multiple sessions exist
**Process**:
#### Step 1.1: Count Active Sessions
```bash
bash(find .workflow/active/ -name "WFS-*" -type d 2>/dev/null | wc -l)
```
#### Step 1.2: Handle Session Selection
**Case A: No Sessions** (count = 0)
```
ERROR: No active workflow sessions found
Run /workflow:plan "task description" to create a session
```
**Case B: Single Session** (count = 1)
```bash
bash(find .workflow/active/ -name "WFS-*" -type d 2>/dev/null | head -1 | xargs basename)
```
Auto-select and continue to Phase 2.
**Case C: Multiple Sessions** (count > 1)
List sessions with metadata and prompt user selection:
```bash
bash(for dir in .workflow/active/WFS-*/; do [ -d "$dir" ] || continue; session=$(basename "$dir"); project=$(jq -r '.project // "Unknown"' "${dir}workflow-session.json" 2>/dev/null || echo "Unknown"); total=$(grep -c '^\- \[' "${dir}TODO_LIST.md" 2>/dev/null || echo 0); completed=$(grep -c '^\- \[x\]' "${dir}TODO_LIST.md" 2>/dev/null || echo 0); if [ "$total" -gt 0 ]; then progress=$((completed * 100 / total)); else progress=0; fi; echo "$session | $project | $completed/$total tasks ($progress%)"; done)
```
**Parse --yes flag**:
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
```
**Conditional Selection**:
```javascript
if (autoYes) {
// Auto mode: Select first session (most recent)
const firstSession = sessions[0]
console.log(`[--yes] Auto-selecting session: ${firstSession.id}`)
selectedSessionId = firstSession.id
// Continue to Phase 2
} else {
// Interactive mode: Use AskUserQuestion to present formatted options (max 4 options shown)
// If more than 4 sessions, show most recent 4 with "Other" option for manual input
const sessions = getActiveSessions() // sorted by last modified
const displaySessions = sessions.slice(0, 4)
AskUserQuestion({
questions: [{
question: "Multiple active sessions detected. Select one:",
header: "Session",
multiSelect: false,
options: displaySessions.map(s => ({
label: s.id,
description: `${s.project} | ${s.progress}`
}))
// Note: User can select "Other" to manually enter session ID
}]
})
}
```
**Input Validation**:
- If user selects from options: Use selected session ID
- If user selects "Other" and provides input: Validate session exists
- If validation fails: Show error and re-prompt or suggest available sessions
Parse user input (supports: number "1", full ID "WFS-auth-system", or partial "auth"), validate selection, and continue to Phase 2.
#### Step 1.3: Load Session Metadata
```bash
bash(cat .workflow/active/${sessionId}/workflow-session.json)
```
**Output**: Store session metadata in memory
**DO NOT read task JSONs yet** - defer until execution phase (lazy loading)
**Resume Mode**: This entire phase is skipped when `--resume-session="session-id"` flag is provided.
### Phase 2: Planning Document Validation
**Applies to**: Normal mode only (skipped in resume mode)
**Purpose**: Validate planning artifacts exist before execution
**Process**:
1. **Check IMPL_PLAN.md**: Verify file exists (defer detailed parsing to Phase 4A)
2. **Check plan.json**: Verify file exists (structured plan overview, used in Phase 4A)
3. **Check TODO_LIST.md**: Verify file exists (defer reading to Phase 3)
4. **Validate Task Directory**: Ensure `.task/` contains at least one IMPL-*.json file
**Key Optimization**: Only existence checks here. Actual file reading happens in later phases.
**Resume Mode**: This phase is skipped when `--resume-session` flag is provided. Resume mode entry point is Phase 3.
### Phase 3: TodoWrite Generation
**Applies to**: Both normal and resume modes (resume mode entry point)
**Step 0: Update Session Status to Active**
Before generating TodoWrite, update session status from "planning" to "active":
```bash
# Update session status (idempotent - safe to run if already active)
jq '.status = "active" | .execution_started_at = (.execution_started_at // now | todate)' \
.workflow/active/${sessionId}/workflow-session.json > tmp.json && \
mv tmp.json .workflow/active/${sessionId}/workflow-session.json
```
This ensures the dashboard shows the session as "ACTIVE" during execution.
**Process**:
1. **Create TodoWrite List**: Generate task list from TODO_LIST.md (not from task JSONs)
- Parse TODO_LIST.md to extract all tasks with current statuses
- Identify first pending task with met dependencies
- Generate comprehensive TodoWrite covering entire workflow
2. **Prepare Session Context**: Inject workflow paths for agent use (using provided session-id)
3. **Validate Prerequisites**: Ensure IMPL_PLAN.md and TODO_LIST.md exist and are valid
**Resume Mode Behavior**:
- Load existing TODO_LIST.md directly from `.workflow/active/{session-id}/`
- Extract current progress from TODO_LIST.md
- Generate TodoWrite from TODO_LIST.md state
- Proceed immediately to agent execution (Phase 4)
### Phase 4: Execution Strategy Selection & Task Execution
**Applies to**: Both normal and resume modes
**Step 4A: Parse Execution Strategy (plan.json preferred, IMPL_PLAN.md fallback)**
Prefer `plan.json` (structured) over `IMPL_PLAN.md` (human-readable) for execution strategy:
1. **If plan.json exists**: Read `recommended_execution`, `complexity`, `task_ids[]`, `shared_context`
2. **Fallback to IMPL_PLAN.md**: Read Section 4 to extract execution model
Extract:
- **Execution Model**: Sequential | Parallel | Phased | TDD Cycles
- **Parallelization Opportunities**: Which tasks can run in parallel
- **Serialization Requirements**: Which tasks must run sequentially
- **Critical Path**: Priority execution order
If neither has execution strategy, use intelligent fallback (analyze task structure).
**Step 4B: Execute Tasks with Lazy Loading**
**Key Optimization**: Read task JSON **only when needed** for execution
**Execution Loop Pattern**:
```
while (TODO_LIST.md has pending tasks) {
next_task_id = getTodoWriteInProgressTask()
task_json = Read(.workflow/active/{session}/.task/{next_task_id}.json) // Lazy load
executeTaskWithAgent(task_json)
updateTodoListMarkCompleted(next_task_id)
advanceTodoWriteToNextTask()
}
```
**Execution Process per Task**:
1. **Identify Next Task**: From TodoWrite, get the next `in_progress` task ID
2. **Load Task JSON on Demand**: Read `.task/{task-id}.json` for current task ONLY
3. **Validate Task Structure**: Ensure required fields exist (id, title, description, depends_on, convergence)
4. **Launch Agent**: Invoke specialized agent with complete context including flow control steps
5. **Monitor Progress**: Track agent execution and handle errors without user interruption
6. **Collect Results**: Gather implementation results and outputs
7. **[with-commit] Auto-Commit**: If `--with-commit` flag enabled, commit changes based on summary
- Read summary from `.summaries/{task-id}-summary.md`
- Extract changed files from summary's "Files Modified" section
- Determine commit type from `meta.type` (feature→feat, bugfix→fix, refactor→refactor)
- Generate commit message: "{type}: {task-title} - {summary-first-line}"
- Commit only modified files (minimal principle): `git add <files> && git commit -m "<message>"`
8. **Continue Workflow**: Identify next pending task from TODO_LIST.md and repeat
**Note**: TODO_LIST.md updates are handled by agents (e.g., code-developer.md), not by the orchestrator.
### Phase 5: Completion
**Applies to**: Both normal and resume modes
**Process**:
1. **Update Task Status**: Mark completed tasks in JSON files
2. **Generate Summary**: Create task summary in `.summaries/`
3. **Update TodoWrite**: Mark current task complete, advance to next
4. **Synchronize State**: Update session state and workflow status
5. **Check Workflow Complete**: Verify all tasks are completed
6. **User Choice**: When all tasks finished, ask user to choose next step:
```javascript
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
if (autoYes) {
// Auto mode: Complete session automatically
console.log(`[--yes] Auto-selecting: Complete Session`)
Skill(skill="workflow:session:complete", args="--yes")
} else {
// Interactive mode: Ask user
AskUserQuestion({
questions: [{
question: "All tasks completed. What would you like to do next?",
header: "Next Step",
multiSelect: false,
options: [
{
label: "Enter Review",
description: "Run specialized review (security/architecture/quality/action-items)"
},
{
label: "Complete Session",
description: "Archive session and update manifest"
}
]
}]
})
}
```
**Based on user selection**:
- **"Enter Review"**: Execute `/workflow:review`
- **"Complete Session"**: Execute `/workflow:session:complete`
### Post-Completion Expansion
完成后询问用户是否扩展为issue(test/enhance/refactor/doc),选中项调用 `/issue:new "{summary} - {dimension}"`
## Execution Strategy (IMPL_PLAN-Driven)
### Strategy Priority
**IMPL_PLAN-Driven Execution (Recommended)**:
1. **Read IMPL_PLAN.md execution strategy** (Section 4: Implementation Strategy)
2. **Follow explicit guidance**:
- Execution Model (Sequential/Parallel/Phased/TDD)
- Parallelization Opportunities (which tasks can run in parallel)
- Serialization Requirements (which tasks must run sequentially)
- Critical Path (priority execution order)
3. **Use TODO_LIST.md for status tracking** only
4. **IMPL_PLAN decides "HOW"**, execute.md implements it
**Intelligent Fallback (When IMPL_PLAN lacks execution details)**:
1. **Analyze task structure**:
- Check `meta.execution_group` in task JSONs
- Analyze `depends_on` relationships
- Understand task complexity and risk
2. **Apply smart defaults**:
- No dependencies + same execution_group → Parallel
- Has dependencies → Sequential (wait for deps)
- Critical/high-risk tasks → Sequential
3. **Conservative approach**: When uncertain, prefer sequential execution
### Execution Models
#### 1. Sequential Execution
**When**: IMPL_PLAN specifies "Sequential" OR no clear parallelization guidance
**Pattern**: Execute tasks one by one in TODO_LIST order
**TodoWrite**: ONE task marked as `in_progress` at a time
#### 2. Parallel Execution
**When**: IMPL_PLAN specifies "Parallel" with clear parallelization opportunities
**Pattern**: Execute independent task groups concurrently by launching multiple agent instances
**TodoWrite**: MULTIPLE tasks (in same batch) marked as `in_progress` simultaneously
**Agent Instantiation**: Launch one agent instance per task (respects ONE AGENT = ONE TASK JSON rule)
#### 3. Phased Execution
**When**: IMPL_PLAN specifies "Phased" with phase breakdown
**Pattern**: Execute tasks in phases, respect phase boundaries
**TodoWrite**: Within each phase, follow Sequential or Parallel rules
#### 4. Intelligent Fallback
**When**: IMPL_PLAN lacks execution strategy details
**Pattern**: Analyze task structure and apply smart defaults
**TodoWrite**: Follow Sequential or Parallel rules based on analysis
### Task Status Logic
```
pending + dependencies_met → executable
completed → skip
blocked → skip until dependencies clear
```
## TodoWrite Coordination
### TodoWrite Rules (Unified)
**Rule 1: Initial Creation**
- **Normal Mode**: Generate TodoWrite from discovered pending tasks for entire workflow
- **Resume Mode**: Generate from existing session state and current progress
**Rule 2: In-Progress Task Count (Execution-Model-Dependent)**
- **Sequential execution**: Mark ONLY ONE task as `in_progress` at a time
- **Parallel batch execution**: Mark ALL tasks in current batch as `in_progress` simultaneously
- **Execution group indicator**: Show `[execution_group: group-id]` for parallel tasks
**Rule 3: Status Updates**
- **Immediate Updates**: Update status after each task/batch completion without user interruption
- **Status Synchronization**: Sync with JSON task files after updates
- **Continuous Tracking**: Maintain TodoWrite throughout entire workflow execution until completion
**Rule 4: Workflow Completion Check**
- When all tasks marked `completed`, prompt user to choose review or complete session
### TodoWrite Tool Usage
**Example 1: Sequential Execution**
```javascript
TodoWrite({
todos: [
{
content: "Execute IMPL-1.1: Design auth schema [code-developer] [FLOW_CONTROL]",
status: "in_progress", // ONE task in progress
activeForm: "Executing IMPL-1.1: Design auth schema"
},
{
content: "Execute IMPL-1.2: Implement auth logic [code-developer] [FLOW_CONTROL]",
status: "pending",
activeForm: "Executing IMPL-1.2: Implement auth logic"
}
]
});
```
**Example 2: Parallel Batch Execution**
```javascript
TodoWrite({
todos: [
{
content: "Execute IMPL-1.1: Build Auth API [code-developer] [execution_group: parallel-auth-api]",
status: "in_progress", // Batch task 1
activeForm: "Executing IMPL-1.1: Build Auth API"
},
{
content: "Execute IMPL-1.2: Build User UI [code-developer] [execution_group: parallel-ui-comp]",
status: "in_progress", // Batch task 2 (running concurrently)
activeForm: "Executing IMPL-1.2: Build User UI"
},
{
content: "Execute IMPL-1.3: Setup Database [code-developer] [execution_group: parallel-db-schema]",
status: "in_progress", // Batch task 3 (running concurrently)
activeForm: "Executing IMPL-1.3: Setup Database"
},
{
content: "Execute IMPL-2.1: Integration Tests [test-fix-agent] [depends_on: IMPL-1.1, IMPL-1.2, IMPL-1.3]",
status: "pending", // Next batch (waits for current batch completion)
activeForm: "Executing IMPL-2.1: Integration Tests"
}
]
});
```
## Agent Execution Pattern
### Flow Control Execution
**[FLOW_CONTROL]** marker indicates task JSON contains `pre_analysis` steps for context preparation.
**Note**: Orchestrator does NOT execute flow control steps - Agent interprets and executes them autonomously.
### Agent Prompt Template
**Path-Based Invocation**: Pass paths and trigger markers, let agent parse task JSON autonomously.
```bash
Task(subagent_type="{meta.agent}",
run_in_background=false,
prompt="Implement task {task.id}: {task.title}
[FLOW_CONTROL]
**Input**:
- Task JSON: {session.task_json_path}
- Context Package: {session.context_package_path}
**Output Location**:
- Workflow: {session.workflow_dir}
- TODO List: {session.todo_list_path}
- Summaries: {session.summaries_dir}
**Execution**: Read task JSON → Execute pre_analysis → Check execution_config.method → (CLI: handoff to CLI tool | Agent: direct implementation) → Update TODO_LIST.md → Generate summary",
description="Implement: {task.id}")
```
**Key Markers**:
- `Implement` keyword: Triggers tech stack detection and guidelines loading
- `[FLOW_CONTROL]`: Triggers pre_analysis execution
**Why Path-Based**: Agent (code-developer.md) autonomously:
- Reads and parses task JSON (description, convergence, implementation, execution_config)
- Executes pre_analysis steps (Phase 1: context gathering)
- Checks execution_config.method (Phase 2: determine mode)
- CLI mode: Builds handoff prompt and executes via ccw cli with resume strategy
- Agent mode: Directly implements using modification_points and logic_flow
- Generates structured summary with integration points
Embedding task content in prompt creates duplication and conflicts with agent's parsing logic.
### Agent Assignment Rules
```
meta.agent specified → Use specified agent
meta.agent missing → Infer from meta.type:
- "feature" → @code-developer
- "test-gen" → @code-developer
- "test-fix" → @test-fix-agent
- "review" → @universal-executor
- "docs" → @doc-generator
```
## Workflow File Structure Reference
```
.workflow/active/WFS-[topic-slug]/
├── workflow-session.json # Session state and metadata
├── plan.json # Structured plan overview (machine-readable)
├── IMPL_PLAN.md # Planning document and requirements (human-readable)
├── TODO_LIST.md # Progress tracking (updated by agents)
├── .task/ # Task definitions (JSON only)
│ ├── IMPL-1.json # Main task definitions
│ └── IMPL-1.1.json # Subtask definitions
├── .summaries/ # Task completion summaries
│ ├── IMPL-1-summary.md # Task completion details
│ └── IMPL-1.1-summary.md # Subtask completion details
└── .process/ # Planning artifacts
├── context-package.json # Smart context package
└── ANALYSIS_RESULTS.md # Planning analysis results
```
## Error Handling & Recovery
### Common Errors & Recovery
| Error Type | Cause | Recovery Strategy | Max Attempts |
|-----------|-------|------------------|--------------|
| **Discovery Errors** |
| No active session | No sessions in `.workflow/active/` | Create or resume session: `/workflow:plan "project"` | N/A |
| Multiple sessions | Multiple sessions in `.workflow/active/` | Prompt user selection | N/A |
| Corrupted session | Invalid JSON files | Recreate session structure or validate files | N/A |
| **Execution Errors** |
| Agent failure | Agent crash/timeout | Retry with simplified context | 2 |
| Flow control error | Command failure | Skip optional, fail critical | 1 per step |
| Context loading error | Missing dependencies | Reload from JSON, use defaults | 3 |
| JSON file corruption | File system issues | Restore from backup/recreate | 1 |
### Error Prevention
- **Pre-flight Checks**: Validate session integrity before execution
- **Backup Strategy**: Create task snapshots before major operations
- **Atomic Updates**: Update JSON files atomically to prevent corruption
- **Dependency Validation**: Check all depends_on references exist
- **Context Verification**: Ensure all required context is available
## Auto-Commit Mode (--with-commit)
**Behavior**: After each agent task completes, automatically commit changes based on summary document.
**Minimal Principle**: Only commit files modified by the completed task.
**Commit Message Format**: `{type}: {task-title} - {summary}`
**Type Mapping** (from `meta.type`):
- `feature``feat` | `bugfix``fix` | `refactor``refactor`
- `test-gen``test` | `docs``docs` | `review``chore`
**Implementation**:
```bash
# 1. Read summary from .summaries/{task-id}-summary.md
# 2. Extract files from "Files Modified" section
# 3. Commit: git add <files> && git commit -m "{type}: {title} - {summary}"
```
**Error Handling**: Skip commit on no changes/missing summary, log errors, continue workflow.

View File

@@ -1,768 +0,0 @@
---
name: lite-execute
description: Execute tasks based on in-memory plan, prompt description, or file content
argument-hint: "[-y|--yes] [--in-memory] [\"task description\"|file-path]"
allowed-tools: TodoWrite(*), Task(*), Bash(*)
---
# Workflow Lite-Execute Command (/workflow:lite-execute)
## Overview
Flexible task execution command supporting three input modes: in-memory plan (from lite-plan), direct prompt description, or file content. Handles execution orchestration, progress tracking, and optional code review.
**Core capabilities:**
- Multi-mode input (in-memory plan, prompt description, or file path)
- Execution orchestration (Agent or Codex) with full context
- Live progress tracking via TodoWrite at execution call level
- Optional code review with selected tool (Gemini, Agent, or custom)
- Context continuity across multiple executions
- Intelligent format detection (Enhanced Task JSON vs plain text)
## Usage
### Command Syntax
```bash
/workflow:lite-execute [FLAGS] <INPUT>
# Flags
--in-memory Use plan from memory (called by lite-plan)
# Arguments
<input> Task description string, or path to file (required)
```
## Input Modes
### Mode 1: In-Memory Plan
**Trigger**: Called by lite-plan after Phase 4 approval with `--in-memory` flag
**Input Source**: `executionContext` global variable set by lite-plan
**Content**: Complete execution context (see Data Structures section)
**Behavior**:
- Skip execution method selection (already set by lite-plan)
- Directly proceed to execution with full context
- All planning artifacts available (exploration, clarifications, plan)
### Mode 2: Prompt Description
**Trigger**: User calls with task description string
**Input**: Simple task description (e.g., "Add unit tests for auth module")
**Behavior**:
- Store prompt as `originalUserInput`
- Create simple execution plan from prompt
- AskUserQuestion: Select execution method (Agent/Codex/Auto)
- AskUserQuestion: Select code review tool (Skip/Gemini/Agent/Other)
- Proceed to execution with `originalUserInput` included
**User Interaction**:
```javascript
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
let userSelection
if (autoYes) {
// Auto mode: Use defaults
console.log(`[--yes] Auto-confirming execution:`)
console.log(` - Execution method: Auto`)
console.log(` - Code review: Skip`)
userSelection = {
execution_method: "Auto",
code_review_tool: "Skip"
}
} else {
// Interactive mode: Ask user
userSelection = AskUserQuestion({
questions: [
{
question: "Select execution method:",
header: "Execution",
multiSelect: false,
options: [
{ label: "Agent", description: "@code-developer agent" },
{ label: "Codex", description: "codex CLI tool" },
{ label: "Auto", description: "Auto-select based on complexity" }
]
},
{
question: "Enable code review after execution?",
header: "Code Review",
multiSelect: false,
options: [
{ label: "Skip", description: "No review" },
{ label: "Gemini Review", description: "Gemini CLI tool" },
{ label: "Codex Review", description: "Git-aware review (prompt OR --uncommitted)" },
{ label: "Agent Review", description: "Current agent review" }
]
}
]
})
}
```
### Mode 3: File Content
**Trigger**: User calls with file path
**Input**: Path to file containing task description or plan.json
**Step 1: Read and Detect Format**
```javascript
fileContent = Read(filePath)
// Attempt JSON parsing
try {
jsonData = JSON.parse(fileContent)
// Check if plan.json from lite-plan session (two-layer format: task_ids[])
if (jsonData.summary && jsonData.approach && jsonData.task_ids) {
planObject = jsonData
originalUserInput = jsonData.summary
isPlanJson = true
// Load tasks from .task/*.json files
const planDir = filePath.replace(/[/\\][^/\\]+$/, '') // parent directory
planObject._loadedTasks = loadTaskFiles(planDir, jsonData.task_ids)
} else {
// Valid JSON but not plan.json - treat as plain text
originalUserInput = fileContent
isPlanJson = false
}
} catch {
// Not valid JSON - treat as plain text prompt
originalUserInput = fileContent
isPlanJson = false
}
```
**Step 2: Create Execution Plan**
If `isPlanJson === true`:
- Use `planObject` directly
- User selects execution method and code review
If `isPlanJson === false`:
- Treat file content as prompt (same behavior as Mode 2)
- Create simple execution plan from content
**Step 3: User Interaction**
- AskUserQuestion: Select execution method (Agent/Codex/Auto)
- AskUserQuestion: Select code review tool
- Proceed to execution with full context
## Helper Functions
```javascript
// Load task files from .task/ directory (two-layer format)
function loadTaskFiles(planDir, taskIds) {
return taskIds.map(id => {
const taskPath = `${planDir}/.task/${id}.json`
return JSON.parse(Read(taskPath))
})
}
// Get tasks array from loaded .task/*.json files
function getTasks(planObject) {
return planObject._loadedTasks || []
}
```
## Execution Process
```
Input Parsing:
└─ Decision (mode detection):
├─ --in-memory flag → Mode 1: Load executionContext → Skip user selection
├─ Ends with .md/.json/.txt → Mode 3: Read file → Detect format
│ ├─ Valid plan.json → Use planObject → User selects method + review
│ └─ Not plan.json → Treat as prompt → User selects method + review
└─ Other → Mode 2: Prompt description → User selects method + review
Execution:
├─ Step 1: Initialize result tracking (previousExecutionResults = [])
├─ Step 2: Task grouping & batch creation
│ ├─ Extract explicit depends_on (no file/keyword inference)
│ ├─ Group: independent tasks → single parallel batch (maximize utilization)
│ ├─ Group: dependent tasks → sequential phases (respect dependencies)
│ └─ Create TodoWrite list for batches
├─ Step 3: Launch execution
│ ├─ Phase 1: All independent tasks (⚡ single batch, concurrent)
│ └─ Phase 2+: Dependent tasks by dependency order
├─ Step 4: Track progress (TodoWrite updates per batch)
└─ Step 5: Code review (if codeReviewTool ≠ "Skip")
Output:
└─ Execution complete with results in previousExecutionResults[]
```
## Detailed Execution Steps
### Step 1: Initialize Execution Tracking
**Operations**:
- Initialize result tracking for multi-execution scenarios
- Set up `previousExecutionResults` array for context continuity
- **In-Memory Mode**: Echo execution strategy from lite-plan for transparency
```javascript
// Initialize result tracking
previousExecutionResults = []
// In-Memory Mode: Echo execution strategy (transparency before execution)
if (executionContext) {
console.log(`
📋 Execution Strategy (from lite-plan):
Method: ${executionContext.executionMethod}
Review: ${executionContext.codeReviewTool}
Tasks: ${getTasks(executionContext.planObject).length}
Complexity: ${executionContext.planObject.complexity}
${executionContext.executorAssignments ? ` Assignments: ${JSON.stringify(executionContext.executorAssignments)}` : ''}
`)
}
```
### Step 2: Task Grouping & Batch Creation
**Dependency Analysis & Grouping Algorithm**:
```javascript
// Use explicit depends_on from plan.json (no inference from file/keywords)
function extractDependencies(tasks) {
const taskIdToIndex = {}
tasks.forEach((t, i) => { taskIdToIndex[t.id] = i })
return tasks.map((task, i) => {
// Only use explicit depends_on from plan.json
const deps = (task.depends_on || [])
.map(depId => taskIdToIndex[depId])
.filter(idx => idx !== undefined && idx < i)
return { ...task, taskIndex: i, dependencies: deps }
})
}
// Group into batches: maximize parallel execution
function createExecutionCalls(tasks, executionMethod) {
const tasksWithDeps = extractDependencies(tasks)
const processed = new Set()
const calls = []
// Phase 1: All independent tasks → single parallel batch (maximize utilization)
const independentTasks = tasksWithDeps.filter(t => t.dependencies.length === 0)
if (independentTasks.length > 0) {
independentTasks.forEach(t => processed.add(t.taskIndex))
calls.push({
method: executionMethod,
executionType: "parallel",
groupId: "P1",
taskSummary: independentTasks.map(t => t.title).join(' | '),
tasks: independentTasks
})
}
// Phase 2: Dependent tasks → sequential batches (respect dependencies)
let sequentialIndex = 1
let remaining = tasksWithDeps.filter(t => !processed.has(t.taskIndex))
while (remaining.length > 0) {
// Find tasks whose dependencies are all satisfied
const ready = remaining.filter(t =>
t.dependencies.every(d => processed.has(d))
)
if (ready.length === 0) {
console.warn('Circular dependency detected, forcing remaining tasks')
ready.push(...remaining)
}
// Group ready tasks (can run in parallel within this phase)
ready.forEach(t => processed.add(t.taskIndex))
calls.push({
method: executionMethod,
executionType: ready.length > 1 ? "parallel" : "sequential",
groupId: ready.length > 1 ? `P${calls.length + 1}` : `S${sequentialIndex++}`,
taskSummary: ready.map(t => t.title).join(ready.length > 1 ? ' | ' : ' → '),
tasks: ready
})
remaining = remaining.filter(t => !processed.has(t.taskIndex))
}
return calls
}
executionCalls = createExecutionCalls(getTasks(planObject), executionMethod).map(c => ({ ...c, id: `[${c.groupId}]` }))
TodoWrite({
todos: executionCalls.map(c => ({
content: `${c.executionType === "parallel" ? "⚡" : "→"} ${c.id} (${c.tasks.length} tasks)`,
status: "pending",
activeForm: `Executing ${c.id}`
}))
})
```
### Step 3: Launch Execution
**Executor Resolution** (任务级 executor 优先于全局设置):
```javascript
// 获取任务的 executor优先使用 executorAssignmentsfallback 到全局 executionMethod
function getTaskExecutor(task) {
const assignments = executionContext?.executorAssignments || {}
if (assignments[task.id]) {
return assignments[task.id].executor // 'gemini' | 'codex' | 'agent'
}
// Fallback: 全局 executionMethod 映射
const method = executionContext?.executionMethod || 'Auto'
if (method === 'Agent') return 'agent'
if (method === 'Codex') return 'codex'
// Auto: 根据复杂度
return planObject.complexity === 'Low' ? 'agent' : 'codex'
}
// 按 executor 分组任务
function groupTasksByExecutor(tasks) {
const groups = { gemini: [], codex: [], agent: [] }
tasks.forEach(task => {
const executor = getTaskExecutor(task)
groups[executor].push(task)
})
return groups
}
```
**Execution Flow**: Parallel batches concurrently → Sequential batches in order
```javascript
const parallel = executionCalls.filter(c => c.executionType === "parallel")
const sequential = executionCalls.filter(c => c.executionType === "sequential")
// Phase 1: Launch all parallel batches (single message with multiple tool calls)
if (parallel.length > 0) {
TodoWrite({ todos: executionCalls.map(c => ({ status: c.executionType === "parallel" ? "in_progress" : "pending" })) })
parallelResults = await Promise.all(parallel.map(c => executeBatch(c)))
previousExecutionResults.push(...parallelResults)
TodoWrite({ todos: executionCalls.map(c => ({ status: parallel.includes(c) ? "completed" : "pending" })) })
}
// Phase 2: Execute sequential batches one by one
for (const call of sequential) {
TodoWrite({ todos: executionCalls.map(c => ({ status: c === call ? "in_progress" : "..." })) })
result = await executeBatch(call)
previousExecutionResults.push(result)
TodoWrite({ todos: executionCalls.map(c => ({ status: "completed" or "pending" })) })
}
```
### Unified Task Prompt Builder
**Task Formatting Principle**: Each task is a self-contained checklist. The executor only needs to know what THIS task requires. Same template for Agent and CLI.
```javascript
function buildExecutionPrompt(batch) {
// Task template (6 parts: Files → Why → How → Reference → Risks → Done)
const formatTask = (t) => `
## ${t.title}
**Scope**: \`${t.scope}\` | **Action**: ${t.action}
### Files
${(t.files || []).map(f => `- **${f.path}** → \`${f.target || ''}\`: ${f.change || (f.changes || []).join(', ') || ''}`).join('\n')}
${t.rationale ? `
### Why this approach (Medium/High)
${t.rationale.chosen_approach}
${t.rationale.decision_factors?.length > 0 ? `\nKey factors: ${t.rationale.decision_factors.join(', ')}` : ''}
${t.rationale.tradeoffs ? `\nTradeoffs: ${t.rationale.tradeoffs}` : ''}
` : ''}
### How to do it
${t.description}
${t.implementation.map(step => `- ${step}`).join('\n')}
${t.code_skeleton ? `
### Code skeleton (High)
${t.code_skeleton.interfaces?.length > 0 ? `**Interfaces**: ${t.code_skeleton.interfaces.map(i => `\`${i.name}\` - ${i.purpose}`).join(', ')}` : ''}
${t.code_skeleton.key_functions?.length > 0 ? `\n**Functions**: ${t.code_skeleton.key_functions.map(f => `\`${f.signature}\` - ${f.purpose}`).join(', ')}` : ''}
${t.code_skeleton.classes?.length > 0 ? `\n**Classes**: ${t.code_skeleton.classes.map(c => `\`${c.name}\` - ${c.purpose}`).join(', ')}` : ''}
` : ''}
### Reference
- Pattern: ${t.reference?.pattern || 'N/A'}
- Files: ${t.reference?.files?.join(', ') || 'N/A'}
${t.reference?.examples ? `- Notes: ${t.reference.examples}` : ''}
${t.risks?.length > 0 ? `
### Risk mitigations (High)
${t.risks.map(r => `- ${r.description} → **${r.mitigation}**`).join('\n')}
` : ''}
### Done when
${(t.convergence?.criteria || []).map(c => `- [ ] ${c}`).join('\n')}
${(t.test?.success_metrics || []).length > 0 ? `\n**Success metrics**: ${t.test.success_metrics.join(', ')}` : ''}`
// Build prompt
const sections = []
if (originalUserInput) sections.push(`## Goal\n${originalUserInput}`)
sections.push(`## Tasks\n${batch.tasks.map(formatTask).join('\n\n---\n')}`)
// Context (reference only)
const context = []
if (previousExecutionResults.length > 0) {
context.push(`### Previous Work\n${previousExecutionResults.map(r => `- ${r.tasksSummary}: ${r.status}`).join('\n')}`)
}
if (clarificationContext) {
context.push(`### Clarifications\n${Object.entries(clarificationContext).map(([q, a]) => `- ${q}: ${a}`).join('\n')}`)
}
if (executionContext?.planObject?.data_flow?.diagram) {
context.push(`### Data Flow\n${executionContext.planObject.data_flow.diagram}`)
}
if (executionContext?.session?.artifacts?.plan) {
context.push(`### Artifacts\nPlan: ${executionContext.session.artifacts.plan}`)
}
// Project guidelines (user-defined constraints from /workflow:session:solidify)
context.push(`### Project Guidelines\n@.workflow/project-guidelines.json`)
if (context.length > 0) sections.push(`## Context\n${context.join('\n\n')}`)
sections.push(`Complete each task according to its "Done when" checklist.`)
return sections.join('\n\n')
}
```
**Option A: Agent Execution**
When to use:
- `getTaskExecutor(task) === "agent"`
-`executionMethod = "Agent"` (全局 fallback)
-`executionMethod = "Auto" AND complexity = "Low"` (全局 fallback)
```javascript
Task(
subagent_type="code-developer",
run_in_background=false,
description=batch.taskSummary,
prompt=buildExecutionPrompt(batch)
)
```
**Result Collection**: After completion, collect result following `executionResult` structure (see Data Structures section)
**Option B: CLI Execution (Codex)**
When to use:
- `getTaskExecutor(task) === "codex"`
-`executionMethod = "Codex"` (全局 fallback)
-`executionMethod = "Auto" AND complexity = "Medium/High"` (全局 fallback)
```bash
ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write
```
**Execution with fixed IDs** (predictable ID pattern):
```javascript
// Launch CLI in background, wait for task hook callback
// Generate fixed execution ID: ${sessionId}-${groupId}
const sessionId = executionContext?.session?.id || 'standalone'
const fixedExecutionId = `${sessionId}-${batch.groupId}` // e.g., "implement-auth-2025-12-13-P1"
// Check if resuming from previous failed execution
const previousCliId = batch.resumeFromCliId || null
// Build command with fixed ID (and optional resume for continuation)
const cli_command = previousCliId
? `ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write --id ${fixedExecutionId} --resume ${previousCliId}`
: `ccw cli -p "${buildExecutionPrompt(batch)}" --tool codex --mode write --id ${fixedExecutionId}`
// Execute in background, stop output and wait for task hook callback
Bash(
command=cli_command,
run_in_background=true
)
// STOP HERE - CLI executes in background, task hook will notify on completion
```
**Resume on Failure** (with fixed ID):
```javascript
// If execution failed or timed out, offer resume option
if (bash_result.status === 'failed' || bash_result.status === 'timeout') {
console.log(`
⚠️ Execution incomplete. Resume available:
Fixed ID: ${fixedExecutionId}
Lookup: ccw cli detail ${fixedExecutionId}
Resume: ccw cli -p "Continue tasks" --resume ${fixedExecutionId} --tool codex --mode write --id ${fixedExecutionId}-retry
`)
// Store for potential retry in same session
batch.resumeFromCliId = fixedExecutionId
}
```
**Result Collection**: After completion, analyze output and collect result following `executionResult` structure (include `cliExecutionId` for resume capability)
**Option C: CLI Execution (Gemini)**
When to use: `getTaskExecutor(task) === "gemini"` (分析类任务)
```bash
# 使用统一的 buildExecutionPrompt切换 tool 和 mode
ccw cli -p "${buildExecutionPrompt(batch)}" --tool gemini --mode analysis --id ${sessionId}-${batch.groupId}
```
### Step 4: Progress Tracking
Progress tracked at batch level (not individual task level). Icons: ⚡ (parallel, concurrent), → (sequential, one-by-one)
### Step 5: Code Review (Optional)
**Skip Condition**: Only run if `codeReviewTool ≠ "Skip"`
**Review Focus**: Verify implementation against plan convergence criteria and test requirements
- Read plan.json + .task/*.json for task convergence criteria and test checklist
- Check each convergence criterion is fulfilled
- Verify success metrics from test field (Medium/High complexity)
- Run unit/integration tests specified in test field
- Validate code quality and identify issues
- Ensure alignment with planned approach and risk mitigations
**Operations**:
- Agent Review: Current agent performs direct review
- Gemini Review: Execute gemini CLI with review prompt
- Codex Review: Two options - (A) with prompt for complex reviews, (B) `--uncommitted` flag only for quick reviews
- Custom tool: Execute specified CLI tool (qwen, etc.)
**Unified Review Template** (All tools use same standard):
**Review Criteria**:
- **Convergence Criteria**: Verify each criterion from task convergence.criteria
- **Test Checklist** (Medium/High): Check unit, integration, success_metrics from task test
- **Code Quality**: Analyze quality, identify issues, suggest improvements
- **Plan Alignment**: Validate implementation matches planned approach and risk mitigations
**Shared Prompt Template** (used by all CLI tools):
```
PURPOSE: Code review for implemented changes against plan convergence criteria and test requirements
TASK: • Verify plan convergence criteria fulfillment • Check test requirements (unit, integration, success_metrics) • Analyze code quality • Identify issues • Suggest improvements • Validate plan adherence and risk mitigations
MODE: analysis
CONTEXT: @**/* @{plan.json} @{.task/*.json} [@{exploration.json}] | Memory: Review lite-execute changes against plan requirements including test checklist
EXPECTED: Quality assessment with:
- Convergence criteria verification (all tasks from .task/*.json)
- Test checklist validation (Medium/High: unit, integration, success_metrics)
- Issue identification
- Recommendations
Explicitly check each convergence criterion and test item from .task/*.json files.
CONSTRAINTS: Focus on plan convergence criteria, test requirements, and plan adherence | analysis=READ-ONLY
```
**Tool-Specific Execution** (Apply shared prompt template above):
```bash
# Method 1: Agent Review (current agent)
# - Read plan.json: ${executionContext.session.artifacts.plan}
# - Apply unified review criteria (see Shared Prompt Template)
# - Report findings directly
# Method 2: Gemini Review (recommended)
ccw cli -p "[Shared Prompt Template with artifacts]" --tool gemini --mode analysis
# CONTEXT includes: @**/* @${plan.json} [@${exploration.json}]
# Method 3: Qwen Review (alternative)
ccw cli -p "[Shared Prompt Template with artifacts]" --tool qwen --mode analysis
# Same prompt as Gemini, different execution engine
# Method 4: Codex Review (git-aware) - Two mutually exclusive options:
# Option A: With custom prompt (reviews uncommitted by default)
ccw cli -p "[Shared Prompt Template with artifacts]" --tool codex --mode review
# Use for complex reviews with specific focus areas
# Option B: Target flag only (no prompt allowed)
ccw cli --tool codex --mode review --uncommitted
# Quick review of uncommitted changes without custom instructions
# ⚠️ IMPORTANT: -p prompt and target flags (--uncommitted/--base/--commit) are MUTUALLY EXCLUSIVE
```
**Multi-Round Review with Fixed IDs**:
```javascript
// Generate fixed review ID
const reviewId = `${sessionId}-review`
// First review pass with fixed ID
const reviewResult = Bash(`ccw cli -p "[Review prompt]" --tool gemini --mode analysis --id ${reviewId}`)
// If issues found, continue review dialog with fixed ID chain
if (hasUnresolvedIssues(reviewResult)) {
// Resume with follow-up questions
Bash(`ccw cli -p "Clarify the security concerns you mentioned" --resume ${reviewId} --tool gemini --mode analysis --id ${reviewId}-followup`)
}
```
**Implementation Note**: Replace `[Shared Prompt Template with artifacts]` placeholder with actual template content, substituting:
- `@{plan.json}``@${executionContext.session.artifacts.plan}`
- `[@{exploration.json}]` → exploration files from artifacts (if exists)
### Step 6: Update Development Index
**Trigger**: After all executions complete (regardless of code review)
**Skip Condition**: Skip if `.workflow/project-tech.json` does not exist
**Operations**:
```javascript
const projectJsonPath = '.workflow/project-tech.json'
if (!fileExists(projectJsonPath)) return // Silent skip
const projectJson = JSON.parse(Read(projectJsonPath))
// Initialize if needed
if (!projectJson.development_index) {
projectJson.development_index = { feature: [], enhancement: [], bugfix: [], refactor: [], docs: [] }
}
// Detect category from keywords
function detectCategory(text) {
text = text.toLowerCase()
if (/\b(fix|bug|error|issue|crash)\b/.test(text)) return 'bugfix'
if (/\b(refactor|cleanup|reorganize)\b/.test(text)) return 'refactor'
if (/\b(doc|readme|comment)\b/.test(text)) return 'docs'
if (/\b(add|new|create|implement)\b/.test(text)) return 'feature'
return 'enhancement'
}
// Detect sub_feature from task file paths
function detectSubFeature(tasks) {
const dirs = tasks.map(t => t.file?.split('/').slice(-2, -1)[0]).filter(Boolean)
const counts = dirs.reduce((a, d) => { a[d] = (a[d] || 0) + 1; return a }, {})
return Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'general'
}
const category = detectCategory(`${planObject.summary} ${planObject.approach}`)
const entry = {
title: planObject.summary.slice(0, 60),
sub_feature: detectSubFeature(getTasks(planObject)),
date: new Date().toISOString().split('T')[0],
description: planObject.approach.slice(0, 100),
status: previousExecutionResults.every(r => r.status === 'completed') ? 'completed' : 'partial',
session_id: executionContext?.session?.id || null
}
projectJson.development_index[category].push(entry)
projectJson.statistics.last_updated = new Date().toISOString()
Write(projectJsonPath, JSON.stringify(projectJson, null, 2))
console.log(`✓ Development index: [${category}] ${entry.title}`)
```
## Best Practices
**Input Modes**: In-memory (lite-plan), prompt (standalone), file (JSON/text)
**Task Grouping**: Based on explicit depends_on only; independent tasks run in single parallel batch
**Execution**: All independent tasks launch concurrently via single Claude message with multiple tool calls
## Error Handling
| Error | Cause | Resolution |
|-------|-------|------------|
| Missing executionContext | --in-memory without context | Error: "No execution context found. Only available when called by lite-plan." |
| File not found | File path doesn't exist | Error: "File not found: {path}. Check file path." |
| Empty file | File exists but no content | Error: "File is empty: {path}. Provide task description." |
| Invalid Enhanced Task JSON | JSON missing required fields | Warning: "Missing required fields. Treating as plain text." |
| Malformed JSON | JSON parsing fails | Treat as plain text (expected for non-JSON files) |
| Execution failure | Agent/Codex crashes | Display error, use fixed ID `${sessionId}-${groupId}` for resume: `ccw cli -p "Continue" --resume <fixed-id> --id <fixed-id>-retry` |
| Execution timeout | CLI exceeded timeout | Use fixed ID for resume with extended timeout |
| Codex unavailable | Codex not installed | Show installation instructions, offer Agent execution |
| Fixed ID not found | Custom ID lookup failed | Check `ccw cli history`, verify date directories |
## Data Structures
### executionContext (Input - Mode 1)
Passed from lite-plan via global variable:
```javascript
{
planObject: {
summary: string,
approach: string,
task_ids: string[], // Task IDs referencing .task/*.json files
task_count: number, // Number of tasks
_loadedTasks: [...], // Populated at runtime from .task/*.json files
estimated_time: string,
recommended_execution: string,
complexity: string
},
// Task file paths (populated for two-layer format)
taskFiles: [{id: string, path: string}] | null,
explorationsContext: {...} | null, // Multi-angle explorations
explorationAngles: string[], // List of exploration angles
explorationManifest: {...} | null, // Exploration manifest
clarificationContext: {...} | null,
executionMethod: "Agent" | "Codex" | "Auto", // 全局默认
codeReviewTool: "Skip" | "Gemini Review" | "Agent Review" | string,
originalUserInput: string,
// 任务级 executor 分配(优先于 executionMethod
executorAssignments: {
[taskId]: { executor: "gemini" | "codex" | "agent", reason: string }
},
// Session artifacts location (saved by lite-plan)
session: {
id: string, // Session identifier: {taskSlug}-{shortTimestamp}
folder: string, // Session folder path: .workflow/.lite-plan/{session-id}
artifacts: {
explorations: [{angle, path}], // exploration-{angle}.json paths
explorations_manifest: string, // explorations-manifest.json path
plan: string // plan.json path (always present)
}
}
}
```
**Artifact Usage**:
- Artifact files contain detailed planning context
- Pass artifact paths to CLI tools and agents for enhanced context
- See execution options below for usage examples
### executionResult (Output)
Collected after each execution call completes:
```javascript
{
executionId: string, // e.g., "[Agent-1]", "[Codex-1]"
status: "completed" | "partial" | "failed",
tasksSummary: string, // Brief description of tasks handled
completionSummary: string, // What was completed
keyOutputs: string, // Files created/modified, key changes
notes: string, // Important context for next execution
fixedCliId: string | null // Fixed CLI execution ID (e.g., "implement-auth-2025-12-13-P1")
}
```
Appended to `previousExecutionResults` array for context continuity in multi-execution scenarios.
## Post-Completion Expansion
完成后询问用户是否扩展为issue(test/enhance/refactor/doc),选中项调用 `/issue:new "{summary} - {dimension}"`
**Fixed ID Pattern**: `${sessionId}-${groupId}` enables predictable lookup without auto-generated timestamps.
**Resume Usage**: If `status` is "partial" or "failed", use `fixedCliId` to resume:
```bash
# Lookup previous execution
ccw cli detail ${fixedCliId}
# Resume with new fixed ID for retry
ccw cli -p "Continue from where we left off" --resume ${fixedCliId} --tool codex --mode write --id ${fixedCliId}-retry
```

View File

@@ -1,878 +0,0 @@
---
name: lite-fix
description: Lightweight bug diagnosis and fix workflow with intelligent severity assessment and optional hotfix mode for production incidents
argument-hint: "[-y|--yes] [--hotfix] \"bug description or issue reference\""
allowed-tools: TodoWrite(*), Task(*), Skill(*), AskUserQuestion(*)
---
# Workflow Lite-Fix Command (/workflow:lite-fix)
## Overview
Intelligent lightweight bug fixing command with dynamic workflow adaptation based on severity assessment. Focuses on diagnosis phases (root cause analysis, impact assessment, fix planning, confirmation) and delegates execution to `/workflow:lite-execute`.
**Core capabilities:**
- Intelligent bug analysis with automatic severity detection
- Dynamic code diagnosis (cli-explore-agent) for root cause identification
- Interactive clarification after diagnosis to gather missing information
- Adaptive fix planning strategy (direct Claude vs cli-lite-planning-agent) based on complexity
- Two-step confirmation: fix-plan display -> multi-dimensional input collection
- Execution execute with complete context handoff to lite-execute
## Usage
```bash
/workflow:lite-fix [FLAGS] <BUG_DESCRIPTION>
# Flags
-y, --yes Skip all confirmations (auto mode)
--hotfix, -h Production hotfix mode (minimal diagnosis, fast fix)
# Arguments
<bug-description> Bug description, error message, or path to .md file (required)
# Examples
/workflow:lite-fix "用户登录失败" # Interactive mode
/workflow:lite-fix --yes "用户登录失败" # Auto mode (no confirmations)
/workflow:lite-fix -y --hotfix "生产环境数据库连接失败" # Auto + hotfix mode
```
## Output Artifacts
| Artifact | Description |
|----------|-------------|
| `diagnosis-{angle}.json` | Per-angle diagnosis results (1-4 files based on severity) |
| `diagnoses-manifest.json` | Index of all diagnosis files |
| `planning-context.md` | Evidence paths + synthesized understanding |
| `fix-plan.json` | Fix plan overview with `task_ids[]` (plan-overview-fix-schema.json) |
| `.task/FIX-*.json` | Independent fix task files (one per task) |
**Output Directory**: `.workflow/.lite-fix/{bug-slug}-{YYYY-MM-DD}/`
**Agent Usage**:
- Low/Medium severity → Direct Claude planning (no agent)
- High/Critical severity → `cli-lite-planning-agent` generates `fix-plan.json`
**Schema Reference**: `~/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json`
## Auto Mode Defaults
When `--yes` or `-y` flag is used:
- **Clarification Questions**: Skipped (no clarification phase)
- **Fix Plan Confirmation**: Auto-selected "Allow"
- **Execution Method**: Auto-selected "Auto"
- **Code Review**: Auto-selected "Skip"
- **Severity**: Uses auto-detected severity (no manual override)
- **Hotfix Mode**: Respects --hotfix flag if present, otherwise normal mode
**Flag Parsing**:
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
const hotfixMode = $ARGUMENTS.includes('--hotfix') || $ARGUMENTS.includes('-h')
```
## Execution Process
```
Phase 1: Bug Analysis & Diagnosis
|- Parse input (description, error message, or .md file)
|- Intelligent severity pre-assessment (Low/Medium/High/Critical)
|- Diagnosis decision (auto-detect or --hotfix flag)
|- Context protection: If file reading >=50k chars -> force cli-explore-agent
+- Decision:
|- needsDiagnosis=true -> Launch parallel cli-explore-agents (1-4 based on severity)
+- needsDiagnosis=false (hotfix) -> Skip directly to Phase 3 (Fix Planning)
Phase 2: Clarification (optional, multi-round)
|- Aggregate clarification_needs from all diagnosis angles
|- Deduplicate similar questions
+- Decision:
|- Has clarifications -> AskUserQuestion (max 4 questions per round, multiple rounds allowed)
+- No clarifications -> Skip to Phase 3
Phase 3: Fix Planning (NO CODE EXECUTION - planning only)
+- Decision (based on Phase 1 severity):
|- Low/Medium -> Load schema: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json -> Direct Claude planning (following schema) -> fix-plan.json -> MUST proceed to Phase 4
+- High/Critical -> cli-lite-planning-agent -> fix-plan.json -> MUST proceed to Phase 4
Phase 4: Confirmation & Selection
|- Display fix-plan summary (tasks, severity, estimated time)
+- AskUserQuestion:
|- Confirm: Allow / Modify / Cancel
|- Execution: Agent / Codex / Auto
+- Review: Gemini / Agent / Skip
Phase 5: Execute
|- Build executionContext (fix-plan + diagnoses + clarifications + selections)
+- Skill(skill="workflow:lite-execute", args="--in-memory --mode bugfix")
```
## Implementation
### Phase 1: Intelligent Multi-Angle Diagnosis
**Session Setup** (MANDATORY - follow exactly):
```javascript
// Helper: Get UTC+8 (China Standard Time) ISO string
const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()
const bugSlug = bug_description.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 40)
const dateStr = getUtc8ISOString().substring(0, 10) // Format: 2025-11-29
const sessionId = `${bugSlug}-${dateStr}` // e.g., "user-avatar-upload-fails-2025-11-29"
const sessionFolder = `.workflow/.lite-fix/${sessionId}`
bash(`mkdir -p ${sessionFolder} && test -d ${sessionFolder} && echo "SUCCESS: ${sessionFolder}" || echo "FAILED: ${sessionFolder}"`)
```
**Diagnosis Decision Logic**:
```javascript
const hotfixMode = $ARGUMENTS.includes('--hotfix') || $ARGUMENTS.includes('-h')
needsDiagnosis = (
!hotfixMode &&
(
bug.lacks_specific_error_message ||
bug.requires_codebase_context ||
bug.needs_execution_tracing ||
bug.root_cause_unclear
)
)
if (!needsDiagnosis) {
// Skip to Phase 2 (Clarification) or Phase 3 (Fix Planning)
proceed_to_next_phase()
}
```
**Context Protection**: File reading >=50k chars -> force `needsDiagnosis=true` (delegate to cli-explore-agent)
**Severity Pre-Assessment** (Intelligent Analysis):
```javascript
// Analyzes bug severity based on:
// - Symptoms: Error messages, crash reports, user complaints
// - Scope: How many users/features are affected?
// - Urgency: Production down vs minor inconvenience
// - Impact: Data loss, security, business impact
const severity = analyzeBugSeverity(bug_description)
// Returns: 'Low' | 'Medium' | 'High' | 'Critical'
// Low: Minor UI issue, localized, no data impact
// Medium: Multiple users affected, degraded functionality
// High: Significant functionality broken, many users affected
// Critical: Production down, data loss risk, security issue
// Angle assignment based on bug type (orchestrator decides, not agent)
const DIAGNOSIS_ANGLE_PRESETS = {
runtime_error: ['error-handling', 'dataflow', 'state-management', 'edge-cases'],
performance: ['performance', 'bottlenecks', 'caching', 'data-access'],
security: ['security', 'auth-patterns', 'dataflow', 'validation'],
data_corruption: ['data-integrity', 'state-management', 'transactions', 'validation'],
ui_bug: ['state-management', 'event-handling', 'rendering', 'data-binding'],
integration: ['api-contracts', 'error-handling', 'timeouts', 'fallbacks']
}
function selectDiagnosisAngles(bugDescription, count) {
const text = bugDescription.toLowerCase()
let preset = 'runtime_error' // default
if (/slow|timeout|performance|lag|hang/.test(text)) preset = 'performance'
else if (/security|auth|permission|access|token/.test(text)) preset = 'security'
else if (/corrupt|data|lost|missing|inconsistent/.test(text)) preset = 'data_corruption'
else if (/ui|display|render|style|click|button/.test(text)) preset = 'ui_bug'
else if (/api|integration|connect|request|response/.test(text)) preset = 'integration'
return DIAGNOSIS_ANGLE_PRESETS[preset].slice(0, count)
}
const selectedAngles = selectDiagnosisAngles(bug_description, severity === 'Critical' ? 4 : (severity === 'High' ? 3 : (severity === 'Medium' ? 2 : 1)))
console.log(`
## Diagnosis Plan
Bug Severity: ${severity}
Selected Angles: ${selectedAngles.join(', ')}
Launching ${selectedAngles.length} parallel diagnoses...
`)
```
**Launch Parallel Diagnoses** - Orchestrator assigns angle to each agent:
```javascript
// Launch agents with pre-assigned diagnosis angles
const diagnosisTasks = selectedAngles.map((angle, index) =>
Task(
subagent_type="cli-explore-agent",
run_in_background=false,
description=`Diagnose: ${angle}`,
prompt=`
## Task Objective
Execute **${angle}** diagnosis for bug root cause analysis. Analyze codebase from this specific angle to discover root cause, affected paths, and fix hints.
## Output Location
**Session Folder**: ${sessionFolder}
**Output File**: ${sessionFolder}/diagnosis-${angle}.json
## Assigned Context
- **Diagnosis Angle**: ${angle}
- **Bug Description**: ${bug_description}
- **Diagnosis Index**: ${index + 1} of ${selectedAngles.length}
## MANDATORY FIRST STEPS (Execute by Agent)
**You (cli-explore-agent) MUST execute these steps in order:**
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{error_keyword_from_bug}" --type ts (locate relevant files)
3. Execute: cat ~/.ccw/workflows/cli-templates/schemas/diagnosis-json-schema.json (get output schema reference)
4. Read: .workflow/project-tech.json (technology stack and architecture context)
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
## Diagnosis Strategy (${angle} focus)
**Step 1: Error Tracing** (Bash)
- rg for error messages, stack traces, log patterns
- git log --since='2 weeks ago' for recent changes
- Trace execution path in affected modules
**Step 2: Root Cause Analysis** (Gemini CLI)
- What code paths lead to this ${angle} issue?
- What edge cases are not handled from ${angle} perspective?
- What recent changes might have introduced this bug?
**Step 3: Write Output**
- Consolidate ${angle} findings into JSON
- Identify ${angle}-specific clarification needs
- Provide fix hints based on ${angle} analysis
## Expected Output
**Schema Reference**: Schema obtained in MANDATORY FIRST STEPS step 3, follow schema exactly
**Required Fields** (all ${angle} focused):
- symptom: Bug symptoms and error messages
- root_cause: Root cause hypothesis from ${angle} perspective
**IMPORTANT**: Use structured format:
\`{file: "src/module/file.ts", line_range: "45-60", issue: "Description", confidence: 0.85}\`
- affected_files: Files involved from ${angle} perspective
**MANDATORY**: Every file MUST use structured object format with ALL required fields:
\`[{path: "src/file.ts", relevance: 0.85, rationale: "Contains handleLogin() at line 45 where null check is missing", change_type: "fix_target", discovery_source: "bash-scan", key_symbols: ["handleLogin"]}]\`
- **rationale** (required): Specific reason why this file is affected (>10 chars, not generic)
- **change_type** (required): fix_target|needs_update|test_coverage|reference_only
- **discovery_source** (recommended): bash-scan|cli-analysis|ace-search|dependency-trace|stack-trace|manual
- **key_symbols** (recommended): Key functions/classes related to the bug
- reproduction_steps: Steps to reproduce the bug
- fix_hints: Suggested fix approaches from ${angle} viewpoint
- dependencies: Dependencies relevant to ${angle} diagnosis
- constraints: ${angle}-specific limitations affecting fix
- clarification_needs: ${angle}-related ambiguities (options array + recommended index)
- _metadata.diagnosis_angle: "${angle}"
- _metadata.diagnosis_index: ${index + 1}
## Success Criteria
- [ ] Schema obtained via cat diagnosis-json-schema.json
- [ ] get_modules_by_depth.sh executed
- [ ] Root cause identified with confidence score
- [ ] At least 3 affected files identified with specific rationale + change_type
- [ ] Every file has rationale >10 chars (not generic like "Contains ${angle} logic")
- [ ] Every file has change_type classification (fix_target/needs_update/etc.)
- [ ] Fix hints are actionable (specific code changes, not generic advice)
- [ ] Reproduction steps are verifiable
- [ ] JSON output follows schema exactly
- [ ] clarification_needs includes options + recommended
## Execution
**Write**: \`${sessionFolder}/diagnosis-${angle}.json\`
**Return**: 2-3 sentence summary of ${angle} diagnosis findings
`
)
)
// Execute all diagnosis tasks in parallel
```
**Auto-discover Generated Diagnosis Files**:
```javascript
// After diagnoses complete, auto-discover all diagnosis-*.json files
const diagnosisFiles = bash(`find ${sessionFolder} -name "diagnosis-*.json" -type f`)
.split('\n')
.filter(f => f.trim())
// Read metadata to build manifest
const diagnosisManifest = {
session_id: sessionId,
bug_description: bug_description,
timestamp: getUtc8ISOString(),
severity: severity,
diagnosis_count: diagnosisFiles.length,
diagnoses: diagnosisFiles.map(file => {
const data = JSON.parse(Read(file))
const filename = path.basename(file)
return {
angle: data._metadata.diagnosis_angle,
file: filename,
path: file,
index: data._metadata.diagnosis_index
}
})
}
Write(`${sessionFolder}/diagnoses-manifest.json`, JSON.stringify(diagnosisManifest, null, 2))
console.log(`
## Diagnosis Complete
Generated diagnosis files in ${sessionFolder}:
${diagnosisManifest.diagnoses.map(d => `- diagnosis-${d.angle}.json (angle: ${d.angle})`).join('\n')}
Manifest: diagnoses-manifest.json
Angles diagnosed: ${diagnosisManifest.diagnoses.map(d => d.angle).join(', ')}
`)
```
**Output**:
- `${sessionFolder}/diagnosis-{angle1}.json`
- `${sessionFolder}/diagnosis-{angle2}.json`
- ... (1-4 files based on severity)
- `${sessionFolder}/diagnoses-manifest.json`
---
### Phase 2: Clarification (Optional, Multi-Round)
**Skip if**: No diagnosis or `clarification_needs` is empty across all diagnoses
**⚠️ CRITICAL**: AskUserQuestion tool limits max 4 questions per call. **MUST execute multiple rounds** to exhaust all clarification needs - do NOT stop at round 1.
**Aggregate clarification needs from all diagnosis angles**:
```javascript
// Load manifest and all diagnosis files
const manifest = JSON.parse(Read(`${sessionFolder}/diagnoses-manifest.json`))
const diagnoses = manifest.diagnoses.map(diag => ({
angle: diag.angle,
data: JSON.parse(Read(diag.path))
}))
// Aggregate clarification needs from all diagnoses
const allClarifications = []
diagnoses.forEach(diag => {
if (diag.data.clarification_needs?.length > 0) {
diag.data.clarification_needs.forEach(need => {
allClarifications.push({
...need,
source_angle: diag.angle
})
})
}
})
// Deduplicate by question similarity
function deduplicateClarifications(clarifications) {
const unique = []
clarifications.forEach(c => {
const isDuplicate = unique.some(u =>
u.question.toLowerCase() === c.question.toLowerCase()
)
if (!isDuplicate) unique.push(c)
})
return unique
}
const uniqueClarifications = deduplicateClarifications(allClarifications)
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
if (autoYes) {
// Auto mode: Skip clarification phase
console.log(`[--yes] Skipping ${uniqueClarifications.length} clarification questions`)
console.log(`Proceeding to fix planning with diagnosis results...`)
// Continue to Phase 3
} else if (uniqueClarifications.length > 0) {
// Interactive mode: Multi-round clarification
// ⚠️ MUST execute ALL rounds until uniqueClarifications exhausted
const BATCH_SIZE = 4
const totalRounds = Math.ceil(uniqueClarifications.length / BATCH_SIZE)
for (let i = 0; i < uniqueClarifications.length; i += BATCH_SIZE) {
const batch = uniqueClarifications.slice(i, i + BATCH_SIZE)
const currentRound = Math.floor(i / BATCH_SIZE) + 1
console.log(`### Clarification Round ${currentRound}/${totalRounds}`)
AskUserQuestion({
questions: batch.map(need => ({
question: `[${need.source_angle}] ${need.question}\n\nContext: ${need.context}`,
header: need.source_angle,
multiSelect: false,
options: need.options.map((opt, index) => {
const isRecommended = need.recommended === index
return {
label: isRecommended ? `${opt}` : opt,
description: isRecommended ? `Use ${opt} approach (Recommended)` : `Use ${opt} approach`
}
})
}))
})
// Store batch responses in clarificationContext before next round
}
}
```
**Output**: `clarificationContext` (in-memory)
---
### Phase 3: Fix Planning
**Planning Strategy Selection** (based on Phase 1 severity):
**IMPORTANT**: Phase 3 is **planning only** - NO code execution. All execution happens in Phase 5 via lite-execute.
**Low/Medium Severity** - Direct planning by Claude:
```javascript
// Step 1: Read schema
const schema = Bash(`cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json`)
// Step 2: Generate fix tasks with NEW field names (Claude directly, no agent)
// Field mapping: modification_points -> files, acceptance -> convergence, verification -> test
const fixTasks = [
{
id: "FIX-001",
title: "...",
description: "...",
scope: "...",
action: "Fix|Update|Refactor|Add|Delete",
depends_on: [],
convergence: {
criteria: ["..."] // Quantified acceptance criteria
},
files: [
{ path: "src/module/file.ts", action: "modify", target: "functionName", change: "Description of change" }
],
implementation: ["Step 1: ...", "Step 2: ..."],
test: {
manual_checks: ["Reproduce issue", "Verify fix"],
success_metrics: ["Issue resolved", "No regressions"]
},
complexity: "Low|Medium",
// Medium severity fields (optional for Low, recommended for Medium)
...(severity === "Medium" ? {
rationale: {
chosen_approach: "Direct fix approach",
alternatives_considered: ["Workaround", "Refactor"],
decision_factors: ["Minimal impact", "Quick turnaround"],
tradeoffs: "Doesn't address underlying issue"
},
test: {
unit: ["test_bug_fix_basic"],
integration: [],
manual_checks: ["Reproduce issue", "Verify fix"],
success_metrics: ["Issue resolved", "No regressions"]
}
} : {})
}
// ... additional tasks as needed
]
// Step 3: Write individual task files to .task/ directory
const taskDir = `${sessionFolder}/.task`
Bash(`mkdir -p "${taskDir}"`)
fixTasks.forEach(task => {
Write(`${taskDir}/${task.id}.json`, JSON.stringify(task, null, 2))
})
// Step 4: Generate fix-plan overview (NO embedded tasks[])
const fixPlan = {
summary: "...",
approach: "...",
task_ids: fixTasks.map(t => t.id),
task_count: fixTasks.length,
fix_context: {
root_cause: "...",
strategy: "immediate_patch|comprehensive_fix|refactor",
severity: severity,
risk_level: "..."
},
estimated_time: "...",
recommended_execution: "Agent",
// Medium complexity fields (optional for Low)
...(severity === "Medium" ? {
design_decisions: [
{
decision: "Use immediate_patch strategy for minimal risk",
rationale: "Keeps changes localized and quick to review",
tradeoff: "Defers comprehensive refactoring"
}
]
} : {}),
_metadata: {
timestamp: getUtc8ISOString(),
source: "direct-planning",
planning_mode: "direct",
plan_type: "fix",
complexity: severity === "Medium" ? "Medium" : "Low"
}
}
// Step 5: Write fix-plan overview to session folder
Write(`${sessionFolder}/fix-plan.json`, JSON.stringify(fixPlan, null, 2))
// Step 6: MUST continue to Phase 4 (Confirmation) - DO NOT execute code here
```
**High/Critical Severity** - Invoke cli-lite-planning-agent:
```javascript
Task(
subagent_type="cli-lite-planning-agent",
run_in_background=false,
description="Generate detailed fix plan",
prompt=`
Generate fix plan using two-layer output format.
## Output Location
**Session Folder**: ${sessionFolder}
**Output Files**:
- ${sessionFolder}/planning-context.md (evidence + understanding)
- ${sessionFolder}/fix-plan.json (fix plan overview -- NO embedded tasks[])
- ${sessionFolder}/.task/FIX-*.json (independent fix task files, one per task)
## Output Schema Reference
Execute: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-fix-schema.json (get schema reference before generating plan)
## Project Context (MANDATORY - Read Both Files)
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
**CRITICAL**: All fix tasks MUST comply with constraints in project-guidelines.json
## Bug Description
${bug_description}
## Multi-Angle Diagnosis Context
${manifest.diagnoses.map(diag => `### Diagnosis: ${diag.angle} (${diag.file})
Path: ${diag.path}
Read this file for detailed ${diag.angle} analysis.`).join('\n\n')}
Total diagnoses: ${manifest.diagnosis_count}
Angles covered: ${manifest.diagnoses.map(d => d.angle).join(', ')}
Manifest: ${sessionFolder}/diagnoses-manifest.json
## User Clarifications
${JSON.stringify(clarificationContext) || "None"}
## Severity Level
${severity}
## Requirements
**Output Format**: Two-layer structure:
- fix-plan.json: Overview with task_ids[] referencing .task/ files (NO tasks[] array)
- .task/FIX-*.json: Independent task files following task-schema.json
**fix-plan.json required fields**:
- summary: 2-3 sentence overview of the fix
- approach: Overall fix approach description
- task_ids: Array of task IDs (e.g., ["FIX-001", "FIX-002"])
- task_count: Number of tasks
- fix_context:
- root_cause: Consolidated root cause from all diagnoses
- strategy: "immediate_patch" | "comprehensive_fix" | "refactor"
- severity: ${severity}
- risk_level: "Low" | "Medium" | "High"
- estimated_time, recommended_execution
- data_flow (High/Critical REQUIRED): How data flows through affected code
- diagram: "A -> B -> C" style flow
- stages: [{stage, input, output, component}]
- design_decisions (High/Critical REQUIRED): Global fix decisions
- [{decision, rationale, tradeoff}]
- _metadata:
- timestamp, source, planning_mode
- plan_type: "fix"
- complexity: "High" | "Critical"
- diagnosis_angles: ${JSON.stringify(manifest.diagnoses.map(d => d.angle))}
**Each .task/FIX-*.json required fields**:
- id: "FIX-001" (prefix FIX-, NOT TASK-)
- title: action verb + target (e.g., "Fix token validation edge case")
- description
- scope: module path (src/auth/) or feature name
- action: "Fix" | "Update" | "Refactor" | "Add" | "Delete"
- depends_on: task IDs this task depends on (use sparingly)
- convergence: { criteria: ["Quantified acceptance criterion 1", "..."] }
- files: ALL files to modify for this fix (group related changes)
- [{ path: "src/file.ts", action: "modify|create|delete", target: "component/function", change: "Description of what changes" }]
- implementation: ["Step 1: ...", "Step 2: ..."] (2-5 steps covering all files)
- test:
- unit: ["test names to add/verify"]
- integration: ["integration test names"]
- manual_checks: ["manual verification steps"]
- success_metrics: ["quantified success criteria"]
**High/Critical complexity fields per task** (REQUIRED):
- rationale:
- chosen_approach: Why this fix approach (not alternatives)
- alternatives_considered: Other approaches evaluated
- decision_factors: Key factors influencing choice
- tradeoffs: Known tradeoffs of this approach
- risks:
- description: Risk description
- probability: Low|Medium|High
- impact: Low|Medium|High
- mitigation: How to mitigate
- fallback: Fallback if fix fails
- code_skeleton (optional): Key interfaces/functions to implement
- interfaces: [{name, definition, purpose}]
- key_functions: [{signature, purpose, returns}]
**Field name rules** (do NOT use old names):
- files[].change (NOT modification_points)
- convergence.criteria (NOT acceptance)
- test (NOT verification at task level)
## Task Grouping Rules
1. **Group by fix area**: All changes for one fix = one task (even if 2-3 files)
2. **Avoid file-per-task**: Do NOT create separate tasks for each file
3. **Substantial tasks**: Each task should represent 10-45 minutes of work
4. **True dependencies only**: Only use depends_on when Task B cannot start without Task A's output
5. **Prefer parallel**: Most tasks should be independent (no depends_on)
6. **Task IDs**: Use FIX-001, FIX-002 prefix (NOT TASK-)
## Execution
1. Read ALL diagnosis files for comprehensive context
2. Execute CLI planning using Gemini (Qwen fallback) with --rule planning-fix-strategy template
3. Synthesize findings from multiple diagnosis angles
4. Generate fix tasks (1-5 tasks):
- Each task file written to \`${sessionFolder}/.task/FIX-NNN.json\`
- For High/Critical: REQUIRED fields (rationale, test, risks, code_skeleton)
5. Generate fix-plan overview:
- Written to \`${sessionFolder}/fix-plan.json\`
- Contains task_ids[] referencing .task/ files (NO embedded tasks[])
- For High/Critical: REQUIRED fields (data_flow, design_decisions)
6. **Write**: \`${sessionFolder}/planning-context.md\` (evidence paths + understanding)
7. **Write**: \`${sessionFolder}/.task/FIX-*.json\` (individual task files)
8. **Write**: \`${sessionFolder}/fix-plan.json\` (plan overview with task_ids[])
9. Return brief completion summary
## Output Format for CLI
Include these sections in your fix-plan output:
- Summary, Root Cause (in fix_context), Strategy (existing)
- Data Flow: Diagram showing affected code paths
- Design Decisions: Key architectural choices in the fix
- Task files: Each with convergence, files, test, rationale (High), risks (High), code_skeleton (High)
`
)
```
**Output**: `${sessionFolder}/fix-plan.json`
---
### Phase 4: Task Confirmation & Execution Selection
**Step 4.1: Display Fix Plan**
```javascript
const fixPlan = JSON.parse(Read(`${sessionFolder}/fix-plan.json`))
// Load tasks from .task/ directory (two-layer format)
const tasks = (fixPlan.task_ids || []).map(id => {
return JSON.parse(Read(`${sessionFolder}/.task/${id}.json`))
})
const taskList = tasks
const fixContext = fixPlan.fix_context || {}
console.log(`
## Fix Plan
**Summary**: ${fixPlan.summary}
**Root Cause**: ${fixContext.root_cause || fixPlan.root_cause}
**Strategy**: ${fixContext.strategy || fixPlan.strategy}
**Tasks** (${taskList.length}):
${taskList.map((t, i) => `${i+1}. ${t.title} (${t.scope})`).join('\n')}
**Severity**: ${fixContext.severity || fixPlan.severity}
**Risk Level**: ${fixContext.risk_level || fixPlan.risk_level}
**Estimated Time**: ${fixPlan.estimated_time}
**Recommended**: ${fixPlan.recommended_execution}
`)
```
**Step 4.2: Collect Confirmation**
```javascript
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
let userSelection
if (autoYes) {
// Auto mode: Use defaults
console.log(`[--yes] Auto-confirming fix plan:`)
console.log(` - Confirmation: Allow`)
console.log(` - Execution: Auto`)
console.log(` - Review: Skip`)
userSelection = {
confirmation: "Allow",
execution_method: "Auto",
code_review_tool: "Skip"
}
} else {
// Interactive mode: Ask user
userSelection = AskUserQuestion({
questions: [
{
question: `Confirm fix plan? (${taskList.length} tasks, ${fixContext.severity || fixPlan.severity} severity)`,
header: "Confirm",
multiSelect: false,
options: [
{ label: "Allow", description: "Proceed as-is" },
{ label: "Modify", description: "Adjust before execution" },
{ label: "Cancel", description: "Abort workflow" }
]
},
{
question: "Execution method:",
header: "Execution",
multiSelect: false,
options: [
{ label: "Agent", description: "@code-developer agent" },
{ label: "Codex", description: "codex CLI tool" },
{ label: "Auto", description: `Auto: ${(fixContext.severity || fixPlan.severity) === 'Low' ? 'Agent' : 'Codex'}` }
]
},
{
question: "Code review after fix?",
header: "Review",
multiSelect: false,
options: [
{ label: "Gemini Review", description: "Gemini CLI" },
{ label: "Agent Review", description: "@code-reviewer" },
{ label: "Skip", description: "No review" }
]
}
]
})
}
```
---
### Phase 5: Execute to Execution
**CRITICAL**: lite-fix NEVER executes code directly. ALL execution MUST go through lite-execute.
**Step 5.1: Build executionContext**
```javascript
// Load manifest and all diagnosis files
const manifest = JSON.parse(Read(`${sessionFolder}/diagnoses-manifest.json`))
const diagnoses = {}
manifest.diagnoses.forEach(diag => {
if (file_exists(diag.path)) {
diagnoses[diag.angle] = JSON.parse(Read(diag.path))
}
})
const fixPlan = JSON.parse(Read(`${sessionFolder}/fix-plan.json`))
const fixSeverity = fixPlan.fix_context?.severity || fixPlan.severity
executionContext = {
mode: "bugfix",
severity: fixSeverity,
planObject: {
...fixPlan,
// Ensure complexity is set based on severity for new field consumption
complexity: fixPlan.complexity || (fixSeverity === 'Critical' ? 'High' : (fixSeverity === 'High' ? 'High' : 'Medium'))
},
// Task files from .task/ directory (two-layer format)
taskFiles: (fixPlan.task_ids || []).map(id => ({
id,
path: `${sessionFolder}/.task/${id}.json`
})),
diagnosisContext: diagnoses,
diagnosisAngles: manifest.diagnoses.map(d => d.angle),
diagnosisManifest: manifest,
clarificationContext: clarificationContext || null,
executionMethod: userSelection.execution_method,
codeReviewTool: userSelection.code_review_tool,
originalUserInput: bug_description,
session: {
id: sessionId,
folder: sessionFolder,
artifacts: {
diagnoses: manifest.diagnoses.map(diag => ({
angle: diag.angle,
path: diag.path
})),
diagnoses_manifest: `${sessionFolder}/diagnoses-manifest.json`,
fix_plan: `${sessionFolder}/fix-plan.json`,
task_dir: `${sessionFolder}/.task`
}
}
}
```
**Step 5.2: Execute**
```javascript
Skill(skill="workflow:lite-execute", args="--in-memory --mode bugfix")
```
## Session Folder Structure
```
.workflow/.lite-fix/{bug-slug}-{YYYY-MM-DD}/
├── diagnosis-{angle1}.json # Diagnosis angle 1
├── diagnosis-{angle2}.json # Diagnosis angle 2
├── diagnosis-{angle3}.json # Diagnosis angle 3 (if applicable)
├── diagnosis-{angle4}.json # Diagnosis angle 4 (if applicable)
├── diagnoses-manifest.json # Diagnosis index
├── planning-context.md # Evidence + understanding
├── fix-plan.json # Fix plan overview (task_ids[], NO embedded tasks[])
└── .task/ # Independent fix task files
├── FIX-001.json # Fix task 1
├── FIX-002.json # Fix task 2
└── ... # Additional fix tasks
```
**Example**:
```
.workflow/.lite-fix/user-avatar-upload-fails-413-2025-11-25/
├── diagnosis-error-handling.json
├── diagnosis-dataflow.json
├── diagnosis-validation.json
├── diagnoses-manifest.json
├── planning-context.md
├── fix-plan.json
└── .task/
├── FIX-001.json
└── FIX-002.json
```
## Error Handling
| Error | Resolution |
|-------|------------|
| Diagnosis agent failure | Skip diagnosis, continue with bug description only |
| Planning agent failure | Fallback to direct planning by Claude |
| Clarification timeout | Use diagnosis findings as-is |
| Confirmation timeout | Save context, display resume instructions |
| Modify loop > 3 times | Suggest breaking task or using /workflow:plan |
| Root cause unclear | Extend diagnosis time or use broader angles |
| Too complex for lite-fix | Escalate to /workflow:plan --mode bugfix |

View File

@@ -1,770 +0,0 @@
---
name: lite-plan
description: Lightweight interactive planning workflow with in-memory planning, code exploration, and execution execute to lite-execute after user confirmation
argument-hint: "[-y|--yes] [-e|--explore] \"task description\"|file.md"
allowed-tools: TodoWrite(*), Task(*), Skill(*), AskUserQuestion(*)
---
# Workflow Lite-Plan Command (/workflow:lite-plan)
## Overview
Intelligent lightweight planning command with dynamic workflow adaptation based on task complexity. Focuses on planning phases (exploration, clarification, planning, confirmation) and delegates execution to `/workflow:lite-execute`.
**Core capabilities:**
- Intelligent task analysis with automatic exploration detection
- Dynamic code exploration (cli-explore-agent) when codebase understanding needed
- Interactive clarification after exploration to gather missing information
- Adaptive planning: Low complexity → Direct Claude; Medium/High → cli-lite-planning-agent
- Two-step confirmation: plan display → multi-dimensional input collection
- Execution execute with complete context handoff to lite-execute
## Usage
```bash
/workflow:lite-plan [FLAGS] <TASK_DESCRIPTION>
# Flags
-y, --yes Skip all confirmations (auto mode)
-e, --explore Force code exploration phase (overrides auto-detection)
# Arguments
<task-description> Task description or path to .md file (required)
# Examples
/workflow:lite-plan "实现JWT认证" # Interactive mode
/workflow:lite-plan --yes "实现JWT认证" # Auto mode (no confirmations)
/workflow:lite-plan -y -e "优化数据库查询性能" # Auto mode + force exploration
```
## Output Artifacts
| Artifact | Description |
|----------|-------------|
| `exploration-{angle}.json` | Per-angle exploration results (1-4 files based on complexity) |
| `explorations-manifest.json` | Index of all exploration files |
| `planning-context.md` | Evidence paths + synthesized understanding |
| `plan.json` | Plan overview with task_ids[] (plan-overview-base-schema.json) |
| `.task/TASK-*.json` | Independent task files (one per task) |
**Output Directory**: `.workflow/.lite-plan/{task-slug}-{YYYY-MM-DD}/`
**Agent Usage**:
- Low complexity → Direct Claude planning (no agent)
- Medium/High complexity → `cli-lite-planning-agent` generates `plan.json`
**Schema Reference**: `~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json`
## Auto Mode Defaults
When `--yes` or `-y` flag is used:
- **Clarification Questions**: Skipped (no clarification phase)
- **Plan Confirmation**: Auto-selected "Allow"
- **Execution Method**: Auto-selected "Auto"
- **Code Review**: Auto-selected "Skip"
**Flag Parsing**:
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
const forceExplore = $ARGUMENTS.includes('--explore') || $ARGUMENTS.includes('-e')
```
## Execution Process
```
Phase 1: Task Analysis & Exploration
├─ Parse input (description or .md file)
├─ intelligent complexity assessment (Low/Medium/High)
├─ Exploration decision (auto-detect or --explore flag)
├─ Context protection: If file reading ≥50k chars → force cli-explore-agent
└─ Decision:
├─ needsExploration=true → Launch parallel cli-explore-agents (1-4 based on complexity)
└─ needsExploration=false → Skip to Phase 2/3
Phase 2: Clarification (optional, multi-round)
├─ Aggregate clarification_needs from all exploration angles
├─ Deduplicate similar questions
└─ Decision:
├─ Has clarifications → AskUserQuestion (max 4 questions per round, multiple rounds allowed)
└─ No clarifications → Skip to Phase 3
Phase 3: Planning (NO CODE EXECUTION - planning only)
└─ Decision (based on Phase 1 complexity):
├─ Low → Load schema: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json → Direct Claude planning (following schema) → plan.json
└─ Medium/High → cli-lite-planning-agent → plan.json (agent internally executes quality check)
Phase 4: Confirmation & Selection
├─ Display plan summary (tasks, complexity, estimated time)
└─ AskUserQuestion:
├─ Confirm: Allow / Modify / Cancel
├─ Execution: Agent / Codex / Auto
└─ Review: Gemini / Agent / Skip
Phase 5: Execute
├─ Build executionContext (plan + explorations + clarifications + selections)
└─ Skill(skill="workflow:lite-execute", args="--in-memory")
```
## Implementation
### Phase 1: Intelligent Multi-Angle Exploration
**Session Setup** (MANDATORY - follow exactly):
```javascript
// Helper: Get UTC+8 (China Standard Time) ISO string
const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()
const taskSlug = task_description.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 40)
const dateStr = getUtc8ISOString().substring(0, 10) // Format: 2025-11-29
const sessionId = `${taskSlug}-${dateStr}` // e.g., "implement-jwt-refresh-2025-11-29"
const sessionFolder = `.workflow/.lite-plan/${sessionId}`
bash(`mkdir -p ${sessionFolder} && test -d ${sessionFolder} && echo "SUCCESS: ${sessionFolder}" || echo "FAILED: ${sessionFolder}"`)
```
**Exploration Decision Logic**:
```javascript
needsExploration = (
flags.includes('--explore') || flags.includes('-e') ||
task.mentions_specific_files ||
task.requires_codebase_context ||
task.needs_architecture_understanding ||
task.modifies_existing_code
)
if (!needsExploration) {
// Skip to Phase 2 (Clarification) or Phase 3 (Planning)
proceed_to_next_phase()
}
```
**⚠️ Context Protection**: File reading ≥50k chars → force `needsExploration=true` (delegate to cli-explore-agent)
**Complexity Assessment** (Intelligent Analysis):
```javascript
// analyzes task complexity based on:
// - Scope: How many systems/modules are affected?
// - Depth: Surface change vs architectural impact?
// - Risk: Potential for breaking existing functionality?
// - Dependencies: How interconnected is the change?
const complexity = analyzeTaskComplexity(task_description)
// Returns: 'Low' | 'Medium' | 'High'
// Low: Single file, isolated change, minimal risk
// Medium: Multiple files, some dependencies, moderate risk
// High: Cross-module, architectural, high risk
// Angle assignment based on task type (orchestrator decides, not agent)
const ANGLE_PRESETS = {
architecture: ['architecture', 'dependencies', 'modularity', 'integration-points'],
security: ['security', 'auth-patterns', 'dataflow', 'validation'],
performance: ['performance', 'bottlenecks', 'caching', 'data-access'],
bugfix: ['error-handling', 'dataflow', 'state-management', 'edge-cases'],
feature: ['patterns', 'integration-points', 'testing', 'dependencies']
}
function selectAngles(taskDescription, count) {
const text = taskDescription.toLowerCase()
let preset = 'feature' // default
if (/refactor|architect|restructure|modular/.test(text)) preset = 'architecture'
else if (/security|auth|permission|access/.test(text)) preset = 'security'
else if (/performance|slow|optimi|cache/.test(text)) preset = 'performance'
else if (/fix|bug|error|issue|broken/.test(text)) preset = 'bugfix'
return ANGLE_PRESETS[preset].slice(0, count)
}
const selectedAngles = selectAngles(task_description, complexity === 'High' ? 4 : (complexity === 'Medium' ? 3 : 1))
// Planning strategy determination
const planningStrategy = complexity === 'Low'
? 'Direct Claude Planning'
: 'cli-lite-planning-agent'
console.log(`
## Exploration Plan
Task Complexity: ${complexity}
Selected Angles: ${selectedAngles.join(', ')}
Planning Strategy: ${planningStrategy}
Launching ${selectedAngles.length} parallel explorations...
`)
```
**Launch Parallel Explorations** - Orchestrator assigns angle to each agent:
**⚠️ CRITICAL - NO BACKGROUND EXECUTION**:
- **MUST NOT use `run_in_background: true`** - exploration results are REQUIRED before planning
```javascript
// Launch agents with pre-assigned angles
const explorationTasks = selectedAngles.map((angle, index) =>
Task(
subagent_type="cli-explore-agent",
run_in_background=false, // ⚠️ MANDATORY: Must wait for results
description=`Explore: ${angle}`,
prompt=`
## Task Objective
Execute **${angle}** exploration for task planning context. Analyze codebase from this specific angle to discover relevant structure, patterns, and constraints.
## Output Location
**Session Folder**: ${sessionFolder}
**Output File**: ${sessionFolder}/exploration-${angle}.json
## Assigned Context
- **Exploration Angle**: ${angle}
- **Task Description**: ${task_description}
- **Exploration Index**: ${index + 1} of ${selectedAngles.length}
## MANDATORY FIRST STEPS (Execute by Agent)
**You (cli-explore-agent) MUST execute these steps in order:**
1. Run: ccw tool exec get_modules_by_depth '{}' (project structure)
2. Run: rg -l "{keyword_from_task}" --type ts (locate relevant files)
3. Execute: cat ~/.ccw/workflows/cli-templates/schemas/explore-json-schema.json (get output schema reference)
4. Read: .workflow/project-tech.json (technology stack and architecture context)
5. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
## Exploration Strategy (${angle} focus)
**Step 1: Structural Scan** (Bash)
- get_modules_by_depth.sh → identify modules related to ${angle}
- find/rg → locate files relevant to ${angle} aspect
- Analyze imports/dependencies from ${angle} perspective
**Step 2: Semantic Analysis** (Gemini CLI)
- How does existing code handle ${angle} concerns?
- What patterns are used for ${angle}?
- Where would new code integrate from ${angle} viewpoint?
**Step 3: Write Output**
- Consolidate ${angle} findings into JSON
- Identify ${angle}-specific clarification needs
## Expected Output
**Schema Reference**: Schema obtained in MANDATORY FIRST STEPS step 3, follow schema exactly
**Required Fields** (all ${angle} focused):
- project_structure: Modules/architecture relevant to ${angle}
- relevant_files: Files affected from ${angle} perspective
**MANDATORY**: Every file MUST use structured object format with ALL required fields:
\`[{path: "src/file.ts", relevance: 0.85, rationale: "Contains AuthService.login() - entry point for JWT token generation", role: "modify_target", discovery_source: "bash-scan", key_symbols: ["AuthService", "login"]}]\`
- **rationale** (required): Specific selection basis tied to ${angle} topic (>10 chars, not generic)
- **role** (required): modify_target|dependency|pattern_reference|test_target|type_definition|integration_point|config|context_only
- **discovery_source** (recommended): bash-scan|cli-analysis|ace-search|dependency-trace|manual
- **key_symbols** (recommended): Key functions/classes/types in the file relevant to the task
- Scores: 0.7+ high priority, 0.5-0.7 medium, <0.5 low
- patterns: ${angle}-related patterns to follow
- dependencies: Dependencies relevant to ${angle}
- integration_points: Where to integrate from ${angle} viewpoint (include file:line locations)
- constraints: ${angle}-specific limitations/conventions
- clarification_needs: ${angle}-related ambiguities (options array + recommended index)
- _metadata.exploration_angle: "${angle}"
## Success Criteria
- [ ] Schema obtained via cat explore-json-schema.json
- [ ] get_modules_by_depth.sh executed
- [ ] At least 3 relevant files identified with specific rationale + role
- [ ] Every file has rationale >10 chars (not generic like "Related to ${angle}")
- [ ] Every file has role classification (modify_target/dependency/etc.)
- [ ] Patterns are actionable (code examples, not generic advice)
- [ ] Integration points include file:line locations
- [ ] Constraints are project-specific to ${angle}
- [ ] JSON output follows schema exactly
- [ ] clarification_needs includes options + recommended
## Execution
**Write**: \`${sessionFolder}/exploration-${angle}.json\`
**Return**: 2-3 sentence summary of ${angle} findings
`
)
)
// Execute all exploration tasks in parallel
```
**Auto-discover Generated Exploration Files**:
```javascript
// After explorations complete, auto-discover all exploration-*.json files
const explorationFiles = bash(`find ${sessionFolder} -name "exploration-*.json" -type f`)
.split('\n')
.filter(f => f.trim())
// Read metadata to build manifest
const explorationManifest = {
session_id: sessionId,
task_description: task_description,
timestamp: getUtc8ISOString(),
complexity: complexity,
exploration_count: explorationCount,
explorations: explorationFiles.map(file => {
const data = JSON.parse(Read(file))
const filename = path.basename(file)
return {
angle: data._metadata.exploration_angle,
file: filename,
path: file,
index: data._metadata.exploration_index
}
})
}
Write(`${sessionFolder}/explorations-manifest.json`, JSON.stringify(explorationManifest, null, 2))
console.log(`
## Exploration Complete
Generated exploration files in ${sessionFolder}:
${explorationManifest.explorations.map(e => `- exploration-${e.angle}.json (angle: ${e.angle})`).join('\n')}
Manifest: explorations-manifest.json
Angles explored: ${explorationManifest.explorations.map(e => e.angle).join(', ')}
`)
```
**Output**:
- `${sessionFolder}/exploration-{angle1}.json`
- `${sessionFolder}/exploration-{angle2}.json`
- ... (1-4 files based on complexity)
- `${sessionFolder}/explorations-manifest.json`
---
### Phase 2: Clarification (Optional, Multi-Round)
**Skip if**: No exploration or `clarification_needs` is empty across all explorations
**⚠️ CRITICAL**: AskUserQuestion tool limits max 4 questions per call. **MUST execute multiple rounds** to exhaust all clarification needs - do NOT stop at round 1.
**Aggregate clarification needs from all exploration angles**:
```javascript
// Load manifest and all exploration files
const manifest = JSON.parse(Read(`${sessionFolder}/explorations-manifest.json`))
const explorations = manifest.explorations.map(exp => ({
angle: exp.angle,
data: JSON.parse(Read(exp.path))
}))
// Aggregate clarification needs from all explorations
const allClarifications = []
explorations.forEach(exp => {
if (exp.data.clarification_needs?.length > 0) {
exp.data.clarification_needs.forEach(need => {
allClarifications.push({
...need,
source_angle: exp.angle
})
})
}
})
// Intelligent deduplication: analyze allClarifications by intent
// - Identify questions with similar intent across different angles
// - Merge similar questions: combine options, consolidate context
// - Produce dedupedClarifications with unique intents only
const dedupedClarifications = intelligentMerge(allClarifications)
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
if (autoYes) {
// Auto mode: Skip clarification phase
console.log(`[--yes] Skipping ${dedupedClarifications.length} clarification questions`)
console.log(`Proceeding to planning with exploration results...`)
// Continue to Phase 3
} else if (dedupedClarifications.length > 0) {
// Interactive mode: Multi-round clarification
const BATCH_SIZE = 4
const totalRounds = Math.ceil(dedupedClarifications.length / BATCH_SIZE)
for (let i = 0; i < dedupedClarifications.length; i += BATCH_SIZE) {
const batch = dedupedClarifications.slice(i, i + BATCH_SIZE)
const currentRound = Math.floor(i / BATCH_SIZE) + 1
console.log(`### Clarification Round ${currentRound}/${totalRounds}`)
AskUserQuestion({
questions: batch.map(need => ({
question: `[${need.source_angle}] ${need.question}\n\nContext: ${need.context}`,
header: need.source_angle.substring(0, 12),
multiSelect: false,
options: need.options.map((opt, index) => ({
label: need.recommended === index ? `${opt}` : opt,
description: need.recommended === index ? `Recommended` : `Use ${opt}`
}))
}))
})
// Store batch responses in clarificationContext before next round
}
}
```
**Output**: `clarificationContext` (in-memory)
---
### Phase 3: Planning
**Planning Strategy Selection** (based on Phase 1 complexity):
**IMPORTANT**: Phase 3 is **planning only** - NO code execution. All execution happens in Phase 5 via lite-execute.
**Executor Assignment** (Claude 智能分配plan 生成后执行):
```javascript
// 分配规则(优先级从高到低):
// 1. 用户明确指定:"用 gemini 分析..." → gemini, "codex 实现..." → codex
// 2. 默认 → agent
const executorAssignments = {} // { taskId: { executor: 'gemini'|'codex'|'agent', reason: string } }
// Load tasks from .task/ directory for executor assignment
const taskFiles = Glob(`${sessionFolder}/.task/TASK-*.json`)
taskFiles.forEach(taskPath => {
const task = JSON.parse(Read(taskPath))
// Claude 根据上述规则语义分析,为每个 task 分配 executor
executorAssignments[task.id] = { executor: '...', reason: '...' }
})
```
**Low Complexity** - Direct planning by Claude:
```javascript
// Step 1: Read schema
const schema = Bash(`cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json`)
// Step 2: ⚠️ MANDATORY - Read and review ALL exploration files
const manifest = JSON.parse(Read(`${sessionFolder}/explorations-manifest.json`))
manifest.explorations.forEach(exp => {
const explorationData = Read(exp.path)
console.log(`\n### Exploration: ${exp.angle}\n${explorationData}`)
})
// Step 3: Generate task objects (Claude directly, no agent)
// ⚠️ Tasks MUST incorporate insights from exploration files read in Step 2
// Task fields use NEW names: convergence.criteria (not acceptance), files[].change (not modification_points), test (not verification)
const tasks = [
{
id: "TASK-001",
title: "...",
description: "...",
depends_on: [],
convergence: { criteria: ["..."] },
files: [{ path: "...", change: "..." }],
implementation: ["..."],
test: "..."
},
// ... more tasks
]
// Step 4: Write task files to .task/ directory
const taskDir = `${sessionFolder}/.task`
Bash(`mkdir -p "${taskDir}"`)
tasks.forEach(task => {
Write(`${taskDir}/${task.id}.json`, JSON.stringify(task, null, 2))
})
// Step 5: Generate plan overview (NO embedded tasks[])
const plan = {
summary: "...",
approach: "...",
task_ids: tasks.map(t => t.id),
task_count: tasks.length,
complexity: "Low",
estimated_time: "...",
recommended_execution: "Agent",
_metadata: {
timestamp: getUtc8ISOString(),
source: "direct-planning",
planning_mode: "direct",
plan_type: "feature"
}
}
// Step 6: Write plan overview to session folder
Write(`${sessionFolder}/plan.json`, JSON.stringify(plan, null, 2))
// Step 7: MUST continue to Phase 4 (Confirmation) - DO NOT execute code here
```
**Medium/High Complexity** - Invoke cli-lite-planning-agent:
```javascript
Task(
subagent_type="cli-lite-planning-agent",
run_in_background=false,
description="Generate detailed implementation plan",
prompt=`
Generate implementation plan and write plan.json.
## Output Location
**Session Folder**: ${sessionFolder}
**Output Files**:
- ${sessionFolder}/planning-context.md (evidence + understanding)
- ${sessionFolder}/plan.json (plan overview -- NO embedded tasks[])
- ${sessionFolder}/.task/TASK-*.json (independent task files, one per task)
## Output Schema Reference
Execute: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json (get schema reference before generating plan)
## Project Context (MANDATORY - Read Both Files)
1. Read: .workflow/project-tech.json (technology stack, architecture, key components)
2. Read: .workflow/project-guidelines.json (user-defined constraints and conventions)
**CRITICAL**: All generated tasks MUST comply with constraints in project-guidelines.json
## Task Description
${task_description}
## Multi-Angle Exploration Context
${manifest.explorations.map(exp => `### Exploration: ${exp.angle} (${exp.file})
Path: ${exp.path}
Read this file for detailed ${exp.angle} analysis.`).join('\n\n')}
Total explorations: ${manifest.exploration_count}
Angles covered: ${manifest.explorations.map(e => e.angle).join(', ')}
Manifest: ${sessionFolder}/explorations-manifest.json
## User Clarifications
${JSON.stringify(clarificationContext) || "None"}
## Complexity Level
${complexity}
## Requirements
Generate plan.json and .task/*.json following the schema obtained above. Key constraints:
- _metadata.exploration_angles: ${JSON.stringify(manifest.explorations.map(e => e.angle))}
**Output Format**: Two-layer structure:
- plan.json: Overview with task_ids[] referencing .task/ files (NO tasks[] array)
- .task/TASK-*.json: Independent task files following task-schema.json
plan.json required fields: summary, approach, task_ids, task_count, _metadata (with plan_type: "feature")
Each task file required fields: id, title, description, depends_on, convergence (with criteria[])
Task fields use: files[].change (not modification_points), convergence.criteria (not acceptance), test (not verification)
## Task Grouping Rules
1. **Group by feature**: All changes for one feature = one task (even if 3-5 files)
2. **Group by context**: Tasks with similar context or related functional changes can be grouped together
3. **Minimize agent count**: Simple, unrelated tasks can also be grouped to reduce agent execution overhead
4. **Avoid file-per-task**: Do NOT create separate tasks for each file
5. **Substantial tasks**: Each task should represent 15-60 minutes of work
6. **True dependencies only**: Only use depends_on when Task B cannot start without Task A's output
7. **Prefer parallel**: Most tasks should be independent (no depends_on)
## Execution
1. Read schema file (cat command above)
2. Execute CLI planning using Gemini (Qwen fallback)
3. Read ALL exploration files for comprehensive context
4. Synthesize findings and generate tasks + plan overview
5. **Write**: \`${sessionFolder}/planning-context.md\` (evidence paths + understanding)
6. **Create**: \`${sessionFolder}/.task/\` directory (mkdir -p)
7. **Write**: \`${sessionFolder}/.task/TASK-001.json\`, \`TASK-002.json\`, etc. (one per task)
8. **Write**: \`${sessionFolder}/plan.json\` (overview with task_ids[], NO tasks[])
9. Return brief completion summary
`
)
```
**Output**: `${sessionFolder}/plan.json`
---
### Phase 4: Task Confirmation & Execution Selection
**Step 4.1: Display Plan**
```javascript
const plan = JSON.parse(Read(`${sessionFolder}/plan.json`))
// Load tasks from .task/ directory
const tasks = (plan.task_ids || []).map(id => {
const taskPath = `${sessionFolder}/.task/${id}.json`
return JSON.parse(Read(taskPath))
})
const taskList = tasks
console.log(`
## Implementation Plan
**Summary**: ${plan.summary}
**Approach**: ${plan.approach}
**Tasks** (${taskList.length}):
${taskList.map((t, i) => `${i+1}. ${t.title} (${t.scope || t.files?.[0]?.path || ''})`).join('\n')}
**Complexity**: ${plan.complexity}
**Estimated Time**: ${plan.estimated_time}
**Recommended**: ${plan.recommended_execution}
`)
```
**Step 4.2: Collect Confirmation**
```javascript
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
let userSelection
if (autoYes) {
// Auto mode: Use defaults
console.log(`[--yes] Auto-confirming plan:`)
console.log(` - Confirmation: Allow`)
console.log(` - Execution: Auto`)
console.log(` - Review: Skip`)
userSelection = {
confirmation: "Allow",
execution_method: "Auto",
code_review_tool: "Skip"
}
} else {
// Interactive mode: Ask user
// Note: Execution "Other" option allows specifying CLI tools from ~/.claude/cli-tools.json
userSelection = AskUserQuestion({
questions: [
{
question: `Confirm plan? (${taskList.length} tasks, ${plan.complexity})`,
header: "Confirm",
multiSelect: false,
options: [
{ label: "Allow", description: "Proceed as-is" },
{ label: "Modify", description: "Adjust before execution" },
{ label: "Cancel", description: "Abort workflow" }
]
},
{
question: "Execution method:",
header: "Execution",
multiSelect: false,
options: [
{ label: "Agent", description: "@code-developer agent" },
{ label: "Codex", description: "codex CLI tool" },
{ label: "Auto", description: `Auto: ${plan.complexity === 'Low' ? 'Agent' : 'Codex'}` }
]
},
{
question: "Code review after execution?",
header: "Review",
multiSelect: false,
options: [
{ label: "Gemini Review", description: "Gemini CLI review" },
{ label: "Codex Review", description: "Git-aware review (prompt OR --uncommitted)" },
{ label: "Agent Review", description: "@code-reviewer agent" },
{ label: "Skip", description: "No review" }
]
}
]
})
}
```
---
### Phase 5: Execute to Execution
**CRITICAL**: lite-plan NEVER executes code directly. ALL execution MUST go through lite-execute.
**Step 5.1: Build executionContext**
```javascript
// Load manifest and all exploration files
const manifest = JSON.parse(Read(`${sessionFolder}/explorations-manifest.json`))
const explorations = {}
manifest.explorations.forEach(exp => {
if (file_exists(exp.path)) {
explorations[exp.angle] = JSON.parse(Read(exp.path))
}
})
const plan = JSON.parse(Read(`${sessionFolder}/plan.json`))
executionContext = {
planObject: plan, // plan overview (no tasks[])
taskFiles: (plan.task_ids || []).map(id => ({
id,
path: `${sessionFolder}/.task/${id}.json`
})),
explorationsContext: explorations,
explorationAngles: manifest.explorations.map(e => e.angle),
explorationManifest: manifest,
clarificationContext: clarificationContext || null,
executionMethod: userSelection.execution_method, // 全局默认,可被 executorAssignments 覆盖
codeReviewTool: userSelection.code_review_tool,
originalUserInput: task_description,
// 任务级 executor 分配(优先于全局 executionMethod
executorAssignments: executorAssignments, // { taskId: { executor, reason } }
session: {
id: sessionId,
folder: sessionFolder,
artifacts: {
explorations: manifest.explorations.map(exp => ({
angle: exp.angle,
path: exp.path
})),
explorations_manifest: `${sessionFolder}/explorations-manifest.json`,
plan: `${sessionFolder}/plan.json`,
task_dir: `${sessionFolder}/.task`
}
}
}
```
**Step 5.2: Execute**
```javascript
Skill(skill="workflow:lite-execute", args="--in-memory")
```
## Session Folder Structure
```
.workflow/.lite-plan/{task-slug}-{YYYY-MM-DD}/
├── exploration-{angle1}.json # Exploration angle 1
├── exploration-{angle2}.json # Exploration angle 2
├── exploration-{angle3}.json # Exploration angle 3 (if applicable)
├── exploration-{angle4}.json # Exploration angle 4 (if applicable)
├── explorations-manifest.json # Exploration index
├── planning-context.md # Evidence paths + understanding
├── plan.json # Plan overview (task_ids[])
└── .task/ # Task files directory
├── TASK-001.json
├── TASK-002.json
└── ...
```
**Example**:
```
.workflow/.lite-plan/implement-jwt-refresh-2025-11-25-14-30-25/
├── exploration-architecture.json
├── exploration-auth-patterns.json
├── exploration-security.json
├── explorations-manifest.json
├── planning-context.md
├── plan.json
└── .task/
├── TASK-001.json
├── TASK-002.json
└── TASK-003.json
```
## Error Handling
| Error | Resolution |
|-------|------------|
| Exploration agent failure | Skip exploration, continue with task description only |
| Planning agent failure | Fallback to direct planning by Claude |
| Clarification timeout | Use exploration findings as-is |
| Confirmation timeout | Save context, display resume instructions |
| Modify loop > 3 times | Suggest breaking task or using /workflow:plan |

View File

@@ -1,602 +0,0 @@
---
name: workflow:multi-cli-plan
description: Multi-CLI collaborative planning workflow with ACE context gathering and iterative cross-verification. Uses cli-discuss-agent for Gemini+Codex+Claude analysis to converge on optimal execution plan.
argument-hint: "[-y|--yes] <task description> [--max-rounds=3] [--tools=gemini,codex] [--mode=parallel|serial]"
allowed-tools: TodoWrite(*), Task(*), AskUserQuestion(*), Read(*), Bash(*), Write(*), mcp__ace-tool__search_context(*)
---
## Auto Mode
When `--yes` or `-y`: Auto-approve plan, use recommended solution and execution method (Agent, Skip review).
# Multi-CLI Collaborative Planning Command
## Quick Start
```bash
# Basic usage
/workflow:multi-cli-plan "Implement user authentication"
# With options
/workflow:multi-cli-plan "Add dark mode support" --max-rounds=3
/workflow:multi-cli-plan "Refactor payment module" --tools=gemini,codex,claude
/workflow:multi-cli-plan "Fix memory leak" --mode=serial
```
**Context Source**: ACE semantic search + Multi-CLI analysis
**Output Directory**: `.workflow/.multi-cli-plan/{session-id}/`
**Default Max Rounds**: 3 (convergence may complete earlier)
**CLI Tools**: @cli-discuss-agent (analysis), @cli-lite-planning-agent (plan generation)
**Execution**: Auto-hands off to `/workflow:lite-execute --in-memory` after plan approval
## What & Why
### Core Concept
Multi-CLI collaborative planning with **three-phase architecture**: ACE context gathering → Iterative multi-CLI discussion → Plan generation. Orchestrator delegates analysis to agents, only handles user decisions and session management.
**Process**:
- **Phase 1**: ACE semantic search gathers codebase context
- **Phase 2**: cli-discuss-agent orchestrates Gemini/Codex/Claude for cross-verified analysis
- **Phase 3-5**: User decision → Plan generation → Execution handoff
**vs Single-CLI Planning**:
- **Single**: One model perspective, potential blind spots
- **Multi-CLI**: Cross-verification catches inconsistencies, builds consensus on solutions
### Value Proposition
1. **Multi-Perspective Analysis**: Gemini + Codex + Claude analyze from different angles
2. **Cross-Verification**: Identify agreements/disagreements, build confidence
3. **User-Driven Decisions**: Every round ends with user decision point
4. **Iterative Convergence**: Progressive refinement until consensus reached
### Orchestrator Boundary (CRITICAL)
- **ONLY command** for multi-CLI collaborative planning
- Manages: Session state, user decisions, agent delegation, phase transitions
- Delegates: CLI execution to @cli-discuss-agent, plan generation to @cli-lite-planning-agent
### Execution Flow
```
Phase 1: Context Gathering
└─ ACE semantic search, extract keywords, build context package
Phase 2: Multi-CLI Discussion (Iterative, via @cli-discuss-agent)
├─ Round N: Agent executes Gemini + Codex + Claude
├─ Cross-verify findings, synthesize solutions
├─ Write synthesis.json to rounds/{N}/
└─ Loop until convergence or max rounds
Phase 3: Present Options
└─ Display solutions with trade-offs from agent output
Phase 4: User Decision
├─ Select solution approach
├─ Select execution method (Agent/Codex/Auto)
├─ Select code review tool (Skip/Gemini/Codex/Agent)
└─ Route:
├─ Approve → Phase 5
├─ Need More Analysis → Return to Phase 2
└─ Cancel → Save session
Phase 5: Plan Generation & Execution Handoff
├─ Generate plan.json + .task/*.json (via @cli-lite-planning-agent, two-layer output)
├─ Build executionContext with user selections and taskFiles
└─ Execute to /workflow:lite-execute --in-memory
```
### Agent Roles
| Agent | Responsibility |
|-------|---------------|
| **Orchestrator** | Session management, ACE context, user decisions, phase transitions, executionContext assembly |
| **@cli-discuss-agent** | Multi-CLI execution (Gemini/Codex/Claude), cross-verification, solution synthesis, synthesis.json output |
| **@cli-lite-planning-agent** | Task decomposition, two-layer output: plan.json (overview with task_ids[]) + .task/*.json (task files) |
## Core Responsibilities
### Phase 1: Context Gathering
**Session Initialization**:
```javascript
const sessionId = `MCP-${taskSlug}-${date}`
const sessionFolder = `.workflow/.multi-cli-plan/${sessionId}`
Bash(`mkdir -p ${sessionFolder}/rounds`)
```
**ACE Context Queries**:
```javascript
const aceQueries = [
`Project architecture related to ${keywords}`,
`Existing implementations of ${keywords[0]}`,
`Code patterns for ${keywords} features`,
`Integration points for ${keywords[0]}`
]
// Execute via mcp__ace-tool__search_context
```
**Context Package** (passed to agent):
- `relevant_files[]` - Files identified by ACE
- `detected_patterns[]` - Code patterns found
- `architecture_insights` - Structure understanding
### Phase 2: Agent Delegation
**Core Principle**: Orchestrator only delegates and reads output - NO direct CLI execution.
**⚠️ CRITICAL - CLI EXECUTION REQUIREMENT**:
- **MUST** execute CLI calls via `Bash` with `run_in_background: true`
- **MUST** wait for hook callback to receive complete results
- **MUST NOT** proceed with next phase until CLI execution fully completes
- Do NOT use `TaskOutput` polling during CLI execution - wait passively for results
- Minimize scope: Proceed only when 100% result available
**Agent Invocation**:
```javascript
Task({
subagent_type: "cli-discuss-agent",
run_in_background: false,
description: `Discussion round ${currentRound}`,
prompt: `
## Input Context
- task_description: ${taskDescription}
- round_number: ${currentRound}
- session: { id: "${sessionId}", folder: "${sessionFolder}" }
- ace_context: ${JSON.stringify(contextPackageage)}
- previous_rounds: ${JSON.stringify(analysisResults)}
- user_feedback: ${userFeedback || 'None'}
- cli_config: { tools: ["gemini", "codex"], mode: "parallel", fallback_chain: ["gemini", "codex", "claude"] }
## Execution Process
1. Parse input context (handle JSON strings)
2. Check if ACE supplementary search needed
3. Build CLI prompts with context
4. Execute CLIs (parallel or serial per cli_config.mode)
5. Parse CLI outputs, handle failures with fallback
6. Perform cross-verification between CLI results
7. Synthesize solutions, calculate scores
8. Calculate convergence, generate clarification questions
9. Write synthesis.json
## Output
Write: ${sessionFolder}/rounds/${currentRound}/synthesis.json
## Completion Checklist
- [ ] All configured CLI tools executed (or fallback triggered)
- [ ] Cross-verification completed with agreements/disagreements
- [ ] 2-3 solutions generated with file:line references
- [ ] Convergence score calculated (0.0-1.0)
- [ ] synthesis.json written with all Primary Fields
`
})
```
**Read Agent Output**:
```javascript
const synthesis = JSON.parse(Read(`${sessionFolder}/rounds/${round}/synthesis.json`))
// Access top-level fields: solutions, convergence, cross_verification, clarification_questions
```
**Convergence Decision**:
```javascript
if (synthesis.convergence.recommendation === 'converged') {
// Proceed to Phase 3
} else if (synthesis.convergence.recommendation === 'user_input_needed') {
// Collect user feedback, return to Phase 2
} else {
// Continue to next round if new_insights && round < maxRounds
}
```
### Phase 3: Present Options
**Display from Agent Output** (no processing):
```javascript
console.log(`
## Solution Options
${synthesis.solutions.map((s, i) => `
**Option ${i+1}: ${s.name}**
Source: ${s.source_cli.join(' + ')}
Effort: ${s.effort} | Risk: ${s.risk}
Pros: ${s.pros.join(', ')}
Cons: ${s.cons.join(', ')}
Files: ${s.affected_files.slice(0,3).map(f => `${f.file}:${f.line}`).join(', ')}
`).join('\n')}
## Cross-Verification
Agreements: ${synthesis.cross_verification.agreements.length}
Disagreements: ${synthesis.cross_verification.disagreements.length}
`)
```
### Phase 4: User Decision
**Decision Options**:
```javascript
AskUserQuestion({
questions: [
{
question: "Which solution approach?",
header: "Solution",
multiSelect: false,
options: solutions.map((s, i) => ({
label: `Option ${i+1}: ${s.name}`,
description: `${s.effort} effort, ${s.risk} risk`
})).concat([
{ label: "Need More Analysis", description: "Return to Phase 2" }
])
},
{
question: "Execution method:",
header: "Execution",
multiSelect: false,
options: [
{ label: "Agent", description: "@code-developer agent" },
{ label: "Codex", description: "codex CLI tool" },
{ label: "Auto", description: "Auto-select based on complexity" }
]
},
{
question: "Code review after execution?",
header: "Review",
multiSelect: false,
options: [
{ label: "Skip", description: "No review" },
{ label: "Gemini Review", description: "Gemini CLI tool" },
{ label: "Codex Review", description: "codex review --uncommitted" },
{ label: "Agent Review", description: "Current agent review" }
]
}
]
})
```
**Routing**:
- Approve + execution method → Phase 5
- Need More Analysis → Phase 2 with feedback
- Cancel → Save session for resumption
### Phase 5: Plan Generation & Execution Handoff
**Step 1: Build Context-Package** (Orchestrator responsibility):
```javascript
// Extract key information from user decision and synthesis
const contextPackage = {
// Core solution details
solution: {
name: selectedSolution.name,
source_cli: selectedSolution.source_cli,
feasibility: selectedSolution.feasibility,
effort: selectedSolution.effort,
risk: selectedSolution.risk,
summary: selectedSolution.summary
},
// Implementation plan (tasks, flow, milestones)
implementation_plan: selectedSolution.implementation_plan,
// Dependencies
dependencies: selectedSolution.dependencies || { internal: [], external: [] },
// Technical concerns
technical_concerns: selectedSolution.technical_concerns || [],
// Consensus from cross-verification
consensus: {
agreements: synthesis.cross_verification.agreements,
resolved_conflicts: synthesis.cross_verification.resolution
},
// User constraints (from Phase 4 feedback)
constraints: userConstraints || [],
// Task context
task_description: taskDescription,
session_id: sessionId
}
// Write context-package for traceability
Write(`${sessionFolder}/context-package.json`, JSON.stringify(contextPackage, null, 2))
```
**Context-Package Schema**:
| Field | Type | Description |
|-------|------|-------------|
| `solution` | object | User-selected solution from synthesis |
| `solution.name` | string | Solution identifier |
| `solution.feasibility` | number | Viability score (0-1) |
| `solution.summary` | string | Brief analysis summary |
| `implementation_plan` | object | Task breakdown with flow and dependencies |
| `implementation_plan.approach` | string | High-level technical strategy |
| `implementation_plan.tasks[]` | array | Discrete tasks with id, name, depends_on, files |
| `implementation_plan.execution_flow` | string | Task sequence (e.g., "T1 → T2 → T3") |
| `implementation_plan.milestones` | string[] | Key checkpoints |
| `dependencies` | object | Module and package dependencies |
| `technical_concerns` | string[] | Risks and blockers |
| `consensus` | object | Cross-verified agreements from multi-CLI |
| `constraints` | string[] | User-specified constraints from Phase 4 |
```json
{
"solution": {
"name": "Strategy Pattern Refactoring",
"source_cli": ["gemini", "codex"],
"feasibility": 0.88,
"effort": "medium",
"risk": "low",
"summary": "Extract payment gateway interface, implement strategy pattern for multi-gateway support"
},
"implementation_plan": {
"approach": "Define interface → Create concrete strategies → Implement factory → Migrate existing code",
"tasks": [
{"id": "T1", "name": "Define PaymentGateway interface", "depends_on": [], "files": [{"file": "src/types/payment.ts", "line": 1, "action": "create"}], "key_point": "Include all existing Stripe methods"},
{"id": "T2", "name": "Implement StripeGateway", "depends_on": ["T1"], "files": [{"file": "src/payment/stripe.ts", "line": 1, "action": "create"}], "key_point": "Wrap existing logic"},
{"id": "T3", "name": "Create GatewayFactory", "depends_on": ["T1"], "files": [{"file": "src/payment/factory.ts", "line": 1, "action": "create"}], "key_point": null},
{"id": "T4", "name": "Migrate processor to use factory", "depends_on": ["T2", "T3"], "files": [{"file": "src/payment/processor.ts", "line": 45, "action": "modify"}], "key_point": "Backward compatible"}
],
"execution_flow": "T1 → (T2 | T3) → T4",
"milestones": ["Interface defined", "Gateway implementations complete", "Migration done"]
},
"dependencies": {
"internal": ["@/lib/payment-gateway", "@/types/payment"],
"external": ["stripe@^14.0.0"]
},
"technical_concerns": ["Existing tests must pass", "No breaking API changes"],
"consensus": {
"agreements": ["Use strategy pattern", "Keep existing API"],
"resolved_conflicts": "Factory over DI for simpler integration"
},
"constraints": ["backward compatible", "no breaking changes to PaymentResult type"],
"task_description": "Refactor payment processing for multi-gateway support",
"session_id": "MCP-payment-refactor-2026-01-14"
}
```
**Step 2: Invoke Planning Agent**:
```javascript
Task({
subagent_type: "cli-lite-planning-agent",
run_in_background: false,
description: "Generate implementation plan",
prompt: `
## Schema Reference
Execute: cat ~/.ccw/workflows/cli-templates/schemas/plan-overview-base-schema.json
Execute: cat ~/.ccw/workflows/cli-templates/schemas/task-schema.json
## Output Format: Two-Layer Structure
- plan.json: Overview with task_ids[] referencing .task/ files (NO tasks[] array)
- .task/TASK-*.json: Independent task files following task-schema.json
plan.json required: summary, approach, task_ids, task_count, _metadata (with plan_type)
Task files required: id, title, description, depends_on, convergence (with criteria[])
Task fields: files[].change (not modification_points), convergence.criteria (not acceptance), test (not verification)
## Context-Package (from orchestrator)
${JSON.stringify(contextPackage, null, 2)}
## Execution Process
1. Read plan-overview-base-schema.json + task-schema.json for output structure
2. Read project-tech.json and project-guidelines.json
3. Parse context-package fields:
- solution: name, feasibility, summary
- implementation_plan: tasks[], execution_flow, milestones
- dependencies: internal[], external[]
- technical_concerns: risks/blockers
- consensus: agreements, resolved_conflicts
- constraints: user requirements
4. Use implementation_plan.tasks[] as task foundation
5. Preserve task dependencies (depends_on) and execution_flow
6. Expand tasks with convergence.criteria (testable completion conditions)
7. Create .task/ directory and write individual TASK-*.json files
8. Generate plan.json with task_ids[] referencing .task/ files
## Output
- ${sessionFolder}/plan.json (overview with task_ids[])
- ${sessionFolder}/.task/TASK-*.json (independent task files)
## Completion Checklist
- [ ] plan.json has task_ids[] and task_count (NO embedded tasks[])
- [ ] .task/*.json files preserve task dependencies from implementation_plan
- [ ] Task execution order follows execution_flow
- [ ] Key_points reflected in task descriptions
- [ ] User constraints applied to implementation
- [ ] convergence.criteria are testable
- [ ] plan.json follows plan-overview-base-schema.json
- [ ] Task files follow task-schema.json
`
})
```
**Step 3: Build executionContext**:
```javascript
// After plan.json is generated by cli-lite-planning-agent
const plan = JSON.parse(Read(`${sessionFolder}/plan.json`))
// Load task files from .task/ directory (two-layer format)
const taskFiles = plan.task_ids.map(id => `${sessionFolder}/.task/${id}.json`)
// Build executionContext (same structure as lite-plan)
executionContext = {
planObject: plan,
taskFiles: taskFiles, // Paths to .task/*.json files (two-layer format)
explorationsContext: null, // Multi-CLI doesn't use exploration files
explorationAngles: [], // No exploration angles
explorationManifest: null, // No manifest
clarificationContext: null, // Store user feedback from Phase 2 if exists
executionMethod: userSelection.execution_method, // From Phase 4
codeReviewTool: userSelection.code_review_tool, // From Phase 4
originalUserInput: taskDescription,
// Optional: Task-level executor assignments
executorAssignments: null, // Could be enhanced in future
session: {
id: sessionId,
folder: sessionFolder,
artifacts: {
explorations: [], // No explorations in multi-CLI workflow
explorations_manifest: null,
plan: `${sessionFolder}/plan.json`,
task_dir: plan.task_ids ? `${sessionFolder}/.task/` : null,
synthesis_rounds: Array.from({length: currentRound}, (_, i) =>
`${sessionFolder}/rounds/${i+1}/synthesis.json`
),
context_package: `${sessionFolder}/context-package.json`
}
}
}
```
**Step 4: Hand off to Execution**:
```javascript
// Execute to lite-execute with in-memory context
Skill(skill="workflow:lite-execute", args="--in-memory")
```
## Output File Structure
```
.workflow/.multi-cli-plan/{MCP-task-slug-YYYY-MM-DD}/
├── session-state.json # Session tracking (orchestrator)
├── rounds/
│ ├── 1/synthesis.json # Round 1 analysis (cli-discuss-agent)
│ ├── 2/synthesis.json # Round 2 analysis (cli-discuss-agent)
│ └── .../
├── context-package.json # Extracted context for planning (orchestrator)
├── plan.json # Plan overview with task_ids[] (NO embedded tasks[])
└── .task/ # Independent task files
├── TASK-001.json # Task file following task-schema.json
├── TASK-002.json
└── ...
```
**File Producers**:
| File | Producer | Content |
|------|----------|---------|
| `session-state.json` | Orchestrator | Session metadata, rounds, decisions |
| `rounds/*/synthesis.json` | cli-discuss-agent | Solutions, convergence, cross-verification |
| `context-package.json` | Orchestrator | Extracted solution, dependencies, consensus for planning |
| `plan.json` | cli-lite-planning-agent | Plan overview with task_ids[] referencing .task/ files |
| `.task/*.json` | cli-lite-planning-agent | Independent task files following task-schema.json |
## synthesis.json Schema
```json
{
"round": 1,
"solutions": [{
"name": "Solution Name",
"source_cli": ["gemini", "codex"],
"feasibility": 0.85,
"effort": "low|medium|high",
"risk": "low|medium|high",
"summary": "Brief analysis summary",
"implementation_plan": {
"approach": "High-level technical approach",
"tasks": [
{"id": "T1", "name": "Task", "depends_on": [], "files": [], "key_point": "..."}
],
"execution_flow": "T1 → T2 → T3",
"milestones": ["Checkpoint 1", "Checkpoint 2"]
},
"dependencies": {"internal": [], "external": []},
"technical_concerns": ["Risk 1", "Blocker 2"]
}],
"convergence": {
"score": 0.85,
"new_insights": false,
"recommendation": "converged|continue|user_input_needed"
},
"cross_verification": {
"agreements": [],
"disagreements": [],
"resolution": "..."
},
"clarification_questions": []
}
```
**Key Planning Fields**:
| Field | Purpose |
|-------|---------|
| `feasibility` | Viability score (0-1) |
| `implementation_plan.tasks[]` | Discrete tasks with dependencies |
| `implementation_plan.execution_flow` | Task sequence visualization |
| `implementation_plan.milestones` | Key checkpoints |
| `technical_concerns` | Risks and blockers |
**Note**: Solutions ranked by internal scoring (array order = priority)
## TodoWrite Structure
**Initialization**:
```javascript
TodoWrite({ todos: [
{ content: "Phase 1: Context Gathering", status: "in_progress", activeForm: "Gathering context" },
{ content: "Phase 2: Multi-CLI Discussion", status: "pending", activeForm: "Running discussion" },
{ content: "Phase 3: Present Options", status: "pending", activeForm: "Presenting options" },
{ content: "Phase 4: User Decision", status: "pending", activeForm: "Awaiting decision" },
{ content: "Phase 5: Plan Generation", status: "pending", activeForm: "Generating plan" }
]})
```
**During Discussion Rounds**:
```javascript
TodoWrite({ todos: [
{ content: "Phase 1: Context Gathering", status: "completed", activeForm: "Gathering context" },
{ content: "Phase 2: Multi-CLI Discussion", status: "in_progress", activeForm: "Running discussion" },
{ content: " → Round 1: Initial analysis", status: "completed", activeForm: "Analyzing" },
{ content: " → Round 2: Deep verification", status: "in_progress", activeForm: "Verifying" },
{ content: "Phase 3: Present Options", status: "pending", activeForm: "Presenting options" },
// ...
]})
```
## Error Handling
| Error | Resolution |
|-------|------------|
| ACE search fails | Fall back to Glob/Grep for file discovery |
| Agent fails | Retry once, then present partial results |
| CLI timeout (in agent) | Agent uses fallback: gemini → codex → claude |
| No convergence | Present best options, flag uncertainty |
| synthesis.json parse error | Request agent retry |
| User cancels | Save session for later resumption |
## Configuration
| Flag | Default | Description |
|------|---------|-------------|
| `--max-rounds` | 3 | Maximum discussion rounds |
| `--tools` | gemini,codex | CLI tools for analysis |
| `--mode` | parallel | Execution mode: parallel or serial |
| `--auto-execute` | false | Auto-execute after approval |
## Best Practices
1. **Be Specific**: Detailed task descriptions improve ACE context quality
2. **Provide Feedback**: Use clarification rounds to refine requirements
3. **Trust Cross-Verification**: Multi-CLI consensus indicates high confidence
4. **Review Trade-offs**: Consider pros/cons before selecting solution
5. **Check synthesis.json**: Review agent output for detailed analysis
6. **Iterate When Needed**: Don't hesitate to request more analysis
## Related Commands
```bash
# Simpler single-round planning
/workflow:lite-plan "task description"
# Issue-driven discovery
/issue:discover-by-prompt "find issues"
# View session files
cat .workflow/.multi-cli-plan/{session-id}/plan.json
cat .workflow/.multi-cli-plan/{session-id}/rounds/1/synthesis.json
cat .workflow/.multi-cli-plan/{session-id}/context-package.json
# Direct execution (if you have plan.json)
/workflow:lite-execute plan.json
```

View File

@@ -1,377 +0,0 @@
---
name: plan-verify
description: Perform READ-ONLY verification analysis between IMPL_PLAN.md, task JSONs, and brainstorming artifacts. Generates structured report with quality gate recommendation. Does NOT modify any files.
argument-hint: "[optional: --session session-id]"
allowed-tools: Read(*), Write(*), Glob(*), Bash(*)
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Goal
Generate a comprehensive verification report that identifies inconsistencies, duplications, ambiguities, and underspecified items between action planning artifacts (`IMPL_PLAN.md`, `task.json`) and brainstorming artifacts (`role analysis documents`). This command MUST run only after `/workflow:plan` has successfully produced complete `IMPL_PLAN.md` and task JSON files.
**Output**: A structured Markdown report saved to `.workflow/active/WFS-{session}/.process/PLAN_VERIFICATION.md` containing:
- Executive summary with quality gate recommendation
- Detailed findings by severity (CRITICAL/HIGH/MEDIUM/LOW)
- Requirements coverage analysis
- Dependency integrity check
- Synthesis alignment validation
- Actionable remediation recommendations
## Operating Constraints
**STRICTLY READ-ONLY FOR SOURCE ARTIFACTS**:
- **MUST NOT** modify `IMPL_PLAN.md`, any `task.json` files, or brainstorming artifacts
- **MUST NOT** create or delete task files
- **MUST ONLY** write the verification report to `.process/PLAN_VERIFICATION.md`
**Synthesis Authority**: The `role analysis documents` are **authoritative** for requirements and design decisions. Any conflicts between IMPL_PLAN/tasks and synthesis are automatically CRITICAL and require adjustment of the plan/tasks—not reinterpretation of requirements.
**Quality Gate Authority**: The verification report provides a binding recommendation (BLOCK_EXECUTION / PROCEED_WITH_FIXES / PROCEED_WITH_CAUTION / PROCEED) based on objective severity criteria. User MUST review critical/high issues before proceeding with implementation.
## Execution Steps
### 1. Initialize Analysis Context
```bash
# Detect active workflow session
IF --session parameter provided:
session_id = provided session
ELSE:
# Auto-detect active session
active_sessions = bash(find .workflow/active/ -name "WFS-*" -type d 2>/dev/null)
IF active_sessions is empty:
ERROR: "No active workflow session found. Use --session <session-id>"
EXIT
ELSE IF active_sessions has multiple entries:
# Use most recently modified session
session_id = bash(ls -td .workflow/active/WFS-*/ 2>/dev/null | head -1 | xargs basename)
ELSE:
session_id = basename(active_sessions[0])
# Derive absolute paths
session_dir = .workflow/active/WFS-{session}
brainstorm_dir = session_dir/.brainstorming
task_dir = session_dir/.task
process_dir = session_dir/.process
session_file = session_dir/workflow-session.json
# Create .process directory if not exists (report output location)
IF NOT EXISTS(process_dir):
bash(mkdir -p "{process_dir}")
# Validate required artifacts
# Note: "role analysis documents" refers to [role]/analysis.md files (e.g., product-manager/analysis.md)
SYNTHESIS_DIR = brainstorm_dir # Contains role analysis files: */analysis.md
IMPL_PLAN = session_dir/IMPL_PLAN.md
TASK_FILES = Glob(task_dir/*.json)
PLANNING_NOTES = session_dir/planning-notes.md # N+1 context and constraints
# Abort if missing - in order of dependency
SESSION_FILE_EXISTS = EXISTS(session_file)
IF NOT SESSION_FILE_EXISTS:
WARNING: "workflow-session.json not found. User intent alignment verification will be skipped."
# Continue execution - this is optional context, not blocking
PLANNING_NOTES_EXISTS = EXISTS(PLANNING_NOTES)
IF NOT PLANNING_NOTES_EXISTS:
WARNING: "planning-notes.md not found. Constraints/N+1 context verification will be skipped."
# Continue execution - optional context
SYNTHESIS_FILES = Glob(brainstorm_dir/*/analysis.md)
IF SYNTHESIS_FILES.count == 0:
ERROR: "No role analysis documents found in .brainstorming/*/analysis.md. Run /workflow:brainstorm:synthesis first"
EXIT
IF NOT EXISTS(IMPL_PLAN):
ERROR: "IMPL_PLAN.md not found. Run /workflow:plan first"
EXIT
IF TASK_FILES.count == 0:
ERROR: "No task JSON files found. Run /workflow:plan first"
EXIT
```
### 2. Load Artifacts (Progressive Disclosure)
Load only minimal necessary context from each artifact:
**From workflow-session.json** (OPTIONAL - Primary Reference for User Intent):
- **ONLY IF EXISTS**: Load user intent context
- Original user prompt/intent (project or description field)
- User's stated goals and objectives
- User's scope definition
- **IF MISSING**: Set user_intent_analysis = "SKIPPED: workflow-session.json not found"
**From planning-notes.md** (OPTIONAL - Constraints & N+1 Context):
- **ONLY IF EXISTS**: Load planning context
- Consolidated Constraints (numbered list from Phase 1-3)
- N+1 Context: Decisions table (Decision | Rationale | Revisit?)
- N+1 Context: Deferred items list
- **IF MISSING**: Set planning_notes_analysis = "SKIPPED: planning-notes.md not found"
**From role analysis documents** (AUTHORITATIVE SOURCE):
- Functional Requirements (IDs, descriptions, acceptance criteria)
- Non-Functional Requirements (IDs, targets)
- Business Requirements (IDs, success metrics)
- Key Architecture Decisions
- Risk factors and mitigation strategies
- Implementation Roadmap (high-level phases)
**From IMPL_PLAN.md**:
- Summary and objectives
- Context Analysis
- Implementation Strategy
- Task Breakdown Summary
- Success Criteria
- Brainstorming Artifacts References (if present)
**From task.json files**:
- Task IDs
- Titles and descriptions
- Status
- Dependencies (depends_on, blocks)
- Context (requirements, focus_paths, acceptance, artifacts)
- Flow control (pre_analysis, implementation_approach)
- Meta (complexity, priority)
### 3. Build Semantic Models
Create internal representations (do not include raw artifacts in output):
**Requirements inventory**:
- Each functional/non-functional/business requirement with stable ID
- Requirement text, acceptance criteria, priority
**Architecture decisions inventory**:
- ADRs from synthesis
- Technology choices
- Data model references
**Task coverage mapping**:
- Map each task to one or more requirements (by ID reference or keyword inference)
- Map each requirement to covering tasks
**Dependency graph**:
- Task-to-task dependencies (depends_on, blocks)
- Requirement-level dependencies (from synthesis)
### 4. Detection Passes (Agent-Driven Multi-Dimensional Analysis)
**Execution Strategy**:
- Single `cli-explore-agent` invocation
- Agent executes multiple CLI analyses internally (different dimensions: A-H)
- Token Budget: 50 findings maximum (aggregate remainder in overflow summary)
- Priority Allocation: CRITICAL (unlimited) → HIGH (15) → MEDIUM (20) → LOW (15)
- Early Exit: If CRITICAL findings > 0 in User Intent/Requirements Coverage, skip LOW/MEDIUM checks
**Execution Order** (Agent orchestrates internally):
1. **Tier 1 (CRITICAL Path)**: A, B, C, I - User intent, coverage, consistency, constraints compliance (full analysis)
2. **Tier 2 (HIGH Priority)**: D, E, J - Dependencies, synthesis alignment, N+1 context validation (limit 15 findings)
3. **Tier 3 (MEDIUM Priority)**: F - Specification quality (limit 20 findings)
4. **Tier 4 (LOW Priority)**: G, H - Duplication, feasibility (limit 15 findings)
---
#### Phase 4.1: Launch Unified Verification Agent
```javascript
Task(
subagent_type="cli-explore-agent",
run_in_background=false,
description="Multi-dimensional plan verification",
prompt=`
## Plan Verification Task
### MANDATORY FIRST STEPS
1. Read: ~/.ccw/workflows/cli-templates/schemas/plan-verify-agent-schema.json (dimensions & rules)
2. Read: ~/.ccw/workflows/cli-templates/schemas/verify-json-schema.json (output schema)
3. Read: ${session_file} (user intent)
4. Read: ${PLANNING_NOTES} (constraints & N+1 context)
5. Read: ${IMPL_PLAN} (implementation plan)
6. Glob: ${task_dir}/*.json (task files)
7. Glob: ${SYNTHESIS_DIR}/*/analysis.md (role analyses)
### Execution Flow
**Load schema → Execute tiered CLI analysis → Aggregate findings → Write JSON**
FOR each tier in [1, 2, 3, 4]:
- Load tier config from plan-verify-agent-schema.json
- Execute: ccw cli -p "PURPOSE: Verify dimensions {tier.dimensions}
TASK: {tier.checks from schema}
CONTEXT: @${session_dir}/**/*
EXPECTED: Findings JSON with dimension, severity, location, summary, recommendation
CONSTRAINTS: Limit {tier.limit} findings
" --tool gemini --mode analysis --rule {tier.rule}
- Parse findings, check early exit condition
- IF tier == 1 AND critical_count > 0: skip tier 3-4
### Output
Write: ${process_dir}/verification-findings.json (follow verify-json-schema.json)
Return: Quality gate decision + 2-3 sentence summary
`
)
```
---
#### Phase 4.2: Load and Organize Findings
```javascript
// Load findings (single parse for all subsequent use)
const data = JSON.parse(Read(`${process_dir}/verification-findings.json`))
const { session_id, timestamp, verification_tiers_completed, findings, summary } = data
const { critical_count, high_count, medium_count, low_count, total_findings, coverage_percentage, recommendation } = summary
// Group by severity and dimension
const bySeverity = Object.groupBy(findings, f => f.severity)
const byDimension = Object.groupBy(findings, f => f.dimension)
// Dimension metadata (from schema)
const DIMS = {
A: "User Intent Alignment", B: "Requirements Coverage", C: "Consistency Validation",
D: "Dependency Integrity", E: "Synthesis Alignment", F: "Task Specification Quality",
G: "Duplication Detection", H: "Feasibility Assessment",
I: "Constraints Compliance", J: "N+1 Context Validation"
}
```
### 5. Generate Report
```javascript
// Helper: render dimension section
const renderDimension = (dim) => {
const items = byDimension[dim] || []
return items.length > 0
? items.map(f => `### ${f.id}: ${f.summary}\n- **Severity**: ${f.severity}\n- **Location**: ${f.location.join(', ')}\n- **Recommendation**: ${f.recommendation}`).join('\n\n')
: `> ✅ No ${DIMS[dim]} issues detected.`
}
// Helper: render severity section
const renderSeverity = (severity, impact) => {
const items = bySeverity[severity] || []
return items.length > 0
? items.map(f => `#### ${f.id}: ${f.summary}\n- **Dimension**: ${f.dimension_name}\n- **Location**: ${f.location.join(', ')}\n- **Impact**: ${impact}\n- **Recommendation**: ${f.recommendation}`).join('\n\n')
: `> ✅ No ${severity.toLowerCase()}-severity issues detected.`
}
// Build Markdown report
const fullReport = `
# Plan Verification Report
**Session**: WFS-${session_id} | **Generated**: ${timestamp}
**Tiers Completed**: ${verification_tiers_completed.join(', ')}
---
## Executive Summary
| Metric | Value | Status |
|--------|-------|--------|
| Risk Level | ${critical_count > 0 ? 'CRITICAL' : high_count > 0 ? 'HIGH' : medium_count > 0 ? 'MEDIUM' : 'LOW'} | ${critical_count > 0 ? '🔴' : high_count > 0 ? '🟠' : medium_count > 0 ? '🟡' : '🟢'} |
| Critical/High/Medium/Low | ${critical_count}/${high_count}/${medium_count}/${low_count} | |
| Coverage | ${coverage_percentage}% | ${coverage_percentage >= 90 ? '🟢' : coverage_percentage >= 75 ? '🟡' : '🔴'} |
**Recommendation**: **${recommendation}**
---
## Findings Summary
| ID | Dimension | Severity | Location | Summary |
|----|-----------|----------|----------|---------|
${findings.map(f => `| ${f.id} | ${f.dimension_name} | ${f.severity} | ${f.location.join(', ')} | ${f.summary} |`).join('\n')}
---
## Analysis by Dimension
${['A','B','C','D','E','F','G','H','I','J'].map(d => `### ${d}. ${DIMS[d]}\n\n${renderDimension(d)}`).join('\n\n---\n\n')}
---
## Findings by Severity
### CRITICAL (${critical_count})
${renderSeverity('CRITICAL', 'Blocks execution')}
### HIGH (${high_count})
${renderSeverity('HIGH', 'Fix before execution recommended')}
### MEDIUM (${medium_count})
${renderSeverity('MEDIUM', 'Address during/after implementation')}
### LOW (${low_count})
${renderSeverity('LOW', 'Optional improvement')}
---
## Next Steps
${recommendation === 'BLOCK_EXECUTION' ? '🛑 **BLOCK**: Fix critical issues → Re-verify' :
recommendation === 'PROCEED_WITH_FIXES' ? '⚠️ **FIX RECOMMENDED**: Address high issues → Re-verify or Execute' :
'✅ **READY**: Proceed to /workflow:execute'}
Re-verify: \`/workflow:plan-verify --session ${session_id}\`
Execute: \`/workflow:execute --resume-session="${session_id}"\`
`
// Write report
Write(`${process_dir}/PLAN_VERIFICATION.md`, fullReport)
console.log(`✅ Report: ${process_dir}/PLAN_VERIFICATION.md\n📊 ${recommendation} | C:${critical_count} H:${high_count} M:${medium_count} L:${low_count} | Coverage:${coverage_percentage}%`)
```
### 6. Next Step Selection
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
const canExecute = recommendation !== 'BLOCK_EXECUTION'
// Auto mode
if (autoYes) {
if (canExecute) {
Skill(skill="workflow:execute", args="--yes --resume-session=\"${session_id}\"")
} else {
console.log(`[--yes] BLOCK_EXECUTION - Fix ${critical_count} critical issues first.`)
}
return
}
// Interactive mode - build options based on quality gate
const options = canExecute
? [
{ label: canExecute && recommendation === 'PROCEED_WITH_FIXES' ? "Execute Anyway" : "Execute (Recommended)",
description: "Proceed to /workflow:execute" },
{ label: "Review Report", description: "Review findings before deciding" },
{ label: "Re-verify", description: "Re-run after manual fixes" }
]
: [
{ label: "Review Report", description: "Review critical issues" },
{ label: "Re-verify", description: "Re-run after fixing issues" }
]
const selection = AskUserQuestion({
questions: [{
question: `Quality gate: ${recommendation}. Next step?`,
header: "Action",
multiSelect: false,
options
}]
})
// Handle selection
if (selection.includes("Execute")) {
Skill(skill="workflow:execute", args="--resume-session=\"${session_id}\"")
} else if (selection === "Re-verify") {
Skill(skill="workflow:plan-verify", args="--session ${session_id}")
}
```

View File

@@ -1,705 +0,0 @@
---
name: plan
description: 5-phase planning workflow with action-planning-agent task generation, outputs IMPL_PLAN.md and task JSONs
argument-hint: "[-y|--yes] \"text description\"|file.md"
allowed-tools: Skill(*), TodoWrite(*), Read(*), Bash(*)
group: workflow
---
## Auto Mode
When `--yes` or `-y`: Auto-continue all phases (skip confirmations), use recommended conflict resolutions.
# Workflow Plan Command (/workflow:plan)
## Coordinator Role
**This command is a pure orchestrator**: Execute 5 slash commands in sequence (including a quality gate), parse their outputs, pass context between them, and ensure complete execution through **automatic continuation**.
**Execution Model - Auto-Continue Workflow with Quality Gate**:
This workflow runs **fully autonomously** once triggered. Phase 3 (conflict resolution) and Phase 4 (task generation) are delegated to specialized agents.
1. **User triggers**: `/workflow:plan "task"`
2. **Phase 1 executes** → Session discovery → Auto-continues
3. **Phase 2 executes** → Context gathering → Auto-continues
4. **Phase 3 executes** (optional, if conflict_risk ≥ medium) → Conflict resolution → Auto-continues
5. **Phase 4 executes** → Task generation (task-generate-agent) → Reports final summary
**Task Attachment Model**:
- Skill execute **expands workflow** by attaching sub-tasks to current TodoWrite
- When a sub-command is executed (e.g., `/workflow:tools:context-gather`), its internal tasks are attached to the orchestrator's TodoWrite
- Orchestrator **executes these attached tasks** sequentially
- After completion, attached tasks are **collapsed** back to high-level phase summary
- This is **task expansion**, not external delegation
**Auto-Continue Mechanism**:
- TodoList tracks current phase status and dynamically manages task attachment/collapse
- When each phase finishes executing, automatically execute next pending phase
- All phases run autonomously without user interaction (clarification handled in brainstorm phase)
- Progress updates shown at each phase for visibility
- **⚠️ CONTINUOUS EXECUTION** - Do not stop until all phases complete
## Core Rules
1. **Start Immediately**: First action is TodoWrite initialization, second action is Phase 1 command execution
2. **No Preliminary Analysis**: Do not read files, analyze structure, or gather context before Phase 1
3. **Parse Every Output**: Extract required data from each command/agent output for next phase
4. **Auto-Continue via TodoList**: Check TodoList status to execute next pending phase automatically
5. **Track Progress**: Update TodoWrite dynamically with task attachment/collapse pattern
6. **Task Attachment Model**: Skill execute **attaches** sub-tasks to current workflow. Orchestrator **executes** these attached tasks itself, then **collapses** them after completion
7. **⚠️ CRITICAL: DO NOT STOP**: Continuous multi-phase workflow. After executing all attached tasks, immediately collapse them and execute next phase
## Execution Process
```
Input Parsing:
└─ Convert user input to structured format (GOAL/SCOPE/CONTEXT)
Phase 1: Session Discovery
└─ /workflow:session:start --auto "structured-description"
└─ Output: sessionId (WFS-xxx)
Phase 2: Context Gathering
└─ /workflow:tools:context-gather --session sessionId "structured-description"
├─ Tasks attached: Analyze structure → Identify integration → Generate package
└─ Output: contextPath + conflict_risk
Phase 3: Conflict Resolution
└─ Decision (conflict_risk check):
├─ conflict_risk ≥ medium → Execute /workflow:tools:conflict-resolution
│ ├─ Tasks attached: Detect conflicts → Present to user → Apply strategies
│ └─ Output: Modified brainstorm artifacts
└─ conflict_risk < medium → Skip to Phase 4
Phase 4: Task Generation
└─ /workflow:tools:task-generate-agent --session sessionId
└─ Output: IMPL_PLAN.md, task JSONs, TODO_LIST.md
Return:
└─ Summary with recommended next steps
```
## 5-Phase Execution
### Phase 1: Session Discovery
**Step 1.1: Execute** - Create or discover workflow session
```javascript
Skill(skill="workflow:session:start", args="--auto \"[structured-task-description]\"")
```
**Task Description Structure**:
```
GOAL: [Clear, concise objective]
SCOPE: [What's included/excluded]
CONTEXT: [Relevant background or constraints]
```
**Example**:
```
GOAL: Build JWT-based authentication system
SCOPE: User registration, login, token validation
CONTEXT: Existing user database schema, REST API endpoints
```
**Parse Output**:
- Extract: `SESSION_ID: WFS-[id]` (store as `sessionId`)
**Validation**:
- Session ID successfully extracted
- Session directory `.workflow/active/[sessionId]/` exists
**Note**: Session directory contains `workflow-session.json` (metadata). Do NOT look for `manifest.json` here - it only exists in `.workflow/archives/` for archived sessions.
**TodoWrite**: Mark phase 1 completed, phase 2 in_progress
**After Phase 1**: Initialize planning-notes.md with user intent
```javascript
// Create planning notes document with N+1 context support
const planningNotesPath = `.workflow/active/${sessionId}/planning-notes.md`
const userGoal = structuredDescription.goal
const userConstraints = structuredDescription.context || "None specified"
Write(planningNotesPath, `# Planning Notes
**Session**: ${sessionId}
**Created**: ${new Date().toISOString()}
## User Intent (Phase 1)
- **GOAL**: ${userGoal}
- **KEY_CONSTRAINTS**: ${userConstraints}
---
## Context Findings (Phase 2)
(To be filled by context-gather)
## Conflict Decisions (Phase 3)
(To be filled if conflicts detected)
## Consolidated Constraints (Phase 4 Input)
1. ${userConstraints}
---
## Task Generation (Phase 4)
(To be filled by action-planning-agent)
## N+1 Context
### Decisions
| Decision | Rationale | Revisit? |
|----------|-----------|----------|
### Deferred
- [ ] (For N+1)
`)
```
Return to user showing Phase 1 results, then auto-continue to Phase 2
---
### Phase 2: Context Gathering
**Step 2.1: Execute** - Gather project context and analyze codebase
```javascript
Skill(skill="workflow:tools:context-gather", args="--session [sessionId] \"[structured-task-description]\"")
```
**Use Same Structured Description**: Pass the same structured format from Phase 1
**Input**: `sessionId` from Phase 1
**Parse Output**:
- Extract: context-package.json path (store as `contextPath`)
- Typical pattern: `.workflow/active/[sessionId]/.process/context-package.json`
**Validation**:
- Context package path extracted
- File exists and is valid JSON
- `prioritized_context` field exists
<!-- TodoWrite: When context-gather executed, INSERT 3 context-gather tasks, mark first as in_progress -->
**TodoWrite Update (Phase 2 Skill executed - tasks attached)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "in_progress", "activeForm": "Executing context gathering"},
{"content": " → Analyze codebase structure", "status": "in_progress", "activeForm": "Analyzing codebase structure"},
{"content": " → Identify integration points", "status": "pending", "activeForm": "Identifying integration points"},
{"content": " → Generate context package", "status": "pending", "activeForm": "Generating context package"},
{"content": "Phase 4: Task Generation", "status": "pending", "activeForm": "Executing task generation"}
]
```
**Note**: Skill execute **attaches** context-gather's 3 tasks. Orchestrator **executes** these tasks sequentially.
<!-- TodoWrite: After Phase 2 tasks complete, REMOVE Phase 2.1-2.3, restore to orchestrator view -->
**TodoWrite Update (Phase 2 completed - tasks collapsed)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
{"content": "Phase 4: Task Generation", "status": "pending", "activeForm": "Executing task generation"}
]
```
**Note**: Phase 2 tasks completed and collapsed to summary.
**After Phase 2**: Update planning-notes.md with context findings, then auto-continue
```javascript
// Read context-package to extract key findings
const contextPackage = JSON.parse(Read(contextPath))
const conflictRisk = contextPackage.conflict_detection?.risk_level || 'low'
const criticalFiles = (contextPackage.exploration_results?.aggregated_insights?.critical_files || [])
.slice(0, 5).map(f => f.path)
const archPatterns = contextPackage.project_context?.architecture_patterns || []
const constraints = contextPackage.exploration_results?.aggregated_insights?.constraints || []
// Append Phase 2 findings to planning-notes.md
Edit(planningNotesPath, {
old: '## Context Findings (Phase 2)\n(To be filled by context-gather)',
new: `## Context Findings (Phase 2)
- **CRITICAL_FILES**: ${criticalFiles.join(', ') || 'None identified'}
- **ARCHITECTURE**: ${archPatterns.join(', ') || 'Not detected'}
- **CONFLICT_RISK**: ${conflictRisk}
- **CONSTRAINTS**: ${constraints.length > 0 ? constraints.join('; ') : 'None'}`
})
// Append Phase 2 constraints to consolidated list
Edit(planningNotesPath, {
old: '## Consolidated Constraints (Phase 4 Input)',
new: `## Consolidated Constraints (Phase 4 Input)
${constraints.map((c, i) => `${i + 2}. [Context] ${c}`).join('\n')}`
})
```
Return to user showing Phase 2 results, then auto-continue to Phase 3/4 (depending on conflict_risk)
---
### Phase 3: Conflict Resolution
**Trigger**: Only execute when context-package.json indicates conflict_risk is "medium" or "high"
**Step 3.1: Execute** - Detect and resolve conflicts with CLI analysis
```javascript
Skill(skill="workflow:tools:conflict-resolution", args="--session [sessionId] --context [contextPath]")
```
**Input**:
- sessionId from Phase 1
- contextPath from Phase 2
- conflict_risk from context-package.json
**Parse Output**:
- Extract: Execution status (success/skipped/failed)
- Verify: conflict-resolution.json file path (if executed)
**Validation**:
- File `.workflow/active/[sessionId]/.process/conflict-resolution.json` exists (if executed)
**Skip Behavior**:
- If conflict_risk is "none" or "low", skip directly to Phase 3.5
- Display: "No significant conflicts detected, proceeding to clarification"
<!-- TodoWrite: If conflict_risk ≥ medium, INSERT 3 conflict-resolution tasks -->
**TodoWrite Update (Phase 3 Skill executed - tasks attached, if conflict_risk ≥ medium)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
{"content": "Phase 3: Conflict Resolution", "status": "in_progress", "activeForm": "Resolving conflicts"},
{"content": " → Detect conflicts with CLI analysis", "status": "in_progress", "activeForm": "Detecting conflicts"},
{"content": " → Present conflicts to user", "status": "pending", "activeForm": "Presenting conflicts"},
{"content": " → Apply resolution strategies", "status": "pending", "activeForm": "Applying resolution strategies"},
{"content": "Phase 4: Task Generation", "status": "pending", "activeForm": "Executing task generation"}
]
```
**Note**: Skill execute **attaches** conflict-resolution's 3 tasks. Orchestrator **executes** these tasks sequentially.
<!-- TodoWrite: After Phase 3 tasks complete, REMOVE Phase 3.1-3.3, restore to orchestrator view -->
**TodoWrite Update (Phase 3 completed - tasks collapsed)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
{"content": "Phase 3: Conflict Resolution", "status": "completed", "activeForm": "Resolving conflicts"},
{"content": "Phase 4: Task Generation", "status": "pending", "activeForm": "Executing task generation"}
]
```
**Note**: Phase 3 tasks completed and collapsed to summary.
**After Phase 3**: Update planning-notes.md with conflict decisions (if executed), then auto-continue
```javascript
// If Phase 3 was executed, update planning-notes.md
if (conflictRisk >= 'medium') {
const conflictResPath = `.workflow/active/${sessionId}/.process/conflict-resolution.json`
if (fs.existsSync(conflictResPath)) {
const conflictRes = JSON.parse(Read(conflictResPath))
const resolved = conflictRes.resolved_conflicts || []
const modifiedArtifacts = conflictRes.modified_artifacts || []
const planningConstraints = conflictRes.planning_constraints || []
// Update Phase 3 section
Edit(planningNotesPath, {
old: '## Conflict Decisions (Phase 3)\n(To be filled if conflicts detected)',
new: `## Conflict Decisions (Phase 3)
- **RESOLVED**: ${resolved.map(r => `${r.type}${r.strategy}`).join('; ') || 'None'}
- **MODIFIED_ARTIFACTS**: ${modifiedArtifacts.join(', ') || 'None'}
- **CONSTRAINTS**: ${planningConstraints.join('; ') || 'None'}`
})
// Append Phase 3 constraints to consolidated list
if (planningConstraints.length > 0) {
const currentNotes = Read(planningNotesPath)
const constraintCount = (currentNotes.match(/^\d+\./gm) || []).length
Edit(planningNotesPath, {
old: '## Consolidated Constraints (Phase 4 Input)',
new: `## Consolidated Constraints (Phase 4 Input)
${planningConstraints.map((c, i) => `${constraintCount + i + 1}. [Conflict] ${c}`).join('\n')}`
})
}
}
}
```
Return to user showing conflict resolution results (if executed) and selected strategies, then auto-continue to Phase 3.5
**Memory State Check**:
- Evaluate current context window usage and memory state
- If memory usage is high (>120K tokens or approaching context limits):
**Step 3.2: Execute** - Optimize memory before proceeding
```javascript
Skill(skill="compact")
```
- Memory compaction is particularly important after analysis phase which may generate extensive documentation
- Ensures optimal performance and prevents context overflow
---
### Phase 3.5: Pre-Task Generation Validation (Optional Quality Gate)
**Purpose**: Optional quality gate before task generation - primarily handled by brainstorm synthesis phase
**Current Behavior**: Auto-skip to Phase 4 (Task Generation)
**Future Enhancement**: Could add additional validation steps like:
- Cross-reference checks between conflict resolution and brainstorm analyses
- Final sanity checks before task generation
- User confirmation prompt for proceeding
**TodoWrite**: Mark phase 3.5 completed (auto-skip), phase 4 in_progress
**After Phase 3.5**: Auto-continue to Phase 4 immediately
---
### Phase 4: Task Generation
**Relationship with Brainstorm Phase**:
- If brainstorm role analyses exist ([role]/analysis.md files), Phase 3 analysis incorporates them as input
- **User's original intent is ALWAYS primary**: New or refined user goals override brainstorm recommendations
- **Role analysis.md files define "WHAT"**: Requirements, design specs, role-specific insights
- **IMPL_PLAN.md defines "HOW"**: Executable task breakdown, dependencies, implementation sequence
- Task generation translates high-level role analyses into concrete, actionable work items
- **Intent priority**: Current user prompt > role analysis.md files > guidance-specification.md
**Step 4.1: Execute** - Generate implementation plan and task JSONs
```javascript
Skill(skill="workflow:tools:task-generate-agent", args="--session [sessionId]")
```
**CLI Execution Note**: CLI tool usage is now determined semantically by action-planning-agent based on user's task description. If user specifies "use Codex/Gemini/Qwen for X", CLI tool usage is controlled by `meta.execution_config.method` per task, not by `command` fields in implementation steps.
**Input**:
- `sessionId` from Phase 1
- **planning-notes.md**: Consolidated constraints from all phases (Phase 1-3)
- Path: `.workflow/active/[sessionId]/planning-notes.md`
- Contains: User intent, context findings, conflict decisions, consolidated constraints
- **Purpose**: Provides structured, minimal context summary to action-planning-agent
**Validation**:
- `.workflow/active/[sessionId]/plan.json` exists (structured plan overview)
- `.workflow/active/[sessionId]/IMPL_PLAN.md` exists
- `.workflow/active/[sessionId]/.task/IMPL-*.json` exists (at least one)
- `.workflow/active/[sessionId]/TODO_LIST.md` exists
<!-- TodoWrite: When task-generate-agent executed, ATTACH 1 agent task -->
**TodoWrite Update (Phase 4 Skill executed - agent task attached)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
{"content": "Phase 4: Task Generation", "status": "in_progress", "activeForm": "Executing task generation"}
]
```
**Note**: Single agent task attached. Agent autonomously completes discovery, planning, and output generation internally.
<!-- TodoWrite: After agent completes, mark task as completed -->
**TodoWrite Update (Phase 4 completed)**:
```json
[
{"content": "Phase 1: Session Discovery", "status": "completed", "activeForm": "Executing session discovery"},
{"content": "Phase 2: Context Gathering", "status": "completed", "activeForm": "Executing context gathering"},
{"content": "Phase 4: Task Generation", "status": "completed", "activeForm": "Executing task generation"}
]
```
**Note**: Agent task completed. No collapse needed (single task).
**Step 4.2: User Decision** - Choose next action
After Phase 4 completes, present user with action choices:
```javascript
console.log(`
✅ Planning complete for session: ${sessionId}
📊 Tasks generated: ${taskCount}
📋 Plan: .workflow/active/${sessionId}/IMPL_PLAN.md
`);
// Ask user for next action
const userChoice = AskUserQuestion({
questions: [{
question: "Planning complete. What would you like to do next?",
header: "Next Action",
multiSelect: false,
options: [
{
label: "Verify Plan Quality (Recommended)",
description: "Run quality verification to catch issues before execution. Checks plan structure, task dependencies, and completeness."
},
{
label: "Start Execution",
description: "Begin implementing tasks immediately. Use this if you've already reviewed the plan or want to start quickly."
},
{
label: "Review Status Only",
description: "View task breakdown and session status without taking further action. You can decide what to do next manually."
}
]
}]
});
// Execute based on user choice
if (userChoice.answers["Next Action"] === "Verify Plan Quality (Recommended)") {
console.log("\n🔍 Starting plan verification...\n");
Skill(skill="workflow:plan-verify", args="--session " + sessionId);
} else if (userChoice.answers["Next Action"] === "Start Execution") {
console.log("\n🚀 Starting task execution...\n");
Skill(skill="workflow:execute", args="--session " + sessionId);
} else if (userChoice.answers["Next Action"] === "Review Status Only") {
console.log("\n📊 Displaying session status...\n");
Skill(skill="workflow:status", args="--session " + sessionId);
}
```
**Return to User**: Based on user's choice, execute the corresponding workflow command.
## TodoWrite Pattern
**Core Concept**: Dynamic task attachment and collapse for real-time visibility into workflow execution.
### Key Principles
1. **Task Attachment** (when Skill executed):
- Sub-command's internal tasks are **attached** to orchestrator's TodoWrite
- **Phase 2, 3**: Multiple sub-tasks attached (e.g., Phase 2.1, 2.2, 2.3)
- **Phase 4**: Single agent task attached (e.g., "Execute task-generate-agent")
- First attached task marked as `in_progress`, others as `pending`
- Orchestrator **executes** these attached tasks sequentially
2. **Task Collapse** (after sub-tasks complete):
- **Applies to Phase 2, 3**: Remove detailed sub-tasks from TodoWrite
- **Collapse** to high-level phase summary
- Example: Phase 2.1-2.3 collapse to "Execute context gathering: completed"
- **Phase 4**: No collapse needed (single task, just mark completed)
- Maintains clean orchestrator-level view
3. **Continuous Execution**:
- After completion, automatically proceed to next pending phase
- No user intervention required between phases
- TodoWrite dynamically reflects current execution state
**Lifecycle Summary**: Initial pending tasks → Phase executed (tasks ATTACHED) → Sub-tasks executed sequentially → Phase completed (tasks COLLAPSED to summary for Phase 2/3, or marked completed for Phase 4) → Next phase begins → Repeat until all phases complete.
**Note**: See individual Phase descriptions for detailed TodoWrite Update examples:
- **Phase 2, 3**: Multiple sub-tasks with attach/collapse pattern
- **Phase 4**: Single agent task (no collapse needed)
## Input Processing
**Convert User Input to Structured Format**:
1. **Simple Text** → Structure it:
```
User: "Build authentication system"
Structured:
GOAL: Build authentication system
SCOPE: Core authentication features
CONTEXT: New implementation
```
2. **Detailed Text** → Extract components:
```
User: "Add JWT authentication with email/password login and token refresh"
Structured:
GOAL: Implement JWT-based authentication
SCOPE: Email/password login, token generation, token refresh endpoints
CONTEXT: JWT token-based security, refresh token rotation
```
3. **File Reference** (e.g., `requirements.md`) → Read and structure:
- Read file content
- Extract goal, scope, requirements
- Format into structured description
## Data Flow
```
User Input (task description)
[Convert to Structured Format]
↓ Structured Description:
↓ GOAL: [objective]
↓ SCOPE: [boundaries]
↓ CONTEXT: [background]
Phase 1: session:start --auto "structured-description"
↓ Output: sessionId
↓ Write: planning-notes.md (User Intent section)
Phase 2: context-gather --session sessionId "structured-description"
↓ Input: sessionId + structured description
↓ Output: contextPath (context-package.json with prioritized_context) + conflict_risk
↓ Update: planning-notes.md (Context Findings + Consolidated Constraints)
Phase 3: conflict-resolution [AUTO-TRIGGERED if conflict_risk ≥ medium]
↓ Input: sessionId + contextPath + conflict_risk
↓ Output: Modified brainstorm artifacts
↓ Update: planning-notes.md (Conflict Decisions + Consolidated Constraints)
↓ Skip if conflict_risk is none/low → proceed directly to Phase 4
Phase 4: task-generate-agent --session sessionId
↓ Input: sessionId + planning-notes.md + context-package.json + brainstorm artifacts
↓ Output: IMPL_PLAN.md, task JSONs, TODO_LIST.md
Return summary to user
```
**Session Memory Flow**: Each phase receives session ID, which provides access to:
- Previous task summaries
- Existing context and analysis
- Brainstorming artifacts (potentially modified by Phase 3)
- Session-specific configuration
## Execution Flow Diagram
```
User triggers: /workflow:plan "Build authentication system"
[TodoWrite Init] 3 orchestrator-level tasks
Phase 1: Session Discovery
→ sessionId extracted
Phase 2: Context Gathering (Skill executed)
→ ATTACH 3 sub-tasks: ← ATTACHED
- → Analyze codebase structure
- → Identify integration points
- → Generate context package
→ Execute sub-tasks sequentially
→ COLLAPSE tasks ← COLLAPSED
→ contextPath + conflict_risk extracted
Conditional Branch: Check conflict_risk
├─ IF conflict_risk ≥ medium:
│ Phase 3: Conflict Resolution (Skill executed)
│ → ATTACH 3 sub-tasks: ← ATTACHED
│ - → Detect conflicts with CLI analysis
│ - → Present conflicts to user
│ - → Apply resolution strategies
│ → Execute sub-tasks sequentially
│ → COLLAPSE tasks ← COLLAPSED
└─ ELSE: Skip Phase 3, proceed to Phase 4
Phase 4: Task Generation (Skill executed)
→ Single agent task (no sub-tasks)
→ Agent autonomously completes internally:
(discovery → planning → output)
→ Outputs: IMPL_PLAN.md, IMPL-*.json, TODO_LIST.md
Return summary to user
```
**Key Points**:
- **← ATTACHED**: Tasks attached to TodoWrite when Skill executed
- Phase 2, 3: Multiple sub-tasks
- Phase 4: Single agent task
- **← COLLAPSED**: Sub-tasks collapsed to summary after completion (Phase 2, 3 only)
- **Phase 4**: Single agent task, no collapse (just mark completed)
- **Conditional Branch**: Phase 3 only executes if conflict_risk ≥ medium
- **Continuous Flow**: No user intervention between phases
## Error Handling
- **Parsing Failure**: If output parsing fails, retry command once, then report error
- **Validation Failure**: If validation fails, report which file/data is missing
- **Command Failure**: Keep phase `in_progress`, report error to user, do not proceed to next phase
## Coordinator Checklist
- **Pre-Phase**: Convert user input to structured format (GOAL/SCOPE/CONTEXT)
- Initialize TodoWrite before any command (Phase 3 added dynamically after Phase 2)
- Execute Phase 1 immediately with structured description
- Parse session ID from Phase 1 output, store in memory
- Pass session ID and structured description to Phase 2 command
- Parse context path from Phase 2 output, store in memory
- **Extract conflict_risk from context-package.json**: Determine Phase 3 execution
- **If conflict_risk ≥ medium**: Launch Phase 3 conflict-resolution with sessionId and contextPath
- Wait for Phase 3 to finish executing (if executed), verify conflict-resolution.json created
- **If conflict_risk is none/low**: Skip Phase 3, proceed directly to Phase 4
- **Build Phase 4 command**: `/workflow:tools:task-generate-agent --session [sessionId]`
- Pass session ID to Phase 4 command
- Verify all Phase 4 outputs
- Update TodoWrite after each phase (dynamically adjust for Phase 3 presence)
- After each phase, automatically continue to next phase based on TodoList status
## Structure Template Reference
**Minimal Structure**:
```
GOAL: [What to achieve]
SCOPE: [What's included]
CONTEXT: [Relevant info]
```
**Detailed Structure** (optional, when more context available):
```
GOAL: [Primary objective]
SCOPE: [Included features/components]
CONTEXT: [Existing system, constraints, dependencies]
REQUIREMENTS: [Specific technical requirements]
CONSTRAINTS: [Limitations or boundaries]
```
**Usage in Commands**:
```bash
# Phase 1
/workflow:session:start --auto "GOAL: Build authentication\nSCOPE: JWT, login, registration\nCONTEXT: REST API"
# Phase 2
/workflow:tools:context-gather --session WFS-123 "GOAL: Build authentication\nSCOPE: JWT, login, registration\nCONTEXT: REST API"
```
## Related Commands
**Prerequisite Commands**:
- `/workflow:brainstorm:artifacts` - Optional: Generate role-based analyses before planning (if complex requirements need multiple perspectives)
- `/workflow:brainstorm:synthesis` - Optional: Refine brainstorm analyses with clarifications
**Called by This Command** (5 phases):
- `/workflow:session:start` - Phase 1: Create or discover workflow session
- `/workflow:tools:context-gather` - Phase 2: Gather project context and analyze codebase
- `/workflow:tools:conflict-resolution` - Phase 3: Detect and resolve conflicts (auto-triggered if conflict_risk ≥ medium)
- `/compact` - Phase 3: Memory optimization (if context approaching limits)
- `/workflow:tools:task-generate-agent` - Phase 4: Generate task JSON files with agent-driven approach
**Follow-up Commands**:
- `/workflow:plan-verify` - Recommended: Verify plan quality and catch issues before execution
- `/workflow:status` - Review task breakdown and current progress
- `/workflow:execute` - Begin implementation of generated tasks

View File

@@ -1,648 +0,0 @@
---
name: replan
description: Interactive workflow replanning with session-level artifact updates and boundary clarification through guided questioning
argument-hint: "[-y|--yes] [--session session-id] [task-id] \"requirements\"|file.md [--interactive]"
allowed-tools: Read(*), Write(*), Edit(*), TodoWrite(*), Glob(*), Bash(*)
---
# Workflow Replan Command
## Overview
Intelligently replans workflow sessions or individual tasks with interactive boundary clarification and comprehensive artifact updates.
**Core Capabilities**:
- **Session Replan**: Updates multiple artifacts (IMPL_PLAN.md, TODO_LIST.md, task JSONs)
- **Task Replan**: Focused updates within session context
- **Interactive Clarification**: Guided questioning to define modification boundaries
- **Impact Analysis**: Automatic detection of affected files and dependencies
- **Backup Management**: Preserves previous versions with restore capability
## Operation Modes
### Session Replan Mode
```bash
# Auto-detect active session
/workflow:replan "添加双因素认证支持"
# Explicit session
/workflow:replan --session WFS-oauth "添加双因素认证支持"
# File-based input
/workflow:replan --session WFS-oauth requirements-update.md
# Interactive mode
/workflow:replan --interactive
```
### Task Replan Mode
```bash
# Direct task update
/workflow:replan IMPL-1 "修改为使用 OAuth2.0 标准"
# Task with explicit session
/workflow:replan --session WFS-oauth IMPL-2 "增加单元测试覆盖率到 90%"
# Interactive mode
/workflow:replan IMPL-1 --interactive
```
## Execution Process
```
Input Parsing:
├─ Parse flags: --session, --interactive
└─ Detect mode: task-id present → Task mode | Otherwise → Session mode
Phase 1: Mode Detection & Session Discovery
├─ Detect operation mode (Task vs Session)
├─ Discover/validate session (--session flag or auto-detect)
└─ Load session context (workflow-session.json, IMPL_PLAN.md, TODO_LIST.md)
Phase 2: Interactive Requirement Clarification
└─ Decision (by mode):
├─ Session mode → 3-4 questions (scope, modules, changes, dependencies)
└─ Task mode → 2 questions (update type, ripple effect)
Phase 3: Impact Analysis & Planning
├─ Analyze required changes
├─ Generate modification plan
└─ User confirmation (Execute / Adjust / Cancel)
Phase 4: Backup Creation
└─ Backup all affected files with manifest
Phase 5: Apply Modifications
├─ Update IMPL_PLAN.md (if needed)
├─ Update TODO_LIST.md (if needed)
├─ Update/Create/Delete task JSONs
└─ Update session metadata
Phase 6: Verification & Summary
├─ Validate consistency (JSON validity, task limits, acyclic dependencies)
└─ Generate change summary
```
## Execution Lifecycle
### Input Parsing
**Parse flags**:
```javascript
const sessionFlag = $ARGUMENTS.match(/--session\s+(\S+)/)?.[1]
const interactive = $ARGUMENTS.includes('--interactive')
const taskIdMatch = $ARGUMENTS.match(/\b(IMPL-\d+(?:\.\d+)?)\b/)
const taskId = taskIdMatch?.[1]
```
### Phase 1: Mode Detection & Session Discovery
**Process**:
1. **Detect Operation Mode**:
- Check if task ID provided (IMPL-N or IMPL-N.M format) → Task mode
- Otherwise → Session mode
2. **Discover/Validate Session**:
- Use `--session` flag if provided
- Otherwise auto-detect from `.workflow/active/`
- Validate session exists
3. **Load Session Context**:
- Read `workflow-session.json`
- List existing tasks
- Read `IMPL_PLAN.md` and `TODO_LIST.md`
4. **Parse Execution Intent** (from requirements text):
```javascript
// Dynamic tool detection from cli-tools.json
// Read enabled tools: ["gemini", "qwen", "codex", ...]
const enabledTools = loadEnabledToolsFromConfig(); // See ~/.claude/cli-tools.json
// Build dynamic patterns from enabled tools
function buildExecPatterns(tools) {
const patterns = {
agent: /改为\s*Agent\s*执行|使用\s*Agent\s*执行/i
};
tools.forEach(tool => {
// Pattern: "使用 {tool} 执行" or "改用 {tool}"
patterns[`cli_${tool}`] = new RegExp(
`使用\\s*(${tool})\\s*执行|改用\\s*(${tool})`, 'i'
);
});
return patterns;
}
const execPatterns = buildExecPatterns(enabledTools);
let executionIntent = null
for (const [key, pattern] of Object.entries(execPatterns)) {
if (pattern.test(requirements)) {
executionIntent = key.startsWith('cli_')
? { method: 'cli', cli_tool: key.replace('cli_', '') }
: { method: 'agent', cli_tool: null }
break
}
}
```
**Output**: Session validated, context loaded, mode determined, **executionIntent parsed**
---
### Auto Mode Support
When `--yes` or `-y` flag is used, the command skips interactive clarification and uses safe defaults:
```javascript
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
```
**Auto Mode Defaults**:
- **Modification Scope**: `tasks_only` (safest - only update task details)
- **Affected Modules**: All modules related to the task
- **Task Changes**: `update_only` (no structural changes)
- **Dependency Changes**: `no` (preserve existing dependencies)
- **User Confirmation**: Auto-confirm execution
**Note**: `--interactive` flag overrides `--yes` flag (forces interactive mode).
---
### Phase 2: Interactive Requirement Clarification
**Purpose**: Define modification scope through guided questioning
**Auto Mode Check**:
```javascript
if (autoYes && !interactive) {
// Use defaults and skip to Phase 3
console.log(`[--yes] Using safe defaults for replan:`)
console.log(` - Scope: tasks_only`)
console.log(` - Changes: update_only`)
console.log(` - Dependencies: preserve existing`)
userSelections = {
scope: 'tasks_only',
modules: 'all_affected',
task_changes: 'update_only',
dependency_changes: false
}
// Proceed to Phase 3
}
```
#### Session Mode Questions
**Q1: Modification Scope**
```javascript
Options:
- 仅更新任务细节 (tasks_only)
- 修改规划方案 (plan_update)
- 重构任务结构 (task_restructure)
- 全面重规划 (comprehensive)
```
**Q2: Affected Modules** (if scope >= plan_update)
```javascript
Options: Dynamically generated from existing tasks' focus_paths
- 认证模块 (src/auth)
- 用户管理 (src/user)
- 全部模块
```
**Q3: Task Changes** (if scope >= task_restructure)
```javascript
Options:
- 添加/删除任务 (add_remove)
- 合并/拆分任务 (merge_split)
- 仅更新内容 (update_only)
// Note: Max 4 options for AskUserQuestion
```
**Q4: Dependency Changes**
```javascript
Options:
- 是,需要重新梳理依赖
- 否,保持现有依赖
```
#### Task Mode Questions
**Q1: Update Type**
```javascript
Options:
- 需求和验收标准 (requirements & acceptance)
- 实现方案 (implementation_approach)
- 文件范围 (focus_paths)
- 依赖关系 (depends_on)
- 全部更新
```
**Q2: Ripple Effect**
```javascript
Options:
- 是,需要同步更新依赖任务
- 否,仅影响当前任务
- 不确定,请帮我分析
```
**Output**: User selections stored, modification boundaries defined
---
### Phase 3: Impact Analysis & Planning
**Step 3.1: Analyze Required Changes**
Determine affected files based on clarification:
```typescript
interface ImpactAnalysis {
affected_files: {
impl_plan: boolean;
todo_list: boolean;
session_meta: boolean;
tasks: string[];
};
operations: {
type: 'create' | 'update' | 'delete' | 'merge' | 'split';
target: string;
reason: string;
}[];
backup_strategy: {
timestamp: string;
files: string[];
};
}
```
**Step 3.2: Generate Modification Plan**
```markdown
## 修改计划
### 影响范围
- [ ] IMPL_PLAN.md: 更新技术方案第 3 节
- [ ] TODO_LIST.md: 添加 2 个新任务,删除 1 个废弃任务
- [ ] IMPL-001.json: 更新实现方案
- [ ] workflow-session.json: 更新任务计数
### 变更操作
1. **创建**: IMPL-004.json (双因素认证实现)
2. **更新**: IMPL-001.json (添加 2FA 准备工作)
3. **删除**: IMPL-003.json (已被新方案替代)
```
**Step 3.3: User Confirmation**
```javascript
// Parse --yes flag
const autoYes = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
if (autoYes) {
// Auto mode: Auto-confirm execution
console.log(`[--yes] Auto-confirming replan execution`)
userConfirmation = '确认执行'
// Proceed to Phase 4
} else {
// Interactive mode: Ask user
AskUserQuestion({
questions: [{
question: "修改计划已生成,请确认操作:",
header: "Confirm",
options: [
{ label: "确认执行", description: "开始应用所有修改" },
{ label: "调整计划", description: "重新回答问题调整范围" },
{ label: "取消操作", description: "放弃本次重规划" }
],
multiSelect: false
}]
})
}
```
**Output**: Modification plan confirmed or adjusted
---
### Phase 4: Backup Creation
**Process**:
1. **Create Backup Directory**:
```bash
timestamp=$(date -u +"%Y-%m-%dT%H-%M-%S")
backup_dir=".workflow/active/$SESSION_ID/.process/backup/replan-$timestamp"
mkdir -p "$backup_dir"
```
2. **Backup All Affected Files**:
- IMPL_PLAN.md
- TODO_LIST.md
- workflow-session.json
- Affected task JSONs
3. **Create Backup Manifest**:
```markdown
# Replan Backup Manifest
**Timestamp**: {timestamp}
**Reason**: {replan_reason}
**Scope**: {modification_scope}
## Restoration Command
cp {backup_dir}/* .workflow/active/{session}/
```
**Output**: All files safely backed up with manifest
---
### Phase 5: Apply Modifications
**Step 5.1: Update IMPL_PLAN.md** (if needed)
Use Edit tool to modify specific sections:
- Update affected technical sections
- Update modification date
**Step 5.2: Update TODO_LIST.md** (if needed)
- Add new tasks with `[ ]` checkbox
- Mark deleted tasks as `[x] ~~task~~ (已废弃)`
- Update modified task descriptions
**Step 5.3: Update Task JSONs**
For each affected task:
```typescript
const updated_task = {
...task,
context: {
...task.context,
requirements: [...updated_requirements],
acceptance: [...updated_acceptance]
},
flow_control: {
...task.flow_control,
implementation_approach: [...updated_steps]
},
// Update execution config if intent detected
...(executionIntent && {
meta: {
...task.meta,
execution_config: {
method: executionIntent.method,
cli_tool: executionIntent.cli_tool,
enable_resume: executionIntent.method !== 'agent'
}
}
})
};
Write({
file_path: `.workflow/active/${SESSION_ID}/.task/${task_id}.json`,
content: JSON.stringify(updated_task, null, 2)
});
```
**Note**: Implementation approach steps are NO LONGER modified. CLI execution is controlled by task-level `meta.execution_config` only.
**Step 5.4: Create New Tasks** (if needed)
Generate complete task JSON with all required fields:
- id, title, status
- meta (type, agent)
- context (requirements, focus_paths, acceptance)
- flow_control (pre_analysis, implementation_approach, target_files)
**Step 5.5: Delete Obsolete Tasks** (if needed)
Move to backup instead of hard delete:
```bash
mv ".workflow/active/$SESSION_ID/.task/{task-id}.json" "$backup_dir/"
```
**Step 5.6: Update Session Metadata**
Update workflow-session.json:
- progress.current_tasks
- progress.last_replan
- replan_history array
**Output**: All modifications applied, artifacts updated
---
### Phase 6: Verification & Summary
**Step 6.1: Verify Consistency**
1. Validate all task JSONs are valid JSON
2. Check task count within limits (max 10)
3. Verify dependency graph is acyclic
**Step 6.2: Generate Change Summary**
```markdown
## 重规划完成
### 会话信息
- **Session**: {session-id}
- **时间**: {timestamp}
- **备份**: {backup-path}
### 变更摘要
**范围**: {scope}
**原因**: {reason}
### 修改的文件
- ✓ IMPL_PLAN.md: {changes}
- ✓ TODO_LIST.md: {changes}
- ✓ Task JSONs: {count} files updated
### 任务变更
- **新增**: {task-ids}
- **删除**: {task-ids}
- **更新**: {task-ids}
### 回滚方法
cp {backup-path}/* .workflow/active/{session}/
```
**Output**: Summary displayed, replan complete
---
## TodoWrite Progress Tracking
### Session Mode Progress
```json
[
{"content": "检测模式和发现会话", "status": "completed", "activeForm": "检测模式和发现会话"},
{"content": "交互式需求明确", "status": "completed", "activeForm": "交互式需求明确"},
{"content": "影响分析和计划生成", "status": "completed", "activeForm": "影响分析和计划生成"},
{"content": "创建备份", "status": "completed", "activeForm": "创建备份"},
{"content": "更新会话产出文件", "status": "completed", "activeForm": "更新会话产出文件"},
{"content": "验证一致性", "status": "completed", "activeForm": "验证一致性"}
]
```
### Task Mode Progress
```json
[
{"content": "检测会话和加载任务", "status": "completed", "activeForm": "检测会话和加载任务"},
{"content": "交互式更新确认", "status": "completed", "activeForm": "交互式更新确认"},
{"content": "应用任务修改", "status": "completed", "activeForm": "应用任务修改"}
]
```
## Error Handling
### Session Errors
```bash
# No active session found
ERROR: No active session found
Run /workflow:session:start to create a session
# Session not found
ERROR: Session WFS-invalid not found
Available sessions: [list]
# No changes specified
WARNING: No modifications specified
Use --interactive mode or provide requirements
```
### Task Errors
```bash
# Task not found
ERROR: Task IMPL-999 not found in session
Available tasks: [list]
# Task completed
WARNING: Task IMPL-001 is completed
Consider creating new task for additional work
# Circular dependency
ERROR: Circular dependency detected
Resolve dependency conflicts before proceeding
```
### Validation Errors
```bash
# Task limit exceeded
ERROR: Replan would create 12 tasks (limit: 10)
Consider: combining tasks, splitting sessions, or removing tasks
# Invalid JSON
ERROR: Generated invalid JSON
Backup preserved, rolling back changes
```
## File Structure
```
.workflow/active/WFS-session-name/
├── workflow-session.json
├── IMPL_PLAN.md
├── TODO_LIST.md
├── .task/
│ ├── IMPL-001.json
│ ├── IMPL-002.json
│ └── IMPL-003.json
└── .process/
├── context-package.json
└── backup/
└── replan-{timestamp}/
├── MANIFEST.md
├── IMPL_PLAN.md
├── TODO_LIST.md
├── workflow-session.json
└── IMPL-*.json
```
## Examples
### Session Replan - Add Feature
```bash
/workflow:replan "添加双因素认证支持"
# Interactive clarification
Q: 修改范围?
A: 全面重规划
Q: 受影响模块?
A: 认证模块, API接口
Q: 任务变更?
A: 添加新任务, 更新内容
# Execution
✓ 创建备份
✓ 更新 IMPL_PLAN.md
✓ 更新 TODO_LIST.md
✓ 创建 IMPL-004.json
✓ 更新 IMPL-001.json, IMPL-002.json
重规划完成! 新增 1 任务,更新 2 任务
```
### Task Replan - Update Requirements
```bash
/workflow:replan IMPL-001 "支持 OAuth2.0 标准"
# Interactive clarification
Q: 更新部分?
A: 需求和验收标准, 实现方案
Q: 影响其他任务?
A: 是,需要同步更新依赖任务
# Execution
✓ 创建备份
✓ 更新 IMPL-001.json
✓ 更新 IMPL-002.json (依赖任务)
任务重规划完成! 更新 2 个任务
```
### Task Replan - Change Execution Method
```bash
/workflow:replan IMPL-001 "改用 Codex 执行"
# Semantic parsing detects executionIntent:
# { method: 'cli', cli_tool: 'codex' }
# Execution (no interactive questions needed)
✓ 创建备份
✓ 更新 IMPL-001.json
- meta.execution_config = { method: 'cli', cli_tool: 'codex', enable_resume: true }
任务执行方式已更新: Agent → CLI (codex)
```
```bash
/workflow:replan IMPL-002 "改为 Agent 执行"
# Semantic parsing detects executionIntent:
# { method: 'agent', cli_tool: null }
# Execution
✓ 创建备份
✓ 更新 IMPL-002.json
- meta.execution_config = { method: 'agent', cli_tool: null }
任务执行方式已更新: CLI → Agent
```

View File

@@ -81,6 +81,7 @@ const navGroupDefinitions: NavGroupDef[] = [
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
{ path: '/teams', labelKey: 'navigation.main.teams', icon: Users },
{ path: '/terminal-dashboard', labelKey: 'navigation.main.terminalDashboard', icon: Terminal },
],
},
{

View File

@@ -0,0 +1,129 @@
// ========================================
// AgentList Component
// ========================================
// Compact list of active orchestration plans from orchestratorStore.
// Shows plan name, current step progress, and status badge.
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Bot, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useOrchestratorStore, selectActivePlans } from '@/stores';
import { Badge } from '@/components/ui/Badge';
import type { OrchestrationRunState } from '@/stores/orchestratorStore';
import type { OrchestrationStatus } from '@/types/orchestrator';
// ========== Status Badge Config ==========
const STATUS_CONFIG: Record<
OrchestrationStatus,
{ variant: 'default' | 'info' | 'success' | 'destructive' | 'secondary' | 'warning'; messageId: string }
> = {
running: { variant: 'info', messageId: 'terminalDashboard.agentList.statusRunning' },
completed: { variant: 'success', messageId: 'terminalDashboard.agentList.statusCompleted' },
failed: { variant: 'destructive', messageId: 'terminalDashboard.agentList.statusFailed' },
paused: { variant: 'warning', messageId: 'terminalDashboard.agentList.statusPaused' },
pending: { variant: 'secondary', messageId: 'terminalDashboard.agentList.statusPending' },
cancelled: { variant: 'secondary', messageId: 'terminalDashboard.agentList.statusPending' },
};
// ========== AgentListItem ==========
function AgentListItem({
runState,
}: {
runState: OrchestrationRunState;
}) {
const { formatMessage } = useIntl();
const { plan, status, stepStatuses } = runState;
const totalSteps = plan.steps.length;
const completedSteps = useMemo(
() =>
Object.values(stepStatuses).filter(
(s) => s.status === 'completed' || s.status === 'skipped'
).length,
[stepStatuses]
);
const config = STATUS_CONFIG[status] ?? STATUS_CONFIG.pending;
const isRunning = status === 'running';
return (
<div
className={cn(
'flex items-center gap-2 px-3 py-2',
'border-b border-border/30 last:border-b-0',
'hover:bg-muted/30 transition-colors'
)}
>
<div className="shrink-0">
{isRunning ? (
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />
) : (
<Bot className="w-3.5 h-3.5 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium truncate">{plan.name}</p>
<p className="text-[10px] text-muted-foreground">
{formatMessage(
{ id: 'terminalDashboard.agentList.stepLabel' },
{ current: completedSteps, total: totalSteps }
)}
</p>
</div>
<Badge variant={config.variant} className="text-[10px] px-1.5 py-0 shrink-0">
{formatMessage({ id: config.messageId })}
</Badge>
</div>
);
}
// ========== AgentList Component ==========
export function AgentList() {
const { formatMessage } = useIntl();
const activePlans = useOrchestratorStore(selectActivePlans);
const planEntries = useMemo(
() => Object.entries(activePlans),
[activePlans]
);
return (
<div className="flex flex-col">
{/* Section header */}
<div className="flex items-center gap-2 px-3 py-2 border-t border-border shrink-0">
<Bot className="w-4 h-4 text-muted-foreground" />
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{formatMessage({ id: 'terminalDashboard.agentList.title' })}
</h3>
{planEntries.length > 0 && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 ml-auto">
{planEntries.length}
</Badge>
)}
</div>
{/* Plan list or empty state */}
{planEntries.length === 0 ? (
<div className="flex items-center justify-center py-4 px-3">
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.agentList.noAgents' })}
</p>
</div>
) : (
<div className="overflow-y-auto max-h-[200px]">
{planEntries.map(([planId, runState]) => (
<AgentListItem key={planId} runState={runState} />
))}
</div>
)}
</div>
);
}
export default AgentList;

View File

@@ -0,0 +1,90 @@
// ========================================
// AssociationHighlight Context
// ========================================
// React context provider for cross-panel association chain highlighting.
// Provides ephemeral UI state for linked-chain highlights shared across
// left/middle/right panels. The highlighted chain indicates which
// Issue, QueueItem, and Session are visually linked.
//
// Design rationale: React context chosen over Zustand store because
// highlight state is ephemeral UI state that does not need persistence
// or cross-page sharing.
import {
createContext,
useContext,
useState,
useCallback,
useMemo,
type ReactNode,
} from 'react';
import type { AssociationChain } from '@/types/terminal-dashboard';
// ========== Context Type ==========
interface AssociationHighlightContextType {
/** Currently highlighted association chain, or null if nothing is highlighted */
chain: AssociationChain | null;
/** Set the highlighted chain (pass null to clear) */
setChain: (chain: AssociationChain | null) => void;
/** Check if a specific entity is part of the current highlighted chain */
isHighlighted: (entityId: string, entityType: 'issue' | 'queue' | 'session') => boolean;
}
// ========== Context ==========
const AssociationHighlightContext = createContext<AssociationHighlightContextType | null>(null);
// ========== Provider ==========
export function AssociationHighlightProvider({ children }: { children: ReactNode }) {
const [chain, setChainState] = useState<AssociationChain | null>(null);
const setChain = useCallback((nextChain: AssociationChain | null) => {
setChainState(nextChain);
}, []);
const isHighlighted = useCallback(
(entityId: string, entityType: 'issue' | 'queue' | 'session'): boolean => {
if (!chain) return false;
switch (entityType) {
case 'issue':
return chain.issueId === entityId;
case 'queue':
return chain.queueItemId === entityId;
case 'session':
return chain.sessionId === entityId;
default:
return false;
}
},
[chain]
);
const value = useMemo<AssociationHighlightContextType>(
() => ({ chain, setChain, isHighlighted }),
[chain, setChain, isHighlighted]
);
return (
<AssociationHighlightContext.Provider value={value}>
{children}
</AssociationHighlightContext.Provider>
);
}
// ========== Consumer Hook ==========
/**
* Hook to access the association highlight context.
* Must be used within an AssociationHighlightProvider.
*/
export function useAssociationHighlight(): AssociationHighlightContextType {
const ctx = useContext(AssociationHighlightContext);
if (!ctx) {
throw new Error(
'useAssociationHighlight must be used within an AssociationHighlightProvider'
);
}
return ctx;
}

View File

@@ -0,0 +1,216 @@
// ========================================
// BottomInspector Component
// ========================================
// Collapsible bottom panel showing the full association chain
// (Issue -> Queue -> Session) for the currently selected entity.
// Consumes issueQueueIntegrationStore for association chain data
// and useAssociationHighlight context for the highlighted chain.
import { useState, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import {
ChevronDown,
ChevronUp,
Info,
AlertCircle,
ListChecks,
Terminal,
ArrowRight,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
useIssueQueueIntegrationStore,
selectAssociationChain,
} from '@/stores/issueQueueIntegrationStore';
import { useQueueExecutionStore } from '@/stores/queueExecutionStore';
import { useCliSessionStore } from '@/stores/cliSessionStore';
import { useAssociationHighlight } from './AssociationHighlight';
// ========== Chain Node ==========
function ChainNode({
icon: Icon,
label,
entityId,
status,
timestamp,
isLast = false,
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
entityId: string | null;
status?: string;
timestamp?: string;
isLast?: boolean;
}) {
if (!entityId) {
return (
<div className="flex items-center gap-2 opacity-40">
<Icon className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-xs text-muted-foreground italic">--</span>
{!isLast && <ArrowRight className="w-3 h-3 text-muted-foreground mx-1" />}
</div>
);
}
return (
<div className="flex items-center gap-2">
<Icon className="w-3.5 h-3.5 text-foreground" />
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-xs font-mono font-semibold text-foreground px-1.5 py-0.5 rounded bg-muted">
{entityId}
</span>
{status && (
<span className="text-[10px] text-muted-foreground px-1 py-0.5 rounded border border-border">
{status}
</span>
)}
{timestamp && (
<span className="text-[10px] text-muted-foreground">
{formatTimestamp(timestamp)}
</span>
)}
{!isLast && <ArrowRight className="w-3 h-3 text-muted-foreground mx-1" />}
</div>
);
}
/** Format ISO timestamp to short readable form */
function formatTimestamp(ts: string): string {
try {
const date = new Date(ts);
return date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
} catch {
return ts;
}
}
// ========== Main Component ==========
export function BottomInspector() {
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(false);
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
const { chain: highlightedChain } = useAssociationHighlight();
// Use highlighted chain from context, fall back to store association chain
const activeChain = highlightedChain ?? associationChain;
const toggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
// Resolve additional details from stores
const chainDetails = useMemo(() => {
if (!activeChain) return null;
const executions = Object.values(useQueueExecutionStore.getState().executions);
const sessions = useCliSessionStore.getState().sessions;
// Find matching execution for queue status
let queueStatus: string | undefined;
let executionTimestamp: string | undefined;
if (activeChain.queueItemId) {
const exec = executions.find((e) => e.queueItemId === activeChain.queueItemId);
if (exec) {
queueStatus = exec.status;
executionTimestamp = exec.startedAt;
}
}
// Find session metadata
let sessionStatus: string | undefined;
let sessionTimestamp: string | undefined;
if (activeChain.sessionId) {
const session = sessions[activeChain.sessionId];
if (session) {
sessionStatus = 'active';
sessionTimestamp = session.createdAt;
}
}
return {
queueStatus,
executionTimestamp,
sessionStatus,
sessionTimestamp,
};
}, [activeChain]);
const hasChain = activeChain !== null;
return (
<div className={cn('border-t border-border bg-muted/30 shrink-0 transition-all duration-200')}>
{/* Toggle button */}
<button
onClick={toggle}
className="flex items-center gap-2 w-full px-4 py-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Info className="w-4 h-4" />
<span className="font-medium">
{formatMessage({ id: 'terminalDashboard.inspector.title' })}
</span>
{hasChain && (
<span className="ml-1 w-2 h-2 rounded-full bg-primary shrink-0" />
)}
{isOpen ? (
<ChevronDown className="w-4 h-4 ml-auto" />
) : (
<ChevronUp className="w-4 h-4 ml-auto" />
)}
</button>
{/* Collapsible content */}
<div
className={cn(
'overflow-hidden transition-all duration-200',
isOpen ? 'max-h-40 opacity-100' : 'max-h-0 opacity-0'
)}
>
<div className="px-4 pb-3">
{hasChain ? (
<div className="space-y-2">
{/* Chain label */}
<p className="text-xs font-medium text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.inspector.associationChain' })}
</p>
{/* Chain visualization: Issue -> Queue -> Session */}
<div className="flex items-center gap-1 flex-wrap">
<ChainNode
icon={AlertCircle}
label="Issue"
entityId={activeChain.issueId}
/>
<ChainNode
icon={ListChecks}
label="Queue"
entityId={activeChain.queueItemId}
status={chainDetails?.queueStatus}
timestamp={chainDetails?.executionTimestamp}
/>
<ChainNode
icon={Terminal}
label="Session"
entityId={activeChain.sessionId}
status={chainDetails?.sessionStatus}
timestamp={chainDetails?.sessionTimestamp}
isLast
/>
</div>
</div>
) : (
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.inspector.noSelection' })}
</p>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,138 @@
// ========================================
// GlobalKpiBar Component
// ========================================
// Top bar showing 3 KPI metrics spanning the full page width.
// Metrics:
// 1. Active Sessions - count from sessionManagerStore (wraps cliSessionStore)
// 2. Queue Size - pending/ready items count from useIssueQueue React Query hook
// 3. Alert Count - total alerts from all terminalMetas
//
// Per design spec (V-001): consumes sessionManagerStore, NOT cliSessionStore directly.
import { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Activity, ListChecks, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
useSessionManagerStore,
selectGroups,
selectTerminalMetas,
} from '@/stores/sessionManagerStore';
import { useIssueQueue } from '@/hooks/useIssues';
import type { TerminalStatus } from '@/types/terminal-dashboard';
// ========== KPI Item ==========
function KpiItem({
icon: Icon,
label,
value,
variant = 'default',
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: number;
variant?: 'default' | 'primary' | 'warning' | 'destructive';
}) {
const variantStyles = {
default: 'text-muted-foreground',
primary: 'text-primary',
warning: 'text-warning',
destructive: 'text-destructive',
};
return (
<div className="flex items-center gap-2">
<Icon className={cn('w-4 h-4', variantStyles[variant])} />
<span className="text-xs text-muted-foreground">{label}</span>
<span className={cn('text-sm font-semibold tabular-nums', variantStyles[variant])}>
{value}
</span>
</div>
);
}
// ========== Main Component ==========
export function GlobalKpiBar() {
const { formatMessage } = useIntl();
const groups = useSessionManagerStore(selectGroups);
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
const queueQuery = useIssueQueue();
// Derive active session count from sessionManagerStore groups
const sessionCount = useMemo(() => {
const allSessionIds = groups.flatMap((g) => g.sessionIds);
// Count sessions that have 'active' status in terminalMetas
let activeCount = 0;
for (const sid of allSessionIds) {
const meta = terminalMetas[sid];
const status: TerminalStatus = meta?.status ?? 'idle';
if (status === 'active') {
activeCount++;
}
}
// If no sessions are managed in groups, return total unique session IDs
// This ensures the KPI shows meaningful data even before grouping
return activeCount > 0 ? activeCount : allSessionIds.length;
}, [groups, terminalMetas]);
// Derive queue pending count from useIssueQueue data
const queuePendingCount = useMemo(() => {
const queue = queueQuery.data;
if (!queue) return 0;
// Count all items across grouped_items
let count = 0;
if (queue.grouped_items) {
for (const items of Object.values(queue.grouped_items)) {
count += items.length;
}
}
// Also count ungrouped tasks and solutions
if (queue.tasks) count += queue.tasks.length;
if (queue.solutions) count += queue.solutions.length;
return count;
}, [queueQuery.data]);
// Derive total alert count from all terminalMetas
const totalAlerts = useMemo(() => {
let count = 0;
for (const meta of Object.values(terminalMetas)) {
count += meta.alertCount;
}
return count;
}, [terminalMetas]);
return (
<div className="flex items-center gap-6 px-4 py-2 border-b border-border bg-muted/30 shrink-0">
<KpiItem
icon={Activity}
label={formatMessage({ id: 'terminalDashboard.kpi.activeSessions' })}
value={sessionCount}
variant="primary"
/>
<div className="w-px h-4 bg-border" />
<KpiItem
icon={ListChecks}
label={formatMessage({ id: 'terminalDashboard.kpi.queueSize' })}
value={queuePendingCount}
variant={queuePendingCount > 0 ? 'warning' : 'default'}
/>
<div className="w-px h-4 bg-border" />
<KpiItem
icon={AlertTriangle}
label={formatMessage({ id: 'terminalDashboard.kpi.alertCount' })}
value={totalAlerts}
variant={totalAlerts > 0 ? 'destructive' : 'default'}
/>
<span className="text-xs text-muted-foreground ml-auto">
{formatMessage({ id: 'terminalDashboard.page.title' })}
</span>
</div>
);
}

View File

@@ -0,0 +1,289 @@
// ========================================
// IssuePanel Component
// ========================================
// Issue list panel for the terminal dashboard middle column.
// Consumes existing useIssues() React Query hook for data fetching.
// Integrates with issueQueueIntegrationStore for selection state
// and association chain highlighting.
import { useMemo, useCallback } from 'react';
import { useIntl } from 'react-intl';
import {
AlertCircle,
ArrowRightToLine,
Loader2,
AlertTriangle,
CircleDot,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import { useIssues } from '@/hooks/useIssues';
import {
useIssueQueueIntegrationStore,
selectSelectedIssueId,
selectAssociationChain,
} from '@/stores/issueQueueIntegrationStore';
import type { Issue } from '@/lib/api';
// ========== Priority Badge ==========
const PRIORITY_STYLES: Record<Issue['priority'], { variant: 'destructive' | 'warning' | 'info' | 'secondary'; label: string }> = {
critical: { variant: 'destructive', label: 'Critical' },
high: { variant: 'warning', label: 'High' },
medium: { variant: 'info', label: 'Medium' },
low: { variant: 'secondary', label: 'Low' },
};
function PriorityBadge({ priority }: { priority: Issue['priority'] }) {
const style = PRIORITY_STYLES[priority] ?? PRIORITY_STYLES.medium;
return (
<Badge variant={style.variant} className="text-[10px] px-1.5 py-0 shrink-0">
{style.label}
</Badge>
);
}
// ========== Status Indicator ==========
function StatusDot({ status }: { status: Issue['status'] }) {
const colorMap: Record<Issue['status'], string> = {
open: 'text-info',
in_progress: 'text-warning',
resolved: 'text-success',
closed: 'text-muted-foreground',
completed: 'text-success',
};
return <CircleDot className={cn('w-3 h-3 shrink-0', colorMap[status] ?? 'text-muted-foreground')} />;
}
// ========== Issue Item ==========
function IssueItem({
issue,
isSelected,
isHighlighted,
onSelect,
onSendToQueue,
}: {
issue: Issue;
isSelected: boolean;
isHighlighted: boolean;
onSelect: () => void;
onSendToQueue: () => void;
}) {
const { formatMessage } = useIntl();
const handleSendToQueue = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
onSendToQueue();
},
[onSendToQueue]
);
return (
<button
type="button"
className={cn(
'w-full text-left px-3 py-2 rounded-md transition-colors',
'hover:bg-muted/60 focus:outline-none focus:ring-1 focus:ring-primary/30',
isSelected && 'bg-primary/10 ring-1 ring-primary/30',
isHighlighted && !isSelected && 'bg-accent/50'
)}
onClick={onSelect}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<StatusDot status={issue.status} />
<span className="text-sm font-medium text-foreground truncate">
{issue.title}
</span>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<PriorityBadge priority={issue.priority} />
<button
type="button"
className={cn(
'p-1 rounded hover:bg-primary/20 transition-colors',
'text-muted-foreground hover:text-primary',
'focus:outline-none focus:ring-1 focus:ring-primary/30'
)}
onClick={handleSendToQueue}
title={formatMessage({ id: 'terminalDashboard.issuePanel.sendToQueue' })}
>
<ArrowRightToLine className="w-3.5 h-3.5" />
</button>
</div>
</div>
{issue.context && (
<p className="mt-0.5 text-xs text-muted-foreground truncate pl-5">
{issue.context}
</p>
)}
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground pl-5">
<span className="font-mono">{issue.id}</span>
{issue.labels && issue.labels.length > 0 && (
<>
<span className="text-border">|</span>
<span className="truncate">{issue.labels.slice(0, 2).join(', ')}</span>
</>
)}
</div>
</button>
);
}
// ========== Empty State ==========
function IssueEmptyState() {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground p-4">
<div className="text-center">
<AlertCircle className="h-10 w-10 mx-auto mb-3 opacity-40" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.issuePanel.noIssues' })}</p>
<p className="text-xs mt-1 opacity-70">
{formatMessage({ id: 'terminalDashboard.issuePanel.noIssuesDesc' })}
</p>
</div>
</div>
);
}
// ========== Error State ==========
function IssueErrorState({ error }: { error: Error }) {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-destructive p-4">
<div className="text-center">
<AlertTriangle className="h-10 w-10 mx-auto mb-3 opacity-60" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.issuePanel.error' })}</p>
<p className="text-xs mt-1 opacity-70">{error.message}</p>
</div>
</div>
);
}
// ========== Main Component ==========
export function IssuePanel() {
const { formatMessage } = useIntl();
const { issues, isLoading, error, openCount } = useIssues();
const selectedIssueId = useIssueQueueIntegrationStore(selectSelectedIssueId);
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
const setSelectedIssue = useIssueQueueIntegrationStore((s) => s.setSelectedIssue);
const buildAssociationChain = useIssueQueueIntegrationStore((s) => s.buildAssociationChain);
// Sort: open/in_progress first, then by priority (critical > high > medium > low)
const sortedIssues = useMemo(() => {
const priorityOrder: Record<string, number> = {
critical: 0,
high: 1,
medium: 2,
low: 3,
};
const statusOrder: Record<string, number> = {
in_progress: 0,
open: 1,
resolved: 2,
completed: 3,
closed: 4,
};
return [...issues].sort((a, b) => {
const sa = statusOrder[a.status] ?? 5;
const sb = statusOrder[b.status] ?? 5;
if (sa !== sb) return sa - sb;
const pa = priorityOrder[a.priority] ?? 3;
const pb = priorityOrder[b.priority] ?? 3;
return pa - pb;
});
}, [issues]);
const handleSelect = useCallback(
(issueId: string) => {
if (selectedIssueId === issueId) {
setSelectedIssue(null);
} else {
buildAssociationChain(issueId, 'issue');
}
},
[selectedIssueId, setSelectedIssue, buildAssociationChain]
);
const handleSendToQueue = useCallback(
(issueId: string) => {
// Select the issue and build chain - queue creation is handled elsewhere
buildAssociationChain(issueId, 'issue');
},
[buildAssociationChain]
);
// Loading state
if (isLoading) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.issuePanel.title' })}
</h3>
</div>
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.issuePanel.title' })}
</h3>
</div>
<IssueErrorState error={error} />
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="px-3 py-2 border-b border-border shrink-0 flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.issuePanel.title' })}
</h3>
{openCount > 0 && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{openCount}
</Badge>
)}
</div>
{/* Issue List */}
{sortedIssues.length === 0 ? (
<IssueEmptyState />
) : (
<div className="flex-1 min-h-0 overflow-y-auto p-1.5 space-y-0.5">
{sortedIssues.map((issue) => (
<IssueItem
key={issue.id}
issue={issue}
isSelected={selectedIssueId === issue.id}
isHighlighted={associationChain?.issueId === issue.id}
onSelect={() => handleSelect(issue.id)}
onSendToQueue={() => handleSendToQueue(issue.id)}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,264 @@
// ========================================
// QueuePanel Component
// ========================================
// Queue list panel for the terminal dashboard middle column.
// Consumes existing useIssueQueue() React Query hook for queue data
// and bridges queueExecutionStore for execution status per item.
// Integrates with issueQueueIntegrationStore for association chain
// highlighting and selection state.
import { useMemo, useCallback } from 'react';
import { useIntl } from 'react-intl';
import {
ListChecks,
Loader2,
AlertTriangle,
ArrowDownToLine,
Clock,
CheckCircle,
XCircle,
Zap,
Ban,
Terminal,
} from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { cn } from '@/lib/utils';
import { useIssueQueue } from '@/hooks/useIssues';
import {
useIssueQueueIntegrationStore,
selectAssociationChain,
} from '@/stores/issueQueueIntegrationStore';
import {
useQueueExecutionStore,
selectByQueueItem,
} from '@/stores/queueExecutionStore';
import type { QueueItem } from '@/lib/api';
// ========== Status Config ==========
type QueueItemStatus = QueueItem['status'];
const STATUS_CONFIG: Record<QueueItemStatus, {
variant: 'info' | 'success' | 'destructive' | 'secondary' | 'warning' | 'outline';
icon: typeof Clock;
label: string;
}> = {
pending: { variant: 'secondary', icon: Clock, label: 'Pending' },
ready: { variant: 'info', icon: Zap, label: 'Ready' },
executing: { variant: 'warning', icon: Loader2, label: 'Executing' },
completed: { variant: 'success', icon: CheckCircle, label: 'Completed' },
failed: { variant: 'destructive', icon: XCircle, label: 'Failed' },
blocked: { variant: 'outline', icon: Ban, label: 'Blocked' },
};
// ========== Queue Item Row ==========
function QueueItemRow({
item,
isHighlighted,
onSelect,
}: {
item: QueueItem;
isHighlighted: boolean;
onSelect: () => void;
}) {
const { formatMessage } = useIntl();
const config = STATUS_CONFIG[item.status] ?? STATUS_CONFIG.pending;
const StatusIcon = config.icon;
// Bridge to queueExecutionStore for execution status
const executions = useQueueExecutionStore(selectByQueueItem(item.item_id));
const activeExec = executions.find((e) => e.status === 'running') ?? executions[0];
return (
<button
type="button"
className={cn(
'w-full text-left px-3 py-2 rounded-md transition-colors',
'hover:bg-muted/60 focus:outline-none focus:ring-1 focus:ring-primary/30',
isHighlighted && 'bg-accent/50 ring-1 ring-accent/30'
)}
onClick={onSelect}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<StatusIcon
className={cn(
'w-3.5 h-3.5 shrink-0',
item.status === 'executing' && 'animate-spin'
)}
/>
<span className="text-sm font-medium text-foreground truncate font-mono">
{item.item_id}
</span>
</div>
<Badge variant={config.variant} className="text-[10px] px-1.5 py-0 shrink-0">
{formatMessage({ id: `terminalDashboard.queuePanel.status.${item.status}` })}
</Badge>
</div>
<div className="mt-1 flex items-center gap-2 text-[10px] text-muted-foreground pl-5">
<span className="font-mono">{item.issue_id}</span>
<span className="text-border">|</span>
<span>
{formatMessage(
{ id: 'terminalDashboard.queuePanel.order' },
{ order: item.execution_order }
)}
</span>
<span className="text-border">|</span>
<span>{item.execution_group}</span>
{activeExec?.sessionKey && (
<>
<span className="text-border">|</span>
<span className="flex items-center gap-0.5">
<Terminal className="w-3 h-3" />
{activeExec.sessionKey}
</span>
</>
)}
</div>
{item.depends_on.length > 0 && (
<div className="mt-0.5 text-[10px] text-muted-foreground/70 pl-5 truncate">
{formatMessage(
{ id: 'terminalDashboard.queuePanel.dependsOn' },
{ deps: item.depends_on.join(', ') }
)}
</div>
)}
</button>
);
}
// ========== Empty State ==========
function QueueEmptyState() {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground p-4">
<div className="text-center">
<ListChecks className="h-10 w-10 mx-auto mb-3 opacity-40" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.noItems' })}</p>
<p className="text-xs mt-1 opacity-70">
{formatMessage({ id: 'terminalDashboard.queuePanel.noItemsDesc' })}
</p>
</div>
</div>
);
}
// ========== Error State ==========
function QueueErrorState({ error }: { error: Error }) {
const { formatMessage } = useIntl();
return (
<div className="flex-1 flex items-center justify-center text-destructive p-4">
<div className="text-center">
<AlertTriangle className="h-10 w-10 mx-auto mb-3 opacity-60" />
<p className="text-sm">{formatMessage({ id: 'terminalDashboard.queuePanel.error' })}</p>
<p className="text-xs mt-1 opacity-70">{error.message}</p>
</div>
</div>
);
}
// ========== Main Component ==========
export function QueuePanel() {
const { formatMessage } = useIntl();
const queueQuery = useIssueQueue();
const associationChain = useIssueQueueIntegrationStore(selectAssociationChain);
const buildAssociationChain = useIssueQueueIntegrationStore((s) => s.buildAssociationChain);
// Flatten all queue items from grouped_items
const allItems = useMemo(() => {
if (!queueQuery.data) return [];
const grouped = queueQuery.data.grouped_items ?? {};
const items: QueueItem[] = [];
for (const group of Object.values(grouped)) {
items.push(...group);
}
// Sort by execution_order
items.sort((a, b) => a.execution_order - b.execution_order);
return items;
}, [queueQuery.data]);
// Count active items (pending + ready + executing)
const activeCount = useMemo(() => {
return allItems.filter(
(item) => item.status === 'pending' || item.status === 'ready' || item.status === 'executing'
).length;
}, [allItems]);
const handleSelect = useCallback(
(queueItemId: string) => {
buildAssociationChain(queueItemId, 'queue');
},
[buildAssociationChain]
);
// Loading state
if (queueQuery.isLoading) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
<div className="flex-1 flex items-center justify-center">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
</div>
);
}
// Error state
if (queueQuery.error) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border shrink-0">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
</div>
<QueueErrorState error={queueQuery.error} />
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Header with flow indicator */}
<div className="px-3 py-2 border-b border-border shrink-0 flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-2">
<ArrowDownToLine className="w-4 h-4 text-muted-foreground" />
<ListChecks className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.queuePanel.title' })}
</h3>
{activeCount > 0 && (
<Badge variant="info" className="text-[10px] px-1.5 py-0">
{activeCount}
</Badge>
)}
</div>
{/* Queue Item List */}
{allItems.length === 0 ? (
<QueueEmptyState />
) : (
<div className="flex-1 min-h-0 overflow-y-auto p-1.5 space-y-0.5">
{allItems.map((item) => (
<QueueItemRow
key={item.item_id}
item={item}
isHighlighted={associationChain?.queueItemId === item.item_id}
onSelect={() => handleSelect(item.item_id)}
/>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,218 @@
// ========================================
// SessionGroupTree Component
// ========================================
// Tree view for session groups with drag-and-drop support.
// Sessions can be dragged between groups. Groups are expandable sections.
// Uses @hello-pangea/dnd for drag-and-drop, sessionManagerStore for state.
import { useState, useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl';
import {
DragDropContext,
Droppable,
Draggable,
type DropResult,
} from '@hello-pangea/dnd';
import {
ChevronRight,
FolderOpen,
Folder,
Plus,
Terminal,
GripVertical,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useSessionManagerStore, selectGroups, selectSessionManagerActiveTerminalId } from '@/stores';
import { useCliSessionStore } from '@/stores/cliSessionStore';
import { Badge } from '@/components/ui/Badge';
// ========== SessionGroupTree Component ==========
export function SessionGroupTree() {
const { formatMessage } = useIntl();
const groups = useSessionManagerStore(selectGroups);
const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId);
const createGroup = useSessionManagerStore((s) => s.createGroup);
const moveSessionToGroup = useSessionManagerStore((s) => s.moveSessionToGroup);
const setActiveTerminal = useSessionManagerStore((s) => s.setActiveTerminal);
const sessions = useCliSessionStore((s) => s.sessions);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
const toggleGroup = useCallback((groupId: string) => {
setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(groupId)) {
next.delete(groupId);
} else {
next.add(groupId);
}
return next;
});
}, []);
const handleCreateGroup = useCallback(() => {
const name = formatMessage({ id: 'terminalDashboard.sessionTree.defaultGroupName' });
createGroup(name);
}, [createGroup, formatMessage]);
const handleSessionClick = useCallback(
(sessionId: string) => {
setActiveTerminal(sessionId);
},
[setActiveTerminal]
);
const handleDragEnd = useCallback(
(result: DropResult) => {
const { draggableId, destination } = result;
if (!destination) return;
// destination.droppableId is the target group ID
const targetGroupId = destination.droppableId;
moveSessionToGroup(draggableId, targetGroupId);
},
[moveSessionToGroup]
);
// Build a lookup for session display names
const sessionNames = useMemo(() => {
const map: Record<string, string> = {};
for (const [key, meta] of Object.entries(sessions)) {
map[key] = meta.tool ? `${meta.tool} - ${meta.shellKind}` : meta.shellKind;
}
return map;
}, [sessions]);
if (groups.length === 0) {
return (
<div className="flex flex-col h-full">
<div className="px-3 py-2 border-b border-border">
<button
onClick={handleCreateGroup}
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 transition-colors"
>
<Plus className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.sessionTree.createGroup' })}
</button>
</div>
<div className="flex-1 flex flex-col items-center justify-center gap-2 text-muted-foreground p-4">
<Folder className="w-8 h-8 opacity-50" />
<p className="text-xs text-center">
{formatMessage({ id: 'terminalDashboard.sessionTree.noGroups' })}
</p>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Create group button */}
<div className="px-3 py-2 border-b border-border shrink-0">
<button
onClick={handleCreateGroup}
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 transition-colors"
>
<Plus className="w-3.5 h-3.5" />
{formatMessage({ id: 'terminalDashboard.sessionTree.createGroup' })}
</button>
</div>
{/* Groups with drag-and-drop */}
<div className="flex-1 overflow-y-auto">
<DragDropContext onDragEnd={handleDragEnd}>
{groups.map((group) => {
const isExpanded = expandedGroups.has(group.id);
return (
<div key={group.id} className="border-b border-border/50 last:border-b-0">
{/* Group header */}
<button
onClick={() => toggleGroup(group.id)}
className={cn(
'flex items-center gap-1.5 w-full px-3 py-2 text-left',
'hover:bg-muted/50 transition-colors text-sm'
)}
>
<ChevronRight
className={cn(
'w-3.5 h-3.5 text-muted-foreground transition-transform shrink-0',
isExpanded && 'rotate-90'
)}
/>
{isExpanded ? (
<FolderOpen className="w-4 h-4 text-blue-500 shrink-0" />
) : (
<Folder className="w-4 h-4 text-blue-400 shrink-0" />
)}
<span className="flex-1 truncate font-medium">{group.name}</span>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{group.sessionIds.length}
</Badge>
</button>
{/* Expanded: droppable session list */}
{isExpanded && (
<Droppable droppableId={group.id}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className={cn(
'min-h-[32px] pb-1',
snapshot.isDraggingOver && 'bg-primary/5'
)}
>
{group.sessionIds.length === 0 ? (
<p className="px-8 py-2 text-xs text-muted-foreground italic">
{formatMessage({ id: 'terminalDashboard.sessionTree.emptyGroup' })}
</p>
) : (
group.sessionIds.map((sessionId, index) => (
<Draggable
key={sessionId}
draggableId={sessionId}
index={index}
>
{(dragProvided, dragSnapshot) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
className={cn(
'flex items-center gap-1.5 mx-1 px-2 py-1.5 rounded-sm cursor-pointer',
'hover:bg-muted/50 transition-colors text-sm',
activeTerminalId === sessionId && 'bg-primary/10 text-primary',
dragSnapshot.isDragging && 'bg-muted shadow-md'
)}
onClick={() => handleSessionClick(sessionId)}
>
<span
{...dragProvided.dragHandleProps}
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0"
>
<GripVertical className="w-3 h-3" />
</span>
<Terminal className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<span className="flex-1 truncate text-xs">
{sessionNames[sessionId] ?? sessionId}
</span>
</div>
)}
</Draggable>
))
)}
{provided.placeholder}
</div>
)}
</Droppable>
)}
</div>
);
})}
</DragDropContext>
</div>
</div>
);
}
export default SessionGroupTree;

View File

@@ -0,0 +1,211 @@
// ========================================
// TerminalInstance Component
// ========================================
// xterm.js terminal wrapper for the Terminal Dashboard.
// Reuses exact integration pattern from TerminalMainArea:
// XTerm instance in ref, FitAddon, ResizeObserver, batched PTY input (30ms),
// output chunk streaming from cliSessionStore.
import { useEffect, useRef, useCallback } from 'react';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { useCliSessionStore } from '@/stores/cliSessionStore';
import { useSessionManagerStore } from '@/stores/sessionManagerStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import {
fetchCliSessionBuffer,
sendCliSessionText,
resizeCliSession,
} from '@/lib/api';
import { cn } from '@/lib/utils';
// ========== Types ==========
interface TerminalInstanceProps {
/** Session key to render terminal for */
sessionId: string;
/** Additional CSS classes */
className?: string;
}
// ========== Component ==========
export function TerminalInstance({ sessionId, className }: TerminalInstanceProps) {
const projectPath = useWorkflowStore(selectProjectPath);
// cliSessionStore selectors
const outputChunks = useCliSessionStore((s) => s.outputChunks);
const setBuffer = useCliSessionStore((s) => s.setBuffer);
const clearOutput = useCliSessionStore((s) => s.clearOutput);
// ========== xterm Refs ==========
const terminalHostRef = useRef<HTMLDivElement | null>(null);
const xtermRef = useRef<XTerm | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const lastChunkIndexRef = useRef<number>(0);
// PTY input batching (30ms, matching TerminalMainArea)
const pendingInputRef = useRef<string>('');
const flushTimerRef = useRef<number | null>(null);
// Track sessionId in a ref so xterm onData callback always has latest value
const sessionIdRef = useRef<string>(sessionId);
sessionIdRef.current = sessionId;
const projectPathRef = useRef<string | null>(projectPath);
projectPathRef.current = projectPath;
// ========== PTY Input Batching ==========
const flushInput = useCallback(async () => {
const key = sessionIdRef.current;
if (!key) return;
const pending = pendingInputRef.current;
pendingInputRef.current = '';
if (!pending) return;
try {
await sendCliSessionText(
key,
{ text: pending, appendNewline: false },
projectPathRef.current || undefined
);
} catch {
// Ignore transient failures
}
}, []);
const scheduleFlush = useCallback(() => {
if (flushTimerRef.current !== null) return;
flushTimerRef.current = window.setTimeout(async () => {
flushTimerRef.current = null;
await flushInput();
}, 30);
}, [flushInput]);
// ========== xterm Lifecycle ==========
// Initialize xterm instance (once per mount)
useEffect(() => {
if (!terminalHostRef.current) return;
if (xtermRef.current) return;
const term = new XTerm({
convertEol: true,
cursorBlink: true,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 12,
scrollback: 5000,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(terminalHostRef.current);
fitAddon.fit();
// Forward keystrokes to backend (batched 30ms)
term.onData((data) => {
if (!sessionIdRef.current) return;
pendingInputRef.current += data;
scheduleFlush();
});
xtermRef.current = term;
fitAddonRef.current = fitAddon;
return () => {
// Flush any pending input before cleanup
if (flushTimerRef.current !== null) {
window.clearTimeout(flushTimerRef.current);
flushTimerRef.current = null;
}
try {
term.dispose();
} finally {
xtermRef.current = null;
fitAddonRef.current = null;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Attach to session: clear terminal and load buffer
useEffect(() => {
const term = xtermRef.current;
const fitAddon = fitAddonRef.current;
if (!term || !fitAddon) return;
lastChunkIndexRef.current = 0;
term.reset();
term.clear();
if (!sessionId) return;
clearOutput(sessionId);
fetchCliSessionBuffer(sessionId, projectPath || undefined)
.then(({ buffer }) => {
setBuffer(sessionId, buffer || '');
})
.catch(() => {
// ignore
})
.finally(() => {
fitAddon.fit();
});
}, [sessionId, projectPath, setBuffer, clearOutput]);
// Stream new output chunks into xterm and forward to monitor worker
useEffect(() => {
const term = xtermRef.current;
if (!term || !sessionId) return;
const chunks = outputChunks[sessionId] ?? [];
const start = lastChunkIndexRef.current;
if (start >= chunks.length) return;
const { feedMonitor } = useSessionManagerStore.getState();
for (let i = start; i < chunks.length; i++) {
term.write(chunks[i].data);
feedMonitor(sessionId, chunks[i].data);
}
lastChunkIndexRef.current = chunks.length;
}, [outputChunks, sessionId]);
// ResizeObserver -> fit + resize backend
useEffect(() => {
const host = terminalHostRef.current;
const term = xtermRef.current;
const fitAddon = fitAddonRef.current;
if (!host || !term || !fitAddon) return;
const resize = () => {
fitAddon.fit();
if (sessionIdRef.current) {
void (async () => {
try {
await resizeCliSession(
sessionIdRef.current,
{ cols: term.cols, rows: term.rows },
projectPathRef.current || undefined
);
} catch {
// ignore
}
})();
}
};
const ro = new ResizeObserver(resize);
ro.observe(host);
return () => ro.disconnect();
}, [sessionId, projectPath]);
// ========== Render ==========
return (
<div
ref={terminalHostRef}
className={cn('h-full w-full bg-black/90', className)}
/>
);
}

View File

@@ -0,0 +1,93 @@
// ========================================
// TerminalTabBar Component
// ========================================
// Horizontal tab strip for terminal sessions in the Terminal Dashboard.
// Renders tabs from sessionManagerStore groups with status indicators and alert badges.
import { useIntl } from 'react-intl';
import { Terminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
useSessionManagerStore,
selectGroups,
selectSessionManagerActiveTerminalId,
selectTerminalMetas,
} from '@/stores/sessionManagerStore';
import type { TerminalStatus } from '@/types/terminal-dashboard';
// ========== Status Styles ==========
const statusDotStyles: Record<TerminalStatus, string> = {
active: 'bg-green-500',
idle: 'bg-gray-400',
error: 'bg-red-500',
};
// ========== Component ==========
export function TerminalTabBar() {
const { formatMessage } = useIntl();
const groups = useSessionManagerStore(selectGroups);
const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId);
const terminalMetas = useSessionManagerStore(selectTerminalMetas);
const setActiveTerminal = useSessionManagerStore((s) => s.setActiveTerminal);
// Flatten all sessionIds from all groups
const allSessionIds = groups.flatMap((g) => g.sessionIds);
if (allSessionIds.length === 0) {
return (
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-muted/30 min-h-[40px]">
<Terminal className="w-3.5 h-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{formatMessage({ id: 'terminalDashboard.tabBar.noTabs' })}
</span>
</div>
);
}
return (
<div className="flex items-center border-b border-border bg-muted/30 overflow-x-auto shrink-0">
{allSessionIds.map((sessionId) => {
const meta = terminalMetas[sessionId];
const title = meta?.title ?? sessionId;
const status: TerminalStatus = meta?.status ?? 'idle';
const alertCount = meta?.alertCount ?? 0;
const isActive = activeTerminalId === sessionId;
return (
<button
key={sessionId}
className={cn(
'flex items-center gap-1.5 px-3 py-2 text-xs border-r border-border',
'whitespace-nowrap transition-colors hover:bg-accent/50',
isActive
? 'bg-background text-foreground border-b-2 border-b-primary'
: 'text-muted-foreground'
)}
onClick={() => setActiveTerminal(sessionId)}
title={title}
>
{/* Status dot */}
<span
className={cn(
'w-2 h-2 rounded-full shrink-0',
statusDotStyles[status]
)}
/>
{/* Title */}
<span className="truncate max-w-[120px]">{title}</span>
{/* Alert badge */}
{alertCount > 0 && (
<span className="ml-1 px-1.5 py-0.5 text-[10px] font-medium leading-none rounded-full bg-destructive text-destructive-foreground shrink-0">
{alertCount > 99 ? '99+' : alertCount}
</span>
)}
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,49 @@
// ========================================
// TerminalWorkbench Component
// ========================================
// Container for the right panel of the Terminal Dashboard.
// Combines TerminalTabBar (tab switching) and TerminalInstance (xterm.js)
// in a flex-col layout. MVP scope: single terminal view (1x1 grid).
import { useIntl } from 'react-intl';
import { Terminal } from 'lucide-react';
import {
useSessionManagerStore,
selectSessionManagerActiveTerminalId,
} from '@/stores/sessionManagerStore';
import { TerminalTabBar } from './TerminalTabBar';
import { TerminalInstance } from './TerminalInstance';
// ========== Component ==========
export function TerminalWorkbench() {
const { formatMessage } = useIntl();
const activeTerminalId = useSessionManagerStore(selectSessionManagerActiveTerminalId);
return (
<div className="flex flex-col h-full">
{/* Tab strip (fixed height) */}
<TerminalTabBar />
{/* Terminal content (flex-1, takes remaining space) */}
{activeTerminalId ? (
<div className="flex-1 min-h-0">
<TerminalInstance sessionId={activeTerminalId} />
</div>
) : (
/* Empty state when no terminal is selected */
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<Terminal className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p className="text-sm font-medium">
{formatMessage({ id: 'terminalDashboard.workbench.noTerminal' })}
</p>
<p className="text-xs mt-1 opacity-70">
{formatMessage({ id: 'terminalDashboard.workbench.noTerminalHint' })}
</p>
</div>
</div>
)}
</div>
);
}

View File

@@ -9,7 +9,6 @@ import { useIntl } from 'react-intl';
import {
X,
Terminal as TerminalIcon,
Plus,
Trash2,
RotateCcw,
Loader2,
@@ -22,7 +21,6 @@ import { useCliSessionStore, type CliSessionMeta } from '@/stores/cliSessionStor
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { QueueExecutionListView } from './QueueExecutionListView';
import {
createCliSession,
fetchCliSessionBuffer,
sendCliSessionText,
resizeCliSession,
@@ -41,14 +39,12 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
const { formatMessage } = useIntl();
const panelView = useTerminalPanelStore((s) => s.panelView);
const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId);
const openTerminal = useTerminalPanelStore((s) => s.openTerminal);
const removeTerminal = useTerminalPanelStore((s) => s.removeTerminal);
const sessions = useCliSessionStore((s) => s.sessions);
const outputChunks = useCliSessionStore((s) => s.outputChunks);
const setBuffer = useCliSessionStore((s) => s.setBuffer);
const clearOutput = useCliSessionStore((s) => s.clearOutput);
const upsertSession = useCliSessionStore((s) => s.upsertSession);
const removeSessionFromStore = useCliSessionStore((s) => s.removeSession);
const projectPath = useWorkflowStore(selectProjectPath);
@@ -69,12 +65,8 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
const flushTimerRef = useRef<number | null>(null);
// Toolbar state
const [isCreating, setIsCreating] = useState(false);
const [isClosing, setIsClosing] = useState(false);
// Available CLI tools
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
const flushInput = useCallback(async () => {
const sessionKey = activeTerminalId;
if (!sessionKey) return;
@@ -204,23 +196,6 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
// ========== CLI Session Actions ==========
const handleCreateSession = useCallback(async (tool: string) => {
if (!projectPath || isCreating) return;
setIsCreating(true);
try {
const created = await createCliSession(
{ workingDir: projectPath, tool },
projectPath
);
upsertSession(created.session);
openTerminal(created.session.sessionKey);
} catch (err) {
console.error('[TerminalMainArea] createCliSession failed:', err);
} finally {
setIsCreating(false);
}
}, [projectPath, isCreating, upsertSession, openTerminal]);
const handleCloseSession = useCallback(async () => {
if (!activeTerminalId || isClosing) return;
setIsClosing(true);
@@ -268,29 +243,11 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
</div>
{/* Toolbar */}
{panelView === 'terminal' && (
{panelView === 'terminal' && activeTerminalId && (
<div className="flex items-center gap-1 px-3 py-1.5 border-b border-border bg-muted/30">
{/* New CLI session buttons */}
{CLI_TOOLS.map((tool) => (
<Button
key={tool}
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1"
disabled={isCreating || !projectPath}
onClick={() => handleCreateSession(tool)}
title={`New ${tool} session`}
>
{isCreating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
{tool}
</Button>
))}
<div className="flex-1" />
{/* Terminal actions */}
{activeTerminalId && (
<>
<Button
variant="ghost"
size="sm"
@@ -310,8 +267,6 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
>
{isClosing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Trash2 className="h-3 w-3" />}
</Button>
</>
)}
</div>
)}
@@ -328,29 +283,12 @@ export function TerminalMainArea({ onClose }: TerminalMainAreaProps) {
/>
</div>
) : (
/* Empty State - with quick launch */
/* Empty State */
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<TerminalIcon className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="text-sm">{formatMessage({ id: 'home.terminalPanel.noTerminalSelected' })}</p>
<p className="text-xs mt-1 mb-4">{formatMessage({ id: 'home.terminalPanel.selectTerminalHint' })}</p>
{projectPath && (
<div className="flex items-center justify-center gap-2">
{CLI_TOOLS.map((tool) => (
<Button
key={tool}
variant="outline"
size="sm"
className="gap-1"
disabled={isCreating}
onClick={() => handleCreateSession(tool)}
>
{isCreating ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
{tool}
</Button>
))}
</div>
)}
<p className="text-xs mt-1">{formatMessage({ id: 'home.terminalPanel.selectTerminalHint' })}</p>
</div>
</div>
)}

View File

@@ -11,7 +11,7 @@ import { cn } from '@/lib/utils';
import { useTerminalPanelStore } from '@/stores/terminalPanelStore';
import { useCliSessionStore, type CliSessionMeta, type CliSessionOutputChunk } from '@/stores/cliSessionStore';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { createCliSession } from '@/lib/api';
import { createCliSession, sendCliSessionText } from '@/lib/api';
// ========== Status Badge Mapping ==========
@@ -45,6 +45,16 @@ const StatusIcon: Record<SessionStatus, React.ComponentType<{ className?: string
idle: Circle,
};
type LaunchMode = 'default' | 'yolo';
const LAUNCH_COMMANDS: Record<string, Record<LaunchMode, string>> = {
claude: { default: 'claude', yolo: 'claude --permission-mode bypassPermissions' },
gemini: { default: 'gemini', yolo: 'gemini --approval-mode yolo' },
qwen: { default: 'qwen', yolo: 'qwen --approval-mode yolo' },
codex: { default: 'codex', yolo: 'codex --full-auto' },
opencode: { default: 'opencode', yolo: 'opencode' },
};
export function TerminalNavBar() {
const panelView = useTerminalPanelStore((s) => s.panelView);
const activeTerminalId = useTerminalPanelStore((s) => s.activeTerminalId);
@@ -61,6 +71,7 @@ export function TerminalNavBar() {
const projectPath = useWorkflowStore(selectProjectPath);
const [isCreating, setIsCreating] = useState(false);
const [showToolMenu, setShowToolMenu] = useState(false);
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
const CLI_TOOLS = ['claude', 'gemini', 'qwen', 'codex', 'opencode'] as const;
@@ -75,12 +86,24 @@ export function TerminalNavBar() {
);
upsertSession(created.session);
openTerminal(created.session.sessionKey);
// Auto-launch CLI tool after PTY is ready
const command = LAUNCH_COMMANDS[tool]?.[launchMode] ?? tool;
setTimeout(() => {
sendCliSessionText(
created.session.sessionKey,
{ text: command, appendNewline: true },
projectPath
).catch((err) =>
console.error('[TerminalNavBar] auto-launch failed:', err)
);
}, 300);
} catch (err) {
console.error('[TerminalNavBar] createCliSession failed:', err);
} finally {
setIsCreating(false);
}
}, [projectPath, isCreating, upsertSession, openTerminal]);
}, [projectPath, isCreating, launchMode, upsertSession, openTerminal]);
const handleQueueClick = () => {
setPanelView('queue');
@@ -173,7 +196,25 @@ export function TerminalNavBar() {
{showToolMenu && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowToolMenu(false)} />
<div className="absolute left-full bottom-0 ml-1 z-50 bg-card border border-border rounded-md shadow-lg py-1 min-w-[120px]">
<div className="absolute left-full bottom-0 ml-1 z-50 bg-card border border-border rounded-md shadow-lg min-w-[140px]">
{/* Mode Toggle */}
<div className="flex items-center gap-1 px-2 py-1.5 border-b border-border">
{(['default', 'yolo'] as const).map((mode) => (
<button
key={mode}
className={cn(
'flex-1 text-xs px-2 py-1 rounded transition-colors',
launchMode === mode
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:bg-accent'
)}
onClick={() => setLaunchMode(mode)}
>
{mode === 'default' ? 'Default' : 'Yolo'}
</button>
))}
</div>
{/* Tool List */}
{CLI_TOOLS.map((tool) => (
<button
key={tool}

View File

@@ -39,6 +39,7 @@ import workspace from './workspace.json';
import help from './help.json';
import cliViewer from './cli-viewer.json';
import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -101,4 +102,5 @@ export default {
...flattenMessages(help, 'help'),
...flattenMessages(cliViewer, 'cliViewer'),
...flattenMessages(team, 'team'),
...flattenMessages(terminalDashboard, 'terminalDashboard'),
} as Record<string, string>;

View File

@@ -35,7 +35,8 @@
"rules": "Rules",
"explorer": "File Explorer",
"graph": "Graph Explorer",
"teams": "Team Execution"
"teams": "Team Execution",
"terminalDashboard": "Terminal Dashboard"
},
"sidebar": {
"collapse": "Collapse",

View File

@@ -0,0 +1,81 @@
{
"page": {
"title": "Terminal Dashboard"
},
"columns": {
"sessions": "Sessions",
"workflow": "Workflow",
"terminal": "Terminal Workbench"
},
"kpi": {
"title": "Dashboard KPI",
"activeSessions": "Active Sessions",
"queueSize": "Queue Size",
"alertCount": "Alerts",
"errorCount": "Errors",
"idleCount": "Idle"
},
"inspector": {
"title": "Inspector",
"noSelection": "Select an item to view details",
"associationChain": "Association Chain"
},
"sessionTree": {
"createGroup": "New Group",
"groupNamePrompt": "Enter group name",
"defaultGroupName": "Untitled Group",
"emptyGroup": "No sessions in this group",
"noGroups": "No session groups yet",
"sessionCount": "{count} sessions",
"dragHint": "Drag sessions between groups"
},
"agentList": {
"title": "Agents",
"noAgents": "No active agents",
"stepLabel": "Step {current}/{total}",
"statusRunning": "Running",
"statusCompleted": "Completed",
"statusFailed": "Failed",
"statusPaused": "Paused",
"statusPending": "Pending"
},
"issuePanel": {
"title": "Issues",
"sendToQueue": "Send to Queue",
"noIssues": "No issues found",
"noIssuesDesc": "Issues will appear here when discovered",
"error": "Failed to load issues"
},
"queuePanel": {
"title": "Queue",
"noItems": "Queue is empty",
"noItemsDesc": "Send issues to queue to start workflow",
"error": "Failed to load queue",
"order": "#{order}",
"dependsOn": "Depends on: {deps}",
"status": {
"pending": "Pending",
"ready": "Ready",
"executing": "Executing",
"completed": "Completed",
"failed": "Failed",
"blocked": "Blocked"
}
},
"tabBar": {
"noTabs": "No terminal sessions"
},
"workbench": {
"noTerminal": "No terminal selected",
"noTerminalHint": "Select a session from the tab bar or create a new one"
},
"placeholder": {
"sessionTree": "Session groups will appear here",
"agentList": "Agent list will appear here",
"issuePanel": "Issue panel will appear here",
"queuePanel": "Queue panel will appear here",
"terminalWorkbench": "Terminal workbench will appear here",
"kpiBar": "KPI metrics will appear here",
"inspector": "Inspector details will appear here"
}
}

View File

@@ -39,6 +39,7 @@ import workspace from './workspace.json';
import help from './help.json';
import cliViewer from './cli-viewer.json';
import team from './team.json';
import terminalDashboard from './terminal-dashboard.json';
/**
* Flattens nested JSON object to dot-separated keys
@@ -101,4 +102,5 @@ export default {
...flattenMessages(help, 'help'),
...flattenMessages(cliViewer, 'cliViewer'),
...flattenMessages(team, 'team'),
...flattenMessages(terminalDashboard, 'terminalDashboard'),
} as Record<string, string>;

View File

@@ -35,7 +35,8 @@
"rules": "规则",
"explorer": "文件浏览器",
"graph": "图浏览器",
"teams": "团队执行"
"teams": "团队执行",
"terminalDashboard": "终端仪表板"
},
"sidebar": {
"collapse": "收起",

View File

@@ -0,0 +1,81 @@
{
"page": {
"title": "终端仪表板"
},
"columns": {
"sessions": "会话",
"workflow": "工作流",
"terminal": "终端工作台"
},
"kpi": {
"title": "仪表板 KPI",
"activeSessions": "活跃会话",
"queueSize": "队列大小",
"alertCount": "告警数",
"errorCount": "错误数",
"idleCount": "空闲数"
},
"inspector": {
"title": "检查器",
"noSelection": "选择一个项目以查看详情",
"associationChain": "关联链路"
},
"sessionTree": {
"createGroup": "新建分组",
"groupNamePrompt": "输入分组名称",
"defaultGroupName": "未命名分组",
"emptyGroup": "此分组暂无会话",
"noGroups": "暂无会话分组",
"sessionCount": "{count} 个会话",
"dragHint": "拖拽会话至其他分组"
},
"agentList": {
"title": "代理",
"noAgents": "暂无活跃代理",
"stepLabel": "步骤 {current}/{total}",
"statusRunning": "运行中",
"statusCompleted": "已完成",
"statusFailed": "已失败",
"statusPaused": "已暂停",
"statusPending": "等待中"
},
"issuePanel": {
"title": "问题",
"sendToQueue": "发送到队列",
"noIssues": "暂无问题",
"noIssuesDesc": "发现问题时将在此显示",
"error": "加载问题失败"
},
"queuePanel": {
"title": "队列",
"noItems": "队列为空",
"noItemsDesc": "将问题发送到队列以启动工作流",
"error": "加载队列失败",
"order": "#{order}",
"dependsOn": "依赖: {deps}",
"status": {
"pending": "等待中",
"ready": "就绪",
"executing": "执行中",
"completed": "已完成",
"failed": "已失败",
"blocked": "已阻塞"
}
},
"tabBar": {
"noTabs": "暂无终端会话"
},
"workbench": {
"noTerminal": "未选择终端",
"noTerminalHint": "从标签栏选择会话或创建新会话"
},
"placeholder": {
"sessionTree": "会话分组将在此显示",
"agentList": "Agent 列表将在此显示",
"issuePanel": "问题面板将在此显示",
"queuePanel": "队列面板将在此显示",
"terminalWorkbench": "终端工作台将在此显示",
"kpiBar": "KPI 指标将在此显示",
"inspector": "检查器详情将在此显示"
}
}

View File

@@ -607,8 +607,9 @@ export function ReviewSessionPage() {
</CardContent>
</Card>
{/* Fix Progress Carousel */}
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
{/* Fix Progress Carousel — disabled: backend /api/fix-progress not implemented yet
See FRONTEND_BACKEND_ALIGNMENT_AUDIT.md for details */}
{/* {sessionId && <FixProgressCarousel sessionId={sessionId} />} */}
{/* Unified Filter Card with Dimension Tabs */}
<Card>

View File

@@ -0,0 +1,96 @@
// ========================================
// Terminal Dashboard Page
// ========================================
// Three-column Allotment layout for terminal execution management.
// Left: session groups + agent list
// Middle: issue + queue workflow panels
// Right: terminal workbench
// Top: GlobalKpiBar (real component)
// Bottom: BottomInspector (collapsible, real component)
// Cross-cutting: AssociationHighlightProvider wraps the layout
import { useIntl } from 'react-intl';
import { Allotment } from 'allotment';
import 'allotment/dist/style.css';
import { FolderTree } from 'lucide-react';
import { SessionGroupTree } from '@/components/terminal-dashboard/SessionGroupTree';
import { AgentList } from '@/components/terminal-dashboard/AgentList';
import { IssuePanel } from '@/components/terminal-dashboard/IssuePanel';
import { QueuePanel } from '@/components/terminal-dashboard/QueuePanel';
import { TerminalWorkbench } from '@/components/terminal-dashboard/TerminalWorkbench';
import { GlobalKpiBar } from '@/components/terminal-dashboard/GlobalKpiBar';
import { BottomInspector } from '@/components/terminal-dashboard/BottomInspector';
import { AssociationHighlightProvider } from '@/components/terminal-dashboard/AssociationHighlight';
// ========== Main Page Component ==========
export function TerminalDashboardPage() {
const { formatMessage } = useIntl();
return (
<div className="flex flex-col h-[calc(100vh-56px)] overflow-hidden">
{/* GlobalKpiBar at top (flex-shrink-0) */}
<GlobalKpiBar />
{/* AssociationHighlightProvider wraps the three-column layout + bottom inspector */}
<AssociationHighlightProvider>
{/* Three-column Allotment layout (flex-1) */}
<div className="flex-1 min-h-0">
<Allotment proportionalLayout={true}>
{/* Left column: Sessions */}
<Allotment.Pane preferredSize={250} minSize={180} maxSize={400}>
<div className="h-full border-r border-border bg-background flex flex-col">
<div className="px-3 py-2 border-b border-border shrink-0">
<h2 className="text-sm font-semibold flex items-center gap-2">
<FolderTree className="w-4 h-4" />
{formatMessage({ id: 'terminalDashboard.columns.sessions' })}
</h2>
</div>
{/* SessionGroupTree takes remaining space */}
<div className="flex-1 min-h-0 overflow-y-auto">
<SessionGroupTree />
</div>
{/* AgentList at bottom with max height */}
<div className="shrink-0">
<AgentList />
</div>
</div>
</Allotment.Pane>
{/* Middle column: Workflow (IssuePanel + QueuePanel vertical split) */}
<Allotment.Pane minSize={300}>
<div className="h-full border-r border-border bg-background overflow-hidden">
<Allotment vertical proportionalLayout={true}>
{/* Top: IssuePanel */}
<Allotment.Pane minSize={120}>
<div className="h-full overflow-hidden">
<IssuePanel />
</div>
</Allotment.Pane>
{/* Bottom: QueuePanel */}
<Allotment.Pane minSize={120}>
<div className="h-full overflow-hidden border-t border-border">
<QueuePanel />
</div>
</Allotment.Pane>
</Allotment>
</div>
</Allotment.Pane>
{/* Right column: Terminal Workbench */}
<Allotment.Pane minSize={300}>
<div className="h-full bg-background overflow-hidden">
<TerminalWorkbench />
</div>
</Allotment.Pane>
</Allotment>
</div>
{/* BottomInspector at bottom (flex-shrink-0) */}
<BottomInspector />
</AssociationHighlightProvider>
</div>
);
}
export default TerminalDashboardPage;

View File

@@ -35,3 +35,4 @@ export { CliViewerPage } from './CliViewerPage';
export { CliSessionSharePage } from './CliSessionSharePage';
export { IssueManagerPage } from './IssueManagerPage';
export { TeamPage } from './TeamPage';
export { TerminalDashboardPage } from './TerminalDashboardPage';

View File

@@ -36,6 +36,7 @@ import {
CliViewerPage,
CliSessionSharePage,
TeamPage,
TerminalDashboardPage,
} from '@/pages';
/**
@@ -169,6 +170,10 @@ const routes: RouteObject[] = [
path: 'teams',
element: <TeamPage />,
},
{
path: 'terminal-dashboard',
element: <TerminalDashboardPage />,
},
// Catch-all route for 404
{
path: '*',
@@ -223,6 +228,7 @@ export const ROUTES = {
EXPLORER: '/explorer',
GRAPH: '/graph',
TEAMS: '/teams',
TERMINAL_DASHBOARD: '/terminal-dashboard',
} as const;
export type RoutePath = (typeof ROUTES)[keyof typeof ROUTES];

View File

@@ -123,6 +123,25 @@ export {
selectPlanStepByExecutionId,
} from './orchestratorStore';
// Session Manager Store
export {
useSessionManagerStore,
selectGroups,
selectLayout,
selectSessionManagerActiveTerminalId,
selectTerminalMetas,
selectTerminalMeta,
} from './sessionManagerStore';
// Issue Queue Integration Store
export {
useIssueQueueIntegrationStore,
selectSelectedIssueId,
selectAssociationChain,
selectQueueByIssue,
selectIssueById,
} from './issueQueueIntegrationStore';
// Terminal Panel Store Types
export type {
PanelView,
@@ -241,3 +260,25 @@ export type {
} from '../types/flow';
export { NODE_TYPE_CONFIGS } from '../types/flow';
// Session Manager Store Types
export type {
SessionGridLayout,
SessionLayout,
TerminalStatus,
TerminalMeta,
SessionGroup,
SessionManagerState,
SessionManagerActions,
SessionManagerStore,
AlertSeverity,
MonitorAlert,
} from '../types/terminal-dashboard';
// Issue Queue Integration Store Types
export type {
AssociationChain,
IssueQueueIntegrationState,
IssueQueueIntegrationActions,
IssueQueueIntegrationStore,
} from '../types/terminal-dashboard';

View File

@@ -0,0 +1,232 @@
// ========================================
// Issue Queue Integration Store
// ========================================
// Zustand store bridging issue/queue data with execution tracking.
// Manages association chain state for highlight linkage across
// Issue <-> QueueItem <-> Terminal Session.
//
// Design principles:
// - Does NOT duplicate issues[]/queue[] arrays (use React Query hooks for data)
// - Bridges queueExecutionStore via getState() for execution status
// - Manages UI-specific integration state (selection, association chain)
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import type {
AssociationChain,
IssueQueueIntegrationState,
IssueQueueIntegrationStore,
} from '../types/terminal-dashboard';
import { useQueueExecutionStore } from './queueExecutionStore';
// ========== Initial State ==========
const initialState: IssueQueueIntegrationState = {
selectedIssueId: null,
associationChain: null,
};
// ========== Store ==========
export const useIssueQueueIntegrationStore = create<IssueQueueIntegrationStore>()(
devtools(
(set) => ({
...initialState,
// ========== Issue Selection ==========
setSelectedIssue: (issueId: string | null) => {
if (issueId === null) {
set(
{ selectedIssueId: null, associationChain: null },
false,
'setSelectedIssue/clear'
);
return;
}
// Resolve association chain from issue ID
const chain = resolveChainFromIssue(issueId);
set(
{ selectedIssueId: issueId, associationChain: chain },
false,
'setSelectedIssue'
);
},
// ========== Association Chain ==========
buildAssociationChain: (
entityId: string,
entityType: 'issue' | 'queue' | 'session'
) => {
let chain: AssociationChain;
switch (entityType) {
case 'issue':
chain = resolveChainFromIssue(entityId);
break;
case 'queue':
chain = resolveChainFromQueueItem(entityId);
break;
case 'session':
chain = resolveChainFromSession(entityId);
break;
default:
chain = { issueId: null, queueItemId: null, sessionId: null };
}
set(
{
associationChain: chain,
selectedIssueId: chain.issueId,
},
false,
'buildAssociationChain'
);
},
// ========== Queue Status Bridge ==========
_updateQueueItemStatus: (
queueItemId: string,
status: string,
sessionId?: string
) => {
// Bridge to queueExecutionStore for execution tracking
const execStore = useQueueExecutionStore.getState();
const executions = Object.values(execStore.executions);
const matchedExec = executions.find(
(e) => e.queueItemId === queueItemId
);
if (matchedExec) {
// Update status in the execution store
const validStatuses = ['pending', 'running', 'completed', 'failed'] as const;
type ValidStatus = (typeof validStatuses)[number];
if (validStatuses.includes(status as ValidStatus)) {
execStore.updateStatus(matchedExec.id, status as ValidStatus);
}
}
// If a sessionId is provided, update the association chain
if (sessionId) {
set(
(state) => {
if (
state.associationChain &&
state.associationChain.queueItemId === queueItemId
) {
return {
associationChain: {
...state.associationChain,
sessionId,
},
};
}
return state;
},
false,
'_updateQueueItemStatus'
);
}
},
}),
{ name: 'IssueQueueIntegrationStore' }
)
);
// ========== Chain Resolution Helpers ==========
/**
* Resolve association chain starting from an issue ID.
* Looks up queueExecutionStore to find linked queue items and sessions.
*/
function resolveChainFromIssue(issueId: string): AssociationChain {
const executions = Object.values(
useQueueExecutionStore.getState().executions
);
// Find the first execution matching this issue
const matched = executions.find((e) => e.issueId === issueId);
if (matched) {
return {
issueId,
queueItemId: matched.queueItemId,
sessionId: matched.sessionKey ?? null,
};
}
return { issueId, queueItemId: null, sessionId: null };
}
/**
* Resolve association chain starting from a queue item ID.
* Looks up queueExecutionStore to find linked issue and session.
*/
function resolveChainFromQueueItem(queueItemId: string): AssociationChain {
const executions = Object.values(
useQueueExecutionStore.getState().executions
);
const matched = executions.find((e) => e.queueItemId === queueItemId);
if (matched) {
return {
issueId: matched.issueId,
queueItemId,
sessionId: matched.sessionKey ?? null,
};
}
return { issueId: null, queueItemId, sessionId: null };
}
/**
* Resolve association chain starting from a session key.
* Looks up queueExecutionStore to find linked issue and queue item.
*/
function resolveChainFromSession(sessionId: string): AssociationChain {
const executions = Object.values(
useQueueExecutionStore.getState().executions
);
const matched = executions.find((e) => e.sessionKey === sessionId);
if (matched) {
return {
issueId: matched.issueId,
queueItemId: matched.queueItemId,
sessionId,
};
}
return { issueId: null, queueItemId: null, sessionId };
}
// ========== Selectors ==========
/** Select currently selected issue ID */
export const selectSelectedIssueId = (state: IssueQueueIntegrationStore) =>
state.selectedIssueId;
/** Select current association chain */
export const selectAssociationChain = (state: IssueQueueIntegrationStore) =>
state.associationChain;
/**
* Select queue executions for a specific issue.
* WARNING: Returns new array each call - use with useMemo in components.
*/
export const selectQueueByIssue =
(issueId: string) =>
(): import('./queueExecutionStore').QueueExecution[] => {
const executions = Object.values(
useQueueExecutionStore.getState().executions
);
return executions.filter((e) => e.issueId === issueId);
};
/**
* Select an issue's execution by issue ID (first matched).
* Returns the execution record or undefined.
*/
export const selectIssueById =
(issueId: string) =>
(): import('./queueExecutionStore').QueueExecution | undefined => {
const executions = Object.values(
useQueueExecutionStore.getState().executions
);
return executions.find((e) => e.issueId === issueId);
};

View File

@@ -0,0 +1,205 @@
// ========================================
// Session Manager Store
// ========================================
// Zustand store for terminal dashboard session management.
// Manages session groups, layout, active terminal, terminal metadata,
// and monitor Web Worker lifecycle.
// Consumes cliSessionStore data via getState() pattern (no data duplication).
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import type {
MonitorAlert,
SessionGroup,
SessionLayout,
SessionManagerState,
SessionManagerStore,
TerminalMeta,
TerminalStatus,
} from '../types/terminal-dashboard';
// ========== Initial State ==========
const initialState: SessionManagerState = {
groups: [],
layout: { grid: '1x1', splits: [1] },
activeTerminalId: null,
terminalMetas: {},
};
// ========== Worker Ref (non-reactive, outside Zustand) ==========
/** Module-level worker reference. Worker objects are not serializable. */
let _workerRef: Worker | null = null;
// ========== Worker Message Handler ==========
function _handleWorkerMessage(event: MessageEvent<MonitorAlert>): void {
const msg = event.data;
if (msg.type !== 'alert') return;
const { sessionId, severity, message } = msg;
// Map severity to terminal status
const statusMap: Record<string, TerminalStatus> = {
critical: 'error',
warning: 'idle',
};
const store = useSessionManagerStore.getState();
const existing = store.terminalMetas[sessionId];
const currentAlertCount = existing?.alertCount ?? 0;
store.updateTerminalMeta(sessionId, {
status: statusMap[severity] ?? 'idle',
alertCount: currentAlertCount + 1,
});
// Log for debugging (non-intrusive)
if (import.meta.env.DEV) {
console.debug(`[MonitorWorker] ${severity}: ${message} (session=${sessionId})`);
}
}
// ========== Store ==========
export const useSessionManagerStore = create<SessionManagerStore>()(
devtools(
(set) => ({
...initialState,
// ========== Group Management ==========
createGroup: (name: string) => {
const id = `group-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
const newGroup: SessionGroup = { id, name, sessionIds: [] };
set(
(state) => ({ groups: [...state.groups, newGroup] }),
false,
'createGroup'
);
},
removeGroup: (groupId: string) => {
set(
(state) => ({ groups: state.groups.filter((g) => g.id !== groupId) }),
false,
'removeGroup'
);
},
moveSessionToGroup: (sessionId: string, groupId: string) => {
set(
(state) => {
const nextGroups = state.groups.map((group) => {
// Remove session from its current group
const filtered = group.sessionIds.filter((id) => id !== sessionId);
// Add to target group
if (group.id === groupId) {
return { ...group, sessionIds: [...filtered, sessionId] };
}
return { ...group, sessionIds: filtered };
});
return { groups: nextGroups };
},
false,
'moveSessionToGroup'
);
},
// ========== Terminal Selection ==========
setActiveTerminal: (sessionId: string | null) => {
set({ activeTerminalId: sessionId }, false, 'setActiveTerminal');
},
// ========== Terminal Metadata ==========
updateTerminalMeta: (sessionId: string, meta: Partial<TerminalMeta>) => {
set(
(state) => {
const existing = state.terminalMetas[sessionId] ?? {
title: sessionId,
status: 'idle' as const,
alertCount: 0,
};
return {
terminalMetas: {
...state.terminalMetas,
[sessionId]: { ...existing, ...meta },
},
};
},
false,
'updateTerminalMeta'
);
},
// ========== Layout Management ==========
setGroupLayout: (layout: SessionLayout) => {
set({ layout }, false, 'setGroupLayout');
},
// ========== Monitor Worker Lifecycle ==========
spawnMonitor: () => {
// Idempotent: only create if not already running
if (_workerRef) return;
try {
_workerRef = new Worker(
new URL('../workers/monitor.worker.ts', import.meta.url),
{ type: 'module' }
);
_workerRef.onmessage = _handleWorkerMessage;
_workerRef.onerror = (err) => {
if (import.meta.env.DEV) {
console.error('[MonitorWorker] error:', err);
}
};
} catch {
// Worker creation can fail in environments without worker support
_workerRef = null;
}
},
terminateMonitor: () => {
if (!_workerRef) return;
_workerRef.terminate();
_workerRef = null;
},
feedMonitor: (sessionId: string, text: string) => {
// Lazily spawn worker on first feed call
if (!_workerRef) {
useSessionManagerStore.getState().spawnMonitor();
}
if (_workerRef) {
_workerRef.postMessage({ type: 'output', sessionId, text });
}
},
}),
{ name: 'SessionManagerStore' }
)
);
// ========== Selectors ==========
/** Select all session groups */
export const selectGroups = (state: SessionManagerStore) => state.groups;
/** Select current terminal layout */
export const selectLayout = (state: SessionManagerStore) => state.layout;
/** Select active terminal session key */
export const selectSessionManagerActiveTerminalId = (state: SessionManagerStore) =>
state.activeTerminalId;
/** Select all terminal metadata records */
export const selectTerminalMetas = (state: SessionManagerStore) => state.terminalMetas;
/** Select terminal metadata for a specific session */
export const selectTerminalMeta =
(sessionId: string) =>
(state: SessionManagerStore): TerminalMeta | undefined =>
state.terminalMetas[sessionId];

View File

@@ -183,3 +183,21 @@ export type {
NodeComplexity,
GraphAnalysis,
} from './graph-explorer';
// ========== Terminal Dashboard Types ==========
export type {
// Session Manager
SessionGridLayout,
SessionLayout,
TerminalStatus,
TerminalMeta,
SessionGroup,
SessionManagerState,
SessionManagerActions,
SessionManagerStore,
// Issue Queue Integration
AssociationChain,
IssueQueueIntegrationState,
IssueQueueIntegrationActions,
IssueQueueIntegrationStore,
} from './terminal-dashboard';

View File

@@ -0,0 +1,120 @@
// ========================================
// Terminal Dashboard Types
// ========================================
// TypeScript interfaces for sessionManagerStore and issueQueueIntegrationStore.
// Domain types for the terminal execution management dashboard.
// ========== Session Manager Types ==========
/** Grid layout preset for terminal workbench */
export type SessionGridLayout = '1x1' | '1x2' | '2x1' | '2x2';
/** Terminal session layout configuration */
export interface SessionLayout {
/** Grid preset */
grid: SessionGridLayout;
/** Split ratios for each pane (normalized 0-1) */
splits: number[];
}
/** Terminal status indicator */
export type TerminalStatus = 'active' | 'idle' | 'error';
/** Metadata for a terminal instance in the dashboard */
export interface TerminalMeta {
/** Display title for the terminal tab */
title: string;
/** Current terminal status */
status: TerminalStatus;
/** Number of unread alerts (errors, warnings) */
alertCount: number;
}
/** Group of terminal sessions */
export interface SessionGroup {
/** Unique group identifier */
id: string;
/** Display name */
name: string;
/** Ordered list of session keys belonging to this group */
sessionIds: string[];
}
/** Session Manager store state (data only) */
export interface SessionManagerState {
/** All session groups */
groups: SessionGroup[];
/** Current terminal layout configuration */
layout: SessionLayout;
/** Currently active terminal session key */
activeTerminalId: string | null;
/** Per-terminal metadata keyed by session key */
terminalMetas: Record<string, TerminalMeta>;
}
/** Alert severity from the monitor worker */
export type AlertSeverity = 'critical' | 'warning';
/** Alert message posted from the monitor worker */
export interface MonitorAlert {
type: 'alert';
sessionId: string;
severity: AlertSeverity;
message: string;
}
/** Session Manager store actions */
export interface SessionManagerActions {
/** Create a new session group */
createGroup: (name: string) => void;
/** Remove a session group by ID */
removeGroup: (groupId: string) => void;
/** Move a session to a different group */
moveSessionToGroup: (sessionId: string, groupId: string) => void;
/** Set the active terminal by session key */
setActiveTerminal: (sessionId: string | null) => void;
/** Update metadata for a specific terminal */
updateTerminalMeta: (sessionId: string, meta: Partial<TerminalMeta>) => void;
/** Set the terminal grid layout */
setGroupLayout: (layout: SessionLayout) => void;
/** Spawn the monitor Web Worker (idempotent) */
spawnMonitor: () => void;
/** Terminate the monitor Web Worker */
terminateMonitor: () => void;
/** Forward a terminal output chunk to the monitor worker */
feedMonitor: (sessionId: string, text: string) => void;
}
export type SessionManagerStore = SessionManagerState & SessionManagerActions;
// ========== Issue Queue Integration Types ==========
/** Association chain linking an issue, queue item, and terminal session */
export interface AssociationChain {
/** Issue identifier (e.g., 'GH-123') */
issueId: string | null;
/** Queue item identifier (e.g., 'Q-456') */
queueItemId: string | null;
/** Terminal session key (e.g., 'T-789') */
sessionId: string | null;
}
/** Issue Queue Integration store state (data only) */
export interface IssueQueueIntegrationState {
/** Currently selected issue ID for highlight linkage */
selectedIssueId: string | null;
/** Current association chain resolved from any selected entity */
associationChain: AssociationChain | null;
}
/** Issue Queue Integration store actions */
export interface IssueQueueIntegrationActions {
/** Set the selected issue ID and trigger association chain resolution */
setSelectedIssue: (issueId: string | null) => void;
/** Build a full association chain from any entity ID (issue, queue item, or session) */
buildAssociationChain: (entityId: string, entityType: 'issue' | 'queue' | 'session') => void;
/** Internal: update queue item status bridging to queueExecutionStore */
_updateQueueItemStatus: (queueItemId: string, status: string, sessionId?: string) => void;
}
export type IssueQueueIntegrationStore = IssueQueueIntegrationState & IssueQueueIntegrationActions;

View File

@@ -0,0 +1,154 @@
// ========================================
// Monitor Web Worker
// ========================================
// Off-main-thread rule-based output analysis for terminal sessions.
// MVP rules:
// 1. Keyword matching: /error|failed|exception/i -> critical alert
// 2. Stall detection: no output for > 60s -> warning alert
//
// Message protocol:
// IN: { type: 'output', sessionId: string, text: string }
// IN: { type: 'reset', sessionId: string } -- reset session tracking
// OUT: { type: 'alert', sessionId: string, severity: string, message: string }
// ========== Types ==========
interface OutputMessage {
type: 'output';
sessionId: string;
text: string;
}
interface ResetMessage {
type: 'reset';
sessionId: string;
}
type IncomingMessage = OutputMessage | ResetMessage;
interface AlertMessage {
type: 'alert';
sessionId: string;
severity: 'critical' | 'warning';
message: string;
}
interface KeywordRule {
pattern: RegExp;
severity: 'critical' | 'warning';
label: string;
}
interface SessionState {
lastActivity: number;
alertCount: number;
/** Track stall alert to avoid repeated notifications per stall period */
stallAlerted: boolean;
}
// ========== Rules ==========
const KEYWORD_RULES: KeywordRule[] = [
{
pattern: /error|failed|exception/i,
severity: 'critical',
label: 'error keyword',
},
];
/** Stall threshold in milliseconds (60 seconds) */
const STALL_THRESHOLD_MS = 60_000;
/** Stall check interval in milliseconds (15 seconds) */
const STALL_CHECK_INTERVAL_MS = 15_000;
// ========== State ==========
const sessions = new Map<string, SessionState>();
// ========== Helpers ==========
function getOrCreateSession(sessionId: string): SessionState {
let state = sessions.get(sessionId);
if (!state) {
state = {
lastActivity: Date.now(),
alertCount: 0,
stallAlerted: false,
};
sessions.set(sessionId, state);
}
return state;
}
function postAlert(alert: AlertMessage): void {
self.postMessage(alert);
}
// ========== Output Processing ==========
function processOutput(sessionId: string, text: string): void {
const state = getOrCreateSession(sessionId);
state.lastActivity = Date.now();
// Reset stall alert flag on new output
state.stallAlerted = false;
// Run keyword rules against text
for (const rule of KEYWORD_RULES) {
if (rule.pattern.test(text)) {
state.alertCount++;
postAlert({
type: 'alert',
sessionId,
severity: rule.severity,
message: `Detected ${rule.label} in output`,
});
// Only report first matching rule per chunk to avoid alert flood
break;
}
}
}
// ========== Stall Detection ==========
function checkStalls(): void {
const now = Date.now();
sessions.forEach((state, sessionId) => {
if (state.stallAlerted) return;
const elapsed = now - state.lastActivity;
if (elapsed > STALL_THRESHOLD_MS) {
state.stallAlerted = true;
state.alertCount++;
const seconds = Math.floor(elapsed / 1000);
postAlert({
type: 'alert',
sessionId,
severity: 'warning',
message: `Session stalled: no output for ${seconds}s`,
});
}
});
}
// ========== Message Handler ==========
self.onmessage = (event: MessageEvent<IncomingMessage>) => {
const msg = event.data;
switch (msg.type) {
case 'output':
processOutput(msg.sessionId, msg.text);
break;
case 'reset':
sessions.delete(msg.sessionId);
break;
}
};
// ========== Periodic Stall Check ==========
const _stallInterval = setInterval(checkStalls, STALL_CHECK_INTERVAL_MS);
// Cleanup on worker termination (best-effort)
self.addEventListener('close', () => {
clearInterval(_stallInterval);
});