@haoyiyin/workflow 0.2.2 → 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 (61) hide show
  1. package/package.json +9 -8
  2. package/src/agents/contracts.ts +559 -0
  3. package/src/agents/dispatcher-enhanced.ts +350 -0
  4. package/src/agents/dispatcher.ts +680 -0
  5. package/src/agents/index.ts +48 -0
  6. package/src/agents/resilience.ts +255 -0
  7. package/src/agents/token-budget.ts +83 -0
  8. package/src/agents/types.ts +73 -0
  9. package/src/guard/main-agent.ts +245 -0
  10. package/src/hooks/builtin/index.ts +8 -0
  11. package/src/hooks/builtin/on-error.ts +23 -0
  12. package/src/hooks/builtin/post-execute.ts +40 -0
  13. package/src/hooks/builtin/post-plan.ts +23 -0
  14. package/src/hooks/builtin/pre-execute.ts +30 -0
  15. package/src/hooks/builtin/pre-plan.ts +26 -0
  16. package/src/hooks/index.ts +7 -0
  17. package/src/hooks/loader.ts +98 -0
  18. package/src/hooks/manager.ts +99 -0
  19. package/src/hooks/types-enhanced.ts +38 -0
  20. package/src/hooks/types.ts +35 -0
  21. package/src/index.ts +127 -0
  22. package/src/persistence/index.ts +17 -0
  23. package/src/persistence/plan-md.ts +141 -0
  24. package/src/persistence/state-md.ts +167 -0
  25. package/src/persistence/types.ts +89 -0
  26. package/src/router/classifier.ts +610 -0
  27. package/src/router/guard.ts +483 -0
  28. package/src/router/index.ts +22 -0
  29. package/src/router/router.ts +108 -0
  30. package/src/router/types.ts +127 -0
  31. package/src/skills/agents-md/SKILL.md +45 -0
  32. package/src/skills/agents-md/index.ts +33 -0
  33. package/src/skills/execute-plan/SKILL.md +60 -0
  34. package/src/skills/execute-plan/index.ts +970 -0
  35. package/src/skills/index.ts +13 -0
  36. package/src/skills/quick-task/SKILL.md +54 -0
  37. package/src/skills/quick-task/index.ts +346 -0
  38. package/src/skills/registry.ts +59 -0
  39. package/src/skills/review-diff/SKILL.md +53 -0
  40. package/src/skills/review-diff/index.ts +394 -0
  41. package/src/skills/skill.ts +59 -0
  42. package/src/skills/systematic-debugging/SKILL.md +56 -0
  43. package/src/skills/systematic-debugging/index.ts +404 -0
  44. package/src/skills/tdd/SKILL.md +52 -0
  45. package/src/skills/tdd/index.ts +409 -0
  46. package/src/skills/to-plan/SKILL.md +56 -0
  47. package/src/skills/to-plan/index-enhanced.ts +551 -0
  48. package/src/skills/to-plan/index.ts +586 -0
  49. package/src/skills/types.ts +47 -0
  50. package/src/state/cleanup.ts +118 -0
  51. package/src/state/index.ts +8 -0
  52. package/src/state/manager.ts +96 -0
  53. package/src/state/persistence.ts +77 -0
  54. package/src/state/types.ts +30 -0
  55. package/src/state/validator.ts +78 -0
  56. package/src/types.ts +102 -0
  57. package/src/utils/compress.ts +347 -0
  58. package/src/utils/git.ts +82 -0
  59. package/src/utils/index.ts +6 -0
  60. package/src/utils/logger.ts +23 -0
  61. package/src/utils/paths.ts +55 -0
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Review Diff Skill - Reviews code changes via reviewer subagents
3
+ *
4
+ * Use case: Review PR, branch, commit, or diff-file
5
+ * Pattern: Get diff → Inspect surface → Check requirements → Check tests →
6
+ * Check quality → Return structured review report
7
+ *
8
+ * Spawns reviewer subagent(s) to review diffs.
9
+ * Returns structured review report with severity-classified issues.
10
+ */
11
+
12
+ import { z } from 'zod'
13
+ import { Skill } from '../skill.js'
14
+ import type { SkillContext } from '../types.js'
15
+ import { createDispatcher } from '../../agents/dispatcher.js'
16
+ import { createMainAgentGuard } from '../../guard/main-agent.js'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Schemas
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const ReviewDiffInputSchema = z.object({
23
+ target: z.string().min(1),
24
+ targetType: z
25
+ .enum(['pr', 'branch', 'commit', 'diff-file'])
26
+ .default('branch'),
27
+ planPath: z.string().optional(),
28
+ focus: z
29
+ .array(z.enum(['security', 'performance', 'correctness', 'style', 'tests']))
30
+ .optional(),
31
+ model: z.string().optional(),
32
+ })
33
+
34
+ const ReviewDiffOutputSchema = z.object({
35
+ decision: z.enum(['approve', 'changes', 'blocked', 'needs-evidence']),
36
+ issues: z.array(
37
+ z.object({
38
+ severity: z.enum(['blocker', 'major', 'minor', 'note']),
39
+ file: z.string().optional(),
40
+ line: z.number().optional(),
41
+ description: z.string(),
42
+ action: z.string(),
43
+ }),
44
+ ),
45
+ specCompliance: z.enum(['pass', 'partial', 'fail']),
46
+ testAssessment: z.enum(['adequate', 'missing', 'failing']),
47
+ strengths: z.array(z.string()),
48
+ summary: z.string(),
49
+ tokensUsed: z.number(),
50
+ })
51
+
52
+ type ReviewDiffInput = z.infer<typeof ReviewDiffInputSchema>
53
+ type ReviewDiffOutput = z.infer<typeof ReviewDiffOutputSchema>
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Prompt builders (pure functions)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ function buildDiffPrompt(targetType: string, target: string): string {
60
+ const commands: Record<string, string> = {
61
+ pr: `gh pr diff ${target}`,
62
+ branch: `git diff origin/${target}...HEAD`,
63
+ commit: `git show ${target}`,
64
+ 'diff-file': `cat ${target}`,
65
+ }
66
+ const cmd = commands[targetType] ?? `git diff ${target}`
67
+ return [
68
+ `Get the diff for ${targetType}: ${target}`,
69
+ '',
70
+ `Run: \`${cmd}\``,
71
+ '',
72
+ 'Return the full diff content. Include changed file paths and line numbers.',
73
+ ].join('\n')
74
+ }
75
+
76
+ function buildSurfacePrompt(diff: string): string {
77
+ return [
78
+ 'Analyze the changed surface from this diff:',
79
+ '',
80
+ diff.slice(0, 5000),
81
+ '',
82
+ 'Report:',
83
+ '- Files changed (list each path)',
84
+ '- Lines added / removed per file',
85
+ '- Scope assessment (localized vs wide-ranging)',
86
+ ].join('\n')
87
+ }
88
+
89
+ function buildSpecCompliancePrompt(
90
+ diff: string,
91
+ _planPath: string,
92
+ ): string {
93
+ return [
94
+ 'Compare implementation to requirements.',
95
+ '',
96
+ 'Read the plan file to understand what was requested.',
97
+ '',
98
+ '## Diff',
99
+ diff.slice(0, 4000),
100
+ '',
101
+ 'Check: Does the implementation satisfy the plan requirements?',
102
+ 'Return: pass, partial, or fail with specific violations listed.',
103
+ ].join('\n')
104
+ }
105
+
106
+ function buildQualityReviewPrompt(
107
+ diff: string,
108
+ focusAreas: string[],
109
+ ): string {
110
+ const focusText =
111
+ focusAreas.length > 0 ? focusAreas.join(', ') : 'all aspects'
112
+
113
+ return [
114
+ `Review code quality focusing on: ${focusText}`,
115
+ '',
116
+ '## Diff',
117
+ diff.slice(0, 5000),
118
+ '',
119
+ '## Check Each Category',
120
+ '',
121
+ '- **Correctness**: Logic errors, edge cases, off-by-one',
122
+ '- **Security**: Injection risks, auth bypass, secrets in code, input validation',
123
+ '- **Performance**: N+1 queries, unnecessary allocations, blocking calls',
124
+ '- **Style**: Naming conventions, consistency with codebase, immutability',
125
+ '- **Error Handling**: Caught errors, graceful degradation, user-facing messages',
126
+ '',
127
+ '## Output Format',
128
+ '',
129
+ 'For each issue found, output one line:',
130
+ '`SEVERITY|FILE|LINE|DESCRIPTION|SUGGESTED_ACTION`',
131
+ '',
132
+ 'Severity values: BLOCKER, MAJOR, MINOR, NOTE',
133
+ 'BLOCKER = must fix before merge (security hole, broken build, data loss)',
134
+ 'MAJOR = should fix (bug, performance problem, missing error handling)',
135
+ 'MINOR = nice to fix (style, naming, minor duplication)',
136
+ 'NOTE = observation, not a problem',
137
+ ].join('\n')
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Parsing helpers (pure functions)
142
+ // ---------------------------------------------------------------------------
143
+
144
+ interface ParsedIssue {
145
+ severity: 'blocker' | 'major' | 'minor' | 'note'
146
+ file?: string
147
+ line?: number
148
+ description: string
149
+ action: string
150
+ }
151
+
152
+ function parseIssues(output: string): ParsedIssue[] {
153
+ const issues: ParsedIssue[] = []
154
+ const lines = output.split('\n')
155
+
156
+ for (const line of lines) {
157
+ const match = line.match(
158
+ /^(BLOCKER|MAJOR|MINOR|NOTE)\s*\|\s*([^|]*)\s*\|\s*(\d+)\s*\|\s*([^|]+)\s*\|\s*(.+)$/i,
159
+ )
160
+ if (match) {
161
+ const file = match[2]?.trim()
162
+ issues.push({
163
+ severity: match[1].toLowerCase() as ParsedIssue['severity'],
164
+ file: file || undefined,
165
+ line: parseInt(match[3], 10),
166
+ description: match[4].trim(),
167
+ action: match[5].trim(),
168
+ })
169
+ }
170
+ }
171
+
172
+ return issues
173
+ }
174
+
175
+ function determineDecision(issues: ParsedIssue[]): ReviewDiffOutput['decision'] {
176
+ if (issues.some((i) => i.severity === 'blocker')) return 'blocked'
177
+ if (issues.some((i) => i.severity === 'major')) return 'changes'
178
+ return 'approve'
179
+ }
180
+
181
+ function assessTests(output: string): ReviewDiffOutput['testAssessment'] {
182
+ const lower = output.toLowerCase()
183
+ if (lower.includes('missing') || lower.includes('no test')) return 'missing'
184
+ if (lower.includes('fail')) return 'failing'
185
+ return 'adequate'
186
+ }
187
+
188
+ function assessSpecCompliance(output: string): ReviewDiffOutput['specCompliance'] {
189
+ const lower = output.toLowerCase()
190
+ if (lower.includes('fail')) return 'fail'
191
+ if (lower.includes('partial')) return 'partial'
192
+ return 'pass'
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Skill class
197
+ // ---------------------------------------------------------------------------
198
+
199
+ export class ReviewDiffSkill extends Skill<ReviewDiffInput, ReviewDiffOutput> {
200
+ constructor() {
201
+ super({
202
+ name: 'review-diff',
203
+ description:
204
+ 'Review code changes (diff/branch/PR) with reviewer subagents. Returns structured report.',
205
+ requires: [],
206
+ inputSchema: ReviewDiffInputSchema as z.ZodType<ReviewDiffInput>,
207
+ outputSchema: ReviewDiffOutputSchema,
208
+ })
209
+ }
210
+
211
+ async execute(
212
+ input: ReviewDiffInput,
213
+ context: SkillContext,
214
+ ): Promise<ReviewDiffOutput> {
215
+ const { config, logger } = context
216
+ const dispatcher = createDispatcher(logger)
217
+ const guard = createMainAgentGuard({}, logger)
218
+
219
+ guard.activateEmbargo()
220
+ let totalTokens = 0
221
+
222
+ try {
223
+ // Step 1: Get diff
224
+ logger.info(
225
+ `[review-diff] Getting diff for ${input.targetType}: ${input.target}`,
226
+ )
227
+ const diffResult = await dispatcher.dispatch(
228
+ {
229
+ role: 'explorer',
230
+ model: input.model || config.defaultModel,
231
+ },
232
+ {
233
+ permissions: {
234
+ readFiles: true,
235
+ searchCode: false,
236
+ runCommands: true,
237
+ writeFiles: false,
238
+ gitOperations: true,
239
+ },
240
+ prompt: buildDiffPrompt(input.targetType, input.target),
241
+ owns: [],
242
+ reads: [],
243
+ },
244
+ )
245
+
246
+ totalTokens += diffResult.tokensUsed
247
+
248
+ if (diffResult.status !== 'success') {
249
+ return {
250
+ decision: 'needs-evidence',
251
+ issues: [
252
+ {
253
+ severity: 'blocker',
254
+ description: 'Cannot access diff',
255
+ action: 'Verify target exists and is accessible',
256
+ },
257
+ ],
258
+ specCompliance: 'fail',
259
+ testAssessment: 'missing',
260
+ strengths: [],
261
+ summary: 'Cannot retrieve diff for review',
262
+ tokensUsed: totalTokens,
263
+ }
264
+ }
265
+
266
+ const diffContent = diffResult.output
267
+
268
+ // Step 2: Inspect changed surface
269
+ logger.info('[review-diff] Inspecting changed surface')
270
+ const surfaceResult = await dispatcher.dispatch(
271
+ {
272
+ role: 'explorer',
273
+ model: input.model || config.defaultModel,
274
+ },
275
+ {
276
+ permissions: {
277
+ readFiles: true,
278
+ searchCode: false,
279
+ runCommands: false,
280
+ writeFiles: false,
281
+ gitOperations: false,
282
+ },
283
+ prompt: buildSurfacePrompt(diffContent),
284
+ owns: [],
285
+ reads: [],
286
+ },
287
+ )
288
+
289
+ totalTokens += surfaceResult.tokensUsed
290
+
291
+ // Step 3: Check requirement compliance (if plan provided)
292
+ let specOutput = 'pass'
293
+ if (input.planPath) {
294
+ logger.info('[review-diff] Checking spec compliance')
295
+ const specResult = await dispatcher.dispatch(
296
+ {
297
+ role: 'reviewer',
298
+ model: input.model || config.defaultModel,
299
+ },
300
+ {
301
+ permissions: {
302
+ readFiles: true,
303
+ searchCode: false,
304
+ runCommands: false,
305
+ writeFiles: false,
306
+ gitOperations: false,
307
+ },
308
+ prompt: buildSpecCompliancePrompt(diffContent, input.planPath),
309
+ owns: [],
310
+ reads: [input.planPath],
311
+ },
312
+ )
313
+
314
+ totalTokens += specResult.tokensUsed
315
+ specOutput = specResult.output
316
+ }
317
+
318
+ // Step 4: Check tests
319
+ logger.info('[review-diff] Assessing test coverage')
320
+ const testResult = await dispatcher.dispatch(
321
+ {
322
+ role: 'reviewer',
323
+ model: input.model || config.defaultModel,
324
+ },
325
+ {
326
+ permissions: {
327
+ readFiles: true,
328
+ searchCode: false,
329
+ runCommands: true,
330
+ writeFiles: false,
331
+ gitOperations: false,
332
+ },
333
+ prompt: [
334
+ 'Check test coverage in the changed code:',
335
+ '',
336
+ diffContent.slice(0, 4000),
337
+ '',
338
+ 'Do changed files have corresponding test changes?',
339
+ 'Are new functions/classes covered by tests?',
340
+ 'Return: adequate, missing, or failing with details.',
341
+ ].join('\n'),
342
+ owns: [],
343
+ reads: [],
344
+ },
345
+ )
346
+
347
+ totalTokens += testResult.tokensUsed
348
+
349
+ // Step 5: Review code quality
350
+ logger.info('[review-diff] Reviewing code quality')
351
+ const qualityResult = await dispatcher.dispatch(
352
+ {
353
+ role: 'reviewer',
354
+ model: input.model || config.defaultModel,
355
+ },
356
+ {
357
+ permissions: {
358
+ readFiles: true,
359
+ searchCode: true,
360
+ runCommands: false,
361
+ writeFiles: false,
362
+ gitOperations: false,
363
+ },
364
+ prompt: buildQualityReviewPrompt(
365
+ diffContent,
366
+ input.focus ?? [],
367
+ ),
368
+ owns: [],
369
+ reads: [],
370
+ },
371
+ )
372
+
373
+ totalTokens += qualityResult.tokensUsed
374
+
375
+ // Step 6: Compile final report
376
+ const issues = parseIssues(qualityResult.output)
377
+
378
+ return {
379
+ decision: determineDecision(issues),
380
+ issues,
381
+ specCompliance: assessSpecCompliance(specOutput),
382
+ testAssessment: assessTests(testResult.output),
383
+ strengths: ['Reviewed all changed files'],
384
+ summary: `Review complete: ${issues.length} issue(s) found (${issues.filter((i) => i.severity === 'blocker').length} blocker, ${issues.filter((i) => i.severity === 'major').length} major)`,
385
+ tokensUsed: totalTokens,
386
+ }
387
+ } finally {
388
+ guard.deactivateEmbargo()
389
+ }
390
+ }
391
+ }
392
+
393
+ export const reviewDiffSkill = new ReviewDiffSkill()
394
+ export default reviewDiffSkill
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Base skill class with validation and error handling
3
+ */
4
+ import { z } from 'zod'
5
+ import type {
6
+ SkillDefinition,
7
+ SkillContext,
8
+ } from './types.js'
9
+
10
+ export abstract class Skill<Input, Output> {
11
+ protected name: string
12
+ protected description: string
13
+ protected requires: string[]
14
+ protected inputSchema?: z.ZodSchema<Input>
15
+ protected outputSchema?: z.ZodSchema<Output>
16
+
17
+ constructor(options: {
18
+ name: string
19
+ description: string
20
+ requires?: string[]
21
+ inputSchema?: z.ZodSchema<Input>
22
+ outputSchema?: z.ZodSchema<Output>
23
+ }) {
24
+ this.name = options.name
25
+ this.description = options.description
26
+ this.requires = options.requires || []
27
+ this.inputSchema = options.inputSchema
28
+ this.outputSchema = options.outputSchema
29
+ }
30
+
31
+ abstract execute(input: Input, context: SkillContext): Promise<Output>
32
+
33
+ async validateInput(input: unknown): Promise<Input> {
34
+ if (this.inputSchema) {
35
+ return this.inputSchema.parse(input)
36
+ }
37
+ return input as Input
38
+ }
39
+
40
+ async validateOutput(output: unknown): Promise<Output> {
41
+ if (this.outputSchema) {
42
+ return this.outputSchema.parse(output)
43
+ }
44
+ return output as Output
45
+ }
46
+
47
+ toDefinition(): SkillDefinition<any, any> {
48
+ return {
49
+ name: this.name,
50
+ description: this.description,
51
+ requires: this.requires,
52
+ execute: async (input: any, context: SkillContext) => {
53
+ const validatedInput = await this.validateInput(input)
54
+ const result = await this.execute(validatedInput, context)
55
+ return await this.validateOutput(result)
56
+ },
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: systematic-debugging
3
+ description: Find root cause before fixing. Spawn debugger subagents for systematic diagnosis. CRITICAL: no fix before root cause confirmed.
4
+ requires: [tdd]
5
+ ---
6
+
7
+ ## ⛔ MAIN AGENT CONSTRAINT
8
+
9
+ You are a THIN DISPATCHER. Your ONLY job is to dispatch subagents.
10
+ You MUST NOT: read source files, search code, write/edit files, run tests, run git commands.
11
+ You MAY only: read plan/state files, dispatch subagents via Agent tool, relay results.
12
+ For EVERY user request (including this skill), dispatch a subagent. Never execute yourself.
13
+
14
+ ---
15
+
16
+ ## Use/Exclude Matrix
17
+
18
+ | Use When | Exclude When |
19
+ |---|---|
20
+ | Bug with unknown root cause | Root cause already known and confirmed |
21
+ | Test failure needing investigation | Typo or syntax error (use quick-task) |
22
+ | Intermittent/flaky behavior | Build/environment issue (not code) |
23
+ | Regression after recent changes | Fix is obvious and trivial |
24
+
25
+ ## Workflow
26
+
27
+ 1. **Reproduce** — Dispatch debugger subagent to reproduce the symptom. Confirm observed vs expected behavior.
28
+ 2. **Gather Evidence** — Dispatch explorer subagent (read-only) to identify suspect files, key functions, failure points, recent changes.
29
+ 3. **Diagnose** — Dispatch debugger subagent to form hypotheses, test top hypothesis, eliminate alternatives, confirm root cause. NO CODE CHANGES in this phase.
30
+ 4. **Apply Fix** — Dispatch debugger subagent (worktree) to apply ONE minimal fix targeting the confirmed root cause.
31
+ 5. **Verify** — Dispatch verifier subagent to re-run reproduction and full test suite.
32
+
33
+ ## Output Spec
34
+
35
+ | Field | Type | Description |
36
+ |---|---|---|
37
+ | `rootCause` | string | Description of confirmed root cause |
38
+ | `confidence` | enum | `high`, `medium`, `low` |
39
+ | `location.file` | string? | File containing the root cause |
40
+ | `location.line` | number? | Line number of root cause |
41
+ | `location.function` | string? | Function containing root cause |
42
+ | `fixApplied` | boolean | Whether the fix was successfully applied |
43
+ | `fixDescription` | string? | Description of the applied fix |
44
+ | `verification` | string | Verification output (PASS/FAIL with details) |
45
+ | `risks` | string[] | Identified risks or follow-ups |
46
+ | `tokensUsed` | number | Total tokens consumed |
47
+
48
+ ## Error Handling
49
+
50
+ | Error | Action |
51
+ |---|---|
52
+ | Cannot reproduce symptom | Return with confidence=low; describe reproduction gap |
53
+ | Diagnosis confidence is low | Apply safer, more conservative fix |
54
+ | Diagnose phase blocked (no fix yet) | Enforced by prompt design; subagent told not to write code |
55
+ | Fix subagent fails | Return fixApplied=false; preserve diagnosis |
56
+ | Verification fails after fix | Return verification FAIL; may need re-diagnosis |