@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,551 @@
1
+ /**
2
+ * Enhanced Plan Skill - Orchestrates analysis, debugging, and planning
3
+ *
4
+ * Architecture: Orchestrator pattern
5
+ * - Analyzes input to determine what preprocessing is needed
6
+ * - May spawn debugging subagent for bug investigation
7
+ * - Delegates actual planning to planner subagent
8
+ * - Wave grouping and validation
9
+ *
10
+ * Examples:
11
+ * "/to-plan 实现登录功能" → Direct planning
12
+ * "/to-plan 出现xxbug,查明原因并制定计划" → Debug → Plan
13
+ * "/to-plan 性能问题,先分析再计划" → Explore → Plan
14
+ */
15
+ import { z } from 'zod'
16
+ import { Skill } from '../skill.js'
17
+ import type { SkillContext } from '../types.js'
18
+ import type { Plan, Task, Wave } from '../../persistence/types.js'
19
+ // HookType constants
20
+ const HookType = {
21
+ PRE_PLAN: 'pre-plan',
22
+ POST_PLAN: 'post-plan',
23
+ ON_ERROR: 'on-error',
24
+ } as const
25
+
26
+ const PlanInputSchema = z.object({
27
+ goal: z.string(),
28
+ constraints: z.array(z.string()).optional(),
29
+ model: z.string().optional(),
30
+ previousIssues: z.array(z.string()).optional(),
31
+ // Analysis hints
32
+ symptom: z.string().optional(), // Bug symptom
33
+ reproduceCommand: z.string().optional(), // How to reproduce
34
+ logs: z.string().optional(), // Error logs
35
+ recentChanges: z.string().optional(), // Recent git changes
36
+ })
37
+
38
+ const PlanOutputSchema = z.object({
39
+ plan: z.custom<Plan>(),
40
+ waves: z.array(z.custom<Wave>()),
41
+ // If debugging was performed
42
+ rootCause: z.string().optional(),
43
+ confidence: z.enum(['high', 'medium', 'low']).optional(),
44
+ // Metadata
45
+ analysisPerformed: z.boolean(),
46
+ tokensUsed: z.number()
47
+ })
48
+
49
+ type PlanInput = z.infer<typeof PlanInputSchema>
50
+ type PlanOutput = z.infer<typeof PlanOutputSchema>
51
+
52
+ export class ToPlanSkill extends Skill<PlanInput, PlanOutput> {
53
+ protected name = 'to-plan'
54
+ protected description = 'Orchestrates analysis, debugging, and planning with fresh-context subagents'
55
+ protected requires = ['systematic-debugging', 'quick-task']
56
+
57
+ async execute(input: PlanInput, context: SkillContext): Promise<PlanOutput> {
58
+ let analysisPerformed = false
59
+ let rootCause: string | undefined
60
+ let confidence: 'high' | 'medium' | 'low' | undefined
61
+ let tokensUsed = 0
62
+
63
+ // 1. Initialize persistence
64
+ if (context.persistence) {
65
+ await context.persistence.state.initialize(input.goal)
66
+ }
67
+
68
+ // 2. Trigger pre-plan hooks
69
+ await this.triggerHook(context, HookType.PRE_PLAN, { goal: input.goal })
70
+
71
+ try {
72
+ // 3. Analyze input and determine workflow
73
+ const analysis = this.analyzeInput(input)
74
+ console.log(`[ToPlanSkill] Analysis: ${analysis.type}`)
75
+
76
+ // 4. Perform preprocessing if needed
77
+ let enrichedGoal = input.goal
78
+
79
+ if (analysis.needsDebugging) {
80
+ console.log('[ToPlanSkill] Bug detected, running systematic debugging...')
81
+
82
+ // Use systematic-debugging skill
83
+ const debugResult = await this.runDebugging(input, context)
84
+
85
+ analysisPerformed = true
86
+ rootCause = debugResult.rootCause
87
+ confidence = debugResult.confidence
88
+ tokensUsed += debugResult.tokensUsed
89
+
90
+ // Enrich goal with root cause
91
+ enrichedGoal = `${input.goal}\n\nRoot Cause (confidence: ${confidence}): ${rootCause}`
92
+
93
+ console.log(`[ToPlanSkill] Debug complete: ${rootCause.slice(0, 100)}...`)
94
+ }
95
+
96
+ if (analysis.needsExploration) {
97
+ console.log('[ToPlanSkill] Exploration needed, gathering context...')
98
+
99
+ // Run exploration
100
+ const exploreResult = await this.runExploration(input, context)
101
+ tokensUsed += exploreResult.tokensUsed
102
+
103
+ // Enrich goal with findings
104
+ enrichedGoal = `${enrichedGoal}\n\nContext: ${exploreResult.findings}`
105
+
106
+ analysisPerformed = true
107
+ }
108
+
109
+ // 5. Generate plan with planner subagent
110
+ console.log('[ToPlanSkill] Generating plan...')
111
+ const planResult = await this.spawnPlannerSubagent(
112
+ { ...input, goal: enrichedGoal },
113
+ context
114
+ )
115
+ tokensUsed += planResult.tokensUsed
116
+
117
+ // 6. Parse and validate plan
118
+ let plan = this.parsePlanResponse(planResult.output)
119
+ let validation = this.validatePlan(plan)
120
+
121
+ // 7. Retry once if validation fails
122
+ if (!validation.valid && !input.previousIssues) {
123
+ console.log('[ToPlanSkill] Validation failed, retrying with feedback...')
124
+ const retryResult = await this.spawnPlannerSubagent(
125
+ {
126
+ ...input,
127
+ goal: enrichedGoal,
128
+ previousIssues: validation.issues
129
+ },
130
+ context
131
+ )
132
+ tokensUsed += retryResult.tokensUsed
133
+ plan = this.parsePlanResponse(retryResult.output)
134
+ validation = this.validatePlan(plan)
135
+ }
136
+
137
+ // 8. Group tasks into waves
138
+ const waves = this.groupIntoWaves(plan)
139
+ plan.waves = waves
140
+
141
+ // 9. Write to persistence
142
+ if (context.persistence) {
143
+ await context.persistence.plan.writePlan(plan)
144
+ await context.persistence.state.recordPlan(plan)
145
+ await context.persistence.state.updatePhase('planning')
146
+ }
147
+
148
+ // 10. Trigger post-plan hooks
149
+ await this.triggerHook(context, HookType.POST_PLAN, {
150
+ plan,
151
+ waves,
152
+ analysisPerformed,
153
+ rootCause
154
+ })
155
+
156
+ return {
157
+ plan,
158
+ waves,
159
+ rootCause,
160
+ confidence,
161
+ analysisPerformed,
162
+ tokensUsed
163
+ }
164
+
165
+ } catch (error) {
166
+ await this.triggerHook(context, HookType.ON_ERROR, {
167
+ phase: 'plan',
168
+ error: (error as Error).message
169
+ })
170
+ throw error
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Analyze input to determine what preprocessing is needed
176
+ */
177
+ private analyzeInput(input: PlanInput): {
178
+ type: 'direct' | 'bug' | 'exploration' | 'combined'
179
+ needsDebugging: boolean
180
+ needsExploration: boolean
181
+ } {
182
+ const goal = input.goal.toLowerCase()
183
+
184
+ // Bug indicators
185
+ const bugPatterns = [
186
+ /bug|错误|故障|崩溃|crash|exception|error/i,
187
+ /查明原因|调查|定位|诊断|debug|fix/i,
188
+ /出现.*问题|发生.*错误|不工作|失败|fail/i
189
+ ]
190
+
191
+ // Exploration indicators
192
+ const explorePatterns = [
193
+ /分析.*代码|了解.*架构|探索|调研/i,
194
+ /refactor|重构.*前/i,
195
+ /不熟悉|不清楚|unknown/i
196
+ ]
197
+
198
+ const hasBugKeywords = bugPatterns.some(p => p.test(goal))
199
+ const hasExploreKeywords = explorePatterns.some(p => p.test(goal))
200
+
201
+ // Explicit symptom provided
202
+ const hasSymptom = !!input.symptom || !!input.logs || !!input.reproduceCommand
203
+
204
+ const needsDebugging = hasBugKeywords || hasSymptom
205
+ const needsExploration = hasExploreKeywords
206
+
207
+ if (needsDebugging && needsExploration) {
208
+ return { type: 'combined', needsDebugging: true, needsExploration: true }
209
+ } else if (needsDebugging) {
210
+ return { type: 'bug', needsDebugging: true, needsExploration: false }
211
+ } else if (needsExploration) {
212
+ return { type: 'exploration', needsDebugging: false, needsExploration: true }
213
+ } else {
214
+ return { type: 'direct', needsDebugging: false, needsExploration: false }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Run systematic debugging using debugging skill
220
+ */
221
+ private async runDebugging(
222
+ input: PlanInput,
223
+ context: SkillContext
224
+ ): Promise<{
225
+ rootCause: string
226
+ confidence: 'high' | 'medium' | 'low'
227
+ tokensUsed: number
228
+ }> {
229
+ // Try to use systematic-debugging skill if available
230
+ const debugSkill = context.skills.get('systematic-debugging')
231
+
232
+ if (debugSkill) {
233
+ console.log('[ToPlanSkill] Using systematic-debugging skill')
234
+ const result = await debugSkill.execute({
235
+ symptom: input.symptom || input.goal,
236
+ reproduceCommand: input.reproduceCommand,
237
+ logs: input.logs,
238
+ recentChanges: input.recentChanges,
239
+ model: input.model
240
+ }, context)
241
+
242
+ return {
243
+ rootCause: result.rootCause,
244
+ confidence: result.confidence,
245
+ tokensUsed: result.tokensUsed
246
+ }
247
+ }
248
+
249
+ // Fallback: direct dispatcher
250
+ console.log('[ToPlanSkill] Fallback: direct debugging subagent')
251
+ const result = await context.dispatcher.dispatch<{
252
+ rootCause: string
253
+ confidence: 'high' | 'medium' | 'low'
254
+ tokensUsed: number
255
+ }>({
256
+ role: 'debugger',
257
+ model: input.model,
258
+ timeout: 180000
259
+ }, {
260
+ permissions: {
261
+ readFiles: true,
262
+ searchCode: true,
263
+ runCommands: true,
264
+ writeFiles: false,
265
+ gitOperations: false
266
+ },
267
+ prompt: this.buildDebuggingPrompt(input),
268
+ owns: [],
269
+ reads: []
270
+ })
271
+
272
+ return result
273
+ }
274
+
275
+ /**
276
+ * Run exploration to gather context
277
+ */
278
+ private async runExploration(
279
+ input: PlanInput,
280
+ context: SkillContext
281
+ ): Promise<{
282
+ findings: string
283
+ tokensUsed: number
284
+ }> {
285
+ const result = await context.dispatcher.dispatch<{
286
+ findings: string
287
+ tokensUsed: number
288
+ }>({
289
+ role: 'explorer',
290
+ model: input.model,
291
+ timeout: 120000
292
+ }, {
293
+ permissions: {
294
+ readFiles: true,
295
+ searchCode: true,
296
+ runCommands: false,
297
+ writeFiles: false,
298
+ gitOperations: false
299
+ },
300
+ prompt: `# Exploration Task
301
+
302
+ Goal: ${input.goal}
303
+
304
+ Explore the codebase to understand:
305
+ 1. Overall architecture and structure
306
+ 2. Relevant files and patterns
307
+ 3. Dependencies and relationships
308
+ 4. Potential challenges or risks
309
+
310
+ Return a concise summary (under 800 tokens) of key findings that will help with planning.`,
311
+ owns: [],
312
+ reads: []
313
+ })
314
+
315
+ return result
316
+ }
317
+
318
+ /**
319
+ * Spawn planner subagent with fresh context
320
+ */
321
+ private async spawnPlannerSubagent(
322
+ input: PlanInput,
323
+ context: SkillContext
324
+ ): Promise<{ output: string; tokensUsed: number }> {
325
+ const result = await context.dispatcher.dispatch<{ plan: string }>(
326
+ {
327
+ role: 'planner',
328
+ model: input.model,
329
+ timeout: 120000
330
+ },
331
+ {
332
+ permissions: {
333
+ readFiles: true,
334
+ searchCode: true,
335
+ runCommands: false,
336
+ writeFiles: false,
337
+ gitOperations: false
338
+ },
339
+ prompt: this.buildPlannerPrompt(input),
340
+ owns: [],
341
+ reads: []
342
+ }
343
+ )
344
+
345
+ return { output: result.plan, tokensUsed: 0 }
346
+ }
347
+
348
+ private buildDebuggingPrompt(input: PlanInput): string {
349
+ return `# Systematic Debugging
350
+
351
+ ## Symptom
352
+ ${input.symptom || input.goal}
353
+
354
+ ## Reproduction
355
+ ${input.reproduceCommand ? `Command: ${input.reproduceCommand}` : 'Reproduce the issue'}
356
+
357
+ ${input.logs ? `## Logs\n${input.logs.slice(0, 2000)}` : ''}
358
+
359
+ ${input.recentChanges ? `## Recent Changes\n${input.recentChanges}` : ''}
360
+
361
+ ## Instructions
362
+ 1. Reproduce the issue
363
+ 2. Gather evidence systematically
364
+ 3. Form and test hypotheses
365
+ 4. Identify root cause with confidence level
366
+ 5. Propose fix
367
+
368
+ ## Output Format
369
+ {
370
+ "rootCause": "detailed description",
371
+ "confidence": "high|medium|low",
372
+ "location": { "file": "path", "line": 123, "function": "name" },
373
+ "proposedFix": "description",
374
+ "verification": "how to verify the fix"
375
+ }`
376
+ }
377
+
378
+ private buildPlannerPrompt(input: PlanInput): string {
379
+ const issueSection = input.previousIssues
380
+ ? `\n\n## Previous Attempt Issues\n${input.previousIssues.map(i => `- ${i}`).join('\n')}\nPlease address these issues in your new plan.`
381
+ : ''
382
+
383
+ return `# Planning Task
384
+
385
+ ## Goal
386
+ ${input.goal}
387
+
388
+ ## Constraints
389
+ ${input.constraints?.map(c => `- ${c}`).join('\n') || '- No specific constraints'}
390
+ ${issueSection}
391
+
392
+ ## Instructions
393
+ 1. Break down into small, independent tasks (<50 lines of change each)
394
+ 2. Identify dependencies between tasks explicitly
395
+ 3. Group into parallel execution waves
396
+ 4. Define clear verification criteria
397
+ 5. If this is a bug fix, include regression tests
398
+
399
+ ## Output Format
400
+ Return ONLY a JSON object:
401
+ {
402
+ "goal": "original goal",
403
+ "tasks": [
404
+ {
405
+ "id": "task-1",
406
+ "description": "What to do",
407
+ "estimatedComplexity": "low|medium|high",
408
+ "relatedFiles": ["optional/path"],
409
+ "verificationCriteria": ["How to verify"]
410
+ }
411
+ ],
412
+ "dependencies": {"task-2": ["task-1"]},
413
+ "verificationCriteria": ["Overall acceptance criteria"],
414
+ "estimatedDuration": 300000,
415
+ "rationale": "Why this plan"
416
+ }`
417
+ }
418
+
419
+ private parsePlanResponse(response: string): Plan {
420
+ try {
421
+ const jsonMatch = response.match(/\{[\s\S]*\}/)
422
+ const jsonStr = jsonMatch ? jsonMatch[0] : response
423
+ const parsed = JSON.parse(jsonStr)
424
+
425
+ return {
426
+ goal: parsed.goal || '',
427
+ tasks: parsed.tasks || [],
428
+ dependencies: parsed.dependencies || {},
429
+ waves: parsed.waves || [],
430
+ verificationCriteria: parsed.verificationCriteria || [],
431
+ estimatedDuration: parsed.estimatedDuration || 0,
432
+ rationale: parsed.rationale || ''
433
+ }
434
+ } catch (error) {
435
+ console.error('[ToPlanSkill] Failed to parse plan:', error)
436
+ return {
437
+ goal: 'Failed to parse',
438
+ tasks: [],
439
+ dependencies: {},
440
+ waves: [],
441
+ verificationCriteria: [],
442
+ estimatedDuration: 0,
443
+ rationale: 'Parse error'
444
+ }
445
+ }
446
+ }
447
+
448
+ private validatePlan(plan: Plan): { valid: boolean; issues: string[] } {
449
+ const issues: string[] = []
450
+
451
+ if (!plan.goal || plan.goal.trim() === '') {
452
+ issues.push('Plan must have a goal')
453
+ }
454
+
455
+ if (plan.tasks.length === 0) {
456
+ issues.push('Plan must have at least one task')
457
+ }
458
+
459
+ // Check for circular dependencies
460
+ const visited = new Set<string>()
461
+ const recursionStack = new Set<string>()
462
+
463
+ const hasCycle = (taskId: string): boolean => {
464
+ visited.add(taskId)
465
+ recursionStack.add(taskId)
466
+
467
+ const deps = plan.dependencies[taskId] || []
468
+ for (const dep of deps) {
469
+ if (!visited.has(dep)) {
470
+ if (hasCycle(dep)) return true
471
+ } else if (recursionStack.has(dep)) {
472
+ issues.push(`Circular dependency: ${taskId} -> ${dep}`)
473
+ return true
474
+ }
475
+ }
476
+
477
+ recursionStack.delete(taskId)
478
+ return false
479
+ }
480
+
481
+ for (const task of plan.tasks) {
482
+ if (!visited.has(task.id)) {
483
+ hasCycle(task.id)
484
+ }
485
+ }
486
+
487
+ // Validate dependencies exist
488
+ for (const [taskId, deps] of Object.entries(plan.dependencies)) {
489
+ if (!plan.tasks.some(t => t.id === taskId)) {
490
+ issues.push(`Dependency references non-existent task: ${taskId}`)
491
+ }
492
+ for (const dep of deps) {
493
+ if (!plan.tasks.some(t => t.id === dep)) {
494
+ issues.push(`Task ${taskId} depends on non-existent task: ${dep}`)
495
+ }
496
+ }
497
+ }
498
+
499
+ return { valid: issues.length === 0, issues }
500
+ }
501
+
502
+ private groupIntoWaves(plan: Plan): Wave[] {
503
+ const waves: Wave[] = []
504
+ const completed = new Set<string>()
505
+ const remaining = new Set(plan.tasks.map(t => t.id))
506
+ let waveIndex = 0
507
+
508
+ while (remaining.size > 0) {
509
+ const waveTasks = plan.tasks.filter(t => {
510
+ if (!remaining.has(t.id)) return false
511
+ const deps = plan.dependencies[t.id] || []
512
+ return deps.every(dep => completed.has(dep))
513
+ })
514
+
515
+ if (waveTasks.length === 0) {
516
+ console.error('[ToPlanSkill] Circular dependency detected')
517
+ break
518
+ }
519
+
520
+ waves.push({
521
+ id: `wave-${waveIndex}`,
522
+ tasks: waveTasks.map(t => t.id)
523
+ })
524
+
525
+ waveTasks.forEach(t => {
526
+ completed.add(t.id)
527
+ remaining.delete(t.id)
528
+ })
529
+
530
+ waveIndex++
531
+ }
532
+
533
+ return waves
534
+ }
535
+
536
+ private async triggerHook(
537
+ context: SkillContext,
538
+ type: typeof HookType[keyof typeof HookType],
539
+ data: Record<string, unknown>
540
+ ): Promise<void> {
541
+ if (context.hooks) {
542
+ try {
543
+ // Import HookContext from types for proper typing
544
+ const hookContext = data as unknown as import('../../types.js').HookContext
545
+ await context.hooks.execute(type as import('../../types.js').HookType, hookContext)
546
+ } catch (error) {
547
+ console.warn(`[ToPlanSkill] Hook ${type} failed:`, error)
548
+ }
549
+ }
550
+ }
551
+ }