@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,118 @@
1
+ /**
2
+ * State cleanup utilities
3
+ */
4
+ import { readdir, rmdir } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import type { PersistenceLayer } from './types.js'
7
+
8
+ export interface CleanupOptions {
9
+ maxAgeDays: number
10
+ maxStates?: number
11
+ dryRun?: boolean
12
+ }
13
+
14
+ export interface CleanupResult {
15
+ deleted: number
16
+ archived: number
17
+ errors: string[]
18
+ paths: string[]
19
+ }
20
+
21
+ export async function cleanupOldStates(
22
+ persistence: PersistenceLayer,
23
+ options: CleanupOptions
24
+ ): Promise<CleanupResult> {
25
+ const result: CleanupResult = {
26
+ deleted: 0,
27
+ archived: 0,
28
+ errors: [],
29
+ paths: [],
30
+ }
31
+
32
+ try {
33
+ const ids = await persistence.list()
34
+ const now = Date.now()
35
+ const maxAgeMs = options.maxAgeDays * 24 * 60 * 60 * 1000
36
+
37
+ for (const id of ids) {
38
+ try {
39
+ const state = await persistence.read(id)
40
+ if (!state) continue
41
+
42
+ const age = now - new Date(state.updatedAt).getTime()
43
+
44
+ if (age > maxAgeMs) {
45
+ if (!options.dryRun) {
46
+ await persistence.archive(id)
47
+ }
48
+ result.archived++
49
+ result.paths.push(id)
50
+ }
51
+ } catch (error) {
52
+ result.errors.push(`Failed to process state ${id}: ${error}`)
53
+ }
54
+ }
55
+
56
+ return result
57
+ } catch (error) {
58
+ result.errors.push(`Cleanup failed: ${error}`)
59
+ return result
60
+ }
61
+ }
62
+
63
+ export async function cleanupEmptyDirs(basePath: string): Promise<void> {
64
+ try {
65
+ const entries = await readdir(basePath, { withFileTypes: true })
66
+
67
+ for (const entry of entries) {
68
+ if (entry.isDirectory()) {
69
+ const fullPath = join(basePath, entry.name)
70
+ const files = await readdir(fullPath)
71
+
72
+ if (files.length === 0) {
73
+ await rmdir(fullPath)
74
+ }
75
+ }
76
+ }
77
+ } catch {
78
+ // Ignore errors
79
+ }
80
+ }
81
+
82
+ export async function getStateStats(basePath: string): Promise<{
83
+ total: number
84
+ active: number
85
+ archived: number
86
+ oldest?: Date
87
+ newest?: Date
88
+ }> {
89
+ const stats = {
90
+ total: 0,
91
+ active: 0,
92
+ archived: 0,
93
+ oldest: undefined as Date | undefined,
94
+ newest: undefined as Date | undefined,
95
+ }
96
+
97
+ try {
98
+ // Count active states
99
+ const activePath = join(basePath, 'active')
100
+ const activeFiles = await readdir(activePath)
101
+ stats.active = activeFiles.filter((f) => f.endsWith('.json')).length
102
+
103
+ // Count archived states
104
+ const archivePath = join(basePath, 'archive')
105
+ const archiveDirs = await readdir(archivePath)
106
+ for (const dir of archiveDirs) {
107
+ const dirPath = join(archivePath, dir)
108
+ const files = await readdir(dirPath)
109
+ stats.archived += files.filter((f) => f.endsWith('.json')).length
110
+ }
111
+
112
+ stats.total = stats.active + stats.archived
113
+
114
+ return stats
115
+ } catch {
116
+ return stats
117
+ }
118
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * State module index
3
+ */
4
+ export type { ExecutionState, StateManager, PersistenceLayer, StateOptions } from './types.js'
5
+ export { StateManagerImpl, createStateManager } from './manager.js'
6
+ export { FilePersistence, createPersistence } from './persistence.js'
7
+ export { validateState, isValidState, sanitizeState } from './validator.js'
8
+ export { cleanupOldStates, cleanupEmptyDirs, getStateStats } from './cleanup.js'
@@ -0,0 +1,96 @@
1
+ /**
2
+ * State manager implementation
3
+ */
4
+ import { nanoid } from 'nanoid'
5
+ import type {
6
+ ExecutionState,
7
+ StateManager,
8
+ PersistenceLayer,
9
+ } from './types.js'
10
+ import type { WorkflowConfig } from '../types.js'
11
+ import { createPersistence } from './persistence.js'
12
+
13
+ export class StateManagerImpl implements StateManager {
14
+ private persistence: PersistenceLayer
15
+
16
+ constructor(persistence: PersistenceLayer) {
17
+ this.persistence = persistence
18
+ }
19
+
20
+ async load(id: string): Promise<ExecutionState | null> {
21
+ return this.persistence.read(id)
22
+ }
23
+
24
+ async save(state: ExecutionState): Promise<void> {
25
+ const updated = {
26
+ ...state,
27
+ updatedAt: new Date(),
28
+ }
29
+ await this.persistence.write(updated)
30
+ }
31
+
32
+ async create(planId: string, _config: WorkflowConfig): Promise<ExecutionState> {
33
+ const now = new Date()
34
+ const state: ExecutionState = {
35
+ id: nanoid(),
36
+ planId,
37
+ completedTasks: [],
38
+ failedTasks: [],
39
+ startedAt: now,
40
+ updatedAt: now,
41
+ status: 'running',
42
+ context: {},
43
+ }
44
+ await this.persistence.write(state)
45
+ return state
46
+ }
47
+
48
+ async update(
49
+ id: string,
50
+ updates: Partial<ExecutionState>
51
+ ): Promise<ExecutionState> {
52
+ const existing = await this.persistence.read(id)
53
+ if (!existing) {
54
+ throw new Error(`State not found: ${id}`)
55
+ }
56
+
57
+ const updated: ExecutionState = {
58
+ ...existing,
59
+ ...updates,
60
+ id: existing.id,
61
+ updatedAt: new Date(),
62
+ }
63
+ await this.persistence.write(updated)
64
+ return updated
65
+ }
66
+
67
+ async archive(id: string): Promise<void> {
68
+ await this.persistence.archive(id)
69
+ }
70
+
71
+ async list(): Promise<ExecutionState[]> {
72
+ const ids = await this.persistence.list()
73
+ const states: ExecutionState[] = []
74
+ for (const id of ids) {
75
+ const state = await this.persistence.read(id)
76
+ if (state) {
77
+ states.push(state)
78
+ }
79
+ }
80
+ return states.sort(
81
+ (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
82
+ )
83
+ }
84
+
85
+ async getActive(): Promise<ExecutionState | null> {
86
+ const states = await this.list()
87
+ return states.find((s) => s.status === 'running') || null
88
+ }
89
+ }
90
+
91
+ export function createStateManager(
92
+ config: WorkflowConfig
93
+ ): StateManager {
94
+ const persistence = createPersistence(config.statePath)
95
+ return new StateManagerImpl(persistence)
96
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * File-based persistence layer for state
3
+ */
4
+ import { join } from 'path'
5
+ import { readFile, writeFile, mkdir, readdir, rename } from 'fs/promises'
6
+ import { existsSync } from 'fs'
7
+ import type { PersistenceLayer, ExecutionState } from './types.js'
8
+ import { validateState, sanitizeState } from './validator.js'
9
+
10
+ export class FilePersistence implements PersistenceLayer {
11
+ private basePath: string
12
+ private archivePath: string
13
+
14
+ constructor(basePath: string) {
15
+ this.basePath = join(basePath, 'active')
16
+ this.archivePath = join(basePath, 'archive')
17
+ }
18
+
19
+ async read(id: string): Promise<ExecutionState | null> {
20
+ const path = join(this.basePath, `${id}.json`)
21
+ try {
22
+ const data = await readFile(path, 'utf-8')
23
+ const parsed = JSON.parse(data)
24
+
25
+ // Validate state structure
26
+ const validation = validateState(parsed)
27
+ if (!validation.valid) {
28
+ console.warn(`[State] Invalid state file ${id}:`, validation.errors)
29
+ return null
30
+ }
31
+
32
+ // Sanitize dates and arrays
33
+ return sanitizeState(parsed)
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ async write(state: ExecutionState): Promise<void> {
40
+ await mkdir(this.basePath, { recursive: true })
41
+ const path = join(this.basePath, `${state.id}.json`)
42
+ await writeFile(path, JSON.stringify(state, null, 2))
43
+ }
44
+
45
+ async delete(id: string): Promise<void> {
46
+ const path = join(this.basePath, `${id}.json`)
47
+ try {
48
+ await import('fs/promises').then((fs) => fs.unlink(path))
49
+ } catch {
50
+ // Ignore if file doesn't exist
51
+ }
52
+ }
53
+
54
+ async list(): Promise<string[]> {
55
+ await mkdir(this.basePath, { recursive: true })
56
+ const files = await readdir(this.basePath)
57
+ return files
58
+ .filter((f) => f.endsWith('.json'))
59
+ .map((f) => f.replace('.json', ''))
60
+ }
61
+
62
+ async archive(id: string): Promise<void> {
63
+ const sourcePath = join(this.basePath, `${id}.json`)
64
+ const date = new Date().toISOString().split('T')[0]
65
+ const archiveDir = join(this.archivePath, date)
66
+ await mkdir(archiveDir, { recursive: true })
67
+ const targetPath = join(archiveDir, `${id}.json`)
68
+
69
+ if (existsSync(sourcePath)) {
70
+ await rename(sourcePath, targetPath)
71
+ }
72
+ }
73
+ }
74
+
75
+ export function createPersistence(basePath: string): PersistenceLayer {
76
+ return new FilePersistence(basePath)
77
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * State management types
3
+ */
4
+ import type { ExecutionState as CoreExecutionState, WorkflowConfig } from '../types.js'
5
+
6
+ // Use alias to avoid naming conflicts
7
+ export type ExecutionState = CoreExecutionState
8
+
9
+ export interface StateManager {
10
+ load(id: string): Promise<ExecutionState | null>
11
+ save(state: ExecutionState): Promise<void>
12
+ create(planId: string, config: WorkflowConfig): Promise<ExecutionState>
13
+ update(id: string, updates: Partial<ExecutionState>): Promise<ExecutionState>
14
+ archive(id: string): Promise<void>
15
+ list(): Promise<ExecutionState[]>
16
+ getActive(): Promise<ExecutionState | null>
17
+ }
18
+
19
+ export interface PersistenceLayer {
20
+ read(id: string): Promise<ExecutionState | null>
21
+ write(state: ExecutionState): Promise<void>
22
+ delete(id: string): Promise<void>
23
+ list(): Promise<string[]>
24
+ archive(id: string): Promise<void>
25
+ }
26
+
27
+ export interface StateOptions {
28
+ statePath: string
29
+ autoArchive: boolean
30
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * State validation utilities
3
+ */
4
+ import type { ExecutionState } from '../types.js'
5
+
6
+ export interface ValidationResult {
7
+ valid: boolean
8
+ errors: string[]
9
+ }
10
+
11
+ export function validateState(state: unknown): ValidationResult {
12
+ const errors: string[] = []
13
+
14
+ if (!state || typeof state !== 'object') {
15
+ return { valid: false, errors: ['State must be an object'] }
16
+ }
17
+
18
+ const s = state as Record<string, unknown>
19
+
20
+ // Required fields
21
+ if (!s.id || typeof s.id !== 'string') {
22
+ errors.push('State must have a string id')
23
+ }
24
+
25
+ if (!s.planId || typeof s.planId !== 'string') {
26
+ errors.push('State must have a string planId')
27
+ }
28
+
29
+ if (!s.status || !['running', 'paused', 'completed', 'failed', 'cancelled'].includes(s.status as string)) {
30
+ errors.push('State must have a valid status')
31
+ }
32
+
33
+ if (!s.startedAt) {
34
+ errors.push('State must have startedAt')
35
+ }
36
+
37
+ if (!s.updatedAt) {
38
+ errors.push('State must have updatedAt')
39
+ }
40
+
41
+ // Optional arrays
42
+ if (s.completedTasks && !Array.isArray(s.completedTasks)) {
43
+ errors.push('completedTasks must be an array')
44
+ }
45
+
46
+ if (s.failedTasks && !Array.isArray(s.failedTasks)) {
47
+ errors.push('failedTasks must be an array')
48
+ }
49
+
50
+ if (s.context && typeof s.context !== 'object') {
51
+ errors.push('context must be an object')
52
+ }
53
+
54
+ return {
55
+ valid: errors.length === 0,
56
+ errors,
57
+ }
58
+ }
59
+
60
+ export function isValidState(state: unknown): state is ExecutionState {
61
+ return validateState(state).valid
62
+ }
63
+
64
+ export function sanitizeState(state: ExecutionState): ExecutionState {
65
+ return {
66
+ ...state,
67
+ id: String(state.id),
68
+ planId: String(state.planId),
69
+ completedTasks: Array.isArray(state.completedTasks) ? state.completedTasks : [],
70
+ failedTasks: Array.isArray(state.failedTasks) ? state.failedTasks : [],
71
+ status: state.status || 'running',
72
+ context: typeof state.context === 'object' && state.context !== null
73
+ ? state.context
74
+ : {},
75
+ startedAt: state.startedAt ? new Date(state.startedAt) : new Date(),
76
+ updatedAt: state.updatedAt ? new Date(state.updatedAt) : new Date(),
77
+ }
78
+ }
package/src/types.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Global type definitions for yi-workflow
3
+ */
4
+
5
+ // Core workflow types
6
+ export interface WorkflowConfig {
7
+ planPath: string
8
+ statePath: string
9
+ autoCleanup: boolean
10
+ autoMerge: boolean
11
+ defaultModel: string
12
+ maxConcurrent: number
13
+ }
14
+
15
+ export interface Plan {
16
+ id: string
17
+ title: string
18
+ description: string
19
+ tasks: Task[]
20
+ createdAt: Date
21
+ updatedAt: Date
22
+ status: PlanStatus
23
+ }
24
+
25
+ export type PlanStatus = 'draft' | 'approved' | 'in_progress' | 'completed' | 'cancelled'
26
+
27
+ export interface Task {
28
+ id: string
29
+ title: string
30
+ description: string
31
+ status: TaskStatus
32
+ dependencies: string[]
33
+ estimatedHours?: number
34
+ actualHours?: number
35
+ }
36
+
37
+ export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'blocked'
38
+
39
+ // Execution state
40
+ export interface ExecutionState {
41
+ id: string
42
+ planId: string
43
+ currentTaskId?: string
44
+ completedTasks: string[]
45
+ failedTasks: string[]
46
+ startedAt: Date
47
+ updatedAt: Date
48
+ status: ExecutionStatus
49
+ context: Record<string, unknown>
50
+ }
51
+
52
+ export type ExecutionStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled'
53
+
54
+ // Hook types
55
+ export type HookType = 'pre-plan' | 'post-plan' | 'pre-execute' | 'post-execute' | 'on-error'
56
+
57
+ export interface HookContext {
58
+ config: WorkflowConfig
59
+ state?: ExecutionState
60
+ plan?: Plan
61
+ error?: Error
62
+ }
63
+
64
+ export interface HookDefinition {
65
+ type: HookType
66
+ name: string
67
+ execute: (context: HookContext) => Promise<void> | void
68
+ }
69
+
70
+ // Skill types
71
+ export interface SkillDefinition {
72
+ name: string
73
+ description: string
74
+ requires?: string[]
75
+ hooks?: {
76
+ pre?: HookDefinition[]
77
+ post?: HookDefinition[]
78
+ error?: HookDefinition[]
79
+ }
80
+ execute: (input: unknown, context: SkillContext) => Promise<unknown>
81
+ }
82
+
83
+ export interface SkillContext {
84
+ config: WorkflowConfig
85
+ state: ExecutionState
86
+ logger: Logger
87
+ }
88
+
89
+ // Logger interface
90
+ export interface Logger {
91
+ info: (message: string, ...args: unknown[]) => void
92
+ warn: (message: string, ...args: unknown[]) => void
93
+ error: (message: string, ...args: unknown[]) => void
94
+ debug: (message: string, ...args: unknown[]) => void
95
+ }
96
+
97
+ // CLI types
98
+ export interface CLIOptions {
99
+ verbose?: boolean
100
+ config?: string
101
+ model?: string
102
+ }