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