@haoyiyin/workflow 0.2.0 → 0.2.3

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