@haoyiyin/workflow 0.2.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/agents/contracts/implementer.d.ts +29 -0
- package/dist/src/agents/contracts/implementer.d.ts.map +1 -0
- package/dist/src/agents/contracts/implementer.js +94 -0
- package/dist/src/agents/contracts/implementer.js.map +1 -0
- package/dist/src/agents/contracts/index.d.ts +11 -0
- package/dist/src/agents/contracts/index.d.ts.map +1 -0
- package/dist/src/agents/contracts/index.js +11 -0
- package/dist/src/agents/contracts/index.js.map +1 -0
- package/dist/src/agents/contracts/planner.d.ts +25 -0
- package/dist/src/agents/contracts/planner.d.ts.map +1 -0
- package/dist/src/agents/contracts/planner.js +107 -0
- package/dist/src/agents/contracts/planner.js.map +1 -0
- package/dist/src/agents/contracts/router.d.ts +24 -0
- package/dist/src/agents/contracts/router.d.ts.map +1 -0
- package/dist/src/agents/contracts/router.js +137 -0
- package/dist/src/agents/contracts/router.js.map +1 -0
- package/dist/src/agents/contracts/verifier.d.ts +27 -0
- package/dist/src/agents/contracts/verifier.d.ts.map +1 -0
- package/dist/src/agents/contracts/verifier.js +115 -0
- package/dist/src/agents/contracts/verifier.js.map +1 -0
- package/dist/src/agents/dispatcher.d.ts +94 -51
- package/dist/src/agents/dispatcher.d.ts.map +1 -1
- package/dist/src/agents/dispatcher.js +207 -164
- package/dist/src/agents/dispatcher.js.map +1 -1
- package/dist/src/persistence/index.d.ts +4 -2
- package/dist/src/persistence/index.d.ts.map +1 -1
- package/dist/src/persistence/index.js +4 -1
- package/dist/src/persistence/index.js.map +1 -1
- package/dist/src/persistence/plan-md.d.ts +3 -2
- package/dist/src/persistence/plan-md.d.ts.map +1 -1
- package/dist/src/persistence/plan-md.js +47 -15
- package/dist/src/persistence/plan-md.js.map +1 -1
- package/dist/src/persistence/state-md.d.ts +2 -0
- package/dist/src/persistence/state-md.d.ts.map +1 -1
- package/dist/src/persistence/state-md.js +40 -22
- package/dist/src/persistence/state-md.js.map +1 -1
- package/dist/src/persistence/types.d.ts +35 -39
- package/dist/src/persistence/types.d.ts.map +1 -1
- package/dist/src/router/namespace/core/intent-router.d.ts +24 -0
- package/dist/src/router/namespace/core/intent-router.d.ts.map +1 -0
- package/dist/src/router/namespace/core/intent-router.js +190 -0
- package/dist/src/router/namespace/core/intent-router.js.map +1 -0
- package/dist/src/router/namespace/core/lifecycle-router.d.ts +28 -0
- package/dist/src/router/namespace/core/lifecycle-router.d.ts.map +1 -0
- package/dist/src/router/namespace/core/lifecycle-router.js +132 -0
- package/dist/src/router/namespace/core/lifecycle-router.js.map +1 -0
- package/dist/src/router/namespace/core/state-router.d.ts +32 -0
- package/dist/src/router/namespace/core/state-router.d.ts.map +1 -0
- package/dist/src/router/namespace/core/state-router.js +157 -0
- package/dist/src/router/namespace/core/state-router.js.map +1 -0
- package/dist/src/router/namespace/domain/code-router.d.ts +26 -0
- package/dist/src/router/namespace/domain/code-router.d.ts.map +1 -0
- package/dist/src/router/namespace/domain/code-router.js +171 -0
- package/dist/src/router/namespace/domain/code-router.js.map +1 -0
- package/dist/src/router/namespace/domain/debug-router.d.ts +25 -0
- package/dist/src/router/namespace/domain/debug-router.d.ts.map +1 -0
- package/dist/src/router/namespace/domain/debug-router.js +139 -0
- package/dist/src/router/namespace/domain/debug-router.js.map +1 -0
- package/dist/src/router/namespace/domain/plan-router.d.ts +29 -0
- package/dist/src/router/namespace/domain/plan-router.d.ts.map +1 -0
- package/dist/src/router/namespace/domain/plan-router.js +160 -0
- package/dist/src/router/namespace/domain/plan-router.js.map +1 -0
- package/dist/src/router/namespace/domain/review-router.d.ts +24 -0
- package/dist/src/router/namespace/domain/review-router.d.ts.map +1 -0
- package/dist/src/router/namespace/domain/review-router.js +116 -0
- package/dist/src/router/namespace/domain/review-router.js.map +1 -0
- package/dist/src/router/namespace/index.d.ts +19 -0
- package/dist/src/router/namespace/index.d.ts.map +1 -0
- package/dist/src/router/namespace/index.js +22 -0
- package/dist/src/router/namespace/index.js.map +1 -0
- package/dist/src/router/namespace/registry.d.ts +67 -0
- package/dist/src/router/namespace/registry.d.ts.map +1 -0
- package/dist/src/router/namespace/registry.js +197 -0
- package/dist/src/router/namespace/registry.js.map +1 -0
- package/dist/src/router/namespace/types.d.ts +124 -0
- package/dist/src/router/namespace/types.d.ts.map +1 -0
- package/dist/src/router/namespace/types.js +20 -0
- package/dist/src/router/namespace/types.js.map +1 -0
- package/dist/src/router/namespace/utility/fallback-router.d.ts +28 -0
- package/dist/src/router/namespace/utility/fallback-router.d.ts.map +1 -0
- package/dist/src/router/namespace/utility/fallback-router.js +88 -0
- package/dist/src/router/namespace/utility/fallback-router.js.map +1 -0
- package/dist/src/router/namespace/utility/quick-task-router.d.ts +28 -0
- package/dist/src/router/namespace/utility/quick-task-router.d.ts.map +1 -0
- package/dist/src/router/namespace/utility/quick-task-router.js +99 -0
- package/dist/src/router/namespace/utility/quick-task-router.js.map +1 -0
- package/dist/src/router/namespace/utility/research-router.d.ts +24 -0
- package/dist/src/router/namespace/utility/research-router.d.ts.map +1 -0
- package/dist/src/router/namespace/utility/research-router.js +84 -0
- package/dist/src/router/namespace/utility/research-router.js.map +1 -0
- package/dist/src/skills/agents-md/index.js +2 -2
- package/dist/src/skills/agents-md/index.js.map +1 -1
- package/dist/src/skills/execute-plan/index.d.ts +45 -65
- package/dist/src/skills/execute-plan/index.d.ts.map +1 -1
- package/dist/src/skills/execute-plan/index.js +325 -551
- package/dist/src/skills/execute-plan/index.js.map +1 -1
- package/dist/src/skills/index.d.ts +1 -0
- package/dist/src/skills/index.d.ts.map +1 -1
- package/dist/src/skills/index.js +1 -0
- package/dist/src/skills/index.js.map +1 -1
- package/dist/src/skills/quick-task/index.d.ts +4 -4
- package/dist/src/skills/quick-task/index.js +1 -1
- package/dist/src/skills/quick-task/index.js.map +1 -1
- package/dist/src/skills/review-diff/index.d.ts +6 -6
- package/dist/src/skills/review-diff/index.js +1 -1
- package/dist/src/skills/review-diff/index.js.map +1 -1
- package/dist/src/skills/router/index.d.ts +101 -0
- package/dist/src/skills/router/index.d.ts.map +1 -0
- package/dist/src/skills/router/index.js +450 -0
- package/dist/src/skills/router/index.js.map +1 -0
- package/dist/src/skills/router/types.d.ts +79 -0
- package/dist/src/skills/router/types.d.ts.map +1 -0
- package/dist/src/skills/router/types.js +8 -0
- package/dist/src/skills/router/types.js.map +1 -0
- package/dist/src/skills/systematic-debugging/index.js +1 -1
- package/dist/src/skills/systematic-debugging/index.js.map +1 -1
- package/dist/src/skills/tdd/index.d.ts +14 -14
- package/dist/src/skills/tdd/index.js +1 -1
- package/dist/src/skills/tdd/index.js.map +1 -1
- package/dist/src/skills/to-plan/index-enhanced.d.ts +4 -4
- package/dist/src/skills/to-plan/index-enhanced.d.ts.map +1 -1
- package/dist/src/skills/to-plan/index-enhanced.js +3 -5
- package/dist/src/skills/to-plan/index-enhanced.js.map +1 -1
- package/dist/src/skills/to-plan/index.d.ts +24 -91
- package/dist/src/skills/to-plan/index.d.ts.map +1 -1
- package/dist/src/skills/to-plan/index.js +214 -409
- package/dist/src/skills/to-plan/index.js.map +1 -1
- package/package.json +3 -5
- package/src/agents/contracts/implementer.ts +122 -0
- package/src/agents/contracts/index.ts +27 -0
- package/src/agents/contracts/planner.ts +129 -0
- package/src/agents/contracts/router.ts +168 -0
- package/src/agents/contracts/verifier.ts +137 -0
- package/src/agents/dispatcher.ts +387 -362
- package/src/persistence/index.ts +10 -4
- package/src/persistence/plan-md.ts +52 -18
- package/src/persistence/state-md.ts +45 -23
- package/src/persistence/types.ts +37 -40
- package/src/router/namespace/README.md +127 -0
- package/src/router/namespace/core/intent-router.ts +221 -0
- package/src/router/namespace/core/lifecycle-router.ts +156 -0
- package/src/router/namespace/core/state-router.ts +192 -0
- package/src/router/namespace/domain/code-router.ts +202 -0
- package/src/router/namespace/domain/debug-router.ts +167 -0
- package/src/router/namespace/domain/plan-router.ts +196 -0
- package/src/router/namespace/domain/review-router.ts +142 -0
- package/src/router/namespace/index.ts +84 -0
- package/src/router/namespace/registry.ts +242 -0
- package/src/router/namespace/types.ts +182 -0
- package/src/router/namespace/utility/fallback-router.ts +107 -0
- package/src/router/namespace/utility/quick-task-router.ts +121 -0
- package/src/router/namespace/utility/research-router.ts +105 -0
- package/src/skills/agents-md/index.ts +2 -2
- package/src/skills/execute-plan/index.ts +419 -673
- package/src/skills/index.ts +1 -0
- package/src/skills/quick-task/index.ts +1 -1
- package/src/skills/review-diff/index.ts +1 -1
- package/src/skills/router/SKILL.md +181 -0
- package/src/skills/router/index.ts +577 -0
- package/src/skills/router/types.ts +90 -0
- package/src/skills/systematic-debugging/index.ts +1 -1
- package/src/skills/tdd/index.ts +1 -1
- package/src/skills/to-plan/index-enhanced.ts +3 -5
- package/src/skills/to-plan/index.ts +231 -502
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Execute Plan Skill - Orchestrates plan execution with persistence integration
|
|
3
3
|
*
|
|
4
4
|
* Architecture: Orchestrator pattern
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
5
|
+
* - Reads PLAN.md from PlanMdManager
|
|
6
|
+
* - Reads/updates STATE.md for progress tracking
|
|
7
|
+
* - Executes waves sequentially, tasks within wave in parallel
|
|
8
|
+
* - Each task dispatches implementer subagent with fresh context
|
|
9
|
+
* - After each wave: updates STATE.md with completed tasks
|
|
10
|
+
* - After all waves: runs verification subagents (goals, quality, tests)
|
|
11
|
+
* - Updates final state based on verification results
|
|
10
12
|
*/
|
|
11
13
|
import { z } from 'zod'
|
|
12
14
|
import { Skill } from '../skill.js'
|
|
13
15
|
import type { SkillContext } from '../types.js'
|
|
14
|
-
import type { Plan, Wave, TaskResult, VerificationResult, DimensionResult } from '../../persistence/types.js'
|
|
16
|
+
import type { Plan, Wave, Task, TaskResult, VerificationResult, DimensionResult } from '../../persistence/types.js'
|
|
15
17
|
import type { HookType } from '../../types.js'
|
|
18
|
+
import { implementerContract, verifierContract, reviewerContract } from '../../agents/contracts.js'
|
|
19
|
+
|
|
16
20
|
// HookType constants (avoiding enum import issues)
|
|
17
21
|
const HookType = {
|
|
18
22
|
PRE_EXECUTE: 'pre-execute',
|
|
@@ -27,13 +31,11 @@ const HookType = {
|
|
|
27
31
|
WAVE_START: 'wave-start',
|
|
28
32
|
WAVE_COMPLETE: 'wave-complete',
|
|
29
33
|
} as const
|
|
30
|
-
import { worktreeContract } from '../../agents/contracts.js'
|
|
31
34
|
|
|
32
35
|
const ExecuteInputSchema = z.object({
|
|
33
|
-
|
|
34
|
-
waves: z.array(z.any()),
|
|
36
|
+
model: z.string().optional(),
|
|
35
37
|
skipVerify: z.boolean().optional(),
|
|
36
|
-
|
|
38
|
+
continueFromState: z.boolean().optional(),
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
const ExecuteOutputSchema = z.object({
|
|
@@ -41,93 +43,110 @@ const ExecuteOutputSchema = z.object({
|
|
|
41
43
|
results: z.array(z.any()),
|
|
42
44
|
verification: z.any(),
|
|
43
45
|
wavesCompleted: z.number(),
|
|
44
|
-
totalWaves: z.number()
|
|
46
|
+
totalWaves: z.number(),
|
|
45
47
|
})
|
|
46
48
|
|
|
47
49
|
type ExecuteInput = z.infer<typeof ExecuteInputSchema>
|
|
48
50
|
type ExecuteOutput = z.infer<typeof ExecuteOutputSchema>
|
|
49
51
|
|
|
52
|
+
interface WaveExecutionResult {
|
|
53
|
+
waveId: string
|
|
54
|
+
results: TaskResult[]
|
|
55
|
+
success: boolean
|
|
56
|
+
error?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
51
60
|
constructor() {
|
|
52
61
|
super({
|
|
53
62
|
name: 'execute-plan',
|
|
54
|
-
description: '
|
|
55
|
-
requires: [
|
|
63
|
+
description: 'Execute plan from PLAN.md with STATE.md tracking, parallel task execution, and verification',
|
|
64
|
+
requires: []
|
|
56
65
|
})
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
async execute(input: ExecuteInput, context: SkillContext): Promise<ExecuteOutput> {
|
|
60
|
-
const { plan, waves } = input
|
|
61
69
|
const results: TaskResult[] = []
|
|
62
70
|
let wavesCompleted = 0
|
|
63
71
|
|
|
64
|
-
// 1.
|
|
65
|
-
if (context.persistence) {
|
|
66
|
-
|
|
72
|
+
// 1. Read PLAN.md from PlanMdManager
|
|
73
|
+
if (!context.persistence) {
|
|
74
|
+
throw new Error('Persistence manager required for execute-plan skill')
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
const plan = await context.persistence.plan.readPlan()
|
|
78
|
+
if (!plan) {
|
|
79
|
+
throw new Error('No PLAN.md found. Run plan skill first.')
|
|
80
|
+
}
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
// 2. Read STATE.md for current progress
|
|
83
|
+
const currentState = await context.persistence.state.readCurrentState()
|
|
84
|
+
console.log(`\n[ExecutePlanSkill] Goal: ${plan.goal}`)
|
|
85
|
+
console.log(`[ExecutePlanSkill] Current phase: ${currentState.phase}`)
|
|
86
|
+
console.log(`[ExecutePlanSkill] Total waves: ${plan.waves.length}, Tasks: ${plan.tasks.length}`)
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
// Update state to executing phase
|
|
89
|
+
await context.persistence.state.updatePhase('executing')
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
results.push(...waveResults)
|
|
91
|
+
// 3. Trigger pre-execute hooks
|
|
92
|
+
await this.triggerHook(context, HookType.PRE_EXECUTE, { plan, state: currentState })
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
94
|
+
try {
|
|
95
|
+
// Determine starting point if continuing from state
|
|
96
|
+
let startWaveIndex = 0
|
|
97
|
+
if (input.continueFromState && currentState.completedTasks.length > 0) {
|
|
98
|
+
startWaveIndex = this.findNextWaveIndex(plan, currentState.completedTasks)
|
|
99
|
+
console.log(`[ExecutePlanSkill] Resuming from wave ${startWaveIndex + 1}`)
|
|
100
|
+
}
|
|
90
101
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
mergeResult = await this.mergeTaskWorktrees(successfulResults, context)
|
|
102
|
+
// 4. Execute waves sequentially
|
|
103
|
+
for (let i = startWaveIndex; i < plan.waves.length; i++) {
|
|
104
|
+
const wave = plan.waves[i]
|
|
105
|
+
console.log(`\n[ExecutePlanSkill] Wave ${wave.id}: ${wave.tasks.length} tasks`)
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
// Trigger wave start hooks
|
|
108
|
+
await this.triggerHook(context, HookType.WAVE_START, { wave, plan, waveIndex: i })
|
|
109
|
+
|
|
110
|
+
// Execute wave with parallel tasks
|
|
111
|
+
const waveResult = await this.executeWave(wave, plan, input.model, context)
|
|
112
|
+
results.push(...waveResult.results)
|
|
113
|
+
|
|
114
|
+
// 5. After each wave: update STATE.md with completed tasks
|
|
115
|
+
for (const taskResult of waveResult.results) {
|
|
116
|
+
if (taskResult.success) {
|
|
117
|
+
await context.persistence.state.recordTaskComplete(taskResult.taskId, taskResult)
|
|
118
|
+
await context.persistence.plan.updateTaskStatus(taskResult.taskId, 'complete')
|
|
119
|
+
} else {
|
|
120
|
+
await context.persistence.state.recordTaskFailed(taskResult.taskId, taskResult.error)
|
|
121
|
+
await context.persistence.plan.updateTaskStatus(taskResult.taskId, 'pending')
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
// Trigger wave complete hooks
|
|
125
|
+
// Trigger wave complete hooks
|
|
107
126
|
await this.triggerHook(context, HookType.WAVE_COMPLETE, {
|
|
108
127
|
wave,
|
|
109
|
-
results:
|
|
110
|
-
plan
|
|
111
|
-
mergeResult
|
|
128
|
+
results: waveResult.results,
|
|
129
|
+
plan
|
|
112
130
|
})
|
|
113
131
|
|
|
114
132
|
wavesCompleted++
|
|
115
133
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
// Error handling for wave failures
|
|
135
|
+
if (!waveResult.success) {
|
|
136
|
+
await this.handleWaveFailure(wave, waveResult.results, context)
|
|
137
|
+
|
|
138
|
+
// Fail-fast: stop execution on wave failure
|
|
139
|
+
console.error(`[ExecutePlanSkill] Wave ${wave.id} failed, stopping execution`)
|
|
121
140
|
break
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
console.log(`[ExecutePlanSkill] Wave ${wave.id} complete ✓`)
|
|
125
144
|
}
|
|
126
145
|
|
|
127
|
-
//
|
|
146
|
+
// 6. After all waves: run verification subagents (goals, quality, tests)
|
|
128
147
|
let verification: VerificationResult
|
|
129
|
-
if (!input.skipVerify && wavesCompleted === waves.length) {
|
|
130
|
-
verification = await this.
|
|
148
|
+
if (!input.skipVerify && wavesCompleted === plan.waves.length) {
|
|
149
|
+
verification = await this.runVerification(plan, results, context)
|
|
131
150
|
} else {
|
|
132
151
|
verification = {
|
|
133
152
|
success: false,
|
|
@@ -137,19 +156,17 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
137
156
|
}
|
|
138
157
|
}
|
|
139
158
|
|
|
140
|
-
//
|
|
141
|
-
const finalPhase = verification.success ? '
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await context.persistence.state.recordVerification(verification)
|
|
145
|
-
}
|
|
159
|
+
// 7. Update final state based on verification results
|
|
160
|
+
const finalPhase = verification.success ? 'completed' : 'failed'
|
|
161
|
+
await context.persistence.state.updatePhase(finalPhase)
|
|
162
|
+
await context.persistence.state.recordVerification(verification)
|
|
146
163
|
|
|
147
|
-
//
|
|
164
|
+
// 8. Trigger post-execute hooks
|
|
148
165
|
await this.triggerHook(context, HookType.POST_EXECUTE, {
|
|
149
166
|
results,
|
|
150
167
|
verification,
|
|
151
168
|
wavesCompleted,
|
|
152
|
-
totalWaves: waves.length
|
|
169
|
+
totalWaves: plan.waves.length
|
|
153
170
|
})
|
|
154
171
|
|
|
155
172
|
return {
|
|
@@ -157,186 +174,259 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
157
174
|
results,
|
|
158
175
|
verification,
|
|
159
176
|
wavesCompleted,
|
|
160
|
-
totalWaves: waves.length
|
|
177
|
+
totalWaves: plan.waves.length
|
|
161
178
|
}
|
|
162
179
|
|
|
163
180
|
} catch (error) {
|
|
164
|
-
// Trigger error hooks
|
|
181
|
+
// Trigger error hooks and update state
|
|
165
182
|
await this.triggerHook(context, HookType.ON_ERROR, {
|
|
166
183
|
phase: 'execute',
|
|
167
184
|
error: (error as Error).message,
|
|
168
185
|
results
|
|
169
186
|
})
|
|
187
|
+
|
|
188
|
+
// Update state to failed
|
|
189
|
+
await context.persistence.state.updatePhase('failed')
|
|
190
|
+
|
|
170
191
|
throw error
|
|
171
192
|
}
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
/**
|
|
175
|
-
*
|
|
176
|
-
|
|
196
|
+
* Find the next wave index based on completed tasks
|
|
197
|
+
*/
|
|
198
|
+
private findNextWaveIndex(plan: Plan, completedTasks: string[]): number {
|
|
199
|
+
const completedSet = new Set(completedTasks)
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < plan.waves.length; i++) {
|
|
202
|
+
const wave = plan.waves[i]
|
|
203
|
+
const waveTaskIds = wave.tasks.map(t => t.id)
|
|
204
|
+
const allCompleted = waveTaskIds.every(id => completedSet.has(id))
|
|
205
|
+
|
|
206
|
+
if (!allCompleted) {
|
|
207
|
+
return i
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return plan.waves.length
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Execute a wave - tasks within wave run in parallel
|
|
216
|
+
* Each task dispatches an implementer subagent with fresh context
|
|
177
217
|
*/
|
|
178
|
-
private async
|
|
218
|
+
private async executeWave(
|
|
179
219
|
wave: Wave,
|
|
180
220
|
plan: Plan,
|
|
181
221
|
model: string | undefined,
|
|
182
222
|
context: SkillContext
|
|
183
|
-
): Promise<
|
|
184
|
-
|
|
185
|
-
const tddSkill = context.skills.get('tdd')
|
|
186
|
-
if (!tddSkill) {
|
|
187
|
-
throw new Error('TDD skill not found in registry')
|
|
188
|
-
}
|
|
223
|
+
): Promise<WaveExecutionResult> {
|
|
224
|
+
const taskResults: TaskResult[] = []
|
|
189
225
|
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
const task = plan.tasks.find(t => t.id === taskId)
|
|
193
|
-
if (!task) throw new Error(`Task not found: ${taskId}`)
|
|
194
|
-
return { taskId, task }
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Execute all tasks in parallel using TDD skill
|
|
198
|
-
const promises = taskConfigs.map(async ({ taskId, task }) => {
|
|
199
|
-
try {
|
|
200
|
-
// Trigger task start hooks
|
|
201
|
-
await this.triggerHook(context, HookType.PRE_EXECUTE, { task, wave })
|
|
226
|
+
// Filter out already completed tasks
|
|
227
|
+
const pendingTasks = wave.tasks.filter(t => t.status !== 'completed')
|
|
202
228
|
|
|
203
|
-
|
|
204
|
-
|
|
229
|
+
if (pendingTasks.length === 0) {
|
|
230
|
+
console.log(` [Wave ${wave.id}] All tasks already completed`)
|
|
231
|
+
return { waveId: wave.id, results: [], success: true }
|
|
232
|
+
}
|
|
205
233
|
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
// Update task statuses to in_progress
|
|
235
|
+
for (const task of pendingTasks) {
|
|
236
|
+
await context.persistence?.plan.updateTaskStatus(task.id, 'in_progress')
|
|
237
|
+
}
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
testsAdded: tddResult.testFile ? [tddResult.testFile] : [],
|
|
214
|
-
duration: tddResult.duration || 0,
|
|
215
|
-
notes: tddResult.summary || '',
|
|
216
|
-
phases: tddResult.phases
|
|
217
|
-
}
|
|
218
|
-
} catch (error) {
|
|
219
|
-
// Task failed
|
|
220
|
-
await this.triggerHook(context, HookType.ON_ERROR, { task, error })
|
|
221
|
-
return {
|
|
222
|
-
taskId,
|
|
223
|
-
success: false,
|
|
224
|
-
filesModified: [],
|
|
225
|
-
testsAdded: [],
|
|
226
|
-
duration: 0,
|
|
227
|
-
notes: '',
|
|
228
|
-
error: (error as Error).message
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
})
|
|
239
|
+
// Execute all tasks in parallel using Promise.allSettled
|
|
240
|
+
const promises = pendingTasks.map(task =>
|
|
241
|
+
this.executeTask(task, wave, plan, model, context)
|
|
242
|
+
)
|
|
232
243
|
|
|
233
|
-
// Wait for all tasks to complete (fail-fast disabled for wave)
|
|
234
244
|
const settledResults = await Promise.allSettled(promises)
|
|
235
245
|
|
|
236
|
-
|
|
246
|
+
// Process results
|
|
247
|
+
let hasFailures = false
|
|
248
|
+
for (const result of settledResults) {
|
|
237
249
|
if (result.status === 'fulfilled') {
|
|
238
|
-
|
|
250
|
+
taskResults.push(result.value)
|
|
251
|
+
if (!result.value.success) {
|
|
252
|
+
hasFailures = true
|
|
253
|
+
}
|
|
239
254
|
} else {
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
hasFailures = true
|
|
256
|
+
taskResults.push({
|
|
257
|
+
taskId: 'unknown',
|
|
242
258
|
success: false,
|
|
243
|
-
filesModified: [],
|
|
244
|
-
testsAdded: [],
|
|
245
259
|
duration: 0,
|
|
246
260
|
notes: '',
|
|
247
261
|
error: result.reason?.message || 'Unknown error'
|
|
248
|
-
}
|
|
262
|
+
})
|
|
249
263
|
}
|
|
250
|
-
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
waveId: wave.id,
|
|
268
|
+
results: taskResults,
|
|
269
|
+
success: !hasFailures
|
|
270
|
+
}
|
|
251
271
|
}
|
|
252
272
|
|
|
253
273
|
/**
|
|
254
|
-
* Execute single task
|
|
255
|
-
*
|
|
256
|
-
* Uses worktreeContract instead of worktree-start skill
|
|
274
|
+
* Execute a single task by dispatching implementer subagent
|
|
275
|
+
* Fresh context for each task with proper permissions
|
|
257
276
|
*/
|
|
258
|
-
private async
|
|
259
|
-
task:
|
|
277
|
+
private async executeTask(
|
|
278
|
+
task: Task,
|
|
279
|
+
wave: Wave,
|
|
280
|
+
plan: Plan,
|
|
260
281
|
model: string | undefined,
|
|
261
|
-
context: SkillContext
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
context: SkillContext
|
|
283
|
+
): Promise<TaskResult> {
|
|
284
|
+
const startTime = Date.now()
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
// Trigger pre-task hooks
|
|
288
|
+
await this.triggerHook(context, HookType.PRE_TASK, { task, wave, plan })
|
|
289
|
+
|
|
290
|
+
console.log(` [Task ${task.id}] ${task.description}`)
|
|
291
|
+
|
|
292
|
+
// Determine files to own and read
|
|
293
|
+
const ownedFiles = task.relatedFiles || []
|
|
294
|
+
const readFiles = this.getRelatedFilesForTask(task, plan)
|
|
295
|
+
|
|
296
|
+
// Dispatch implementer subagent with fresh context
|
|
297
|
+
const result = await context.dispatcher.dispatch(
|
|
298
|
+
{ role: 'implementer', model },
|
|
299
|
+
implementerContract({
|
|
300
|
+
task: task.description,
|
|
301
|
+
ownedFiles,
|
|
302
|
+
readFiles,
|
|
303
|
+
constraints: [
|
|
304
|
+
'Follow immutability patterns - create new objects, never mutate',
|
|
305
|
+
'Handle errors comprehensively with try/catch',
|
|
306
|
+
'Validate inputs using zod schemas',
|
|
307
|
+
'Keep functions small (<50 lines)',
|
|
308
|
+
'Files should be focused (<800 lines)'
|
|
309
|
+
]
|
|
310
|
+
})
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const duration = Date.now() - startTime
|
|
314
|
+
|
|
315
|
+
// Parse result
|
|
316
|
+
const output = (result as { output?: string }).output || '{}'
|
|
317
|
+
const parsed = this.parseImplementerOutput(output)
|
|
318
|
+
|
|
319
|
+
const taskResult: TaskResult = {
|
|
320
|
+
taskId: task.id,
|
|
321
|
+
success: parsed.success,
|
|
322
|
+
duration,
|
|
323
|
+
notes: parsed.summary || '',
|
|
324
|
+
filesModified: parsed.filesChanged || []
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Trigger post-task hooks
|
|
328
|
+
await this.triggerHook(context, HookType.POST_TASK, { task, result: taskResult })
|
|
329
|
+
await this.triggerHook(context, HookType.TASK_COMPLETE, { task, result: taskResult })
|
|
330
|
+
|
|
331
|
+
return taskResult
|
|
332
|
+
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const duration = Date.now() - startTime
|
|
335
|
+
|
|
336
|
+
// Trigger error hooks
|
|
337
|
+
await this.triggerHook(context, HookType.ON_ERROR, {
|
|
338
|
+
task,
|
|
339
|
+
error: (error as Error).message
|
|
278
340
|
})
|
|
279
|
-
)
|
|
280
341
|
|
|
281
|
-
|
|
282
|
-
|
|
342
|
+
return {
|
|
343
|
+
taskId: task.id,
|
|
344
|
+
success: false,
|
|
345
|
+
duration,
|
|
346
|
+
notes: '',
|
|
347
|
+
error: (error as Error).message
|
|
348
|
+
}
|
|
283
349
|
}
|
|
350
|
+
}
|
|
284
351
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Get related files for a task based on dependencies
|
|
354
|
+
*/
|
|
355
|
+
private getRelatedFilesForTask(task: Task, plan: Plan): string[] {
|
|
356
|
+
const files = new Set<string>()
|
|
357
|
+
|
|
358
|
+
// Add files from task's relatedFiles
|
|
359
|
+
if (task.relatedFiles) {
|
|
360
|
+
task.relatedFiles.forEach(f => files.add(f))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Add files from dependency tasks
|
|
364
|
+
const depTaskIds = plan.dependencies[task.id] || []
|
|
365
|
+
for (const depId of depTaskIds) {
|
|
366
|
+
const depTask = plan.tasks.find(t => t.id === depId)
|
|
367
|
+
if (depTask?.relatedFiles) {
|
|
368
|
+
depTask.relatedFiles.forEach(f => files.add(f))
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return Array.from(files)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Parse implementer subagent output
|
|
377
|
+
*/
|
|
378
|
+
private parseImplementerOutput(output: string): {
|
|
379
|
+
success: boolean
|
|
380
|
+
summary?: string
|
|
381
|
+
filesChanged?: string[]
|
|
382
|
+
error?: string
|
|
383
|
+
} {
|
|
384
|
+
try {
|
|
385
|
+
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
386
|
+
if (jsonMatch) {
|
|
387
|
+
const parsed = JSON.parse(jsonMatch[1])
|
|
388
|
+
return {
|
|
389
|
+
success: parsed.success ?? true,
|
|
390
|
+
summary: parsed.summary,
|
|
391
|
+
filesChanged: parsed.filesChanged || []
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fallback: check for success indicators
|
|
396
|
+
if (output.includes('success') || output.includes('completed')) {
|
|
397
|
+
return { success: true }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { success: false, error: 'Could not parse output' }
|
|
401
|
+
} catch {
|
|
402
|
+
return { success: false, error: 'Failed to parse JSON output' }
|
|
312
403
|
}
|
|
313
404
|
}
|
|
314
405
|
|
|
315
406
|
/**
|
|
316
|
-
* Run verification
|
|
317
|
-
*
|
|
407
|
+
* Run verification subagents after all waves complete
|
|
408
|
+
* - Goals verification: verifierContract
|
|
409
|
+
* - Quality verification: reviewerContract
|
|
410
|
+
* - Tests verification: dispatcher with test runner
|
|
318
411
|
*/
|
|
319
|
-
private async
|
|
412
|
+
private async runVerification(
|
|
320
413
|
plan: Plan,
|
|
321
414
|
results: TaskResult[],
|
|
322
415
|
context: SkillContext
|
|
323
416
|
): Promise<VerificationResult> {
|
|
324
417
|
console.log('\n[ExecutePlanSkill] Running verification...')
|
|
325
418
|
|
|
326
|
-
|
|
327
|
-
await context.persistence.state.updatePhase('verifying')
|
|
328
|
-
}
|
|
419
|
+
await context.persistence?.state.updatePhase('executing')
|
|
329
420
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
421
|
+
// Get changed files from results
|
|
422
|
+
const changedFiles = results
|
|
423
|
+
.flatMap(r => r.filesModified || [])
|
|
424
|
+
.filter((f, i, arr) => arr.indexOf(f) === i) // unique
|
|
335
425
|
|
|
336
|
-
//
|
|
426
|
+
// Run verification dimensions in parallel
|
|
337
427
|
const [goalsResult, qualityResult, testsResult] = await Promise.all([
|
|
338
|
-
this.
|
|
339
|
-
this.
|
|
428
|
+
this.verifyGoals(plan, changedFiles, context),
|
|
429
|
+
this.verifyQuality(changedFiles, context),
|
|
340
430
|
this.verifyTests(results, context)
|
|
341
431
|
])
|
|
342
432
|
|
|
@@ -350,34 +440,42 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
350
440
|
}
|
|
351
441
|
|
|
352
442
|
console.log(`[ExecutePlanSkill] Verification: ${verification.success ? 'PASSED' : 'FAILED'}`)
|
|
443
|
+
console.log(` - Goals: ${goalsResult.success ? '✓' : '✗'}`)
|
|
444
|
+
console.log(` - Quality: ${qualityResult.success ? '✓' : '✗'}`)
|
|
445
|
+
console.log(` - Tests: ${testsResult.success ? '✓' : '✗'}`)
|
|
353
446
|
|
|
354
447
|
return verification
|
|
355
448
|
}
|
|
356
449
|
|
|
357
450
|
/**
|
|
358
|
-
* Verify goals using
|
|
451
|
+
* Verify goals using verifierContract subagent
|
|
359
452
|
*/
|
|
360
|
-
private async
|
|
453
|
+
private async verifyGoals(
|
|
361
454
|
plan: Plan,
|
|
362
|
-
|
|
363
|
-
|
|
455
|
+
changedFiles: string[],
|
|
456
|
+
context: SkillContext
|
|
364
457
|
): Promise<DimensionResult> {
|
|
365
458
|
try {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
459
|
+
const result = await context.dispatcher.dispatch(
|
|
460
|
+
{ role: 'verifier' },
|
|
461
|
+
verifierContract({
|
|
462
|
+
goal: plan.goal,
|
|
463
|
+
plan: this.formatPlanSummary(plan),
|
|
464
|
+
changedFiles,
|
|
465
|
+
acceptanceCriteria: plan.verificationCriteria
|
|
466
|
+
})
|
|
467
|
+
)
|
|
373
468
|
|
|
374
|
-
const
|
|
469
|
+
const output = (result as { output?: string }).output || '{}'
|
|
470
|
+
const parsed = this.parseVerifierOutput(output)
|
|
375
471
|
|
|
376
472
|
return {
|
|
377
|
-
success:
|
|
378
|
-
details:
|
|
379
|
-
evidence:
|
|
380
|
-
|
|
473
|
+
success: parsed.verdict === 'pass',
|
|
474
|
+
details: parsed,
|
|
475
|
+
evidence: parsed.criteriaResults
|
|
476
|
+
?.filter((c: { status: string }) => c.status === 'pass')
|
|
477
|
+
.map((c: { criterion: string }) => c.criterion),
|
|
478
|
+
gaps: parsed.gaps || []
|
|
381
479
|
}
|
|
382
480
|
} catch (error) {
|
|
383
481
|
return {
|
|
@@ -388,27 +486,41 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
388
486
|
}
|
|
389
487
|
|
|
390
488
|
/**
|
|
391
|
-
* Verify
|
|
489
|
+
* Verify quality using reviewerContract subagent
|
|
392
490
|
*/
|
|
393
|
-
private async
|
|
394
|
-
|
|
395
|
-
context: SkillContext
|
|
396
|
-
reviewSkill: any
|
|
491
|
+
private async verifyQuality(
|
|
492
|
+
changedFiles: string[],
|
|
493
|
+
context: SkillContext
|
|
397
494
|
): Promise<DimensionResult> {
|
|
398
495
|
try {
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
496
|
+
const result = await context.dispatcher.dispatch(
|
|
497
|
+
{ role: 'reviewer' },
|
|
498
|
+
reviewerContract({
|
|
499
|
+
target: 'Implementation quality review',
|
|
500
|
+
reviewFiles: changedFiles,
|
|
501
|
+
focusAreas: ['correctness', 'security', 'performance', 'style'],
|
|
502
|
+
criteria: [
|
|
503
|
+
'No hardcoded secrets',
|
|
504
|
+
'Proper error handling',
|
|
505
|
+
'Input validation',
|
|
506
|
+
'Immutability patterns',
|
|
507
|
+
'Test coverage >= 80%'
|
|
508
|
+
]
|
|
509
|
+
})
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
const output = (result as { output?: string }).output || '{}'
|
|
513
|
+
const parsed = this.parseReviewerOutput(output)
|
|
404
514
|
|
|
405
|
-
const
|
|
515
|
+
const hasCritical = parsed.findings?.some(
|
|
516
|
+
(f: { severity: string }) => f.severity === 'critical'
|
|
517
|
+
)
|
|
406
518
|
|
|
407
519
|
return {
|
|
408
|
-
success: !
|
|
409
|
-
details:
|
|
410
|
-
evidence:
|
|
411
|
-
gaps:
|
|
520
|
+
success: !hasCritical && parsed.verdict !== 'request_changes',
|
|
521
|
+
details: parsed,
|
|
522
|
+
evidence: parsed.strengths,
|
|
523
|
+
gaps: hasCritical ? ['Critical issues found'] : []
|
|
412
524
|
}
|
|
413
525
|
} catch (error) {
|
|
414
526
|
return {
|
|
@@ -419,7 +531,7 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
419
531
|
}
|
|
420
532
|
|
|
421
533
|
/**
|
|
422
|
-
* Verify tests by
|
|
534
|
+
* Verify tests by running test suite
|
|
423
535
|
*/
|
|
424
536
|
private async verifyTests(
|
|
425
537
|
results: TaskResult[],
|
|
@@ -431,32 +543,44 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
431
543
|
|
|
432
544
|
// Try to run full test suite
|
|
433
545
|
try {
|
|
434
|
-
const testResult = await context.dispatcher.dispatch
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
546
|
+
const testResult = await context.dispatcher.dispatch(
|
|
547
|
+
{ role: 'verifier', timeout: 120000 },
|
|
548
|
+
{
|
|
549
|
+
permissions: {
|
|
550
|
+
readFiles: true,
|
|
551
|
+
searchCode: true,
|
|
552
|
+
runCommands: true,
|
|
553
|
+
writeFiles: false,
|
|
554
|
+
gitOperations: false
|
|
555
|
+
},
|
|
556
|
+
prompt: `Run the full test suite and report coverage.
|
|
557
|
+
|
|
558
|
+
Commands:
|
|
559
|
+
1. npm test -- --coverage --watchAll=false 2>&1 || true
|
|
560
|
+
2. cat coverage/lcov-report/index.html 2>/dev/null | grep -o '[^>]*%' | head -1 || echo "N/A"
|
|
561
|
+
|
|
562
|
+
Return JSON:
|
|
563
|
+
{
|
|
564
|
+
"passed": boolean,
|
|
565
|
+
"coverage": number,
|
|
566
|
+
"testCount": number,
|
|
567
|
+
"failedTests": number
|
|
568
|
+
}`,
|
|
569
|
+
owns: [],
|
|
570
|
+
reads: []
|
|
571
|
+
}
|
|
572
|
+
)
|
|
452
573
|
|
|
453
|
-
const
|
|
574
|
+
const output = (testResult as { output?: string }).output || '{}'
|
|
575
|
+
const parsed = this.parseTestOutput(output)
|
|
576
|
+
|
|
577
|
+
const coverageOk = parsed.coverage >= 80
|
|
454
578
|
|
|
455
579
|
return {
|
|
456
|
-
success: allHaveTests && allPassed && coverageOk,
|
|
457
|
-
details: { coverage:
|
|
458
|
-
evidence: coverageOk ? [`Coverage: ${
|
|
459
|
-
gaps: !coverageOk ? [`Coverage ${
|
|
580
|
+
success: allHaveTests && allPassed && coverageOk && parsed.passed,
|
|
581
|
+
details: { coverage: parsed.coverage, allHaveTests, allPassed },
|
|
582
|
+
evidence: coverageOk ? [`Coverage: ${parsed.coverage}%`] : [],
|
|
583
|
+
gaps: !coverageOk ? [`Coverage ${parsed.coverage}% < 80%`] : []
|
|
460
584
|
}
|
|
461
585
|
} catch {
|
|
462
586
|
// Fallback to basic check
|
|
@@ -470,482 +594,103 @@ export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
|
470
594
|
}
|
|
471
595
|
|
|
472
596
|
/**
|
|
473
|
-
* Handle wave failure
|
|
597
|
+
* Handle wave failure with proper error handling
|
|
474
598
|
*/
|
|
475
599
|
private async handleWaveFailure(
|
|
476
600
|
wave: Wave,
|
|
477
|
-
|
|
601
|
+
failedResults: TaskResult[],
|
|
478
602
|
context: SkillContext
|
|
479
603
|
): Promise<void> {
|
|
480
|
-
|
|
604
|
+
const failedTasks = failedResults.filter(r => !r.success)
|
|
605
|
+
|
|
606
|
+
console.error(`\n[ExecutePlanSkill] Wave ${wave.id} failure handler:`)
|
|
607
|
+
console.error(` Failed tasks: ${failedTasks.map(t => t.taskId).join(', ')}`)
|
|
481
608
|
|
|
609
|
+
// Update state for failed tasks
|
|
610
|
+
for (const result of failedTasks) {
|
|
611
|
+
await context.persistence?.state.recordTaskFailed(result.taskId, result.error)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Trigger error hooks
|
|
482
615
|
await this.triggerHook(context, HookType.ON_ERROR, {
|
|
483
|
-
phase: '
|
|
616
|
+
phase: 'wave',
|
|
484
617
|
wave,
|
|
485
618
|
failedTasks
|
|
486
619
|
})
|
|
487
620
|
}
|
|
488
621
|
|
|
489
622
|
/**
|
|
490
|
-
*
|
|
491
|
-
* Called after each successful wave
|
|
492
|
-
*
|
|
493
|
-
* Conflict Resolution Strategy:
|
|
494
|
-
* 1. Try merge --no-ff
|
|
495
|
-
* 2. If conflict, analyze with debugger subagent
|
|
496
|
-
* 3. Auto-resolve based on conflict type (imports, adjacent changes, etc.)
|
|
497
|
-
* 4. If cannot auto-resolve, try rebase-merging or manual merge
|
|
498
|
-
* 5. Last resort: mark for manual resolution
|
|
623
|
+
* Format plan summary for verifier
|
|
499
624
|
*/
|
|
500
|
-
private
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
merged: string[]
|
|
505
|
-
conflicts: string[]
|
|
506
|
-
manualResolution: string[]
|
|
507
|
-
}> {
|
|
508
|
-
console.log('[ExecutePlanSkill] Merging task worktrees...')
|
|
509
|
-
|
|
510
|
-
const merged: string[] = []
|
|
511
|
-
const conflicts: string[] = []
|
|
512
|
-
const manualResolution: string[] = []
|
|
513
|
-
|
|
514
|
-
for (const result of waveResults) {
|
|
515
|
-
if (!result.success || !result.branchName) continue
|
|
516
|
-
|
|
517
|
-
try {
|
|
518
|
-
const mergeResult = await this.mergeSingleBranch(
|
|
519
|
-
result.taskId,
|
|
520
|
-
result.branchName,
|
|
521
|
-
context
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
if (mergeResult.status === 'success') {
|
|
525
|
-
merged.push(result.taskId)
|
|
526
|
-
console.log(` [Merge] ${result.taskId}: ✓ ${result.branchName}`)
|
|
527
|
-
} else if (mergeResult.status === 'auto-resolved') {
|
|
528
|
-
merged.push(result.taskId)
|
|
529
|
-
conflicts.push(`${result.taskId} (auto-resolved: ${mergeResult.conflicts.join(', ')})`)
|
|
530
|
-
console.log(` [Merge] ${result.taskId}: ✓ (auto-resolved ${mergeResult.conflicts.length} conflicts)`)
|
|
531
|
-
} else {
|
|
532
|
-
manualResolution.push(result.taskId)
|
|
533
|
-
console.error(` [Merge] ${result.taskId}: ✗ needs manual resolution`)
|
|
534
|
-
}
|
|
535
|
-
} catch (error) {
|
|
536
|
-
manualResolution.push(result.taskId)
|
|
537
|
-
console.error(` [Merge] ${result.taskId}: ✗ ${(error as Error).message}`)
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return { merged, conflicts, manualResolution }
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Merge a single branch with conflict detection and auto-resolution
|
|
546
|
-
*/
|
|
547
|
-
private async mergeSingleBranch(
|
|
548
|
-
taskId: string,
|
|
549
|
-
branchName: string,
|
|
550
|
-
context: SkillContext
|
|
551
|
-
): Promise<{
|
|
552
|
-
status: 'success' | 'auto-resolved' | 'failed'
|
|
553
|
-
conflicts: string[]
|
|
554
|
-
}> {
|
|
555
|
-
// Step 1: Attempt merge
|
|
556
|
-
const mergeAttempt = await context.dispatcher.dispatch({
|
|
557
|
-
role: 'general',
|
|
558
|
-
timeout: 60000
|
|
559
|
-
}, {
|
|
560
|
-
permissions: {
|
|
561
|
-
readFiles: true,
|
|
562
|
-
searchCode: false,
|
|
563
|
-
runCommands: true,
|
|
564
|
-
writeFiles: false,
|
|
565
|
-
gitOperations: true
|
|
566
|
-
},
|
|
567
|
-
prompt: `Attempt to merge branch ${branchName} to current branch.
|
|
568
|
-
|
|
569
|
-
Commands:
|
|
570
|
-
1. git merge ${branchName} --no-ff -m "Merge ${branchName}" 2>&1
|
|
571
|
-
2. Check exit code and output
|
|
572
|
-
|
|
573
|
-
If successful: report status=success
|
|
574
|
-
If conflicts: list conflicting files with git diff --name-only --diff-filter=U
|
|
575
|
-
|
|
576
|
-
Return JSON:
|
|
577
|
-
{
|
|
578
|
-
"status": "success|conflict",
|
|
579
|
-
"conflictingFiles": ["path/to/file"],
|
|
580
|
-
"mergeOutput": "..."
|
|
581
|
-
}`,
|
|
582
|
-
owns: [],
|
|
583
|
-
reads: []
|
|
584
|
-
})
|
|
585
|
-
|
|
586
|
-
const mergeResult = this.parseMergeResult((mergeAttempt as { output: string }).output)
|
|
587
|
-
|
|
588
|
-
if (mergeResult.status === 'success') {
|
|
589
|
-
return { status: 'success', conflicts: [] }
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Step 2: Auto-resolve conflicts
|
|
593
|
-
console.log(` [Merge] ${taskId}: Detected conflicts in ${mergeResult.conflictingFiles.length} files`)
|
|
594
|
-
const resolved = await this.autoResolveConflicts(
|
|
595
|
-
mergeResult.conflictingFiles,
|
|
596
|
-
branchName,
|
|
597
|
-
context
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
if (resolved.success) {
|
|
601
|
-
return { status: 'auto-resolved', conflicts: mergeResult.conflictingFiles }
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Step 3: Abort and mark for manual resolution
|
|
605
|
-
await context.dispatcher.dispatch({
|
|
606
|
-
role: 'general',
|
|
607
|
-
timeout: 30000
|
|
608
|
-
}, {
|
|
609
|
-
permissions: {
|
|
610
|
-
readFiles: false,
|
|
611
|
-
searchCode: false,
|
|
612
|
-
runCommands: true,
|
|
613
|
-
writeFiles: false,
|
|
614
|
-
gitOperations: true
|
|
615
|
-
},
|
|
616
|
-
prompt: `git merge --abort`,
|
|
617
|
-
owns: [],
|
|
618
|
-
reads: []
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
return { status: 'failed', conflicts: mergeResult.conflictingFiles }
|
|
625
|
+
private formatPlanSummary(plan: Plan): string {
|
|
626
|
+
return plan.waves.map(w =>
|
|
627
|
+
`- ${w.id}: ${w.tasks.map(t => t.id).join(', ')}`
|
|
628
|
+
).join('\n')
|
|
622
629
|
}
|
|
623
630
|
|
|
624
631
|
/**
|
|
625
|
-
* Parse
|
|
632
|
+
* Parse verifier output
|
|
626
633
|
*/
|
|
627
|
-
private
|
|
628
|
-
|
|
629
|
-
|
|
634
|
+
private parseVerifierOutput(output: string): {
|
|
635
|
+
verdict: string
|
|
636
|
+
criteriaResults?: Array<{ criterion: string; status: string; evidence?: string }>
|
|
637
|
+
gaps?: string[]
|
|
630
638
|
} {
|
|
631
639
|
try {
|
|
632
640
|
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
633
641
|
if (jsonMatch) {
|
|
634
|
-
|
|
635
|
-
return {
|
|
636
|
-
status: parsed.status || 'conflict',
|
|
637
|
-
conflictingFiles: parsed.conflictingFiles || []
|
|
638
|
-
}
|
|
642
|
+
return JSON.parse(jsonMatch[1])
|
|
639
643
|
}
|
|
640
644
|
} catch {
|
|
641
|
-
// Fallback
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Text-based detection
|
|
645
|
-
if (output.includes('CONFLICT') || output.includes('Automatic merge failed')) {
|
|
646
|
-
const fileMatches = output.matchAll(/CONFLICT\s*\([^)]+\):\s*(.+)/g)
|
|
647
|
-
const files: string[] = []
|
|
648
|
-
for (const match of fileMatches) {
|
|
649
|
-
files.push(match[1].trim())
|
|
650
|
-
}
|
|
651
|
-
return { status: 'conflict', conflictingFiles: files }
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
return { status: 'success', conflictingFiles: [] }
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Auto-resolve merge conflicts using debugger subagent
|
|
659
|
-
*/
|
|
660
|
-
private async autoResolveConflicts(
|
|
661
|
-
conflictingFiles: string[],
|
|
662
|
-
branchName: string,
|
|
663
|
-
context: SkillContext
|
|
664
|
-
): Promise<{ success: boolean; resolved: string[]; failed: string[] }> {
|
|
665
|
-
const resolved: string[] = []
|
|
666
|
-
const failed: string[] = []
|
|
667
|
-
|
|
668
|
-
for (const file of conflictingFiles) {
|
|
669
|
-
const resolution = await this.resolveSingleFile(file, branchName, context)
|
|
670
|
-
|
|
671
|
-
if (resolution.success) {
|
|
672
|
-
resolved.push(file)
|
|
673
|
-
} else {
|
|
674
|
-
failed.push(file)
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return {
|
|
679
|
-
success: resolved.length > 0 && failed.length === 0,
|
|
680
|
-
resolved,
|
|
681
|
-
failed
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Resolve a single file conflict
|
|
687
|
-
*/
|
|
688
|
-
private async resolveSingleFile(
|
|
689
|
-
file: string,
|
|
690
|
-
branchName: string,
|
|
691
|
-
context: SkillContext
|
|
692
|
-
): Promise<{ success: boolean; method: string }> {
|
|
693
|
-
// Get both versions of the file
|
|
694
|
-
const fileAnalysis = await context.dispatcher.dispatch({
|
|
695
|
-
role: 'debugger',
|
|
696
|
-
timeout: 60000
|
|
697
|
-
}, {
|
|
698
|
-
permissions: {
|
|
699
|
-
readFiles: true,
|
|
700
|
-
searchCode: false,
|
|
701
|
-
runCommands: true,
|
|
702
|
-
writeFiles: false,
|
|
703
|
-
gitOperations: true
|
|
704
|
-
},
|
|
705
|
-
prompt: `Analyze conflict in file: ${file}
|
|
706
|
-
|
|
707
|
-
Commands:
|
|
708
|
-
1. git show :1:${file} > /tmp/base.txt # common ancestor
|
|
709
|
-
2. git show :2:${file} > /tmp/ours.txt # current branch
|
|
710
|
-
3. git show :3:${file} > /tmp/theirs.txt # merging branch
|
|
711
|
-
4. cat ${file} # conflict markers
|
|
712
|
-
|
|
713
|
-
Analyze:
|
|
714
|
-
- Type of conflict (import additions, adjacent changes, overlapping edits)
|
|
715
|
-
- Are changes logically compatible?
|
|
716
|
-
- Can they be combined?
|
|
717
|
-
|
|
718
|
-
Return JSON:
|
|
719
|
-
{
|
|
720
|
-
"conflictType": "imports|adjacent|overlapping|unrelated",
|
|
721
|
-
"canAutoResolve": boolean,
|
|
722
|
-
"strategy": "accept-ours|accept-theirs|combine|manual"
|
|
723
|
-
}`,
|
|
724
|
-
owns: [],
|
|
725
|
-
reads: [file]
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
const analysis = this.parseConflictAnalysis((fileAnalysis as { output: string }).output)
|
|
729
|
-
|
|
730
|
-
if (!analysis.canAutoResolve) {
|
|
731
|
-
return { success: false, method: 'manual-required' }
|
|
645
|
+
// Fallback
|
|
732
646
|
}
|
|
733
|
-
|
|
734
|
-
// Attempt resolution based on strategy
|
|
735
|
-
const resolution = await this.applyConflictStrategy(
|
|
736
|
-
file,
|
|
737
|
-
analysis.strategy,
|
|
738
|
-
branchName,
|
|
739
|
-
context
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
return resolution
|
|
647
|
+
return { verdict: 'fail' }
|
|
743
648
|
}
|
|
744
649
|
|
|
745
650
|
/**
|
|
746
|
-
* Parse
|
|
651
|
+
* Parse reviewer output
|
|
747
652
|
*/
|
|
748
|
-
private
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
653
|
+
private parseReviewerOutput(output: string): {
|
|
654
|
+
verdict: string
|
|
655
|
+
findings?: Array<{ severity: string; file: string; message: string }>
|
|
656
|
+
strengths?: string[]
|
|
752
657
|
} {
|
|
753
658
|
try {
|
|
754
659
|
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
755
660
|
if (jsonMatch) {
|
|
756
|
-
|
|
757
|
-
return {
|
|
758
|
-
conflictType: parsed.conflictType || 'unknown',
|
|
759
|
-
canAutoResolve: parsed.canAutoResolve || false,
|
|
760
|
-
strategy: parsed.strategy || 'manual'
|
|
761
|
-
}
|
|
661
|
+
return JSON.parse(jsonMatch[1])
|
|
762
662
|
}
|
|
763
663
|
} catch {
|
|
764
|
-
//
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return { conflictType: 'unknown', canAutoResolve: false, strategy: 'manual' }
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Apply conflict resolution strategy
|
|
772
|
-
*/
|
|
773
|
-
private async applyConflictStrategy(
|
|
774
|
-
file: string,
|
|
775
|
-
strategy: string,
|
|
776
|
-
branchName: string,
|
|
777
|
-
context: SkillContext
|
|
778
|
-
): Promise<{ success: boolean; method: string }> {
|
|
779
|
-
let commands: string
|
|
780
|
-
|
|
781
|
-
switch (strategy) {
|
|
782
|
-
case 'accept-ours':
|
|
783
|
-
commands = `git checkout --ours "${file}" && git add "${file}"`
|
|
784
|
-
break
|
|
785
|
-
case 'accept-theirs':
|
|
786
|
-
commands = `git checkout --theirs "${file}" && git add "${file}"`
|
|
787
|
-
break
|
|
788
|
-
case 'combine':
|
|
789
|
-
// Use implementer to intelligently combine changes
|
|
790
|
-
return this.combineFileChanges(file, branchName, context)
|
|
791
|
-
default:
|
|
792
|
-
return { success: false, method: 'manual-required' }
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
try {
|
|
796
|
-
await context.dispatcher.dispatch({
|
|
797
|
-
role: 'general',
|
|
798
|
-
timeout: 30000
|
|
799
|
-
}, {
|
|
800
|
-
permissions: {
|
|
801
|
-
readFiles: false,
|
|
802
|
-
searchCode: false,
|
|
803
|
-
runCommands: true,
|
|
804
|
-
writeFiles: false,
|
|
805
|
-
gitOperations: true
|
|
806
|
-
},
|
|
807
|
-
prompt: commands,
|
|
808
|
-
owns: [],
|
|
809
|
-
reads: []
|
|
810
|
-
})
|
|
811
|
-
|
|
812
|
-
return { success: true, method: strategy }
|
|
813
|
-
} catch {
|
|
814
|
-
return { success: false, method: `${strategy}-failed` }
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Intelligently combine changes from both branches
|
|
820
|
-
*/
|
|
821
|
-
private async combineFileChanges(
|
|
822
|
-
file: string,
|
|
823
|
-
branchName: string,
|
|
824
|
-
context: SkillContext
|
|
825
|
-
): Promise<{ success: boolean; method: string }> {
|
|
826
|
-
// Use implementer subagent to merge changes intelligently
|
|
827
|
-
const combineResult = await context.dispatcher.dispatch({
|
|
828
|
-
role: 'implementer',
|
|
829
|
-
timeout: 60000
|
|
830
|
-
}, {
|
|
831
|
-
permissions: {
|
|
832
|
-
readFiles: true,
|
|
833
|
-
searchCode: false,
|
|
834
|
-
runCommands: true,
|
|
835
|
-
writeFiles: true,
|
|
836
|
-
gitOperations: true
|
|
837
|
-
},
|
|
838
|
-
prompt: `Resolve merge conflict in ${file} by combining both changes.
|
|
839
|
-
|
|
840
|
-
Steps:
|
|
841
|
-
1. git show :1:${file} > /tmp/base.ts
|
|
842
|
-
2. git show :2:${file} > /tmp/ours.ts
|
|
843
|
-
3. git show :3:${file} > /tmp/theirs.ts
|
|
844
|
-
4. Read all three versions
|
|
845
|
-
5. Create merged version that includes both sets of changes
|
|
846
|
-
6. Resolve conflict markers and write to ${file}
|
|
847
|
-
7. git add ${file}
|
|
848
|
-
8. Verify file is valid (syntax check if applicable)
|
|
849
|
-
|
|
850
|
-
Rules:
|
|
851
|
-
- Keep changes from both branches where possible
|
|
852
|
-
- For imports: combine import lists
|
|
853
|
-
- For functions: merge non-overlapping additions
|
|
854
|
-
- Preserve code style and formatting
|
|
855
|
-
- Add comment if resolution is complex
|
|
856
|
-
|
|
857
|
-
Return JSON:
|
|
858
|
-
{
|
|
859
|
-
"success": boolean,
|
|
860
|
-
"resolution": "description of changes made"
|
|
861
|
-
}`,
|
|
862
|
-
owns: [file],
|
|
863
|
-
reads: [file]
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
const result = this.parseResolutionResult((combineResult as { output: string }).output)
|
|
867
|
-
|
|
868
|
-
return {
|
|
869
|
-
success: result.success,
|
|
870
|
-
method: result.success ? 'combine-intelligent' : 'combine-failed'
|
|
664
|
+
// Fallback
|
|
871
665
|
}
|
|
666
|
+
return { verdict: 'request_changes' }
|
|
872
667
|
}
|
|
873
668
|
|
|
874
669
|
/**
|
|
875
|
-
* Parse
|
|
670
|
+
* Parse test output
|
|
876
671
|
*/
|
|
877
|
-
private
|
|
878
|
-
|
|
879
|
-
|
|
672
|
+
private parseTestOutput(output: string): {
|
|
673
|
+
passed: boolean
|
|
674
|
+
coverage: number
|
|
675
|
+
testCount?: number
|
|
676
|
+
failedTests?: number
|
|
880
677
|
} {
|
|
881
678
|
try {
|
|
882
679
|
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
883
680
|
if (jsonMatch) {
|
|
884
|
-
|
|
885
|
-
return {
|
|
886
|
-
success: parsed.success || false,
|
|
887
|
-
resolution: parsed.resolution
|
|
888
|
-
}
|
|
681
|
+
return JSON.parse(jsonMatch[1])
|
|
889
682
|
}
|
|
890
|
-
} catch {
|
|
891
|
-
// Check for success indicators in text
|
|
892
|
-
if (output.includes('success') || output.includes('resolved')) {
|
|
893
|
-
return { success: true }
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
683
|
|
|
897
|
-
|
|
898
|
-
|
|
684
|
+
// Fallback parsing
|
|
685
|
+
const coverageMatch = output.match(/(\d+(?:\.\d+)?)%/)
|
|
686
|
+
const passed = !output.includes('FAIL') && !output.includes('failed')
|
|
899
687
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
private parseWorktreeOutput(output: string): {
|
|
904
|
-
worktreePath: string
|
|
905
|
-
branchName: string
|
|
906
|
-
baseBranch: string
|
|
907
|
-
repoRoot: string
|
|
908
|
-
status: string
|
|
909
|
-
error?: string
|
|
910
|
-
} {
|
|
911
|
-
try {
|
|
912
|
-
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
913
|
-
if (jsonMatch) {
|
|
914
|
-
const parsed = JSON.parse(jsonMatch[1])
|
|
915
|
-
return {
|
|
916
|
-
worktreePath: parsed.worktreePath || '',
|
|
917
|
-
branchName: parsed.branchName || '',
|
|
918
|
-
baseBranch: parsed.baseBranch || 'main',
|
|
919
|
-
repoRoot: parsed.repoRoot || '',
|
|
920
|
-
status: parsed.status || 'unknown',
|
|
921
|
-
error: parsed.error
|
|
922
|
-
}
|
|
688
|
+
return {
|
|
689
|
+
passed,
|
|
690
|
+
coverage: coverageMatch ? parseFloat(coverageMatch[1]) : 0
|
|
923
691
|
}
|
|
924
692
|
} catch {
|
|
925
|
-
|
|
926
|
-
const pathMatch = output.match(/worktreePath["\']?\s*[:=]\s*["\']([^"\']+)["\']/i)
|
|
927
|
-
const branchMatch = output.match(/branchName["\']?\s*[:=]\s*["\']([^"\']+)["\']/i)
|
|
928
|
-
const statusMatch = output.match(/status["\']?\s*[:=]\s*["\'](created|reused|failed)["\']/i)
|
|
929
|
-
|
|
930
|
-
if (pathMatch) {
|
|
931
|
-
return {
|
|
932
|
-
worktreePath: pathMatch[1],
|
|
933
|
-
branchName: branchMatch?.[1] || '',
|
|
934
|
-
baseBranch: 'main',
|
|
935
|
-
repoRoot: '',
|
|
936
|
-
status: statusMatch?.[1] || 'unknown'
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// Default fallback
|
|
942
|
-
return {
|
|
943
|
-
worktreePath: '',
|
|
944
|
-
branchName: '',
|
|
945
|
-
baseBranch: 'main',
|
|
946
|
-
repoRoot: '',
|
|
947
|
-
status: 'failed',
|
|
948
|
-
error: 'Could not parse worktree output'
|
|
693
|
+
return { passed: false, coverage: 0 }
|
|
949
694
|
}
|
|
950
695
|
}
|
|
951
696
|
|
|
@@ -967,4 +712,5 @@ Return JSON:
|
|
|
967
712
|
}
|
|
968
713
|
}
|
|
969
714
|
}
|
|
715
|
+
|
|
970
716
|
export const executePlanSkill = new ExecutePlanSkill()
|