@haoyiyin/workflow 0.2.11 → 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.
Files changed (164) hide show
  1. package/dist/src/agents/contracts/implementer.d.ts +29 -0
  2. package/dist/src/agents/contracts/implementer.d.ts.map +1 -0
  3. package/dist/src/agents/contracts/implementer.js +94 -0
  4. package/dist/src/agents/contracts/implementer.js.map +1 -0
  5. package/dist/src/agents/contracts/index.d.ts +11 -0
  6. package/dist/src/agents/contracts/index.d.ts.map +1 -0
  7. package/dist/src/agents/contracts/index.js +11 -0
  8. package/dist/src/agents/contracts/index.js.map +1 -0
  9. package/dist/src/agents/contracts/planner.d.ts +25 -0
  10. package/dist/src/agents/contracts/planner.d.ts.map +1 -0
  11. package/dist/src/agents/contracts/planner.js +107 -0
  12. package/dist/src/agents/contracts/planner.js.map +1 -0
  13. package/dist/src/agents/contracts/router.d.ts +24 -0
  14. package/dist/src/agents/contracts/router.d.ts.map +1 -0
  15. package/dist/src/agents/contracts/router.js +137 -0
  16. package/dist/src/agents/contracts/router.js.map +1 -0
  17. package/dist/src/agents/contracts/verifier.d.ts +27 -0
  18. package/dist/src/agents/contracts/verifier.d.ts.map +1 -0
  19. package/dist/src/agents/contracts/verifier.js +115 -0
  20. package/dist/src/agents/contracts/verifier.js.map +1 -0
  21. package/dist/src/agents/dispatcher.d.ts +94 -51
  22. package/dist/src/agents/dispatcher.d.ts.map +1 -1
  23. package/dist/src/agents/dispatcher.js +207 -164
  24. package/dist/src/agents/dispatcher.js.map +1 -1
  25. package/dist/src/persistence/index.d.ts +4 -2
  26. package/dist/src/persistence/index.d.ts.map +1 -1
  27. package/dist/src/persistence/index.js +4 -1
  28. package/dist/src/persistence/index.js.map +1 -1
  29. package/dist/src/persistence/plan-md.d.ts +3 -2
  30. package/dist/src/persistence/plan-md.d.ts.map +1 -1
  31. package/dist/src/persistence/plan-md.js +47 -15
  32. package/dist/src/persistence/plan-md.js.map +1 -1
  33. package/dist/src/persistence/state-md.d.ts +2 -0
  34. package/dist/src/persistence/state-md.d.ts.map +1 -1
  35. package/dist/src/persistence/state-md.js +40 -22
  36. package/dist/src/persistence/state-md.js.map +1 -1
  37. package/dist/src/persistence/types.d.ts +35 -39
  38. package/dist/src/persistence/types.d.ts.map +1 -1
  39. package/dist/src/router/namespace/core/intent-router.d.ts +24 -0
  40. package/dist/src/router/namespace/core/intent-router.d.ts.map +1 -0
  41. package/dist/src/router/namespace/core/intent-router.js +190 -0
  42. package/dist/src/router/namespace/core/intent-router.js.map +1 -0
  43. package/dist/src/router/namespace/core/lifecycle-router.d.ts +28 -0
  44. package/dist/src/router/namespace/core/lifecycle-router.d.ts.map +1 -0
  45. package/dist/src/router/namespace/core/lifecycle-router.js +132 -0
  46. package/dist/src/router/namespace/core/lifecycle-router.js.map +1 -0
  47. package/dist/src/router/namespace/core/state-router.d.ts +32 -0
  48. package/dist/src/router/namespace/core/state-router.d.ts.map +1 -0
  49. package/dist/src/router/namespace/core/state-router.js +157 -0
  50. package/dist/src/router/namespace/core/state-router.js.map +1 -0
  51. package/dist/src/router/namespace/domain/code-router.d.ts +26 -0
  52. package/dist/src/router/namespace/domain/code-router.d.ts.map +1 -0
  53. package/dist/src/router/namespace/domain/code-router.js +171 -0
  54. package/dist/src/router/namespace/domain/code-router.js.map +1 -0
  55. package/dist/src/router/namespace/domain/debug-router.d.ts +25 -0
  56. package/dist/src/router/namespace/domain/debug-router.d.ts.map +1 -0
  57. package/dist/src/router/namespace/domain/debug-router.js +139 -0
  58. package/dist/src/router/namespace/domain/debug-router.js.map +1 -0
  59. package/dist/src/router/namespace/domain/plan-router.d.ts +29 -0
  60. package/dist/src/router/namespace/domain/plan-router.d.ts.map +1 -0
  61. package/dist/src/router/namespace/domain/plan-router.js +160 -0
  62. package/dist/src/router/namespace/domain/plan-router.js.map +1 -0
  63. package/dist/src/router/namespace/domain/review-router.d.ts +24 -0
  64. package/dist/src/router/namespace/domain/review-router.d.ts.map +1 -0
  65. package/dist/src/router/namespace/domain/review-router.js +116 -0
  66. package/dist/src/router/namespace/domain/review-router.js.map +1 -0
  67. package/dist/src/router/namespace/index.d.ts +19 -0
  68. package/dist/src/router/namespace/index.d.ts.map +1 -0
  69. package/dist/src/router/namespace/index.js +22 -0
  70. package/dist/src/router/namespace/index.js.map +1 -0
  71. package/dist/src/router/namespace/registry.d.ts +67 -0
  72. package/dist/src/router/namespace/registry.d.ts.map +1 -0
  73. package/dist/src/router/namespace/registry.js +197 -0
  74. package/dist/src/router/namespace/registry.js.map +1 -0
  75. package/dist/src/router/namespace/types.d.ts +124 -0
  76. package/dist/src/router/namespace/types.d.ts.map +1 -0
  77. package/dist/src/router/namespace/types.js +20 -0
  78. package/dist/src/router/namespace/types.js.map +1 -0
  79. package/dist/src/router/namespace/utility/fallback-router.d.ts +28 -0
  80. package/dist/src/router/namespace/utility/fallback-router.d.ts.map +1 -0
  81. package/dist/src/router/namespace/utility/fallback-router.js +88 -0
  82. package/dist/src/router/namespace/utility/fallback-router.js.map +1 -0
  83. package/dist/src/router/namespace/utility/quick-task-router.d.ts +28 -0
  84. package/dist/src/router/namespace/utility/quick-task-router.d.ts.map +1 -0
  85. package/dist/src/router/namespace/utility/quick-task-router.js +99 -0
  86. package/dist/src/router/namespace/utility/quick-task-router.js.map +1 -0
  87. package/dist/src/router/namespace/utility/research-router.d.ts +24 -0
  88. package/dist/src/router/namespace/utility/research-router.d.ts.map +1 -0
  89. package/dist/src/router/namespace/utility/research-router.js +84 -0
  90. package/dist/src/router/namespace/utility/research-router.js.map +1 -0
  91. package/dist/src/skills/agents-md/index.js +2 -2
  92. package/dist/src/skills/agents-md/index.js.map +1 -1
  93. package/dist/src/skills/execute-plan/index.d.ts +45 -65
  94. package/dist/src/skills/execute-plan/index.d.ts.map +1 -1
  95. package/dist/src/skills/execute-plan/index.js +325 -551
  96. package/dist/src/skills/execute-plan/index.js.map +1 -1
  97. package/dist/src/skills/index.d.ts +1 -0
  98. package/dist/src/skills/index.d.ts.map +1 -1
  99. package/dist/src/skills/index.js +1 -0
  100. package/dist/src/skills/index.js.map +1 -1
  101. package/dist/src/skills/quick-task/index.d.ts +4 -4
  102. package/dist/src/skills/quick-task/index.js +1 -1
  103. package/dist/src/skills/quick-task/index.js.map +1 -1
  104. package/dist/src/skills/review-diff/index.d.ts +6 -6
  105. package/dist/src/skills/review-diff/index.js +1 -1
  106. package/dist/src/skills/review-diff/index.js.map +1 -1
  107. package/dist/src/skills/router/index.d.ts +101 -0
  108. package/dist/src/skills/router/index.d.ts.map +1 -0
  109. package/dist/src/skills/router/index.js +450 -0
  110. package/dist/src/skills/router/index.js.map +1 -0
  111. package/dist/src/skills/router/types.d.ts +79 -0
  112. package/dist/src/skills/router/types.d.ts.map +1 -0
  113. package/dist/src/skills/router/types.js +8 -0
  114. package/dist/src/skills/router/types.js.map +1 -0
  115. package/dist/src/skills/systematic-debugging/index.js +1 -1
  116. package/dist/src/skills/systematic-debugging/index.js.map +1 -1
  117. package/dist/src/skills/tdd/index.d.ts +14 -14
  118. package/dist/src/skills/tdd/index.js +1 -1
  119. package/dist/src/skills/tdd/index.js.map +1 -1
  120. package/dist/src/skills/to-plan/index-enhanced.d.ts +4 -4
  121. package/dist/src/skills/to-plan/index-enhanced.d.ts.map +1 -1
  122. package/dist/src/skills/to-plan/index-enhanced.js +3 -5
  123. package/dist/src/skills/to-plan/index-enhanced.js.map +1 -1
  124. package/dist/src/skills/to-plan/index.d.ts +24 -91
  125. package/dist/src/skills/to-plan/index.d.ts.map +1 -1
  126. package/dist/src/skills/to-plan/index.js +214 -409
  127. package/dist/src/skills/to-plan/index.js.map +1 -1
  128. package/package.json +3 -5
  129. package/src/agents/contracts/implementer.ts +122 -0
  130. package/src/agents/contracts/index.ts +27 -0
  131. package/src/agents/contracts/planner.ts +129 -0
  132. package/src/agents/contracts/router.ts +168 -0
  133. package/src/agents/contracts/verifier.ts +137 -0
  134. package/src/agents/dispatcher.ts +387 -362
  135. package/src/persistence/index.ts +10 -4
  136. package/src/persistence/plan-md.ts +52 -18
  137. package/src/persistence/state-md.ts +45 -23
  138. package/src/persistence/types.ts +37 -40
  139. package/src/router/namespace/README.md +127 -0
  140. package/src/router/namespace/core/intent-router.ts +221 -0
  141. package/src/router/namespace/core/lifecycle-router.ts +156 -0
  142. package/src/router/namespace/core/state-router.ts +192 -0
  143. package/src/router/namespace/domain/code-router.ts +202 -0
  144. package/src/router/namespace/domain/debug-router.ts +167 -0
  145. package/src/router/namespace/domain/plan-router.ts +196 -0
  146. package/src/router/namespace/domain/review-router.ts +142 -0
  147. package/src/router/namespace/index.ts +84 -0
  148. package/src/router/namespace/registry.ts +242 -0
  149. package/src/router/namespace/types.ts +182 -0
  150. package/src/router/namespace/utility/fallback-router.ts +107 -0
  151. package/src/router/namespace/utility/quick-task-router.ts +121 -0
  152. package/src/router/namespace/utility/research-router.ts +105 -0
  153. package/src/skills/agents-md/index.ts +2 -2
  154. package/src/skills/execute-plan/index.ts +419 -673
  155. package/src/skills/index.ts +1 -0
  156. package/src/skills/quick-task/index.ts +1 -1
  157. package/src/skills/review-diff/index.ts +1 -1
  158. package/src/skills/router/SKILL.md +181 -0
  159. package/src/skills/router/index.ts +577 -0
  160. package/src/skills/router/types.ts +90 -0
  161. package/src/skills/systematic-debugging/index.ts +1 -1
  162. package/src/skills/tdd/index.ts +1 -1
  163. package/src/skills/to-plan/index-enhanced.ts +3 -5
  164. package/src/skills/to-plan/index.ts +231 -502
@@ -1,25 +1,24 @@
1
1
  /**
2
- * To Plan Skill - Creates implementation plans via multi-step subagent dispatch
2
+ * To Plan Skill - Thin orchestrator that delegates all planning to subagent
3
3
  *
4
- * 5-Step Process:
5
- * Step 1: Classify the request (type, complexity, risk, exploration areas)
6
- * Step 2: Dispatch 1-3 read-only exploration subagents (in parallel)
7
- * Step 3: Synthesize findings into a unified plan structure
8
- * Step 4: Write the plan to disk
9
- * Step 5: Dispatch plan-checker subagent to validate the plan
4
+ * Architecture:
5
+ * 1. Initialize STATE.md with goal
6
+ * 2. Dispatch planner subagent with FRESH context (no main agent state)
7
+ * 3. Parse planner output into Plan structure
8
+ * 4. Group tasks into waves via topological sort
9
+ * 5. Write PLAN.md
10
+ * 6. Update STATE.md with plan
10
11
  *
11
- * Main Agent NEVER reads source files directly all exploration is delegated.
12
+ * The skill is THIN - all heavy lifting done by planner subagent.
12
13
  */
13
14
 
14
15
  import { z } from 'zod'
15
16
  import { Skill } from '../skill.js'
16
17
  import type { SkillContext } from '../types.js'
17
- import { createDispatcher } from '../../agents/dispatcher.js'
18
- import type { SubagentContract, SubagentConfig } from '../../agents/types.js'
19
- import { researcherContract } from '../../agents/contracts.js'
20
- import { createMainAgentGuard } from '../../guard/main-agent.js'
21
- import { mkdir, writeFile } from 'fs/promises'
22
- import { join } from 'path'
18
+ import { StateMdManager } from '../../persistence/state-md.js'
19
+ import { PlanMdManager } from '../../persistence/plan-md.js'
20
+ import type { Plan, Task, Wave } from '../../persistence/types.js'
21
+ import type { SubagentConfig, SubagentContract } from '../../agents/types.js'
23
22
 
24
23
  // ---------------------------------------------------------------------------
25
24
  // Schemas
@@ -28,335 +27,188 @@ import { join } from 'path'
28
27
  const ToPlanInputSchema = z.object({
29
28
  goal: z.string().min(1, 'Goal is required'),
30
29
  context: z.string().optional(),
31
- outputPath: z.string().optional(),
30
+ constraints: z.array(z.string()).optional(),
32
31
  model: z.string().optional(),
33
- filesHint: z.array(z.string()).optional(),
34
- workType: z.enum(['feature', 'bugfix', 'migration', 'cleanup', 'refactor']).optional(),
35
- })
36
-
37
- const TaskSchema = z.object({
38
- id: z.string(),
39
- title: z.string(),
40
- description: z.string(),
41
- owns: z.array(z.string()),
42
- reads: z.array(z.string()),
43
- dependencies: z.array(z.string()),
44
- verification: z.string().optional(),
32
+ outputPath: z.string().optional(),
45
33
  })
46
34
 
47
35
  const ToPlanOutputSchema = z.object({
48
36
  planPath: z.string(),
49
- executionModel: z.enum(['single-agent', 'ordered-non-parallel', 'fan-out-fan-in']),
50
- classification: z.object({
51
- type: z.string(),
52
- complexity: z.enum(['low', 'medium', 'high']),
53
- risk: z.enum(['low', 'medium', 'high']),
54
- }),
55
- planCheckerPassed: z.boolean(),
56
- tasks: z.array(TaskSchema),
37
+ waves: z.array(z.custom<Wave>()),
38
+ taskCount: z.number(),
57
39
  summary: z.string(),
58
40
  tokensUsed: z.number(),
59
41
  })
60
42
 
61
43
  type ToPlanInput = z.infer<typeof ToPlanInputSchema>
62
44
  type ToPlanOutput = z.infer<typeof ToPlanOutputSchema>
63
- type TaskDef = z.infer<typeof TaskSchema>
64
45
 
65
46
  // ---------------------------------------------------------------------------
66
- // Pure helper types
47
+ // Planner Subagent Prompt
67
48
  // ---------------------------------------------------------------------------
68
49
 
69
- interface Classification {
70
- type: string
71
- complexity: 'low' | 'medium' | 'high'
72
- risk: 'low' | 'medium' | 'high'
73
- explorationAreas: string[]
74
- }
50
+ function buildPlannerPrompt(input: ToPlanInput): string {
51
+ return `# Planning Task
75
52
 
76
- interface SynthesizedPlan {
77
- executionModel: 'single-agent' | 'ordered-non-parallel' | 'fan-out-fan-in'
78
- tasks: TaskDef[]
79
- summary: string
80
- }
53
+ ## Goal
54
+ ${input.goal}
81
55
 
82
- // ---------------------------------------------------------------------------
83
- // Step 1: Classify prompt builder
84
- // ---------------------------------------------------------------------------
56
+ ${input.context ? `## Context\n${input.context}\n` : ''}
57
+ ${input.constraints ? `## Constraints\n${input.constraints.map(c => `- ${c}`).join('\n')}\n` : ''}
85
58
 
86
- function buildClassifyPrompt(goal: string, context?: string, workType?: string, researchFindings?: string): string {
87
- return [
88
- '## Classify This Request',
89
- '',
90
- `**Goal**: ${goal}`,
91
- context ? `**Context**: ${context}` : '',
92
- workType ? `**Hinted Work Type**: ${workType}` : '',
93
- researchFindings ? `## Research Findings\n\n${researchFindings.slice(0, 1000)}` : '',
94
- '',
95
- 'Determine:',
96
- '- **Type**: feature, bugfix, migration, cleanup, refactor, or other',
97
- '- **Complexity**: low (1-3 files), medium (3-10 files), high (10+ files / architectural)',
98
- '- **Risk**: low (isolated, well-tested), medium (shared code), high (core systems, low coverage)',
99
- '- **Exploration Areas**: 1-3 key directories or file patterns to explore first',
100
- '',
101
- 'Output as JSON:',
102
- '```json',
103
- '{ "type": "...", "complexity": "low|medium|high", "risk": "low|medium|high", "explorationAreas": ["path/area1", "path/area2"] }',
104
- '```',
105
- ].join('\n')
106
- }
59
+ ## Instructions
107
60
 
108
- // ---------------------------------------------------------------------------
109
- // Step 2: Exploration prompt builder (one per area)
110
- // ---------------------------------------------------------------------------
61
+ Create a detailed implementation plan. Break the work into small, concrete tasks.
111
62
 
112
- function buildExplorePrompt(area: string, goal: string, context?: string): string {
113
- return [
114
- '## Exploration Task',
115
- '',
116
- `**Goal**: ${goal}`,
117
- context ? `**Context**: ${context}` : '',
118
- `**Focus Area**: ${area}`,
119
- '',
120
- '## Instructions',
121
- '',
122
- '1. Search and read files within this area',
123
- '2. Identify: existing patterns, relevant abstractions, dependencies, test coverage',
124
- '3. Note any existing implementation that relates to the goal',
125
- '4. Flag surprises, risks, or undocumented behavior',
126
- '',
127
- '## Constraints',
128
- '- READ ONLY — do not modify any files',
129
- '- Focus only on the assigned area',
130
- '- Be thorough but concise',
131
- ].join('\n')
132
- }
63
+ ### Task Requirements
64
+ - Each task should be completable in 1-5 minutes by a subagent
65
+ - Tasks should be independent where possible
66
+ - Each task must have clear owns[] (files to modify) and reads[] (files to read)
67
+ - Dependencies must be explicitly declared
133
68
 
134
- // ---------------------------------------------------------------------------
135
- // Step 3: Synthesize prompt builder
136
- // ---------------------------------------------------------------------------
69
+ ### Output Format
137
70
 
138
- function buildSynthesizePrompt(
139
- goal: string,
140
- context: string | undefined,
141
- classification: Classification,
142
- explorationOutputs: string[],
143
- ): string {
144
- const explorationsBlock = explorationOutputs
145
- .map((output, i) => `### Exploration ${i + 1}\n\n${output.slice(0, 2000)}`)
146
- .join('\n\n')
147
-
148
- return [
149
- '## Synthesize Findings into an Implementation Plan',
150
- '',
151
- `**Goal**: ${goal}`,
152
- context ? `**Context**: ${context}` : '',
153
- '',
154
- '## Classification',
155
- `Type: ${classification.type}, Complexity: ${classification.complexity}, Risk: ${classification.risk}`,
156
- '',
157
- '## Exploration Results',
158
- explorationsBlock,
159
- '',
160
- '## Instructions',
161
- '',
162
- '1. Cross-reference all exploration findings',
163
- '2. Identify conflicts or gaps in understanding',
164
- '3. Determine the execution model:',
165
- ' - **single-agent**: one implementer can do everything',
166
- ' - **ordered-non-parallel**: tasks must run in dependency order',
167
- ' - **fan-out-fan-in**: independent tasks in parallel, then merge',
168
- '4. Break work into concrete tasks (typically 3-8 tasks)',
169
- '',
170
- '## Task Rules',
171
- '- Each task: id, title, description, owns[], reads[], dependencies[], verification',
172
- '- **owns[]**: files the task CREATES or MODIFIES',
173
- '- **reads[]**: files the task reads for context',
174
- '- **dependencies[]**: task IDs that must finish first',
175
- '- **verification**: how to confirm the task is done correctly',
176
- '- Tasks sized for 1-5 minutes of subagent work',
177
- '',
178
- 'Output as JSON:',
179
- '```json',
180
- '{ "executionModel": "...", "tasks": [...], "summary": "..." }',
181
- '```',
182
- ].join('\n')
71
+ Return ONLY a JSON object with this exact structure:
72
+
73
+ \`\`\`json
74
+ {
75
+ "goal": "restated goal",
76
+ "tasks": [
77
+ {
78
+ "id": "task-1",
79
+ "description": "Clear description of what to do",
80
+ "estimatedComplexity": "low|medium|high",
81
+ "owns": ["files/to/create/or/modify"],
82
+ "reads": ["files/to/read/for/context"],
83
+ "dependencies": ["task-ids-that-must-complete-first"],
84
+ "verificationCriteria": ["how to verify this task is done"]
85
+ }
86
+ ],
87
+ "dependencies": {
88
+ "task-2": ["task-1"]
89
+ },
90
+ "verificationCriteria": [
91
+ "Overall acceptance criteria for the plan"
92
+ ],
93
+ "estimatedDuration": 300000,
94
+ "rationale": "Brief explanation of the plan structure"
95
+ }
96
+ \`\`\`
97
+
98
+ ## Rules
99
+ 1. Use descriptive task IDs like "setup-config", "implement-core", "add-tests"
100
+ 2. Keep task descriptions under 100 characters
101
+ 3. owns[] and reads[] should be relative file paths
102
+ 4. No circular dependencies
103
+ 5. Include at least one verification criterion per task`
183
104
  }
184
105
 
185
106
  // ---------------------------------------------------------------------------
186
- // Step 4: Write plan prompt builder
107
+ // Parsing Helpers
187
108
  // ---------------------------------------------------------------------------
188
109
 
189
- function buildWritePlanPrompt(
190
- goal: string,
191
- context: string | undefined,
192
- classification: Classification,
193
- synthesis: SynthesizedPlan,
194
- planPath: string,
195
- ): string {
196
- const tasksBlock = synthesis.tasks
197
- .map(
198
- (t) =>
199
- `- **Task ${t.id}**: ${t.title} — ${t.description} (owns: ${t.owns.join(', ') || 'none'}, reads: ${t.reads.join(', ') || 'none'}, deps: ${t.dependencies.join(', ') || 'none'})`,
200
- )
201
- .join('\n')
202
-
203
- return [
204
- `Write the implementation plan to: ${planPath}`,
205
- '',
206
- `**Goal**: ${goal}`,
207
- context ? `**Context**: ${context}` : '',
208
- `**Type**: ${classification.type}, **Complexity**: ${classification.complexity}, **Risk**: ${classification.risk}`,
209
- `**Execution Model**: ${synthesis.executionModel}`,
210
- '',
211
- '## Tasks to Document',
212
- tasksBlock,
213
- '',
214
- '## Plan Format',
215
- '',
216
- 'Write a markdown file with these sections:',
217
- '1. `# Goal` — restate the goal',
218
- '2. `## Context` background information',
219
- '3. `## Execution Model` — how tasks should be executed',
220
- '4. `## Tasks` — numbered sections per task with title, description, owns, reads, dependencies, verification',
221
- '5. `## Risks` — identified risks and mitigation strategies',
222
- ].join('\n')
110
+ function parsePlanFromResponse(response: string): Plan {
111
+ try {
112
+ // Extract JSON from markdown code block or use raw response
113
+ const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/)
114
+ const jsonStr = jsonMatch ? jsonMatch[1].trim() : response.trim()
115
+ const parsed = JSON.parse(jsonStr)
116
+
117
+ // Transform to Plan structure
118
+ const tasks: Task[] = (parsed.tasks || []).map((t: Record<string, unknown>) => ({
119
+ id: String(t.id || ''),
120
+ description: String(t.description || ''),
121
+ status: 'pending',
122
+ dependencies: Array.isArray(t.dependencies) ? t.dependencies.map(String) : [],
123
+ estimatedComplexity: (t.estimatedComplexity as 'low' | 'medium' | 'high') || 'medium',
124
+ }))
125
+
126
+ const dependencies: Record<string, string[]> = parsed.dependencies || {}
127
+
128
+ // Ensure all tasks have entry in dependencies
129
+ for (const task of tasks) {
130
+ if (!dependencies[task.id]) {
131
+ dependencies[task.id] = task.dependencies
132
+ }
133
+ }
134
+
135
+ return {
136
+ goal: String(parsed.goal || ''),
137
+ tasks,
138
+ dependencies,
139
+ waves: [], // Will be computed via topological sort
140
+ verificationCriteria: Array.isArray(parsed.verificationCriteria)
141
+ ? parsed.verificationCriteria.map(String)
142
+ : [],
143
+ estimatedDuration: Number(parsed.estimatedDuration) || 0,
144
+ }
145
+ } catch (error) {
146
+ throw new Error(`Failed to parse planner output: ${(error as Error).message}`)
147
+ }
223
148
  }
224
149
 
225
150
  // ---------------------------------------------------------------------------
226
- // Step 5: Plan check prompt builder
151
+ // Topological Sort for Wave Grouping
227
152
  // ---------------------------------------------------------------------------
228
153
 
229
- function buildPlanCheckPrompt(planPath: string): string {
230
- return [
231
- '## Validate the Implementation Plan',
232
- '',
233
- `Read the plan file: ${planPath}`,
234
- '',
235
- '## Checklist',
236
- '',
237
- '1. Do tasks cover all requirements?',
238
- '2. Does every task have clear owns[] and reads[]?',
239
- '3. Are dependencies acyclic and valid (referenced IDs exist)?',
240
- '4. Are tasks small enough for subagent execution (<5 min)?',
241
- '5. Does each task have a verification step?',
242
- '6. Are identified risks addressed?',
243
- '7. Could tasks conflict by owning the same file?',
244
- '',
245
- 'Return: PASS or BLOCKED with specific issues.',
246
- ].join('\n')
247
- }
154
+ function groupTasksIntoWaves(plan: Plan): Wave[] {
155
+ const waves: Wave[] = []
156
+ const completed = new Set<string>()
157
+ const remaining = new Set(plan.tasks.map(t => t.id))
158
+ const taskMap = new Map(plan.tasks.map(t => [t.id, t]))
159
+ let waveIndex = 0
248
160
 
249
- // ---------------------------------------------------------------------------
250
- // Parsing helpers (pure functions)
251
- // ---------------------------------------------------------------------------
161
+ // Kahn's algorithm for topological sort with wave grouping
162
+ while (remaining.size > 0) {
163
+ // Find all tasks whose dependencies are satisfied
164
+ const waveTaskIds: string[] = []
165
+
166
+ for (const taskId of remaining) {
167
+ const deps = plan.dependencies[taskId] || []
168
+ const allDepsCompleted = deps.every(dep => completed.has(dep))
252
169
 
253
- function parseClassification(output: string): Classification {
254
- const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
255
- if (jsonMatch) {
256
- try {
257
- const parsed = JSON.parse(jsonMatch[1].trim())
258
- return {
259
- type: parsed.type || 'feature',
260
- complexity: parsed.complexity || 'medium',
261
- risk: parsed.risk || 'medium',
262
- explorationAreas: Array.isArray(parsed.explorationAreas)
263
- ? parsed.explorationAreas.slice(0, 3)
264
- : ['src/'],
170
+ if (allDepsCompleted) {
171
+ waveTaskIds.push(taskId)
265
172
  }
266
- } catch {
267
- // fall through to defaults
268
173
  }
269
- }
270
- return {
271
- type: 'feature',
272
- complexity: 'medium',
273
- risk: 'medium',
274
- explorationAreas: ['src/'],
275
- }
276
- }
277
174
 
278
- function parseSynthesis(output: string): SynthesizedPlan {
279
- const jsonMatch = output.match(/```json\s*([\s\S]*?)\s*```/)
280
- if (jsonMatch) {
281
- try {
282
- const parsed = JSON.parse(jsonMatch[1].trim())
283
- return {
284
- executionModel: parsed.executionModel || 'ordered-non-parallel',
285
- tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
286
- summary: parsed.summary || 'Plan synthesized',
287
- }
288
- } catch {
289
- // fall through to defaults
175
+ if (waveTaskIds.length === 0) {
176
+ // Circular dependency detected
177
+ const remainingIds = Array.from(remaining).join(', ')
178
+ throw new Error(`Circular dependency detected among tasks: ${remainingIds}`)
290
179
  }
291
- }
292
- return {
293
- executionModel: 'ordered-non-parallel',
294
- tasks: [
295
- {
296
- id: '1',
297
- title: 'Implementation',
298
- description: 'Core implementation work',
299
- owns: [],
300
- reads: [],
301
- dependencies: [],
302
- verification: 'Run tests',
303
- },
304
- ],
305
- summary: output.slice(0, 200),
306
- }
307
- }
308
180
 
309
- function checkPlanPassed(output: string): boolean {
310
- return !output.toLowerCase().includes('blocked')
311
- }
181
+ // Create wave with full task objects
182
+ const waveTasks = waveTaskIds
183
+ .map(id => taskMap.get(id))
184
+ .filter((t): t is Task => t !== undefined)
312
185
 
313
- // ---------------------------------------------------------------------------
314
- // Fallback plan writer (used when write subagent fails)
315
- // ---------------------------------------------------------------------------
186
+ waves.push({
187
+ id: `wave-${waveIndex}`,
188
+ tasks: waveTasks,
189
+ })
316
190
 
317
- function buildFallbackPlan(
318
- goal: string,
319
- context: string | undefined,
320
- synthesis: SynthesizedPlan,
321
- ): string {
322
- const tasksBlock = synthesis.tasks
323
- .map(
324
- (t) =>
325
- [
326
- `### Task ${t.id}: ${t.title}`,
327
- '',
328
- t.description,
329
- '',
330
- `- **Owns**: ${t.owns.join(', ') || 'none'}`,
331
- `- **Reads**: ${t.reads.join(', ') || 'none'}`,
332
- `- **Depends on**: ${t.dependencies.join(', ') || 'none'}`,
333
- `- **Verification**: ${t.verification || 'Run tests'}`,
334
- ].join('\n'),
335
- )
336
- .join('\n\n')
337
-
338
- return [
339
- `# ${goal}`,
340
- '',
341
- context ? `## Context\n\n${context}\n` : '',
342
- `## Execution Model\n\n${synthesis.executionModel}`,
343
- '',
344
- '## Tasks',
345
- '',
346
- tasksBlock,
347
- ].join('\n')
191
+ // Mark tasks as completed and remove from remaining
192
+ for (const taskId of waveTaskIds) {
193
+ completed.add(taskId)
194
+ remaining.delete(taskId)
195
+ }
196
+
197
+ waveIndex++
198
+ }
199
+
200
+ return waves
348
201
  }
349
202
 
350
203
  // ---------------------------------------------------------------------------
351
- // Skill class
204
+ // Skill Class
352
205
  // ---------------------------------------------------------------------------
353
206
 
354
207
  export class ToPlanSkill extends Skill<ToPlanInput, ToPlanOutput> {
355
208
  constructor() {
356
209
  super({
357
210
  name: 'to-plan',
358
- description:
359
- 'Create implementation plans via 5-step subagent dispatch: classify, explore, synthesize, write, check',
211
+ description: 'Thin orchestrator that delegates planning to subagent with fresh context',
360
212
  requires: [],
361
213
  inputSchema: ToPlanInputSchema,
362
214
  outputSchema: ToPlanOutputSchema,
@@ -364,220 +216,97 @@ export class ToPlanSkill extends Skill<ToPlanInput, ToPlanOutput> {
364
216
  }
365
217
 
366
218
  async execute(input: ToPlanInput, context: SkillContext): Promise<ToPlanOutput> {
367
- const { config, logger } = context
368
- const dispatcher = createDispatcher(logger)
369
- const guard = createMainAgentGuard({}, logger)
370
- const model = input.model || config.defaultModel
371
- let totalTokens = 0
372
-
373
- if (!model) {
374
- throw new Error('Model is required. Set defaultModel in config or pass model in input.')
219
+ const { config, logger, dispatcher } = context
220
+ const model = input.model || config.defaultModel || 'claude-sonnet-4.5'
221
+ let tokensUsed = 0
222
+
223
+ // Determine paths
224
+ const statePath = input.outputPath
225
+ ? input.outputPath.replace(/\.md$/, '-STATE.md')
226
+ : '.pi/state/STATE.md'
227
+ const planPath = input.outputPath || '.pi/state/PLAN.md'
228
+
229
+ // Initialize managers
230
+ const stateManager = new StateMdManager(statePath)
231
+ const planManager = new PlanMdManager(planPath)
232
+
233
+ // Step 1: Initialize STATE.md with goal
234
+ logger.info('[to-plan] Initializing STATE.md')
235
+ await stateManager.initialize(input.goal)
236
+
237
+ // Step 2: Dispatch planner subagent with FRESH context
238
+ logger.info('[to-plan] Dispatching planner subagent with fresh context')
239
+
240
+ const plannerConfig: SubagentConfig = {
241
+ role: 'planner',
242
+ model,
243
+ timeout: 120000,
375
244
  }
376
245
 
377
- guard.activateEmbargo()
378
-
379
- try {
380
- // ---- Step 0: Research external context (if needed) ----
381
- let researchSummary = ''
382
- const needsResearch = /api|library|framework|migrate|upgrade|version|best practice/i.test(input.goal)
383
- if (needsResearch) {
384
- logger.info(`[to-plan] Step 0: Researching "${input.goal.slice(0, 80)}"`)
385
- const researchResult = await dispatcher.dispatch(
386
- { role: 'researcher', model, tokenBudget: 12000 },
387
- researcherContract({
388
- topic: input.goal,
389
- scope: 'technical',
390
- questions: [
391
- 'What are the latest best practices?',
392
- 'Are there known issues or breaking changes?',
393
- 'What are recommended approaches?'
394
- ],
395
- timeRange: '1y'
396
- })
397
- )
398
- totalTokens += researchResult.tokensUsed
399
- researchSummary = researchResult.output.slice(0, 1500)
400
- logger.info(`[to-plan] Research complete: ${researchSummary.slice(0, 100)}...`)
401
- }
246
+ const plannerContract: SubagentContract = {
247
+ permissions: {
248
+ readFiles: true,
249
+ searchCode: true,
250
+ runCommands: false,
251
+ writeFiles: false,
252
+ gitOperations: false,
253
+ },
254
+ prompt: buildPlannerPrompt(input),
255
+ owns: [],
256
+ reads: [],
257
+ }
402
258
 
403
- // ---- Step 1: Classify the request ----
404
- logger.info(`[to-plan] Step 1: Classifying "${input.goal.slice(0, 80)}"`)
405
- const classifyResult = await dispatcher.dispatch(
406
- { role: 'explorer', model, tokenBudget: 8000 },
407
- {
408
- permissions: {
409
- readFiles: true,
410
- searchCode: false,
411
- runCommands: false,
412
- writeFiles: false,
413
- gitOperations: false,
414
- },
415
- prompt: buildClassifyPrompt(input.goal, input.context, input.workType, researchSummary),
416
- owns: [],
417
- reads: [],
418
- },
419
- )
420
-
421
- totalTokens += classifyResult.tokensUsed
422
- const classification = parseClassification(classifyResult.output)
423
- logger.info(
424
- `[to-plan] Classified: type=${classification.type}, complexity=${classification.complexity}, risk=${classification.risk}`,
425
- )
426
-
427
- // ---- Step 2: Dispatch 1-3 read-only exploration subagents (parallel) ----
428
- const areas = classification.explorationAreas.slice(0, 3)
429
- logger.info(
430
- `[to-plan] Step 2: Dispatching ${areas.length} exploration subagent(s) in parallel`,
431
- )
432
-
433
- const exploreConfigs: SubagentConfig[] = areas.map(() => ({
434
- role: 'explorer' as const,
435
- model,
436
- tokenBudget: 16000,
437
- }))
438
-
439
- const exploreContracts: SubagentContract[] = areas.map((area) => ({
440
- permissions: {
441
- readFiles: true,
442
- searchCode: true,
443
- runCommands: false,
444
- writeFiles: false,
445
- gitOperations: false,
446
- },
447
- prompt: buildExplorePrompt(area, input.goal, input.context),
448
- owns: [],
449
- reads: input.filesHint ?? [],
450
- }))
451
-
452
- const exploreResults =
453
- areas.length === 1
454
- ? [await dispatcher.dispatch(exploreConfigs[0], exploreContracts[0])]
455
- : await dispatcher.dispatchParallel(exploreConfigs, exploreContracts)
456
-
457
- for (const result of exploreResults) {
458
- totalTokens += result.tokensUsed
459
- }
259
+ const plannerResult = await dispatcher.dispatch<{ output: string; tokensUsed: number }>(
260
+ plannerConfig,
261
+ plannerContract
262
+ )
460
263
 
461
- const successfulExplorations = exploreResults.filter((r) => r.status === 'success')
462
- logger.info(
463
- `[to-plan] ${successfulExplorations.length}/${areas.length} explorations succeeded`,
464
- )
465
-
466
- // ---- Step 3: Synthesize findings ----
467
- logger.info('[to-plan] Step 3: Synthesizing findings')
468
- const synthesizeResult = await dispatcher.dispatch(
469
- { role: 'planner', model },
470
- {
471
- permissions: {
472
- readFiles: true,
473
- searchCode: false,
474
- runCommands: false,
475
- writeFiles: false,
476
- gitOperations: false,
477
- },
478
- prompt: buildSynthesizePrompt(
479
- input.goal,
480
- input.context,
481
- classification,
482
- successfulExplorations.map((r) => r.output),
483
- ),
484
- owns: [],
485
- reads: [],
486
- },
487
- )
488
-
489
- totalTokens += synthesizeResult.tokensUsed
490
- const synthesis = parseSynthesis(synthesizeResult.output)
491
- logger.info(
492
- `[to-plan] Synthesized: ${synthesis.tasks.length} tasks, model=${synthesis.executionModel}`,
493
- )
494
-
495
- // ---- Step 4: Write plan to disk ----
496
- const date = new Date().toISOString().split('T')[0]
497
- const sanitized = input.goal
498
- .toLowerCase()
499
- .replace(/[^a-z0-9]+/g, '-')
500
- .slice(0, 50)
501
- const planPath =
502
- input.outputPath ||
503
- join(config.planPath, `${date}-${sanitized}-implementation-plan.md`)
504
-
505
- await mkdir(config.planPath, { recursive: true })
506
-
507
- logger.info(`[to-plan] Step 4: Writing plan to ${planPath}`)
508
- const writeResult = await dispatcher.dispatch(
509
- { role: 'implementer', model },
510
- {
511
- permissions: {
512
- readFiles: false,
513
- searchCode: false,
514
- runCommands: false,
515
- writeFiles: true,
516
- gitOperations: false,
517
- },
518
- prompt: buildWritePlanPrompt(
519
- input.goal,
520
- input.context,
521
- classification,
522
- synthesis,
523
- planPath,
524
- ),
525
- owns: [planPath],
526
- reads: [],
527
- },
528
- )
529
-
530
- totalTokens += writeResult.tokensUsed
531
-
532
- // If the write subagent failed, write a fallback plan
533
- if (writeResult.status !== 'success') {
534
- const fallback = buildFallbackPlan(input.goal, input.context, synthesis)
535
- await writeFile(planPath, fallback, 'utf-8')
536
- logger.warn('[to-plan] Write subagent failed; wrote fallback plan')
537
- }
264
+ tokensUsed += plannerResult.tokensUsed || 0
265
+ logger.info(`[to-plan] Planner subagent completed, used ${plannerResult.tokensUsed} tokens`)
538
266
 
539
- // ---- Step 5: Dispatch plan-checker subagent ----
540
- logger.info('[to-plan] Step 5: Validating plan')
541
- const checkerResult = await dispatcher.dispatch(
542
- { role: 'reviewer', model },
543
- {
544
- permissions: {
545
- readFiles: true,
546
- searchCode: false,
547
- runCommands: false,
548
- writeFiles: false,
549
- gitOperations: false,
550
- },
551
- prompt: buildPlanCheckPrompt(planPath),
552
- owns: [],
553
- reads: [planPath],
554
- },
555
- )
556
-
557
- totalTokens += checkerResult.tokensUsed
558
- const planCheckerPassed = checkPlanPassed(checkerResult.output)
559
- logger.info(`[to-plan] Plan checker: ${planCheckerPassed ? 'PASS' : 'BLOCKED'}`)
560
-
561
- return {
562
- planPath,
563
- executionModel: synthesis.executionModel,
564
- classification: {
565
- type: classification.type,
566
- complexity: classification.complexity,
567
- risk: classification.risk,
568
- },
569
- planCheckerPassed,
570
- tasks: synthesis.tasks,
571
- summary: [
572
- `Plan created with ${synthesis.tasks.length} tasks.`,
573
- `Execution model: ${synthesis.executionModel}.`,
574
- `Checker: ${planCheckerPassed ? 'PASS' : 'BLOCKED'}.`,
575
- `Used ${totalTokens} tokens.`,
576
- ].join(' '),
577
- tokensUsed: totalTokens,
578
- }
579
- } finally {
580
- guard.deactivateEmbargo()
267
+ // Step 3: Parse plan from subagent output
268
+ logger.info('[to-plan] Parsing plan from subagent output')
269
+ const plan = parsePlanFromResponse(plannerResult.output)
270
+ logger.info(`[to-plan] Parsed ${plan.tasks.length} tasks`)
271
+
272
+ // Step 4: Group tasks into waves via topological sort
273
+ logger.info('[to-plan] Grouping tasks into waves')
274
+ const waves = groupTasksIntoWaves(plan)
275
+ plan.waves = waves
276
+ logger.info(`[to-plan] Created ${waves.length} waves for parallel execution`)
277
+
278
+ // Log wave details
279
+ for (const wave of waves) {
280
+ const taskIds = wave.tasks.map(t => t.id).join(', ')
281
+ logger.info(`[to-plan] ${wave.id}: ${taskIds}`)
282
+ }
283
+
284
+ // Step 5: Write PLAN.md
285
+ logger.info(`[to-plan] Writing plan to ${planPath}`)
286
+ await planManager.writePlan(plan)
287
+
288
+ // Step 6: Update STATE.md with plan
289
+ logger.info('[to-plan] Recording plan in STATE.md')
290
+ await stateManager.recordPlan(plan)
291
+ await stateManager.updatePhase('planning')
292
+ await stateManager.updateTokenUsage(tokensUsed)
293
+
294
+ // Build summary
295
+ const summary = [
296
+ `Plan created with ${plan.tasks.length} tasks`,
297
+ `Organized into ${waves.length} parallel waves`,
298
+ `Estimated duration: ${plan.estimatedDuration}ms`,
299
+ `Used ${tokensUsed} tokens`,
300
+ ].join('. ')
301
+
302
+ logger.info(`[to-plan] ${summary}`)
303
+
304
+ return {
305
+ planPath,
306
+ waves,
307
+ taskCount: plan.tasks.length,
308
+ summary,
309
+ tokensUsed,
581
310
  }
582
311
  }
583
312
  }