@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.
Files changed (62) hide show
  1. package/package.json +15 -10
  2. package/scripts/postinstall.js +2 -2
  3. package/src/agents/contracts.ts +559 -0
  4. package/src/agents/dispatcher-enhanced.ts +350 -0
  5. package/src/agents/dispatcher.ts +680 -0
  6. package/src/agents/index.ts +48 -0
  7. package/src/agents/resilience.ts +255 -0
  8. package/src/agents/token-budget.ts +83 -0
  9. package/src/agents/types.ts +73 -0
  10. package/src/guard/main-agent.ts +245 -0
  11. package/src/hooks/builtin/index.ts +8 -0
  12. package/src/hooks/builtin/on-error.ts +23 -0
  13. package/src/hooks/builtin/post-execute.ts +40 -0
  14. package/src/hooks/builtin/post-plan.ts +23 -0
  15. package/src/hooks/builtin/pre-execute.ts +30 -0
  16. package/src/hooks/builtin/pre-plan.ts +26 -0
  17. package/src/hooks/index.ts +7 -0
  18. package/src/hooks/loader.ts +98 -0
  19. package/src/hooks/manager.ts +99 -0
  20. package/src/hooks/types-enhanced.ts +38 -0
  21. package/src/hooks/types.ts +35 -0
  22. package/src/index.ts +127 -0
  23. package/src/persistence/index.ts +17 -0
  24. package/src/persistence/plan-md.ts +141 -0
  25. package/src/persistence/state-md.ts +167 -0
  26. package/src/persistence/types.ts +89 -0
  27. package/src/router/classifier.ts +610 -0
  28. package/src/router/guard.ts +483 -0
  29. package/src/router/index.ts +22 -0
  30. package/src/router/router.ts +108 -0
  31. package/src/router/types.ts +127 -0
  32. package/src/skills/agents-md/SKILL.md +45 -0
  33. package/src/skills/agents-md/index.ts +33 -0
  34. package/src/skills/execute-plan/SKILL.md +60 -0
  35. package/src/skills/execute-plan/index.ts +970 -0
  36. package/src/skills/index.ts +13 -0
  37. package/src/skills/quick-task/SKILL.md +54 -0
  38. package/src/skills/quick-task/index.ts +346 -0
  39. package/src/skills/registry.ts +59 -0
  40. package/src/skills/review-diff/SKILL.md +53 -0
  41. package/src/skills/review-diff/index.ts +394 -0
  42. package/src/skills/skill.ts +59 -0
  43. package/src/skills/systematic-debugging/SKILL.md +56 -0
  44. package/src/skills/systematic-debugging/index.ts +404 -0
  45. package/src/skills/tdd/SKILL.md +52 -0
  46. package/src/skills/tdd/index.ts +409 -0
  47. package/src/skills/to-plan/SKILL.md +56 -0
  48. package/src/skills/to-plan/index-enhanced.ts +551 -0
  49. package/src/skills/to-plan/index.ts +586 -0
  50. package/src/skills/types.ts +47 -0
  51. package/src/state/cleanup.ts +118 -0
  52. package/src/state/index.ts +8 -0
  53. package/src/state/manager.ts +96 -0
  54. package/src/state/persistence.ts +77 -0
  55. package/src/state/types.ts +30 -0
  56. package/src/state/validator.ts +78 -0
  57. package/src/types.ts +102 -0
  58. package/src/utils/compress.ts +347 -0
  59. package/src/utils/git.ts +82 -0
  60. package/src/utils/index.ts +6 -0
  61. package/src/utils/logger.ts +23 -0
  62. 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()