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