mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: add workflow-wave-plan skill and A2UI debug logging
- Add CSV Wave planning and execution skill (explore via wave → conflict resolution → execution CSV with linked exploration context) - Add debug NDJSON logging for A2UI submit-all and multi-answer polling
This commit is contained in:
774
.claude/skills/workflow-wave-plan/SKILL.md
Normal file
774
.claude/skills/workflow-wave-plan/SKILL.md
Normal file
@@ -0,0 +1,774 @@
|
||||
---
|
||||
name: workflow-wave-plan
|
||||
description: CSV Wave planning and execution - explore via wave, resolve conflicts, execute from CSV with linked exploration context. Triggers on "workflow:wave-plan".
|
||||
argument-hint: "<task description> [--yes|-y] [--concurrency|-c N]"
|
||||
allowed-tools: Task, AskUserQuestion, Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# Workflow Wave Plan
|
||||
|
||||
CSV Wave-based planning and execution. Uses structured CSV state for both exploration and execution, with cross-phase context propagation via `context_from` linking.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Requirement
|
||||
↓
|
||||
┌─ Phase 1: Decompose ─────────────────────┐
|
||||
│ Analyze requirement → explore.csv │
|
||||
│ (1 row per exploration angle) │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 2: Wave Explore ──────────────────┐
|
||||
│ Wave loop: spawn Explore agents │
|
||||
│ → findings/key_files → explore.csv │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 3: Synthesize & Plan ─────────────┐
|
||||
│ Read explore findings → cross-reference │
|
||||
│ → resolve conflicts → tasks.csv │
|
||||
│ (context_from links to E* explore rows) │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 4: Wave Execute ──────────────────┐
|
||||
│ Wave loop: build prev_context from CSV │
|
||||
│ → spawn code-developer agents per wave │
|
||||
│ → results → tasks.csv │
|
||||
└────────────────────┬──────────────────────┘
|
||||
↓
|
||||
┌─ Phase 5: Aggregate ─────────────────────┐
|
||||
│ results.csv + context.md + summary │
|
||||
└───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Context Flow
|
||||
|
||||
```
|
||||
explore.csv tasks.csv
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ E1: arch │──────────→│ T1: setup│ context_from: E1;E2
|
||||
│ findings │ │ prev_ctx │← E1+E2 findings
|
||||
├──────────┤ ├──────────┤
|
||||
│ E2: deps │──────────→│ T2: impl │ context_from: E1;T1
|
||||
│ findings │ │ prev_ctx │← E1+T1 findings
|
||||
├──────────┤ ├──────────┤
|
||||
│ E3: test │──┐ ┌───→│ T3: test │ context_from: E3;T2
|
||||
│ findings │ └───┘ │ prev_ctx │← E3+T2 findings
|
||||
└──────────┘ └──────────┘
|
||||
|
||||
Two context channels:
|
||||
1. Directed: context_from → prev_context (from CSV findings)
|
||||
2. Broadcast: discoveries.ndjson (append-only shared board)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSV Schemas
|
||||
|
||||
### explore.csv
|
||||
|
||||
| Column | Type | Set By | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `id` | string | Decomposer | E1, E2, ... |
|
||||
| `angle` | string | Decomposer | Exploration angle name |
|
||||
| `description` | string | Decomposer | What to explore from this angle |
|
||||
| `focus` | string | Decomposer | Keywords and focus areas |
|
||||
| `deps` | string | Decomposer | Semicolon-separated dep IDs (usually empty) |
|
||||
| `wave` | integer | Wave Engine | Wave number (usually 1) |
|
||||
| `status` | enum | Agent | pending / completed / failed |
|
||||
| `findings` | string | Agent | Discoveries (max 800 chars) |
|
||||
| `key_files` | string | Agent | Relevant files (semicolon-separated) |
|
||||
| `error` | string | Agent | Error message if failed |
|
||||
|
||||
### tasks.csv
|
||||
|
||||
| Column | Type | Set By | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `id` | string | Planner | T1, T2, ... |
|
||||
| `title` | string | Planner | Task title |
|
||||
| `description` | string | Planner | Self-contained task description |
|
||||
| `deps` | string | Planner | Dependency task IDs: T1;T2 |
|
||||
| `context_from` | string | Planner | Context source IDs: **E1;E2;T1** |
|
||||
| `wave` | integer | Wave Engine | Wave number (computed from deps) |
|
||||
| `status` | enum | Agent | pending / completed / failed / skipped |
|
||||
| `findings` | string | Agent | Execution findings (max 500 chars) |
|
||||
| `files_modified` | string | Agent | Files modified (semicolon-separated) |
|
||||
| `error` | string | Agent | Error if failed |
|
||||
|
||||
**context_from prefix convention**: `E*` → explore.csv lookup, `T*` → tasks.csv lookup.
|
||||
|
||||
---
|
||||
|
||||
## Session Structure
|
||||
|
||||
```
|
||||
.workflow/.wave-plan/{session-id}/
|
||||
├── explore.csv # Exploration state
|
||||
├── tasks.csv # Execution state
|
||||
├── discoveries.ndjson # Shared discovery board
|
||||
├── explore-results/ # Detailed per-angle results
|
||||
│ ├── E1.json
|
||||
│ └── E2.json
|
||||
├── task-results/ # Detailed per-task results
|
||||
│ ├── T1.json
|
||||
│ └── T2.json
|
||||
├── results.csv # Final results export
|
||||
└── context.md # Full context summary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Initialization
|
||||
|
||||
```javascript
|
||||
const getUtc8ISOString = () => new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString()
|
||||
|
||||
// Parse flags
|
||||
const AUTO_YES = $ARGUMENTS.includes('--yes') || $ARGUMENTS.includes('-y')
|
||||
const concurrencyMatch = $ARGUMENTS.match(/(?:--concurrency|-c)\s+(\d+)/)
|
||||
const maxConcurrency = concurrencyMatch ? parseInt(concurrencyMatch[1]) : 4
|
||||
|
||||
const requirement = $ARGUMENTS
|
||||
.replace(/--yes|-y|--concurrency\s+\d+|-c\s+\d+/g, '')
|
||||
.trim()
|
||||
|
||||
const slug = requirement.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
||||
.substring(0, 40)
|
||||
const dateStr = getUtc8ISOString().substring(0, 10).replace(/-/g, '')
|
||||
const sessionId = `wp-${slug}-${dateStr}`
|
||||
const sessionFolder = `.workflow/.wave-plan/${sessionId}`
|
||||
|
||||
Bash(`mkdir -p ${sessionFolder}/explore-results ${sessionFolder}/task-results`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Decompose → explore.csv
|
||||
|
||||
### 1.1 Analyze Requirement
|
||||
|
||||
```javascript
|
||||
const complexity = analyzeComplexity(requirement)
|
||||
// Low: 1 angle | Medium: 2-3 angles | High: 3-4 angles
|
||||
|
||||
const ANGLE_PRESETS = {
|
||||
architecture: ['architecture', 'dependencies', 'integration-points', 'modularity'],
|
||||
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(text, count) {
|
||||
let preset = 'feature'
|
||||
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|broken/.test(text)) preset = 'bugfix'
|
||||
return ANGLE_PRESETS[preset].slice(0, count)
|
||||
}
|
||||
|
||||
const angleCount = complexity === 'High' ? 4 : complexity === 'Medium' ? 3 : 1
|
||||
const angles = selectAngles(requirement, angleCount)
|
||||
```
|
||||
|
||||
### 1.2 Generate explore.csv
|
||||
|
||||
```javascript
|
||||
const header = 'id,angle,description,focus,deps,wave,status,findings,key_files,error'
|
||||
const rows = angles.map((angle, i) => {
|
||||
const id = `E${i + 1}`
|
||||
const desc = `Explore codebase from ${angle} perspective for: ${requirement}`
|
||||
return `"${id}","${angle}","${escCSV(desc)}","${angle}","",1,"pending","","",""`
|
||||
})
|
||||
|
||||
Write(`${sessionFolder}/explore.csv`, [header, ...rows].join('\n'))
|
||||
```
|
||||
|
||||
All exploration rows default to wave 1 (independent parallel). If angle dependencies exist, compute waves.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Wave Explore
|
||||
|
||||
Execute exploration waves using `Task(Explore)` agents.
|
||||
|
||||
### 2.1 Wave Loop
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const maxExploreWave = Math.max(...exploreCSV.map(r => parseInt(r.wave)))
|
||||
|
||||
for (let wave = 1; wave <= maxExploreWave; wave++) {
|
||||
const waveRows = exploreCSV.filter(r =>
|
||||
parseInt(r.wave) === wave && r.status === 'pending'
|
||||
)
|
||||
if (waveRows.length === 0) continue
|
||||
|
||||
// Skip rows with failed dependencies
|
||||
const validRows = waveRows.filter(r => {
|
||||
if (!r.deps) return true
|
||||
return r.deps.split(';').filter(Boolean).every(depId => {
|
||||
const dep = exploreCSV.find(d => d.id === depId)
|
||||
return dep && dep.status === 'completed'
|
||||
})
|
||||
})
|
||||
|
||||
waveRows.filter(r => !validRows.includes(r)).forEach(r => {
|
||||
r.status = 'skipped'
|
||||
r.error = 'Dependency failed/skipped'
|
||||
})
|
||||
|
||||
// ★ Spawn ALL explore agents in SINGLE message → parallel execution
|
||||
const results = validRows.map(row =>
|
||||
Task({
|
||||
subagent_type: "Explore",
|
||||
run_in_background: false,
|
||||
description: `Explore: ${row.angle}`,
|
||||
prompt: buildExplorePrompt(row, requirement, sessionFolder)
|
||||
})
|
||||
)
|
||||
|
||||
// Collect results from JSON files → update explore.csv
|
||||
validRows.forEach((row, i) => {
|
||||
const resultPath = `${sessionFolder}/explore-results/${row.id}.json`
|
||||
if (fileExists(resultPath)) {
|
||||
const result = JSON.parse(Read(resultPath))
|
||||
row.status = result.status || 'completed'
|
||||
row.findings = truncate(result.findings, 800)
|
||||
row.key_files = Array.isArray(result.key_files)
|
||||
? result.key_files.join(';')
|
||||
: (result.key_files || '')
|
||||
row.error = result.error || ''
|
||||
} else {
|
||||
// Fallback: parse from agent output text
|
||||
row.status = 'completed'
|
||||
row.findings = truncate(results[i], 800)
|
||||
}
|
||||
})
|
||||
|
||||
writeCSV(`${sessionFolder}/explore.csv`, exploreCSV)
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Explore Agent Prompt
|
||||
|
||||
```javascript
|
||||
function buildExplorePrompt(row, requirement, sessionFolder) {
|
||||
return `## Exploration: ${row.angle}
|
||||
|
||||
**Requirement**: ${requirement}
|
||||
**Focus**: ${row.focus}
|
||||
|
||||
## Instructions
|
||||
Explore the codebase from the **${row.angle}** perspective:
|
||||
1. Discover relevant files, modules, and patterns
|
||||
2. Identify integration points and dependencies
|
||||
3. Note constraints, risks, and conventions
|
||||
4. Find existing patterns to follow
|
||||
|
||||
## Output
|
||||
Write findings to: ${sessionFolder}/explore-results/${row.id}.json
|
||||
|
||||
JSON format:
|
||||
{
|
||||
"status": "completed",
|
||||
"findings": "Concise summary of ${row.angle} discoveries (max 800 chars)",
|
||||
"key_files": ["relevant/file1.ts", "relevant/file2.ts"],
|
||||
"details": {
|
||||
"patterns": ["pattern descriptions"],
|
||||
"integration_points": [{"file": "path", "description": "..."}],
|
||||
"constraints": ["constraint descriptions"],
|
||||
"recommendations": ["recommendation descriptions"]
|
||||
}
|
||||
}
|
||||
|
||||
Also provide a 2-3 sentence summary.`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Synthesize & Plan → tasks.csv
|
||||
|
||||
Read exploration findings, cross-reference, resolve conflicts, generate execution tasks.
|
||||
|
||||
### 3.1 Load Explore Results
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const completed = exploreCSV.filter(r => r.status === 'completed')
|
||||
|
||||
// Load detailed result JSONs where available
|
||||
const detailedResults = {}
|
||||
completed.forEach(r => {
|
||||
const path = `${sessionFolder}/explore-results/${r.id}.json`
|
||||
if (fileExists(path)) detailedResults[r.id] = JSON.parse(Read(path))
|
||||
})
|
||||
```
|
||||
|
||||
### 3.2 Conflict Resolution Protocol
|
||||
|
||||
Cross-reference findings across all exploration angles:
|
||||
|
||||
```javascript
|
||||
// 1. Identify common files referenced by multiple angles
|
||||
const fileRefs = {}
|
||||
completed.forEach(r => {
|
||||
r.key_files.split(';').filter(Boolean).forEach(f => {
|
||||
if (!fileRefs[f]) fileRefs[f] = []
|
||||
fileRefs[f].push({ angle: r.angle, id: r.id })
|
||||
})
|
||||
})
|
||||
const sharedFiles = Object.entries(fileRefs).filter(([_, refs]) => refs.length > 1)
|
||||
|
||||
// 2. Detect conflicting recommendations
|
||||
// Compare recommendations from different angles for same file/module
|
||||
// Flag contradictions (angle A says "refactor X" vs angle B says "extend X")
|
||||
|
||||
// 3. Resolution rules:
|
||||
// a. Safety first — when approaches conflict, choose safer option
|
||||
// b. Consistency — prefer approaches aligned with existing patterns
|
||||
// c. Scope — prefer minimal-change approaches
|
||||
// d. Document — note all resolved conflicts for transparency
|
||||
|
||||
const synthesis = {
|
||||
sharedFiles,
|
||||
conflicts: detectConflicts(completed, detailedResults),
|
||||
resolutions: [],
|
||||
allKeyFiles: [...new Set(completed.flatMap(r => r.key_files.split(';').filter(Boolean)))]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Generate tasks.csv
|
||||
|
||||
Decompose into execution tasks based on synthesized exploration:
|
||||
|
||||
```javascript
|
||||
// Task decomposition rules:
|
||||
// 1. Group by feature/module (not per-file)
|
||||
// 2. Each description is self-contained (agent sees only its row + prev_context)
|
||||
// 3. deps only when task B requires task A's output
|
||||
// 4. context_from links relevant explore rows (E*) and predecessor tasks (T*)
|
||||
// 5. Prefer parallel (minimize deps)
|
||||
// 6. Use exploration findings: key_files → target files, patterns → references,
|
||||
// integration_points → dependency relationships, constraints → included in description
|
||||
|
||||
const tasks = []
|
||||
// Claude decomposes requirement using exploration synthesis
|
||||
// Example:
|
||||
// tasks.push({ id: 'T1', title: 'Setup types', description: '...', deps: '', context_from: 'E1;E2' })
|
||||
// tasks.push({ id: 'T2', title: 'Implement core', description: '...', deps: 'T1', context_from: 'E1;E2;T1' })
|
||||
// tasks.push({ id: 'T3', title: 'Add tests', description: '...', deps: 'T2', context_from: 'E3;T2' })
|
||||
|
||||
// Compute waves
|
||||
const waves = computeWaves(tasks)
|
||||
tasks.forEach(t => { t.wave = waves[t.id] })
|
||||
|
||||
// Write tasks.csv
|
||||
const header = 'id,title,description,deps,context_from,wave,status,findings,files_modified,error'
|
||||
const rows = tasks.map(t =>
|
||||
`"${t.id}","${escCSV(t.title)}","${escCSV(t.description)}","${t.deps}","${t.context_from}",${t.wave},"pending","","",""`
|
||||
)
|
||||
|
||||
Write(`${sessionFolder}/tasks.csv`, [header, ...rows].join('\n'))
|
||||
```
|
||||
|
||||
### 3.4 User Confirmation
|
||||
|
||||
```javascript
|
||||
if (!AUTO_YES) {
|
||||
const maxWave = Math.max(...tasks.map(t => t.wave))
|
||||
|
||||
console.log(`
|
||||
## Execution Plan
|
||||
|
||||
Explore: ${completed.length} angles completed
|
||||
Conflicts resolved: ${synthesis.conflicts.length}
|
||||
Tasks: ${tasks.length} across ${maxWave} waves
|
||||
|
||||
${Array.from({length: maxWave}, (_, i) => i + 1).map(w => {
|
||||
const wt = tasks.filter(t => t.wave === w)
|
||||
return `### Wave ${w} (${wt.length} tasks, concurrent)
|
||||
${wt.map(t => ` - [${t.id}] ${t.title} (from: ${t.context_from})`).join('\n')}`
|
||||
}).join('\n')}
|
||||
`)
|
||||
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: `Proceed with ${tasks.length} tasks across ${maxWave} waves?`,
|
||||
header: "Confirm",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Execute", description: "Proceed with wave execution" },
|
||||
{ label: "Modify", description: `Edit ${sessionFolder}/tasks.csv then re-run` },
|
||||
{ label: "Cancel", description: "Abort" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Wave Execute
|
||||
|
||||
Execute tasks from tasks.csv in wave order, with prev_context built from both explore.csv and tasks.csv.
|
||||
|
||||
### 4.1 Wave Loop
|
||||
|
||||
```javascript
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
const failedIds = new Set()
|
||||
const skippedIds = new Set()
|
||||
|
||||
let tasksCSV = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
const maxWave = Math.max(...tasksCSV.map(r => parseInt(r.wave)))
|
||||
|
||||
for (let wave = 1; wave <= maxWave; wave++) {
|
||||
// Re-read master CSV (updated by previous wave)
|
||||
tasksCSV = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
|
||||
const waveRows = tasksCSV.filter(r =>
|
||||
parseInt(r.wave) === wave && r.status === 'pending'
|
||||
)
|
||||
if (waveRows.length === 0) continue
|
||||
|
||||
// Skip on failed dependencies (cascade)
|
||||
const validRows = []
|
||||
for (const row of waveRows) {
|
||||
const deps = (row.deps || '').split(';').filter(Boolean)
|
||||
if (deps.some(d => failedIds.has(d) || skippedIds.has(d))) {
|
||||
skippedIds.add(row.id)
|
||||
row.status = 'skipped'
|
||||
row.error = 'Dependency failed/skipped'
|
||||
continue
|
||||
}
|
||||
validRows.push(row)
|
||||
}
|
||||
|
||||
if (validRows.length === 0) {
|
||||
writeCSV(`${sessionFolder}/tasks.csv`, tasksCSV)
|
||||
continue
|
||||
}
|
||||
|
||||
// Build prev_context for each row from explore.csv + tasks.csv
|
||||
validRows.forEach(row => {
|
||||
row._prev_context = buildPrevContext(row.context_from, exploreCSV, tasksCSV)
|
||||
})
|
||||
|
||||
// ★ Spawn ALL task agents in SINGLE message → parallel execution
|
||||
const results = validRows.map(row =>
|
||||
Task({
|
||||
subagent_type: "code-developer",
|
||||
run_in_background: false,
|
||||
description: row.title,
|
||||
prompt: buildExecutePrompt(row, requirement, sessionFolder)
|
||||
})
|
||||
)
|
||||
|
||||
// Collect results → update tasks.csv
|
||||
validRows.forEach((row, i) => {
|
||||
const resultPath = `${sessionFolder}/task-results/${row.id}.json`
|
||||
if (fileExists(resultPath)) {
|
||||
const result = JSON.parse(Read(resultPath))
|
||||
row.status = result.status || 'completed'
|
||||
row.findings = truncate(result.findings, 500)
|
||||
row.files_modified = Array.isArray(result.files_modified)
|
||||
? result.files_modified.join(';')
|
||||
: (result.files_modified || '')
|
||||
row.error = result.error || ''
|
||||
} else {
|
||||
row.status = 'completed'
|
||||
row.findings = truncate(results[i], 500)
|
||||
}
|
||||
|
||||
if (row.status === 'failed') failedIds.add(row.id)
|
||||
delete row._prev_context // runtime-only, don't persist
|
||||
})
|
||||
|
||||
writeCSV(`${sessionFolder}/tasks.csv`, tasksCSV)
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 prev_context Builder
|
||||
|
||||
The key function linking exploration context to execution:
|
||||
|
||||
```javascript
|
||||
function buildPrevContext(contextFrom, exploreCSV, tasksCSV) {
|
||||
if (!contextFrom) return 'No previous context available'
|
||||
|
||||
const ids = contextFrom.split(';').filter(Boolean)
|
||||
const entries = []
|
||||
|
||||
ids.forEach(id => {
|
||||
if (id.startsWith('E')) {
|
||||
// ← Look up in explore.csv (cross-phase link)
|
||||
const row = exploreCSV.find(r => r.id === id)
|
||||
if (row && row.status === 'completed' && row.findings) {
|
||||
entries.push(`[Explore ${row.angle}] ${row.findings}`)
|
||||
if (row.key_files) entries.push(` Key files: ${row.key_files}`)
|
||||
}
|
||||
} else if (id.startsWith('T')) {
|
||||
// ← Look up in tasks.csv (same-phase link)
|
||||
const row = tasksCSV.find(r => r.id === id)
|
||||
if (row && row.status === 'completed' && row.findings) {
|
||||
entries.push(`[Task ${row.id}: ${row.title}] ${row.findings}`)
|
||||
if (row.files_modified) entries.push(` Modified: ${row.files_modified}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return entries.length > 0 ? entries.join('\n') : 'No previous context available'
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Execute Agent Prompt
|
||||
|
||||
```javascript
|
||||
function buildExecutePrompt(row, requirement, sessionFolder) {
|
||||
return `## Task: ${row.title}
|
||||
|
||||
**ID**: ${row.id}
|
||||
**Goal**: ${requirement}
|
||||
|
||||
## Description
|
||||
${row.description}
|
||||
|
||||
## Previous Context (from exploration and predecessor tasks)
|
||||
${row._prev_context}
|
||||
|
||||
## Discovery Board
|
||||
Read shared discoveries first: ${sessionFolder}/discoveries.ndjson (if exists)
|
||||
After execution, append any discoveries:
|
||||
echo '{"ts":"<ISO>","worker":"${row.id}","type":"<type>","data":{...}}' >> ${sessionFolder}/discoveries.ndjson
|
||||
|
||||
## Instructions
|
||||
1. Read the relevant files identified in the context above
|
||||
2. Implement changes described in the task description
|
||||
3. Ensure changes are consistent with exploration findings
|
||||
4. Test changes if applicable
|
||||
|
||||
## Output
|
||||
Write results to: ${sessionFolder}/task-results/${row.id}.json
|
||||
|
||||
{
|
||||
"status": "completed",
|
||||
"findings": "What was done (max 500 chars)",
|
||||
"files_modified": ["file1.ts", "file2.ts"],
|
||||
"error": ""
|
||||
}`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Aggregate
|
||||
|
||||
### 5.1 Generate Results
|
||||
|
||||
```javascript
|
||||
const finalTasks = parseCSV(Read(`${sessionFolder}/tasks.csv`))
|
||||
const exploreCSV = parseCSV(Read(`${sessionFolder}/explore.csv`))
|
||||
|
||||
Bash(`cp "${sessionFolder}/tasks.csv" "${sessionFolder}/results.csv"`)
|
||||
|
||||
const completed = finalTasks.filter(r => r.status === 'completed')
|
||||
const failed = finalTasks.filter(r => r.status === 'failed')
|
||||
const skipped = finalTasks.filter(r => r.status === 'skipped')
|
||||
const maxWave = Math.max(...finalTasks.map(r => parseInt(r.wave)))
|
||||
```
|
||||
|
||||
### 5.2 Generate context.md
|
||||
|
||||
```javascript
|
||||
const contextMd = `# Wave Plan Results
|
||||
|
||||
**Requirement**: ${requirement}
|
||||
**Session**: ${sessionId}
|
||||
**Timestamp**: ${getUtc8ISOString()}
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Count |
|
||||
|--------|-------|
|
||||
| Explore Angles | ${exploreCSV.length} |
|
||||
| Total Tasks | ${finalTasks.length} |
|
||||
| Completed | ${completed.length} |
|
||||
| Failed | ${failed.length} |
|
||||
| Skipped | ${skipped.length} |
|
||||
| Waves | ${maxWave} |
|
||||
|
||||
## Exploration Results
|
||||
|
||||
${exploreCSV.map(e => `### ${e.id}: ${e.angle} (${e.status})
|
||||
${e.findings || 'N/A'}
|
||||
Key files: ${e.key_files || 'none'}`).join('\n\n')}
|
||||
|
||||
## Task Results
|
||||
|
||||
${finalTasks.map(t => `### ${t.id}: ${t.title} (${t.status})
|
||||
- Context from: ${t.context_from || 'none'}
|
||||
- Wave: ${t.wave}
|
||||
- Findings: ${t.findings || 'N/A'}
|
||||
- Files: ${t.files_modified || 'none'}
|
||||
${t.error ? `- Error: ${t.error}` : ''}`).join('\n\n')}
|
||||
|
||||
## All Modified Files
|
||||
|
||||
${[...new Set(finalTasks.flatMap(t =>
|
||||
(t.files_modified || '').split(';')).filter(Boolean)
|
||||
)].map(f => '- ' + f).join('\n') || 'None'}
|
||||
`
|
||||
|
||||
Write(`${sessionFolder}/context.md`, contextMd)
|
||||
```
|
||||
|
||||
### 5.3 Summary & Next Steps
|
||||
|
||||
```javascript
|
||||
console.log(`
|
||||
## Wave Plan Complete
|
||||
|
||||
Session: ${sessionFolder}
|
||||
Explore: ${exploreCSV.filter(r => r.status === 'completed').length}/${exploreCSV.length} angles
|
||||
Tasks: ${completed.length}/${finalTasks.length} completed, ${failed.length} failed, ${skipped.length} skipped
|
||||
Waves: ${maxWave}
|
||||
|
||||
Files:
|
||||
- explore.csv — exploration state
|
||||
- tasks.csv — execution state
|
||||
- results.csv — final results
|
||||
- context.md — full report
|
||||
- discoveries.ndjson — shared discoveries
|
||||
`)
|
||||
|
||||
if (!AUTO_YES && failed.length > 0) {
|
||||
AskUserQuestion({
|
||||
questions: [{
|
||||
question: `${failed.length} tasks failed. Next action?`,
|
||||
header: "Next Step",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Retry Failed", description: "Reset failed + skipped, re-execute Phase 4" },
|
||||
{ label: "View Report", description: "Display context.md" },
|
||||
{ label: "Done", description: "Complete session" }
|
||||
]
|
||||
}]
|
||||
})
|
||||
// If Retry: reset failed/skipped status to pending, re-run Phase 4
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Utilities
|
||||
|
||||
### Wave Computation (Kahn's BFS)
|
||||
|
||||
```javascript
|
||||
function computeWaves(tasks) {
|
||||
const inDegree = {}, adj = {}, depth = {}
|
||||
tasks.forEach(t => { inDegree[t.id] = 0; adj[t.id] = []; depth[t.id] = 1 })
|
||||
|
||||
tasks.forEach(t => {
|
||||
const deps = (t.deps || '').split(';').filter(Boolean)
|
||||
deps.forEach(dep => {
|
||||
if (adj[dep]) { adj[dep].push(t.id); inDegree[t.id]++ }
|
||||
})
|
||||
})
|
||||
|
||||
const queue = Object.keys(inDegree).filter(id => inDegree[id] === 0)
|
||||
queue.forEach(id => { depth[id] = 1 })
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()
|
||||
adj[current].forEach(next => {
|
||||
depth[next] = Math.max(depth[next], depth[current] + 1)
|
||||
inDegree[next]--
|
||||
if (inDegree[next] === 0) queue.push(next)
|
||||
})
|
||||
}
|
||||
|
||||
if (Object.values(inDegree).some(d => d > 0)) {
|
||||
throw new Error('Circular dependency detected')
|
||||
}
|
||||
|
||||
return depth // { taskId: waveNumber }
|
||||
}
|
||||
```
|
||||
|
||||
### CSV Helpers
|
||||
|
||||
```javascript
|
||||
function escCSV(s) { return String(s || '').replace(/"/g, '""') }
|
||||
|
||||
function parseCSV(content) {
|
||||
const lines = content.trim().split('\n')
|
||||
const header = lines[0].split(',').map(h => h.replace(/"/g, '').trim())
|
||||
return lines.slice(1).filter(l => l.trim()).map(line => {
|
||||
const values = parseCSVLine(line)
|
||||
const row = {}
|
||||
header.forEach((col, i) => { row[col] = (values[i] || '').replace(/^"|"$/g, '') })
|
||||
return row
|
||||
})
|
||||
}
|
||||
|
||||
function writeCSV(path, rows) {
|
||||
if (rows.length === 0) return
|
||||
// Exclude runtime-only columns (prefixed with _)
|
||||
const cols = Object.keys(rows[0]).filter(k => !k.startsWith('_'))
|
||||
const header = cols.join(',')
|
||||
const lines = rows.map(r =>
|
||||
cols.map(c => `"${escCSV(r[c])}"`).join(',')
|
||||
)
|
||||
Write(path, [header, ...lines].join('\n'))
|
||||
}
|
||||
|
||||
function truncate(s, max) {
|
||||
s = String(s || '')
|
||||
return s.length > max ? s.substring(0, max - 3) + '...' : s
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Discovery Board Protocol
|
||||
|
||||
Shared `discoveries.ndjson` — append-only NDJSON accessible to all agents across all phases.
|
||||
|
||||
```jsonl
|
||||
{"ts":"...","worker":"E1","type":"code_pattern","data":{"name":"repo-pattern","file":"src/repos/Base.ts"}}
|
||||
{"ts":"...","worker":"T2","type":"integration_point","data":{"file":"src/auth/index.ts","exports":["auth"]}}
|
||||
```
|
||||
|
||||
**Types**: `code_pattern`, `integration_point`, `convention`, `blocker`, `tech_stack`
|
||||
**Rules**: Read first → write immediately → deduplicate → append-only
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Resolution |
|
||||
|-------|------------|
|
||||
| Explore agent failure | Mark as failed in explore.csv, exclude from planning |
|
||||
| Execute agent failure | Mark as failed, skip dependents (cascade) |
|
||||
| Circular dependency | Abort wave computation, report cycle |
|
||||
| All explores failed | Fallback: plan directly from requirement |
|
||||
| CSV parse error | Re-validate format |
|
||||
| discoveries.ndjson corrupt | Ignore malformed lines |
|
||||
|
||||
---
|
||||
|
||||
## Core Rules
|
||||
|
||||
1. **Wave Order is Sacred**: Never execute wave N before wave N-1 completes
|
||||
2. **CSV is Source of Truth**: Read master CSV before each wave, write after
|
||||
3. **Context via CSV**: prev_context built from CSV findings, not from memory
|
||||
4. **E* ↔ T* Linking**: tasks.csv `context_from` references explore.csv rows for cross-phase context
|
||||
5. **Skip on Failure**: Failed dep → skip dependent (cascade)
|
||||
6. **Discovery Board Append-Only**: Never clear or modify discoveries.ndjson
|
||||
7. **Explore Before Execute**: Phase 2 completes before Phase 4 starts
|
||||
8. **DO NOT STOP**: Continuous execution until all waves complete or remaining skipped
|
||||
@@ -341,6 +341,77 @@ export class A2UIWebSocketHandler {
|
||||
): boolean {
|
||||
const params = action.parameters ?? {};
|
||||
const questionId = typeof params.questionId === 'string' ? params.questionId : undefined;
|
||||
|
||||
// Handle submit-all first - it uses compositeId instead of questionId
|
||||
if (action.actionId === 'submit-all') {
|
||||
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
|
||||
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
|
||||
if (!compositeId || !questionIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// DEBUG: NDJSON log for submit-all received
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H2',
|
||||
event: 'submit_all_received_in_handleQuestionAction',
|
||||
compositeId,
|
||||
questionCount: questionIds.length
|
||||
}));
|
||||
|
||||
// Collect answers for all sub-questions
|
||||
const answers: QuestionAnswer[] = [];
|
||||
for (const qId of questionIds) {
|
||||
const singleSel = this.singleSelectSelections.get(qId);
|
||||
const multiSel = this.multiSelectSelections.get(qId);
|
||||
const inputVal = this.inputValues.get(qId);
|
||||
const otherText = this.inputValues.get(`__other__:${qId}`);
|
||||
|
||||
if (singleSel !== undefined) {
|
||||
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
|
||||
answers.push({ questionId: qId, value, cancelled: false });
|
||||
} else if (multiSel !== undefined) {
|
||||
const values = Array.from(multiSel).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
answers.push({ questionId: qId, value: values, cancelled: false });
|
||||
} else if (inputVal !== undefined) {
|
||||
answers.push({ questionId: qId, value: inputVal, cancelled: false });
|
||||
} else {
|
||||
answers.push({ questionId: qId, value: '', cancelled: false });
|
||||
}
|
||||
|
||||
// Cleanup per-question tracking
|
||||
this.singleSelectSelections.delete(qId);
|
||||
this.multiSelectSelections.delete(qId);
|
||||
this.inputValues.delete(qId);
|
||||
this.inputValues.delete(`__other__:${qId}`);
|
||||
}
|
||||
|
||||
// Call multi-answer callback
|
||||
let handled = false;
|
||||
if (this.multiAnswerCallback) {
|
||||
handled = this.multiAnswerCallback(compositeId, answers);
|
||||
}
|
||||
if (!handled) {
|
||||
// Store for HTTP polling retrieval
|
||||
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
|
||||
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H2',
|
||||
event: 'answer_stored_for_polling',
|
||||
compositeId,
|
||||
answerCount: answers.length
|
||||
}));
|
||||
}
|
||||
this.activeSurfaces.delete(compositeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// For other actions, questionId is required
|
||||
if (!questionId) {
|
||||
return false;
|
||||
}
|
||||
@@ -446,60 +517,6 @@ export class A2UIWebSocketHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'submit-all': {
|
||||
// Multi-question composite submit
|
||||
const compositeId = typeof params.compositeId === 'string' ? params.compositeId : undefined;
|
||||
const questionIds = Array.isArray(params.questionIds) ? params.questionIds as string[] : undefined;
|
||||
if (!compositeId || !questionIds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect answers for all sub-questions
|
||||
const answers: QuestionAnswer[] = [];
|
||||
for (const qId of questionIds) {
|
||||
const singleSel = this.singleSelectSelections.get(qId);
|
||||
const multiSel = this.multiSelectSelections.get(qId);
|
||||
const inputVal = this.inputValues.get(qId);
|
||||
const otherText = this.inputValues.get(`__other__:${qId}`);
|
||||
|
||||
if (singleSel !== undefined) {
|
||||
// Resolve __other__ to actual text input
|
||||
const value = singleSel === '__other__' && otherText ? otherText : singleSel;
|
||||
answers.push({ questionId: qId, value, cancelled: false });
|
||||
} else if (multiSel !== undefined) {
|
||||
// Resolve __other__ in multi-select: replace with actual text
|
||||
const values = Array.from(multiSel).map(v =>
|
||||
v === '__other__' && otherText ? otherText : v
|
||||
);
|
||||
answers.push({ questionId: qId, value: values, cancelled: false });
|
||||
} else if (inputVal !== undefined) {
|
||||
answers.push({ questionId: qId, value: inputVal, cancelled: false });
|
||||
} else {
|
||||
// No value recorded — include empty
|
||||
answers.push({ questionId: qId, value: '', cancelled: false });
|
||||
}
|
||||
|
||||
// Cleanup per-question tracking
|
||||
this.singleSelectSelections.delete(qId);
|
||||
this.multiSelectSelections.delete(qId);
|
||||
this.inputValues.delete(qId);
|
||||
this.inputValues.delete(`__other__:${qId}`);
|
||||
}
|
||||
|
||||
// Call multi-answer callback
|
||||
let handled = false;
|
||||
if (this.multiAnswerCallback) {
|
||||
handled = this.multiAnswerCallback(compositeId, answers);
|
||||
}
|
||||
if (!handled) {
|
||||
// Store for HTTP polling retrieval
|
||||
this.resolvedMultiAnswers.set(compositeId, { compositeId, answers, timestamp: Date.now() });
|
||||
}
|
||||
// Always clean up UI state
|
||||
this.activeSurfaces.delete(compositeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -611,8 +611,29 @@ export function handleAnswer(answer: QuestionAnswer): boolean {
|
||||
* @returns True if answer was processed
|
||||
*/
|
||||
export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]): boolean {
|
||||
const handleStartTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for handleMultiAnswer start
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_start',
|
||||
compositeId,
|
||||
answerCount: answers.length
|
||||
}));
|
||||
|
||||
const pending = getPendingQuestion(compositeId);
|
||||
if (!pending) {
|
||||
// DEBUG: NDJSON log for missing pending question
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_no_pending',
|
||||
compositeId,
|
||||
elapsedMs: Date.now() - handleStartTime
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -625,6 +646,17 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
|
||||
});
|
||||
|
||||
removePendingQuestion(compositeId);
|
||||
|
||||
// DEBUG: NDJSON log for handleMultiAnswer complete
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H3',
|
||||
event: 'handle_multi_answer_complete',
|
||||
compositeId,
|
||||
elapsedMs: Date.now() - handleStartTime
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -637,9 +669,24 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
|
||||
*/
|
||||
function startAnswerPolling(questionId: string, isComposite: boolean = false): void {
|
||||
const pollPath = `/api/a2ui/answer?questionId=${encodeURIComponent(questionId)}&composite=${isComposite}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for polling start
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'polling_start',
|
||||
questionId,
|
||||
isComposite,
|
||||
port: DASHBOARD_PORT,
|
||||
firstPollDelayMs: POLL_INTERVAL_MS
|
||||
}));
|
||||
|
||||
console.error(`[A2UI-Poll] Starting polling for questionId=${questionId}, composite=${isComposite}, port=${DASHBOARD_PORT}`);
|
||||
|
||||
let pollCount = 0;
|
||||
|
||||
const poll = () => {
|
||||
// Stop if the question was already resolved or timed out
|
||||
if (!hasPendingQuestion(questionId)) {
|
||||
@@ -647,6 +694,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
|
||||
return;
|
||||
}
|
||||
|
||||
pollCount++;
|
||||
const pollStartTime = Date.now();
|
||||
|
||||
// DEBUG: NDJSON log for each poll attempt
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'poll_attempt',
|
||||
questionId,
|
||||
pollCount,
|
||||
elapsedMs: pollStartTime - startTime
|
||||
}));
|
||||
|
||||
const req = http.get({ hostname: '127.0.0.1', port: DASHBOARD_PORT, path: pollPath, timeout: 2000 }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
||||
@@ -667,6 +728,20 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
|
||||
|
||||
console.error(`[A2UI-Poll] Answer received for questionId=${questionId}:`, JSON.stringify(parsed).slice(0, 200));
|
||||
|
||||
// DEBUG: NDJSON log for answer received
|
||||
console.log(JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'DEBUG',
|
||||
hid: 'H1',
|
||||
event: 'answer_received',
|
||||
questionId,
|
||||
pollCount,
|
||||
elapsedMs: Date.now() - startTime,
|
||||
pollLatencyMs: Date.now() - pollStartTime,
|
||||
isComposite,
|
||||
answerPreview: JSON.stringify(parsed).slice(0, 100)
|
||||
}));
|
||||
|
||||
if (isComposite && Array.isArray(parsed.answers)) {
|
||||
const ok = handleMultiAnswer(questionId, parsed.answers as QuestionAnswer[]);
|
||||
console.error(`[A2UI-Poll] handleMultiAnswer result: ${ok}`);
|
||||
|
||||
Reference in New Issue
Block a user