@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,48 @@
1
+ /**
2
+ * Agents module index
3
+ *
4
+ * Public API surface for the subagent dispatch engine.
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ SubagentConfig,
10
+ SubagentContract,
11
+ SubagentResult,
12
+ SubagentArtifact,
13
+ SubagentRole,
14
+ PermissionSet,
15
+ MainAgentGuard,
16
+ } from './types.js';
17
+
18
+ // Dispatcher
19
+ export {
20
+ SubagentDispatcher,
21
+ createDispatcher,
22
+ createProcessExecutor,
23
+ } from './dispatcher.js';
24
+
25
+ export type {
26
+ SubagentExecutor,
27
+ ExecutorOptions,
28
+ ExecutorResult,
29
+ ProcessExecutorConfig,
30
+ DispatcherOptions,
31
+ } from './dispatcher.js';
32
+
33
+ // Contracts
34
+ export {
35
+ explorerContract,
36
+ implementerContract,
37
+ reviewerContract,
38
+ debuggerContract,
39
+ verifierContract,
40
+ } from './contracts.js';
41
+
42
+ export type {
43
+ ExplorerParams,
44
+ ImplementerParams,
45
+ ReviewerParams,
46
+ DebuggerParams,
47
+ VerifierParams,
48
+ } from './contracts.js';
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Circuit breaker and retry mechanisms for resilient subagent dispatch
3
+ */
4
+
5
+ // Circuit breaker states
6
+ enum CircuitState {
7
+ CLOSED = 'closed', // Normal operation
8
+ OPEN = 'open', // Circuit broken, reject requests
9
+ HALF_OPEN = 'half-open' // Testing if service recovered
10
+ }
11
+
12
+ export class CircuitBreakerError extends Error {
13
+ constructor(message: string) {
14
+ super(message)
15
+ this.name = 'CircuitBreakerError'
16
+ }
17
+ }
18
+
19
+ export class RetryExhaustedError extends Error {
20
+ constructor(message: string) {
21
+ super(message)
22
+ this.name = 'RetryExhaustedError'
23
+ }
24
+ }
25
+
26
+ export interface CircuitBreakerConfig {
27
+ failureThreshold: number // Failures before opening circuit
28
+ successThreshold: number // Successes to close from half-open
29
+ timeout: number // How long to stay open (ms)
30
+ }
31
+
32
+ export interface CircuitBreakerMetrics {
33
+ state: CircuitState
34
+ failureCount: number
35
+ successCount: number
36
+ timeUntilRetry: number
37
+ }
38
+
39
+ export class CircuitBreaker {
40
+ private state: CircuitState = CircuitState.CLOSED
41
+ private failureCount: number = 0
42
+ private successCount: number = 0
43
+ private nextAttempt: number = 0
44
+ private config: CircuitBreakerConfig
45
+
46
+ constructor(config: Partial<CircuitBreakerConfig> = {}) {
47
+ this.config = {
48
+ failureThreshold: 5,
49
+ successThreshold: 3,
50
+ timeout: 60000,
51
+ ...config
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Execute operation with circuit breaker protection
57
+ */
58
+ async execute<T>(
59
+ operation: () => Promise<T>,
60
+ fallback?: () => T
61
+ ): Promise<T> {
62
+ // Check circuit state
63
+ if (this.state === CircuitState.OPEN) {
64
+ if (Date.now() < this.nextAttempt) {
65
+ // Still in timeout, use fallback or throw
66
+ if (fallback) {
67
+ console.log('[CircuitBreaker] Using fallback')
68
+ return fallback()
69
+ }
70
+ throw new CircuitBreakerError(
71
+ `Circuit open. Retry after ${new Date(this.nextAttempt).toISOString()}`
72
+ )
73
+ }
74
+ // Timeout elapsed, try half-open
75
+ this.state = CircuitState.HALF_OPEN
76
+ console.log('[CircuitBreaker] Entering HALF_OPEN state')
77
+ }
78
+
79
+ try {
80
+ const result = await operation()
81
+ this.onSuccess()
82
+ return result
83
+ } catch (error) {
84
+ this.onFailure()
85
+ throw error
86
+ }
87
+ }
88
+
89
+ private onSuccess(): void {
90
+ this.failureCount = 0
91
+
92
+ if (this.state === CircuitState.HALF_OPEN) {
93
+ this.successCount++
94
+ if (this.successCount >= this.config.successThreshold) {
95
+ console.log('[CircuitBreaker] CLOSED - service recovered')
96
+ this.state = CircuitState.CLOSED
97
+ this.successCount = 0
98
+ }
99
+ }
100
+ }
101
+
102
+ private onFailure(): void {
103
+ this.failureCount++
104
+
105
+ if (this.state === CircuitState.HALF_OPEN) {
106
+ // Failed in half-open, go back to open
107
+ this.tripCircuit()
108
+ } else if (this.failureCount >= this.config.failureThreshold) {
109
+ // Reached threshold, open circuit
110
+ this.tripCircuit()
111
+ }
112
+ }
113
+
114
+ private tripCircuit(): void {
115
+ console.log(`[CircuitBreaker] OPEN - failures: ${this.failureCount}`)
116
+ this.state = CircuitState.OPEN
117
+ this.nextAttempt = Date.now() + this.config.timeout
118
+ this.successCount = 0
119
+ }
120
+
121
+ getState(): CircuitState {
122
+ return this.state
123
+ }
124
+
125
+ getMetrics(): CircuitBreakerMetrics {
126
+ return {
127
+ state: this.state,
128
+ failureCount: this.failureCount,
129
+ successCount: this.successCount,
130
+ timeUntilRetry: Math.max(0, this.nextAttempt - Date.now())
131
+ }
132
+ }
133
+ }
134
+
135
+ export interface RetryPolicyConfig {
136
+ maxRetries: number
137
+ baseDelay: number
138
+ maxDelay: number
139
+ backoffMultiplier: number
140
+ retryableErrors: string[]
141
+ }
142
+
143
+ export class RetryPolicy {
144
+ private config: RetryPolicyConfig
145
+
146
+ constructor(config: Partial<RetryPolicyConfig> = {}) {
147
+ this.config = {
148
+ maxRetries: 3,
149
+ baseDelay: 1000,
150
+ maxDelay: 30000,
151
+ backoffMultiplier: 2,
152
+ retryableErrors: ['RateLimitError', 'TimeoutError', 'NetworkError', 'BudgetExceededError'],
153
+ ...config
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Execute operation with retry logic
159
+ */
160
+ async execute<T>(operation: () => Promise<T>): Promise<T> {
161
+ let lastError: Error | undefined
162
+
163
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
164
+ try {
165
+ return await operation()
166
+ } catch (error) {
167
+ lastError = error as Error
168
+
169
+ if (!this.isRetryable(error)) {
170
+ throw error
171
+ }
172
+
173
+ if (attempt < this.config.maxRetries) {
174
+ const delay = this.calculateDelay(attempt)
175
+ console.log(`[RetryPolicy] Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`)
176
+ await this.sleep(delay)
177
+ }
178
+ }
179
+ }
180
+
181
+ throw new RetryExhaustedError(
182
+ `Failed after ${this.config.maxRetries} retries: ${lastError?.message}`
183
+ )
184
+ }
185
+
186
+ private isRetryable(error: unknown): boolean {
187
+ if (!(error instanceof Error)) return false
188
+
189
+ const errorName = error.constructor.name
190
+ return this.config.retryableErrors.some(re =>
191
+ errorName.includes(re) || error.message.includes(re)
192
+ )
193
+ }
194
+
195
+ private calculateDelay(attempt: number): number {
196
+ // Exponential backoff with jitter
197
+ const exponentialDelay = this.config.baseDelay *
198
+ Math.pow(this.config.backoffMultiplier, attempt)
199
+ const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1)
200
+ return Math.min(exponentialDelay + jitter, this.config.maxDelay)
201
+ }
202
+
203
+ private sleep(ms: number): Promise<void> {
204
+ return new Promise(resolve => setTimeout(resolve, ms))
205
+ }
206
+ }
207
+
208
+ export interface ResilientDispatchOptions<T> {
209
+ fallback?: () => T
210
+ onRetry?: (attempt: number, error: Error) => void
211
+ onCircuitOpen?: () => void
212
+ }
213
+
214
+ export class ResilientDispatcher {
215
+ private circuitBreaker: CircuitBreaker
216
+ private retryPolicy: RetryPolicy
217
+
218
+ constructor(config?: {
219
+ circuitBreaker?: Partial<CircuitBreakerConfig>
220
+ retryPolicy?: Partial<RetryPolicyConfig>
221
+ }) {
222
+ this.circuitBreaker = new CircuitBreaker(config?.circuitBreaker)
223
+ this.retryPolicy = new RetryPolicy(config?.retryPolicy)
224
+ }
225
+
226
+ /**
227
+ * Dispatch with both circuit breaker and retry
228
+ */
229
+ async dispatch<T>(
230
+ operation: () => Promise<T>,
231
+ options: ResilientDispatchOptions<T> = {}
232
+ ): Promise<T> {
233
+ return this.circuitBreaker.execute(
234
+ async () => {
235
+ return await this.retryPolicy.execute(operation)
236
+ },
237
+ options.fallback
238
+ )
239
+ }
240
+
241
+ /**
242
+ * Get health metrics
243
+ */
244
+ getHealth(): {
245
+ circuitState: CircuitState
246
+ metrics: CircuitBreakerMetrics
247
+ } {
248
+ return {
249
+ circuitState: this.circuitBreaker.getState(),
250
+ metrics: this.circuitBreaker.getMetrics()
251
+ }
252
+ }
253
+ }
254
+
255
+ export { CircuitState }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Token budget management for context window management
3
+ */
4
+
5
+ export interface TokenUsage {
6
+ used: number
7
+ limit: number
8
+ percentage: number
9
+ }
10
+
11
+ export interface TokenBudgetConfig {
12
+ limit: number
13
+ warningThreshold: number
14
+ }
15
+
16
+ export class TokenBudget {
17
+ private used: number = 0
18
+ private readonly limit: number
19
+ private readonly warningThreshold: number
20
+
21
+ constructor(config: TokenBudgetConfig = { limit: 1_000_000, warningThreshold: 0.8 }) {
22
+ this.limit = config.limit
23
+ this.warningThreshold = config.warningThreshold
24
+ }
25
+
26
+ /**
27
+ * Check if we can afford the estimated tokens
28
+ */
29
+ canAfford(estimatedTokens: number): boolean {
30
+ return this.used + estimatedTokens <= this.limit
31
+ }
32
+
33
+ /**
34
+ * Record token usage
35
+ */
36
+ recordUsage(tokens: number): void {
37
+ this.used += tokens
38
+
39
+ if (this.used > this.limit * this.warningThreshold) {
40
+ console.warn(`[TokenBudget] Warning: ${this.used}/${this.limit} tokens used (${Math.round(this.getUsage().percentage)}%)`)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get remaining tokens
46
+ */
47
+ getRemaining(): number {
48
+ return Math.max(0, this.limit - this.used)
49
+ }
50
+
51
+ /**
52
+ * Get current usage stats
53
+ */
54
+ getUsage(): TokenUsage {
55
+ return {
56
+ used: this.used,
57
+ limit: this.limit,
58
+ percentage: (this.used / this.limit) * 100
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Estimate tokens for a text string (rough approximation)
64
+ * ~4 chars per token for English text
65
+ */
66
+ static estimateTokens(text: string): number {
67
+ return Math.ceil(text.length / 4)
68
+ }
69
+
70
+ /**
71
+ * Reset budget (for testing or new milestone)
72
+ */
73
+ reset(): void {
74
+ this.used = 0
75
+ }
76
+ }
77
+
78
+ export class BudgetExceededError extends Error {
79
+ constructor(message: string = 'Token budget exceeded') {
80
+ super(message)
81
+ this.name = 'BudgetExceededError'
82
+ }
83
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Subagent system types
3
+ */
4
+
5
+ export type SubagentRole =
6
+ | 'explorer' // Read-only exploration
7
+ | 'implementer' // Code implementation
8
+ | 'reviewer' // Code review
9
+ | 'planner' // Plan creation
10
+ | 'debugger' // Root cause analysis
11
+ | 'verifier' // Goal verification
12
+ | 'researcher' // Internet research and information gathering
13
+ | 'general' // Catch-all
14
+
15
+ export interface SubagentConfig {
16
+ role: SubagentRole
17
+ model?: string
18
+ isolation?: 'worktree'
19
+ tokenBudget?: number
20
+ timeout?: number
21
+ }
22
+
23
+ export interface SubagentContract {
24
+ /** What this subagent is allowed to do */
25
+ permissions: PermissionSet
26
+ /** Exact prompt to send */
27
+ prompt: string
28
+ /** Required output format */
29
+ outputSchema?: Record<string, unknown>
30
+ /** Files this subagent owns */
31
+ owns: string[]
32
+ /** Files this subagent may read */
33
+ reads: string[]
34
+ }
35
+
36
+ export interface PermissionSet {
37
+ readFiles: boolean
38
+ searchCode: boolean
39
+ runCommands: boolean
40
+ writeFiles: boolean
41
+ gitOperations: boolean
42
+ }
43
+
44
+ export interface SubagentResult {
45
+ id: string
46
+ role: SubagentRole
47
+ status: 'success' | 'failure' | 'timeout'
48
+ output: string
49
+ artifacts: SubagentArtifact[]
50
+ tokensUsed: number
51
+ duration: number
52
+ errors: string[]
53
+ }
54
+
55
+ export interface SubagentArtifact {
56
+ type: 'file' | 'diff' | 'test-result' | 'report'
57
+ path?: string
58
+ content: string
59
+ }
60
+
61
+ /** Controls what the main agent is allowed to do */
62
+ export interface MainAgentGuard {
63
+ /** Is the main agent currently allowed to read source files? */
64
+ canReadSource: boolean
65
+ /** Is the main agent currently allowed to search the codebase? */
66
+ canSearchCodebase: boolean
67
+ /** Is the main agent currently allowed to write files? */
68
+ canWriteFiles: boolean
69
+ /** Is the main agent in "embargo" mode (must delegate everything)? */
70
+ embargoActive: boolean
71
+ /** Active subagent IDs that must complete before guard lifts */
72
+ pendingSubagents: string[]
73
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Main Agent Guard - Enforces that Main Agent NEVER does actual work
3
+ *
4
+ * This is the CORE architectural constraint:
5
+ * - Main Agent = Orchestrator only
6
+ * - All real work = Subagents only
7
+ */
8
+
9
+ import type { Logger } from '../types.js'
10
+
11
+ export interface GuardConfig {
12
+ /** Token budget before forced checkpoint */
13
+ tokenBudget: number
14
+ /** Whether embargo is active */
15
+ embargoActive: boolean
16
+ /** List of prohibited operations */
17
+ prohibited: ProhibitedOperations
18
+ }
19
+
20
+ export interface ProhibitedOperations {
21
+ readSourceFiles: boolean
22
+ searchCodebase: boolean
23
+ writeFiles: boolean
24
+ runBuildCommands: boolean
25
+ runTests: boolean
26
+ editLargeFiles: boolean
27
+ longRunningTasks: boolean
28
+ }
29
+
30
+ export interface OperationRequest {
31
+ type: 'read' | 'search' | 'write' | 'run' | 'edit'
32
+ target?: string
33
+ estimatedLines?: number
34
+ estimatedDuration?: number
35
+ }
36
+
37
+ export interface GuardDecision {
38
+ allowed: boolean
39
+ /** If not allowed, which subagent to dispatch instead */
40
+ dispatchInstead?: string
41
+ /** Why this operation was blocked */
42
+ reason: string
43
+ /** Suggested action */
44
+ suggestion: string
45
+ }
46
+
47
+ export class MainAgentGuard {
48
+ private config: GuardConfig
49
+ private logger: Logger
50
+ private tokensUsed: number = 0
51
+
52
+ constructor(config: GuardConfig, logger: Logger) {
53
+ this.config = config
54
+ this.logger = logger
55
+ }
56
+
57
+ /**
58
+ * Check if an operation is allowed for Main Agent
59
+ * ALWAYS returns false for actual work - forces subagent dispatch
60
+ */
61
+ checkOperation(request: OperationRequest): GuardDecision {
62
+ // ALWAYS block file reads (except config files)
63
+ if (request.type === 'read' && this.isSourceFile(request.target)) {
64
+ return {
65
+ allowed: false,
66
+ dispatchInstead: 'explorer',
67
+ reason: 'Main Agent is prohibited from reading source files',
68
+ suggestion: 'Dispatch explorer subagent to read and summarize',
69
+ }
70
+ }
71
+
72
+ // ALWAYS block code search
73
+ if (request.type === 'search') {
74
+ return {
75
+ allowed: false,
76
+ dispatchInstead: 'explorer',
77
+ reason: 'Main Agent is prohibited from searching codebase',
78
+ suggestion: 'Dispatch explorer subagent with search query',
79
+ }
80
+ }
81
+
82
+ // ALWAYS block file writes
83
+ if (request.type === 'write') {
84
+ return {
85
+ allowed: false,
86
+ dispatchInstead: 'implementer',
87
+ reason: 'Main Agent is prohibited from writing files',
88
+ suggestion: 'Dispatch implementer subagent to write files',
89
+ }
90
+ }
91
+
92
+ // ALWAYS block edits (except tiny ones)
93
+ if (request.type === 'edit') {
94
+ if ((request.estimatedLines || 0) > 10) {
95
+ return {
96
+ allowed: false,
97
+ dispatchInstead: 'implementer',
98
+ reason: 'Main Agent is prohibited from editing large files',
99
+ suggestion: 'Dispatch implementer subagent for edits >10 lines',
100
+ }
101
+ }
102
+ }
103
+
104
+ // ALWAYS block long-running commands
105
+ if (request.type === 'run') {
106
+ if ((request.estimatedDuration || 0) > 30) {
107
+ return {
108
+ allowed: false,
109
+ dispatchInstead: 'runner',
110
+ reason: 'Main Agent is prohibited from long-running commands',
111
+ suggestion: 'Dispatch runner subagent for commands >30s',
112
+ }
113
+ }
114
+
115
+ // Block build/test commands
116
+ if (this.isBuildCommand(request.target)) {
117
+ return {
118
+ allowed: false,
119
+ dispatchInstead: 'runner',
120
+ reason: 'Main Agent is prohibited from build/test commands',
121
+ suggestion: 'Dispatch runner subagent for build/test',
122
+ }
123
+ }
124
+ }
125
+
126
+ // Check token budget
127
+ if (this.tokensUsed > this.config.tokenBudget) {
128
+ return {
129
+ allowed: false,
130
+ dispatchInstead: 'checkpoint',
131
+ reason: `Token budget exceeded: ${this.tokensUsed}/${this.config.tokenBudget}`,
132
+ suggestion: 'Force checkpoint: summarize and ask user to continue',
133
+ }
134
+ }
135
+
136
+ // Check embargo
137
+ if (this.config.embargoActive) {
138
+ return {
139
+ allowed: false,
140
+ dispatchInstead: 'coordinator',
141
+ reason: 'Embargo active: all work must be delegated to subagents',
142
+ suggestion: 'Wait for subagents to complete or dispatch coordinator',
143
+ }
144
+ }
145
+
146
+ // Only allow meta-operations (orchestration)
147
+ return {
148
+ allowed: true,
149
+ reason: 'Operation permitted for orchestration',
150
+ suggestion: 'Proceed with orchestration',
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Assert that Main Agent is NOT doing work - throws if violated
156
+ */
157
+ assertNotDoingWork(operation: string): void {
158
+ const decision = this.checkOperation({
159
+ type: this.inferOperationType(operation),
160
+ target: operation,
161
+ })
162
+
163
+ if (!decision.allowed) {
164
+ const error = new Error(
165
+ `🚫 MAIN AGENT GUARD VIOLATION\n` +
166
+ `Operation: ${operation}\n` +
167
+ `Reason: ${decision.reason}\n` +
168
+ `Action: ${decision.suggestion}`
169
+ )
170
+ this.logger.error(error.message)
171
+ throw error
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Track token usage
177
+ */
178
+ trackTokens(tokens: number): void {
179
+ this.tokensUsed += tokens
180
+ if (this.tokensUsed > this.config.tokenBudget * 0.8) {
181
+ this.logger.warn(
182
+ `⚠️ Token budget at ${Math.round((this.tokensUsed / this.config.tokenBudget) * 100)}%`
183
+ )
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Activate embargo mode (post subagent dispatch)
189
+ */
190
+ activateEmbargo(): void {
191
+ this.config.embargoActive = true
192
+ this.logger.info('🔒 Embargo activated: Main Agent hands off to subagents')
193
+ }
194
+
195
+ /**
196
+ * Deactivate embargo (all subagents complete)
197
+ */
198
+ deactivateEmbargo(): void {
199
+ this.config.embargoActive = false
200
+ this.logger.info('🔓 Embargo lifted: Main Agent can orchestrate')
201
+ }
202
+
203
+ private isSourceFile(path?: string): boolean {
204
+ if (!path) return false
205
+ const sourceExts = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs', '.cpp', '.c', '.h']
206
+ return sourceExts.some((ext) => path.endsWith(ext))
207
+ }
208
+
209
+ private isBuildCommand(cmd?: string): boolean {
210
+ if (!cmd) return false
211
+ const buildCmds = ['npm run build', 'tsc', 'vite build', 'webpack', 'rollup', 'esbuild', 'jest', 'vitest', 'pytest']
212
+ return buildCmds.some((bc) => cmd.includes(bc))
213
+ }
214
+
215
+ private inferOperationType(operation: string): 'read' | 'search' | 'write' | 'run' | 'edit' {
216
+ if (operation.includes('read') || operation.includes('open')) return 'read'
217
+ if (operation.includes('search') || operation.includes('find') || operation.includes('grep')) return 'search'
218
+ if (operation.includes('write') || operation.includes('create')) return 'write'
219
+ if (operation.includes('edit') || operation.includes('modify')) return 'edit'
220
+ return 'run'
221
+ }
222
+ }
223
+
224
+ export function createMainAgentGuard(
225
+ config: Partial<GuardConfig> = {},
226
+ logger: Logger
227
+ ): MainAgentGuard {
228
+ return new MainAgentGuard(
229
+ {
230
+ tokenBudget: config.tokenBudget || 60000,
231
+ embargoActive: config.embargoActive || false,
232
+ prohibited: {
233
+ readSourceFiles: true,
234
+ searchCodebase: true,
235
+ writeFiles: true,
236
+ runBuildCommands: true,
237
+ runTests: true,
238
+ editLargeFiles: true,
239
+ longRunningTasks: true,
240
+ ...config.prohibited,
241
+ },
242
+ },
243
+ logger
244
+ )
245
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Built-in hooks index
3
+ */
4
+ export * as prePlan from './pre-plan.js'
5
+ export * as postPlan from './post-plan.js'
6
+ export * as preExecute from './pre-execute.js'
7
+ export * as postExecute from './post-execute.js'
8
+ export * as onError from './on-error.js'