@haoyiyin/workflow 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -8
- package/src/agents/contracts.ts +559 -0
- package/src/agents/dispatcher-enhanced.ts +350 -0
- package/src/agents/dispatcher.ts +680 -0
- package/src/agents/index.ts +48 -0
- package/src/agents/resilience.ts +255 -0
- package/src/agents/token-budget.ts +83 -0
- package/src/agents/types.ts +73 -0
- package/src/guard/main-agent.ts +245 -0
- package/src/hooks/builtin/index.ts +8 -0
- package/src/hooks/builtin/on-error.ts +23 -0
- package/src/hooks/builtin/post-execute.ts +40 -0
- package/src/hooks/builtin/post-plan.ts +23 -0
- package/src/hooks/builtin/pre-execute.ts +30 -0
- package/src/hooks/builtin/pre-plan.ts +26 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/loader.ts +98 -0
- package/src/hooks/manager.ts +99 -0
- package/src/hooks/types-enhanced.ts +38 -0
- package/src/hooks/types.ts +35 -0
- package/src/index.ts +127 -0
- package/src/persistence/index.ts +17 -0
- package/src/persistence/plan-md.ts +141 -0
- package/src/persistence/state-md.ts +167 -0
- package/src/persistence/types.ts +89 -0
- package/src/router/classifier.ts +610 -0
- package/src/router/guard.ts +483 -0
- package/src/router/index.ts +22 -0
- package/src/router/router.ts +108 -0
- package/src/router/types.ts +127 -0
- package/src/skills/agents-md/SKILL.md +45 -0
- package/src/skills/agents-md/index.ts +33 -0
- package/src/skills/execute-plan/SKILL.md +60 -0
- package/src/skills/execute-plan/index.ts +970 -0
- package/src/skills/index.ts +13 -0
- package/src/skills/quick-task/SKILL.md +54 -0
- package/src/skills/quick-task/index.ts +346 -0
- package/src/skills/registry.ts +59 -0
- package/src/skills/review-diff/SKILL.md +53 -0
- package/src/skills/review-diff/index.ts +394 -0
- package/src/skills/skill.ts +59 -0
- package/src/skills/systematic-debugging/SKILL.md +56 -0
- package/src/skills/systematic-debugging/index.ts +404 -0
- package/src/skills/tdd/SKILL.md +52 -0
- package/src/skills/tdd/index.ts +409 -0
- package/src/skills/to-plan/SKILL.md +56 -0
- package/src/skills/to-plan/index-enhanced.ts +551 -0
- package/src/skills/to-plan/index.ts +586 -0
- package/src/skills/types.ts +47 -0
- package/src/state/cleanup.ts +118 -0
- package/src/state/index.ts +8 -0
- package/src/state/manager.ts +96 -0
- package/src/state/persistence.ts +77 -0
- package/src/state/types.ts +30 -0
- package/src/state/validator.ts +78 -0
- package/src/types.ts +102 -0
- package/src/utils/compress.ts +347 -0
- package/src/utils/git.ts +82 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +23 -0
- package/src/utils/paths.ts +55 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* To Plan Skill - Creates implementation plans via multi-step subagent dispatch
|
|
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
|
|
10
|
+
*
|
|
11
|
+
* Main Agent NEVER reads source files directly — all exploration is delegated.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { Skill } from '../skill.js'
|
|
16
|
+
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'
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Schemas
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const ToPlanInputSchema = z.object({
|
|
29
|
+
goal: z.string().min(1, 'Goal is required'),
|
|
30
|
+
context: z.string().optional(),
|
|
31
|
+
outputPath: z.string().optional(),
|
|
32
|
+
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(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const ToPlanOutputSchema = z.object({
|
|
48
|
+
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),
|
|
57
|
+
summary: z.string(),
|
|
58
|
+
tokensUsed: z.number(),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
type ToPlanInput = z.infer<typeof ToPlanInputSchema>
|
|
62
|
+
type ToPlanOutput = z.infer<typeof ToPlanOutputSchema>
|
|
63
|
+
type TaskDef = z.infer<typeof TaskSchema>
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Pure helper types
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
interface Classification {
|
|
70
|
+
type: string
|
|
71
|
+
complexity: 'low' | 'medium' | 'high'
|
|
72
|
+
risk: 'low' | 'medium' | 'high'
|
|
73
|
+
explorationAreas: string[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SynthesizedPlan {
|
|
77
|
+
executionModel: 'single-agent' | 'ordered-non-parallel' | 'fan-out-fan-in'
|
|
78
|
+
tasks: TaskDef[]
|
|
79
|
+
summary: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Step 1: Classify prompt builder
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
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
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Step 2: Exploration prompt builder (one per area)
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
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
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Step 3: Synthesize prompt builder
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
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')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Step 4: Write plan prompt builder
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
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')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Step 5: Plan check prompt builder
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
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
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Parsing helpers (pure functions)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
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/'],
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// fall through to defaults
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
type: 'feature',
|
|
272
|
+
complexity: 'medium',
|
|
273
|
+
risk: 'medium',
|
|
274
|
+
explorationAreas: ['src/'],
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
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
|
|
290
|
+
}
|
|
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
|
+
|
|
309
|
+
function checkPlanPassed(output: string): boolean {
|
|
310
|
+
return !output.toLowerCase().includes('blocked')
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Fallback plan writer (used when write subagent fails)
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
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')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// Skill class
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
export class ToPlanSkill extends Skill<ToPlanInput, ToPlanOutput> {
|
|
355
|
+
constructor() {
|
|
356
|
+
super({
|
|
357
|
+
name: 'to-plan',
|
|
358
|
+
description:
|
|
359
|
+
'Create implementation plans via 5-step subagent dispatch: classify, explore, synthesize, write, check',
|
|
360
|
+
requires: [],
|
|
361
|
+
inputSchema: ToPlanInputSchema,
|
|
362
|
+
outputSchema: ToPlanOutputSchema,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
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.')
|
|
375
|
+
}
|
|
376
|
+
|
|
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
|
+
}
|
|
402
|
+
|
|
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
|
+
}
|
|
460
|
+
|
|
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
|
+
}
|
|
538
|
+
|
|
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()
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export const toPlanSkill = new ToPlanSkill()
|
|
586
|
+
export default toPlanSkill
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill type definitions
|
|
3
|
+
*/
|
|
4
|
+
import type { ExecutionState, WorkflowConfig, Logger } from '../types.js'
|
|
5
|
+
import type { PersistenceManager } from '../persistence/types.js'
|
|
6
|
+
import type { ResilientDispatcher } from '../agents/dispatcher-enhanced.js'
|
|
7
|
+
import type { HookManagerInterface } from '../hooks/types.js'
|
|
8
|
+
|
|
9
|
+
export interface SkillContext {
|
|
10
|
+
config: WorkflowConfig
|
|
11
|
+
state: ExecutionState
|
|
12
|
+
logger: Logger
|
|
13
|
+
/** Access to other skills for composition */
|
|
14
|
+
skills: SkillRegistry
|
|
15
|
+
/** Persistence layer for STATE.md and PLAN.md */
|
|
16
|
+
persistence?: PersistenceManager
|
|
17
|
+
/** Dispatcher for spawning subagents */
|
|
18
|
+
dispatcher: ResilientDispatcher
|
|
19
|
+
/** Hook manager for lifecycle events */
|
|
20
|
+
hooks?: HookManagerInterface
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SkillDefinition<Input = unknown, Output = unknown> {
|
|
24
|
+
name: string
|
|
25
|
+
description: string
|
|
26
|
+
requires?: string[]
|
|
27
|
+
execute: (input: Input, context: SkillContext) => Promise<Output>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SkillRegistry {
|
|
31
|
+
register(skill: SkillDefinition<any, any>): void
|
|
32
|
+
unregister(name: string): void
|
|
33
|
+
get(name: string): SkillDefinition<any, any> | null
|
|
34
|
+
list(): SkillDefinition<any, any>[]
|
|
35
|
+
getDependencies(name: string): string[]
|
|
36
|
+
resolveOrder(skillNames: string[]): string[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ExecutionResult<T = unknown> {
|
|
40
|
+
success: boolean
|
|
41
|
+
data?: T
|
|
42
|
+
error?: string
|
|
43
|
+
metadata?: {
|
|
44
|
+
duration: number
|
|
45
|
+
tokensUsed?: number
|
|
46
|
+
}
|
|
47
|
+
}
|