@haoyiyin/workflow 0.2.0 → 0.2.3
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/package.json +15 -10
- package/scripts/postinstall.js +2 -2
- package/src/agents/contracts.ts +559 -0
- package/src/agents/dispatcher-enhanced.ts +350 -0
- package/src/agents/dispatcher.ts +680 -0
- package/src/agents/index.ts +48 -0
- package/src/agents/resilience.ts +255 -0
- package/src/agents/token-budget.ts +83 -0
- package/src/agents/types.ts +73 -0
- package/src/guard/main-agent.ts +245 -0
- package/src/hooks/builtin/index.ts +8 -0
- package/src/hooks/builtin/on-error.ts +23 -0
- package/src/hooks/builtin/post-execute.ts +40 -0
- package/src/hooks/builtin/post-plan.ts +23 -0
- package/src/hooks/builtin/pre-execute.ts +30 -0
- package/src/hooks/builtin/pre-plan.ts +26 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/loader.ts +98 -0
- package/src/hooks/manager.ts +99 -0
- package/src/hooks/types-enhanced.ts +38 -0
- package/src/hooks/types.ts +35 -0
- package/src/index.ts +127 -0
- package/src/persistence/index.ts +17 -0
- package/src/persistence/plan-md.ts +141 -0
- package/src/persistence/state-md.ts +167 -0
- package/src/persistence/types.ts +89 -0
- package/src/router/classifier.ts +610 -0
- package/src/router/guard.ts +483 -0
- package/src/router/index.ts +22 -0
- package/src/router/router.ts +108 -0
- package/src/router/types.ts +127 -0
- package/src/skills/agents-md/SKILL.md +45 -0
- package/src/skills/agents-md/index.ts +33 -0
- package/src/skills/execute-plan/SKILL.md +60 -0
- package/src/skills/execute-plan/index.ts +970 -0
- package/src/skills/index.ts +13 -0
- package/src/skills/quick-task/SKILL.md +54 -0
- package/src/skills/quick-task/index.ts +346 -0
- package/src/skills/registry.ts +59 -0
- package/src/skills/review-diff/SKILL.md +53 -0
- package/src/skills/review-diff/index.ts +394 -0
- package/src/skills/skill.ts +59 -0
- package/src/skills/systematic-debugging/SKILL.md +56 -0
- package/src/skills/systematic-debugging/index.ts +404 -0
- package/src/skills/tdd/SKILL.md +52 -0
- package/src/skills/tdd/index.ts +409 -0
- package/src/skills/to-plan/SKILL.md +56 -0
- package/src/skills/to-plan/index-enhanced.ts +551 -0
- package/src/skills/to-plan/index.ts +586 -0
- package/src/skills/types.ts +47 -0
- package/src/state/cleanup.ts +118 -0
- package/src/state/index.ts +8 -0
- package/src/state/manager.ts +96 -0
- package/src/state/persistence.ts +77 -0
- package/src/state/types.ts +30 -0
- package/src/state/validator.ts +78 -0
- package/src/types.ts +102 -0
- package/src/utils/compress.ts +347 -0
- package/src/utils/git.ts +82 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +23 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Execute Plan Skill - Orchestrates TDD and Review skills
|
|
3
|
+
*
|
|
4
|
+
* Architecture: Orchestrator pattern
|
|
5
|
+
* - Executes waves sequentially
|
|
6
|
+
* - Each task uses TDD skill (RED/GREEN/REFACTOR)
|
|
7
|
+
* - Verification uses review-diff skill
|
|
8
|
+
* - Lifecycle managed via hooks
|
|
9
|
+
* - Task-level worktree isolation via worktreeContract subagent
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { Skill } from '../skill.js'
|
|
13
|
+
import type { SkillContext } from '../types.js'
|
|
14
|
+
import type { Plan, Wave, TaskResult, VerificationResult, DimensionResult } from '../../persistence/types.js'
|
|
15
|
+
import type { HookType } from '../../types.js'
|
|
16
|
+
// HookType constants (avoiding enum import issues)
|
|
17
|
+
const HookType = {
|
|
18
|
+
PRE_EXECUTE: 'pre-execute',
|
|
19
|
+
POST_EXECUTE: 'post-execute',
|
|
20
|
+
PRE_WAVE: 'pre-wave',
|
|
21
|
+
POST_WAVE: 'post-wave',
|
|
22
|
+
PRE_TASK: 'pre-task',
|
|
23
|
+
POST_TASK: 'post-task',
|
|
24
|
+
ON_ERROR: 'on-error',
|
|
25
|
+
STATE_CHANGE: 'state-change',
|
|
26
|
+
TASK_COMPLETE: 'task-complete',
|
|
27
|
+
WAVE_START: 'wave-start',
|
|
28
|
+
WAVE_COMPLETE: 'wave-complete',
|
|
29
|
+
} as const
|
|
30
|
+
import { worktreeContract } from '../../agents/contracts.js'
|
|
31
|
+
|
|
32
|
+
const ExecuteInputSchema = z.object({
|
|
33
|
+
plan: z.any(),
|
|
34
|
+
waves: z.array(z.any()),
|
|
35
|
+
skipVerify: z.boolean().optional(),
|
|
36
|
+
model: z.string().optional()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const ExecuteOutputSchema = z.object({
|
|
40
|
+
success: z.boolean(),
|
|
41
|
+
results: z.array(z.any()),
|
|
42
|
+
verification: z.any(),
|
|
43
|
+
wavesCompleted: z.number(),
|
|
44
|
+
totalWaves: z.number()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
type ExecuteInput = z.infer<typeof ExecuteInputSchema>
|
|
48
|
+
type ExecuteOutput = z.infer<typeof ExecuteOutputSchema>
|
|
49
|
+
|
|
50
|
+
export class ExecutePlanSkill extends Skill<ExecuteInput, ExecuteOutput> {
|
|
51
|
+
constructor() {
|
|
52
|
+
super({
|
|
53
|
+
name: 'execute-plan',
|
|
54
|
+
description: 'Orchestrate plan execution with task-level worktree isolation using TDD skill and review-diff for verification',
|
|
55
|
+
requires: ['tdd', 'review-diff']
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async execute(input: ExecuteInput, context: SkillContext): Promise<ExecuteOutput> {
|
|
60
|
+
const { plan, waves } = input
|
|
61
|
+
const results: TaskResult[] = []
|
|
62
|
+
let wavesCompleted = 0
|
|
63
|
+
|
|
64
|
+
// 1. Update state
|
|
65
|
+
if (context.persistence) {
|
|
66
|
+
await context.persistence.state.updatePhase('executing')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Trigger pre-execute hooks
|
|
70
|
+
await this.triggerHook(context, HookType.PRE_EXECUTE, { plan, waves })
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// 3. Execute waves sequentially
|
|
74
|
+
for (const wave of waves) {
|
|
75
|
+
console.log(`\n[ExecutePlanSkill] Wave ${wave.id}: ${wave.tasks.length} tasks`)
|
|
76
|
+
|
|
77
|
+
// Trigger wave start hooks
|
|
78
|
+
await this.triggerHook(context, HookType.WAVE_START, { wave, plan })
|
|
79
|
+
|
|
80
|
+
// Execute wave using TDD skill for each task
|
|
81
|
+
const waveResults = await this.executeWaveWithTDD(wave, plan, input.model, context)
|
|
82
|
+
results.push(...waveResults)
|
|
83
|
+
|
|
84
|
+
// Update state with completed tasks
|
|
85
|
+
if (context.persistence) {
|
|
86
|
+
for (const result of waveResults) {
|
|
87
|
+
await context.persistence.state.recordTaskComplete(result.taskId, result)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Merge task worktrees back to main branch
|
|
92
|
+
const successfulResults = waveResults.filter(r => r.success && r.branchName)
|
|
93
|
+
let mergeResult: { merged: string[]; conflicts: string[]; manualResolution: string[] } = { merged: [], conflicts: [], manualResolution: [] }
|
|
94
|
+
if (successfulResults.length > 0) {
|
|
95
|
+
mergeResult = await this.mergeTaskWorktrees(successfulResults, context)
|
|
96
|
+
|
|
97
|
+
// Report merge status
|
|
98
|
+
if (mergeResult.conflicts.length > 0) {
|
|
99
|
+
console.warn(` [Merge] Auto-resolved conflicts in: ${mergeResult.conflicts.join(', ')}`)
|
|
100
|
+
}
|
|
101
|
+
if (mergeResult.manualResolution.length > 0) {
|
|
102
|
+
console.error(` [Merge] Needs manual resolution: ${mergeResult.manualResolution.join(', ')}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Trigger wave complete hooks (with merge info)
|
|
107
|
+
await this.triggerHook(context, HookType.WAVE_COMPLETE, {
|
|
108
|
+
wave,
|
|
109
|
+
results: waveResults,
|
|
110
|
+
plan,
|
|
111
|
+
mergeResult
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
wavesCompleted++
|
|
115
|
+
|
|
116
|
+
// Check for failures - fail-fast
|
|
117
|
+
const failedTasks = waveResults.filter(r => !r.success)
|
|
118
|
+
if (failedTasks.length > 0) {
|
|
119
|
+
console.error(`[ExecutePlanSkill] Wave ${wave.id} failed:`, failedTasks.map(t => t.taskId))
|
|
120
|
+
await this.handleWaveFailure(wave, failedTasks, context)
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`[ExecutePlanSkill] Wave ${wave.id} complete ✓`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 4. Run verification using review-diff skill
|
|
128
|
+
let verification: VerificationResult
|
|
129
|
+
if (!input.skipVerify && wavesCompleted === waves.length) {
|
|
130
|
+
verification = await this.runVerificationWithReviewDiff(plan, results, context)
|
|
131
|
+
} else {
|
|
132
|
+
verification = {
|
|
133
|
+
success: false,
|
|
134
|
+
dimensions: {
|
|
135
|
+
goals: { success: false, gaps: ['Verification skipped or incomplete'] }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 5. Update final state
|
|
141
|
+
const finalPhase = verification.success ? 'complete' : 'verifying'
|
|
142
|
+
if (context.persistence) {
|
|
143
|
+
await context.persistence.state.updatePhase(finalPhase)
|
|
144
|
+
await context.persistence.state.recordVerification(verification)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 6. Trigger post-execute hooks
|
|
148
|
+
await this.triggerHook(context, HookType.POST_EXECUTE, {
|
|
149
|
+
results,
|
|
150
|
+
verification,
|
|
151
|
+
wavesCompleted,
|
|
152
|
+
totalWaves: waves.length
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: results.every(r => r.success) && verification.success,
|
|
157
|
+
results,
|
|
158
|
+
verification,
|
|
159
|
+
wavesCompleted,
|
|
160
|
+
totalWaves: waves.length
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Trigger error hooks
|
|
165
|
+
await this.triggerHook(context, HookType.ON_ERROR, {
|
|
166
|
+
phase: 'execute',
|
|
167
|
+
error: (error as Error).message,
|
|
168
|
+
results
|
|
169
|
+
})
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Execute a wave using TDD skill for each task
|
|
176
|
+
* Parallel execution with Promise.allSettled
|
|
177
|
+
*/
|
|
178
|
+
private async executeWaveWithTDD(
|
|
179
|
+
wave: Wave,
|
|
180
|
+
plan: Plan,
|
|
181
|
+
model: string | undefined,
|
|
182
|
+
context: SkillContext
|
|
183
|
+
): Promise<TaskResult[]> {
|
|
184
|
+
// Get TDD skill from registry
|
|
185
|
+
const tddSkill = context.skills.get('tdd')
|
|
186
|
+
if (!tddSkill) {
|
|
187
|
+
throw new Error('TDD skill not found in registry')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Prepare task contexts
|
|
191
|
+
const taskConfigs = wave.tasks.map(taskId => {
|
|
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 })
|
|
202
|
+
|
|
203
|
+
// Execute task with TDD
|
|
204
|
+
const tddResult = await this.executeTaskWithTDD(task, model, context, tddSkill)
|
|
205
|
+
|
|
206
|
+
// Trigger task complete hooks
|
|
207
|
+
await this.triggerHook(context, HookType.TASK_COMPLETE, { task, result: tddResult })
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
taskId,
|
|
211
|
+
success: tddResult.status === 'success',
|
|
212
|
+
filesModified: tddResult.filesModified || [],
|
|
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
|
+
})
|
|
232
|
+
|
|
233
|
+
// Wait for all tasks to complete (fail-fast disabled for wave)
|
|
234
|
+
const settledResults = await Promise.allSettled(promises)
|
|
235
|
+
|
|
236
|
+
return settledResults.map((result, index) => {
|
|
237
|
+
if (result.status === 'fulfilled') {
|
|
238
|
+
return result.value
|
|
239
|
+
} else {
|
|
240
|
+
return {
|
|
241
|
+
taskId: taskConfigs[index].taskId,
|
|
242
|
+
success: false,
|
|
243
|
+
filesModified: [],
|
|
244
|
+
testsAdded: [],
|
|
245
|
+
duration: 0,
|
|
246
|
+
notes: '',
|
|
247
|
+
error: result.reason?.message || 'Unknown error'
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Execute single task using TDD skill with task-level worktree isolation
|
|
255
|
+
* Each task gets its own worktree for true isolation
|
|
256
|
+
* Uses worktreeContract instead of worktree-start skill
|
|
257
|
+
*/
|
|
258
|
+
private async executeTaskWithTDD(
|
|
259
|
+
task: { id: string; description: string; relatedFiles?: string[] },
|
|
260
|
+
model: string | undefined,
|
|
261
|
+
context: SkillContext,
|
|
262
|
+
tddSkill: any
|
|
263
|
+
): Promise<any> {
|
|
264
|
+
// Step 1: Create isolated worktree using worktreeContract subagent
|
|
265
|
+
const taskBranchName = `agent/task-${task.id}-${Date.now().toString(36)}`
|
|
266
|
+
console.log(` [Worktree] Creating for ${task.id}: ${taskBranchName}`)
|
|
267
|
+
|
|
268
|
+
const worktreeResult: {
|
|
269
|
+
status: string
|
|
270
|
+
output: string
|
|
271
|
+
errors?: string[]
|
|
272
|
+
} = await context.dispatcher.dispatch(
|
|
273
|
+
{ role: 'general', model },
|
|
274
|
+
worktreeContract({
|
|
275
|
+
branchName: taskBranchName,
|
|
276
|
+
baseBranch: 'main',
|
|
277
|
+
isolation: true
|
|
278
|
+
})
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if (worktreeResult.status !== 'success') {
|
|
282
|
+
throw new Error(`Failed to create worktree for ${task.id}: ${worktreeResult.errors?.join(', ') || 'Unknown error'}`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Parse worktree output
|
|
286
|
+
const worktreeData = this.parseWorktreeOutput(worktreeResult.output)
|
|
287
|
+
const worktreePath = worktreeData.worktreePath
|
|
288
|
+
console.log(` [Worktree] Created at: ${worktreePath}`)
|
|
289
|
+
|
|
290
|
+
// Step 2: Determine target file and test file from task
|
|
291
|
+
const targetFile = task.relatedFiles?.[0] || 'src/index.ts'
|
|
292
|
+
const testFile = targetFile.replace(/\.ts$/, '.test.ts')
|
|
293
|
+
|
|
294
|
+
console.log(` [TDD] ${task.id}: ${task.description}`)
|
|
295
|
+
|
|
296
|
+
// Step 3: Execute TDD skill - it will use isolation: 'worktree' internally
|
|
297
|
+
// The TDD skill's implementer subagents will work within their own worktrees
|
|
298
|
+
// but the task-level worktree is already isolated
|
|
299
|
+
const tddResult = await tddSkill.execute({
|
|
300
|
+
behavior: task.description,
|
|
301
|
+
targetFile,
|
|
302
|
+
testFile,
|
|
303
|
+
testLevel: 'unit',
|
|
304
|
+
model
|
|
305
|
+
}, context)
|
|
306
|
+
|
|
307
|
+
// Step 4: Record worktree info in result for potential merge later
|
|
308
|
+
return {
|
|
309
|
+
...tddResult,
|
|
310
|
+
worktreePath,
|
|
311
|
+
branchName: taskBranchName
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Run verification using review-diff skill
|
|
317
|
+
* Returns structured verification result
|
|
318
|
+
*/
|
|
319
|
+
private async runVerificationWithReviewDiff(
|
|
320
|
+
plan: Plan,
|
|
321
|
+
results: TaskResult[],
|
|
322
|
+
context: SkillContext
|
|
323
|
+
): Promise<VerificationResult> {
|
|
324
|
+
console.log('\n[ExecutePlanSkill] Running verification...')
|
|
325
|
+
|
|
326
|
+
if (context.persistence) {
|
|
327
|
+
await context.persistence.state.updatePhase('verifying')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const reviewSkill = context.skills.get('review-diff')
|
|
331
|
+
if (!reviewSkill) {
|
|
332
|
+
console.warn('[ExecutePlanSkill] review-diff skill not found, skipping verification')
|
|
333
|
+
return { success: true, dimensions: {} }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Parallel verification dimensions
|
|
337
|
+
const [goalsResult, qualityResult, testsResult] = await Promise.all([
|
|
338
|
+
this.verifyGoalsWithReviewDiff(plan, context, reviewSkill),
|
|
339
|
+
this.verifyQualityWithReviewDiff(results, context, reviewSkill),
|
|
340
|
+
this.verifyTests(results, context)
|
|
341
|
+
])
|
|
342
|
+
|
|
343
|
+
const verification: VerificationResult = {
|
|
344
|
+
success: goalsResult.success && qualityResult.success && testsResult.success,
|
|
345
|
+
dimensions: {
|
|
346
|
+
goals: goalsResult,
|
|
347
|
+
quality: qualityResult,
|
|
348
|
+
tests: testsResult
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`[ExecutePlanSkill] Verification: ${verification.success ? 'PASSED' : 'FAILED'}`)
|
|
353
|
+
|
|
354
|
+
return verification
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Verify goals using review-diff skill
|
|
359
|
+
*/
|
|
360
|
+
private async verifyGoalsWithReviewDiff(
|
|
361
|
+
plan: Plan,
|
|
362
|
+
context: SkillContext,
|
|
363
|
+
reviewSkill: any
|
|
364
|
+
): Promise<DimensionResult> {
|
|
365
|
+
try {
|
|
366
|
+
// Review current changes against plan
|
|
367
|
+
const review = await reviewSkill.execute({
|
|
368
|
+
target: '.',
|
|
369
|
+
targetType: 'branch',
|
|
370
|
+
focus: ['correctness'],
|
|
371
|
+
planPath: '.pi/state/PLAN.md'
|
|
372
|
+
}, context)
|
|
373
|
+
|
|
374
|
+
const passed = review.specCompliance === 'pass' || review.specCompliance === 'partial'
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
success: passed,
|
|
378
|
+
details: review,
|
|
379
|
+
evidence: review.strengths,
|
|
380
|
+
gaps: passed ? [] : review.issues?.filter((i: any) => i.severity === 'blocker' || i.severity === 'major').map((i: any) => i.description)
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return {
|
|
384
|
+
success: false,
|
|
385
|
+
gaps: [`Goal verification failed: ${(error as Error).message}`]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Verify code quality using review-diff skill
|
|
392
|
+
*/
|
|
393
|
+
private async verifyQualityWithReviewDiff(
|
|
394
|
+
results: TaskResult[],
|
|
395
|
+
context: SkillContext,
|
|
396
|
+
reviewSkill: any
|
|
397
|
+
): Promise<DimensionResult> {
|
|
398
|
+
try {
|
|
399
|
+
const review = await reviewSkill.execute({
|
|
400
|
+
target: '.',
|
|
401
|
+
targetType: 'branch',
|
|
402
|
+
focus: ['security', 'performance', 'style']
|
|
403
|
+
}, context)
|
|
404
|
+
|
|
405
|
+
const hasBlockers = review.issues?.some((i: any) => i.severity === 'blocker')
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
success: !hasBlockers && review.decision !== 'blocked',
|
|
409
|
+
details: review,
|
|
410
|
+
evidence: review.strengths,
|
|
411
|
+
gaps: hasBlockers ? ['Critical issues found'] : []
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
return {
|
|
415
|
+
success: false,
|
|
416
|
+
gaps: [`Quality verification failed: ${(error as Error).message}`]
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Verify tests by checking TDD results
|
|
423
|
+
*/
|
|
424
|
+
private async verifyTests(
|
|
425
|
+
results: TaskResult[],
|
|
426
|
+
context: SkillContext
|
|
427
|
+
): Promise<DimensionResult> {
|
|
428
|
+
// Check if all tasks have tests
|
|
429
|
+
const allHaveTests = results.every(r => r.testsAdded && r.testsAdded.length > 0)
|
|
430
|
+
const allPassed = results.every(r => r.success)
|
|
431
|
+
|
|
432
|
+
// Try to run full test suite
|
|
433
|
+
try {
|
|
434
|
+
const testResult = await context.dispatcher.dispatch<{
|
|
435
|
+
coverage: number
|
|
436
|
+
passed: boolean
|
|
437
|
+
}>({
|
|
438
|
+
role: 'verifier',
|
|
439
|
+
timeout: 120000
|
|
440
|
+
}, {
|
|
441
|
+
permissions: {
|
|
442
|
+
readFiles: true,
|
|
443
|
+
searchCode: true,
|
|
444
|
+
runCommands: true,
|
|
445
|
+
writeFiles: false,
|
|
446
|
+
gitOperations: false
|
|
447
|
+
},
|
|
448
|
+
prompt: `Run the full test suite and report coverage.\n\nReturn JSON: { "passed": boolean, "coverage": number }`,
|
|
449
|
+
owns: [],
|
|
450
|
+
reads: []
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const coverageOk = testResult.coverage >= 80
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
success: allHaveTests && allPassed && coverageOk,
|
|
457
|
+
details: { coverage: testResult.coverage, allHaveTests, allPassed },
|
|
458
|
+
evidence: coverageOk ? [`Coverage: ${testResult.coverage}%`] : [],
|
|
459
|
+
gaps: !coverageOk ? [`Coverage ${testResult.coverage}% < 80%`] : []
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// Fallback to basic check
|
|
463
|
+
return {
|
|
464
|
+
success: allHaveTests && allPassed,
|
|
465
|
+
details: { allHaveTests, allPassed },
|
|
466
|
+
evidence: allHaveTests ? ['All tasks have tests'] : [],
|
|
467
|
+
gaps: !allHaveTests ? ['Some tasks missing tests'] : []
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Handle wave failure
|
|
474
|
+
*/
|
|
475
|
+
private async handleWaveFailure(
|
|
476
|
+
wave: Wave,
|
|
477
|
+
failedTasks: TaskResult[],
|
|
478
|
+
context: SkillContext
|
|
479
|
+
): Promise<void> {
|
|
480
|
+
console.error(`[ExecutePlanSkill] Wave failure handler`)
|
|
481
|
+
|
|
482
|
+
await this.triggerHook(context, HookType.ON_ERROR, {
|
|
483
|
+
phase: 'execute',
|
|
484
|
+
wave,
|
|
485
|
+
failedTasks
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Merge completed task worktrees back to main worktree with automatic conflict resolution
|
|
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
|
|
499
|
+
*/
|
|
500
|
+
private async mergeTaskWorktrees(
|
|
501
|
+
waveResults: TaskResult[],
|
|
502
|
+
context: SkillContext
|
|
503
|
+
): Promise<{
|
|
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 }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Parse merge result from subagent output
|
|
626
|
+
*/
|
|
627
|
+
private parseMergeResult(output: string): {
|
|
628
|
+
status: 'success' | 'conflict'
|
|
629
|
+
conflictingFiles: string[]
|
|
630
|
+
} {
|
|
631
|
+
try {
|
|
632
|
+
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
633
|
+
if (jsonMatch) {
|
|
634
|
+
const parsed = JSON.parse(jsonMatch[1])
|
|
635
|
+
return {
|
|
636
|
+
status: parsed.status || 'conflict',
|
|
637
|
+
conflictingFiles: parsed.conflictingFiles || []
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
// Fallback to text parsing
|
|
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' }
|
|
732
|
+
}
|
|
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
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Parse conflict analysis from subagent output
|
|
747
|
+
*/
|
|
748
|
+
private parseConflictAnalysis(output: string): {
|
|
749
|
+
conflictType: string
|
|
750
|
+
canAutoResolve: boolean
|
|
751
|
+
strategy: string
|
|
752
|
+
} {
|
|
753
|
+
try {
|
|
754
|
+
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
755
|
+
if (jsonMatch) {
|
|
756
|
+
const parsed = JSON.parse(jsonMatch[1])
|
|
757
|
+
return {
|
|
758
|
+
conflictType: parsed.conflictType || 'unknown',
|
|
759
|
+
canAutoResolve: parsed.canAutoResolve || false,
|
|
760
|
+
strategy: parsed.strategy || 'manual'
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch {
|
|
764
|
+
// Default fallback
|
|
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'
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Parse resolution result
|
|
876
|
+
*/
|
|
877
|
+
private parseResolutionResult(output: string): {
|
|
878
|
+
success: boolean
|
|
879
|
+
resolution?: string
|
|
880
|
+
} {
|
|
881
|
+
try {
|
|
882
|
+
const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
|
|
883
|
+
if (jsonMatch) {
|
|
884
|
+
const parsed = JSON.parse(jsonMatch[1])
|
|
885
|
+
return {
|
|
886
|
+
success: parsed.success || false,
|
|
887
|
+
resolution: parsed.resolution
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
} catch {
|
|
891
|
+
// Check for success indicators in text
|
|
892
|
+
if (output.includes('success') || output.includes('resolved')) {
|
|
893
|
+
return { success: true }
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return { success: false }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* Parse worktree output from subagent
|
|
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
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
925
|
+
// Fallback: try to extract from text
|
|
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'
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Helper to trigger hooks with error handling
|
|
954
|
+
*/
|
|
955
|
+
private async triggerHook(
|
|
956
|
+
context: SkillContext,
|
|
957
|
+
type: typeof HookType[keyof typeof HookType],
|
|
958
|
+
data: Record<string, unknown>
|
|
959
|
+
): Promise<void> {
|
|
960
|
+
if (context.hooks) {
|
|
961
|
+
try {
|
|
962
|
+
const hookContext = data as unknown as import('../../types.js').HookContext
|
|
963
|
+
await context.hooks.execute(type as HookType, hookContext)
|
|
964
|
+
} catch (error) {
|
|
965
|
+
console.warn(`[ExecutePlanSkill] Hook ${type} failed:`, error)
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
export const executePlanSkill = new ExecutePlanSkill()
|