@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,483 @@
1
+ /**
2
+ * Main Agent Guard - enforces that the main agent NEVER does substantive work.
3
+ *
4
+ * The guard is the enforcement mechanism for the "thin dispatcher" pattern.
5
+ * When embargo is active, the main agent is blocked from everything except
6
+ * meta-operations (reading plan files, state files, and dispatching subagents).
7
+ *
8
+ * Usage:
9
+ * ```typescript
10
+ * const guard = MainAgentGuard.create({
11
+ * defaultModel: 'haiku',
12
+ * planningThreshold: 0.6,
13
+ * maxMainAgentTokens: 10000,
14
+ * autoRoute: true,
15
+ * })
16
+ *
17
+ * const check = guard.checkOperation('read-source', { path: 'src/main.ts' })
18
+ * if (!check.allowed) {
19
+ * throw new Error(check.reason)
20
+ * }
21
+ * ```
22
+ */
23
+
24
+ import type { RouterConfig } from './types.js'
25
+ import { MAIN_AGENT_PROHIBITED } from './types.js'
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Guard-specific types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /** Result of a guard check. */
32
+ export interface GuardCheckResult {
33
+ /** Whether the operation is permitted */
34
+ allowed: boolean
35
+ /** Human-readable reason for the decision */
36
+ reason: string
37
+ /** The specific rule that triggered (for debugging) */
38
+ rule?: string
39
+ }
40
+
41
+ /** Context passed to guard checks for richer decision-making. */
42
+ export interface GuardContext {
43
+ /** The operation being attempted */
44
+ operation: string
45
+ /** Optional file path involved */
46
+ path?: string
47
+ /** Optional subagent ID if dispatching */
48
+ subagentId?: string
49
+ }
50
+
51
+ /** Snapshot of guard state for monitoring/debugging. */
52
+ export interface GuardStatus {
53
+ embargoActive: boolean
54
+ pendingSubagents: string[]
55
+ violations: GuardViolation[]
56
+ prohibitedOperations: string[]
57
+ allowedPaths: string[]
58
+ forbiddenPaths: string[]
59
+ }
60
+
61
+ /** Recorded violation for audit trail. */
62
+ export interface GuardViolation {
63
+ operation: string
64
+ path?: string
65
+ timestamp: Date
66
+ reason: string
67
+ }
68
+
69
+ /** Operation categories recognized by the guard. */
70
+ export type GuardOperation =
71
+ | 'read-source'
72
+ | 'search-codebase'
73
+ | 'write-files'
74
+ | 'edit-files'
75
+ | 'run-build'
76
+ | 'run-tests'
77
+ | 'git-commit'
78
+ | 'git-push'
79
+ | 'read-diffs'
80
+ | 'long-running-task'
81
+ | 'dispatch-subagent'
82
+ | 'read-meta-file'
83
+ | 'relay-results'
84
+ | 'read-plan'
85
+ | 'read-state'
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Helpers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ const META_OPERATIONS: ReadonlySet<string> = new Set([
92
+ 'dispatch-subagent',
93
+ 'relay-results',
94
+ 'read-meta-file',
95
+ 'read-plan',
96
+ 'read-state',
97
+ 'list-plans',
98
+ 'check-state',
99
+ 'read-config',
100
+ ])
101
+
102
+ const ALWAYS_ALLOWED_PATHS: ReadonlyArray<string> = [
103
+ '.pi/plans/**',
104
+ '.pi/yi-workflow/state/**',
105
+ 'SKILL.md',
106
+ 'AGENTS.md',
107
+ 'CLAUDE.md',
108
+ 'package.json',
109
+ '.claude/**',
110
+ ]
111
+
112
+ const ALWAYS_FORBIDDEN_PATHS: ReadonlyArray<string> = [
113
+ 'src/**',
114
+ 'lib/**',
115
+ 'app/**',
116
+ 'dist/**',
117
+ 'node_modules/**',
118
+ '**/*.test.ts',
119
+ '**/*.spec.ts',
120
+ '**/*.test.tsx',
121
+ '**/*.spec.tsx',
122
+ ]
123
+
124
+ /**
125
+ * Parses a simple glob pattern into a regex for matching.
126
+ * Supports ** for recursive matching and * for single-segment.
127
+ */
128
+ function globToRegex(pattern: string): RegExp {
129
+ const escaped = pattern
130
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
131
+ .replace(/\*\*/g, '___DOUBLESTAR___')
132
+ .replace(/\*/g, '[^/]*')
133
+ .replace(/___DOUBLESTAR___/g, '.*')
134
+
135
+ return new RegExp(`^${escaped}$`)
136
+ }
137
+
138
+ /** Check whether a path matches any of the given glob patterns. */
139
+ function matchesAnyGlob(filePath: string, patterns: readonly string[]): boolean {
140
+ const normalized = filePath.replace(/^\.\//, '')
141
+ for (const pattern of patterns) {
142
+ const regex = globToRegex(pattern)
143
+ if (regex.test(normalized)) {
144
+ return true
145
+ }
146
+ }
147
+ return false
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // MainAgentGuard
152
+ // ---------------------------------------------------------------------------
153
+
154
+ /**
155
+ * Enforces the "main agent as thin dispatcher" constraint.
156
+ *
157
+ * When embargoActive is true, the main agent is FORBIDDEN from doing
158
+ * ANY substantive work. It can only read meta-files and dispatch subagents.
159
+ */
160
+ export class MainAgentGuard {
161
+ private readonly config: Readonly<RouterConfig>
162
+ private readonly violations: GuardViolation[]
163
+ private readonly pendingSubagents: Set<string>
164
+ private embargoActive: boolean
165
+ private tokensUsed: number
166
+
167
+ constructor(config: RouterConfig) {
168
+ this.config = Object.freeze({ ...config })
169
+ this.violations = []
170
+ this.pendingSubagents = new Set()
171
+ this.embargoActive = false
172
+ this.tokensUsed = 0
173
+ }
174
+
175
+ // -----------------------------------------------------------------------
176
+ // Factory methods
177
+ // -----------------------------------------------------------------------
178
+
179
+ /** Create a guard with embargo active (strictest mode). */
180
+ static createEmbargoAll(config: RouterConfig): MainAgentGuard {
181
+ const guard = new MainAgentGuard(config)
182
+ guard.activateEmbargo()
183
+ return guard
184
+ }
185
+
186
+ /** Create a guard with embargo lifted (use with extreme caution). */
187
+ static createDisabled(config: RouterConfig): MainAgentGuard {
188
+ return new MainAgentGuard(config)
189
+ }
190
+
191
+ /** Convenience: create with default config. */
192
+ static create(config: RouterConfig): MainAgentGuard {
193
+ return new MainAgentGuard(config)
194
+ }
195
+
196
+ // -----------------------------------------------------------------------
197
+ // Operation checks
198
+ // -----------------------------------------------------------------------
199
+
200
+ /**
201
+ * Check whether an operation is permitted for the main agent.
202
+ *
203
+ * @param operation - The operation being attempted
204
+ * @returns GuardCheckResult with allowed flag and reason
205
+ */
206
+ checkOperation(operation: GuardOperation): GuardCheckResult {
207
+ // If embargo is inactive, everything is permitted
208
+ if (!this.embargoActive) {
209
+ return {
210
+ allowed: true,
211
+ reason: 'Embargo is inactive - main agent may operate',
212
+ rule: 'embargo-off',
213
+ }
214
+ }
215
+
216
+ // Meta-operations are always allowed (they ARE the main agent's job)
217
+ if (META_OPERATIONS.has(operation)) {
218
+ return {
219
+ allowed: true,
220
+ reason: 'Meta-operation permitted for orchestration',
221
+ rule: 'meta-operation',
222
+ }
223
+ }
224
+
225
+ // Check against prohibited operations
226
+ const prohibitionMap: Record<string, boolean> = {
227
+ 'read-source': MAIN_AGENT_PROHIBITED.readSourceFiles,
228
+ 'search-codebase': MAIN_AGENT_PROHIBITED.searchCodebase,
229
+ 'write-files': MAIN_AGENT_PROHIBITED.writeFiles,
230
+ 'edit-files': MAIN_AGENT_PROHIBITED.writeFiles,
231
+ 'run-build': MAIN_AGENT_PROHIBITED.runBuildCommands,
232
+ 'run-tests': MAIN_AGENT_PROHIBITED.runTests,
233
+ 'git-commit': MAIN_AGENT_PROHIBITED.writeFiles,
234
+ 'git-push': MAIN_AGENT_PROHIBITED.writeFiles,
235
+ 'read-diffs': MAIN_AGENT_PROHIBITED.readSourceFiles,
236
+ 'long-running-task': MAIN_AGENT_PROHIBITED.longRunningTasks,
237
+ }
238
+
239
+ if (prohibitionMap[operation]) {
240
+ return {
241
+ allowed: false,
242
+ reason: `Operation "${operation}" is prohibited for the main agent`,
243
+ rule: 'prohibited-operation',
244
+ }
245
+ }
246
+
247
+ // Unknown operations under embargo: deny by default
248
+ return {
249
+ allowed: false,
250
+ reason: `Operation "${operation}" is not recognized and embargo is active`,
251
+ rule: 'unknown-operation-deny',
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Check whether a file path is permitted to be read by the main agent.
257
+ */
258
+ checkReadPath(filePath: string): GuardCheckResult {
259
+ if (!this.embargoActive) {
260
+ return { allowed: true, reason: 'Embargo is inactive', rule: 'embargo-off' }
261
+ }
262
+
263
+ const normalized = filePath.replace(/^\.\//, '')
264
+
265
+ // Explicitly forbidden paths take precedence
266
+ if (matchesAnyGlob(normalized, ALWAYS_FORBIDDEN_PATHS)) {
267
+ return {
268
+ allowed: false,
269
+ reason: `Path "${filePath}" is forbidden under embargo`,
270
+ rule: 'forbidden-path',
271
+ }
272
+ }
273
+
274
+ // Check if path is in the allowed list
275
+ if (matchesAnyGlob(normalized, ALWAYS_ALLOWED_PATHS)) {
276
+ return { allowed: true, reason: 'Path is in the allowed list', rule: 'allowed-path' }
277
+ }
278
+
279
+ // Default: deny
280
+ return {
281
+ allowed: false,
282
+ reason: `Path "${filePath}" is not in the allowed list and embargo is active`,
283
+ rule: 'default-deny-path',
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Comprehensive check combining operation and optional file path/context.
289
+ */
290
+ check(context: GuardContext): GuardCheckResult {
291
+ // Check the operation
292
+ const opCheck = this.checkOperation(context.operation as GuardOperation)
293
+ if (!opCheck.allowed) {
294
+ return opCheck
295
+ }
296
+
297
+ // If a path is provided, check read access
298
+ if (context.path) {
299
+ const pathCheck = this.checkReadPath(context.path)
300
+ if (!pathCheck.allowed) {
301
+ return pathCheck
302
+ }
303
+ }
304
+
305
+ // Check token budget
306
+ if (this.tokensUsed > this.config.maxMainAgentTokens) {
307
+ return {
308
+ allowed: false,
309
+ reason: `Token budget exceeded: ${this.tokensUsed}/${this.config.maxMainAgentTokens}`,
310
+ rule: 'token-budget-exceeded',
311
+ }
312
+ }
313
+
314
+ return { allowed: true, reason: 'All checks passed' }
315
+ }
316
+
317
+ /**
318
+ * Assert an operation is allowed. Throws if violated.
319
+ */
320
+ assertAllowed(context: GuardContext): void {
321
+ const result = this.check(context)
322
+ if (!result.allowed) {
323
+ const error = new Error(
324
+ `GUARD VIOLATION: ${result.reason}\n` +
325
+ `Operation: ${context.operation}\n` +
326
+ `${context.path ? `Path: ${context.path}\n` : ''}` +
327
+ `Rule: ${result.rule}`
328
+ )
329
+ this.violations.push({
330
+ operation: context.operation,
331
+ path: context.path,
332
+ timestamp: new Date(),
333
+ reason: result.reason,
334
+ })
335
+ throw error
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Record a violation without throwing (for audit trail).
341
+ */
342
+ recordViolation(context: GuardContext): void {
343
+ const result = this.check(context)
344
+ this.violations.push({
345
+ operation: context.operation,
346
+ path: context.path,
347
+ timestamp: new Date(),
348
+ reason: result.reason,
349
+ })
350
+ }
351
+
352
+ // -----------------------------------------------------------------------
353
+ // Embargo control
354
+ // -----------------------------------------------------------------------
355
+
356
+ /** Activate embargo mode - main agent must delegate everything. */
357
+ activateEmbargo(): void {
358
+ this.embargoActive = true
359
+ }
360
+
361
+ /** Deactivate embargo mode - main agent regains limited capabilities. */
362
+ deactivateEmbargo(): void {
363
+ this.embargoActive = false
364
+ }
365
+
366
+ /** Check if embargo is currently active. */
367
+ isEmbargoActive(): boolean {
368
+ return this.embargoActive
369
+ }
370
+
371
+ // -----------------------------------------------------------------------
372
+ // Subagent tracking
373
+ // -----------------------------------------------------------------------
374
+
375
+ /** Register a dispatched subagent. */
376
+ registerSubagent(id: string): void {
377
+ this.pendingSubagents.add(id)
378
+ }
379
+
380
+ /** Mark a subagent as completed. */
381
+ completeSubagent(id: string): void {
382
+ this.pendingSubagents.delete(id)
383
+ }
384
+
385
+ /** Check if there are any subagents still pending. */
386
+ hasPendingSubagents(): boolean {
387
+ return this.pendingSubagents.size > 0
388
+ }
389
+
390
+ // -----------------------------------------------------------------------
391
+ // Token tracking
392
+ // -----------------------------------------------------------------------
393
+
394
+ /** Track token usage by the main agent. */
395
+ trackTokens(count: number): void {
396
+ this.tokensUsed += count
397
+ }
398
+
399
+ /** Get current token usage. */
400
+ getTokensUsed(): number {
401
+ return this.tokensUsed
402
+ }
403
+
404
+ /** Percentage of token budget consumed (0-1). */
405
+ tokenBudgetUtilization(): number {
406
+ return this.tokensUsed / this.config.maxMainAgentTokens
407
+ }
408
+
409
+ // -----------------------------------------------------------------------
410
+ // Status and reporting
411
+ // -----------------------------------------------------------------------
412
+
413
+ /** Current guard status snapshot. */
414
+ getStatus(): GuardStatus {
415
+ return {
416
+ embargoActive: this.embargoActive,
417
+ pendingSubagents: [...this.pendingSubagents],
418
+ violations: [...this.violations],
419
+ prohibitedOperations: Object.entries(MAIN_AGENT_PROHIBITED)
420
+ .filter(([, prohibited]) => prohibited)
421
+ .map(([key]) => key),
422
+ allowedPaths: [...ALWAYS_ALLOWED_PATHS],
423
+ forbiddenPaths: [...ALWAYS_FORBIDDEN_PATHS],
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Generate the system prompt fragment that enforces guard rules.
429
+ * Injected into the main agent's system prompt.
430
+ */
431
+ generateSystemPromptFragment(): string {
432
+ if (!this.embargoActive) {
433
+ return `
434
+ ## Main Agent Guard: INACTIVE
435
+
436
+ The guard is currently disabled. You may operate normally,
437
+ but prefer delegation to subagents for substantive work where possible.
438
+ `
439
+ }
440
+
441
+ const prohibited = Object.entries(MAIN_AGENT_PROHIBITED)
442
+ .filter(([, v]) => v)
443
+ .map(([k]) => `- ❌ ${k}`)
444
+ .join('\n')
445
+
446
+ const allowed = [...ALWAYS_ALLOWED_PATHS].map((p) => `- ${p}`).join('\n')
447
+
448
+ const pendingCount = this.pendingSubagents.size
449
+ const tokenPct = Math.round(this.tokenBudgetUtilization() * 100)
450
+
451
+ return `
452
+ ## CRITICAL: Main Agent Guard — EMBARGO ACTIVE
453
+
454
+ You are a **thin dispatcher**, NOT an executor. Your only job:
455
+ 1. Classify the user's request
456
+ 2. Dispatch subagents to do the actual work
457
+ 3. Relay subagent results back to the user
458
+
459
+ ### Prohibited (NEVER do these):
460
+ ${prohibited}
461
+
462
+ ### Allowed to read ONLY:
463
+ ${allowed}
464
+
465
+ ### Status:
466
+ - Token budget: ${tokenPct}% used (${this.tokensUsed}/${this.config.maxMainAgentTokens})
467
+ - Pending subagents: ${pendingCount}
468
+ - Violations recorded: ${this.violations.length}
469
+
470
+ ### Routing rules:
471
+ - Trivial typo/whitespace → handle directly (1-2 char fix)
472
+ - Small scoped change → dispatch quick-task agent
473
+ - New feature / complex change → dispatch to-plan → then execute-plan
474
+ - Bug/debugging → dispatch systematic-debugging agent
475
+ - Code review → dispatch review-diff agent
476
+ - Research/exploration → dispatch explorer agent
477
+ - TDD work → dispatch tdd agent
478
+ - Everything else → dispatch general subagent
479
+
480
+ ### NEVER execute any task yourself. ALWAYS use the Agent tool.
481
+ `
482
+ }
483
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Router module index
3
+ *
4
+ * The router ensures ALL user requests go through subagents.
5
+ * Main Agent is a "thin dispatcher" — it NEVER executes tasks itself.
6
+ */
7
+
8
+ export type {
9
+ IntentClassification,
10
+ IntentType,
11
+ RouterConfig,
12
+ RoutingDecision,
13
+ SubagentContract,
14
+ PermissionSet,
15
+ RouterInput,
16
+ RouterOutput,
17
+ MAIN_AGENT_PROHIBITED,
18
+ ROUTE_KEYWORDS,
19
+ } from './types.js'
20
+
21
+ export { createRouter } from './router.js'
22
+ export type { Router } from './router.js'
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Router - wires up classification and guard into a single dispatch pipeline.
3
+ *
4
+ * The createRouter factory produces an object that:
5
+ * 1. Classifies user requests
6
+ * 2. Validates against main-agent guard rules
7
+ * 3. Produces a routing decision (what subagent to dispatch)
8
+ *
9
+ * Usage:
10
+ * ```typescript
11
+ * import { createRouter } from './router/router.js'
12
+ * import type { RouterConfig } from './router/types.js'
13
+ *
14
+ * const config: RouterConfig = {
15
+ * defaultModel: 'haiku',
16
+ * planningThreshold: 0.6,
17
+ * maxMainAgentTokens: 10000,
18
+ * autoRoute: true,
19
+ * }
20
+ *
21
+ * const router = createRouter(config)
22
+ * const output = router.route({
23
+ * request: "Fix the typo in src/utils.ts",
24
+ * planFileExists: false,
25
+ * isExecutingPlan: false,
26
+ * hasUncommittedChanges: true,
27
+ * mentionedFiles: ['src/utils.ts'],
28
+ * })
29
+ * // output.classification.type === 'quick-task'
30
+ * ```
31
+ */
32
+
33
+ import type { RouterConfig, RouterInput, RouterOutput } from './types.js'
34
+ import { RequestClassifier } from './classifier.js'
35
+ import type { ClassificationContext } from './classifier.js'
36
+ import { MainAgentGuard } from './guard.js'
37
+
38
+ /**
39
+ * High-level router interface.
40
+ */
41
+ export interface Router {
42
+ /** Classify a request and produce a routing decision */
43
+ route: (input: RouterInput) => RouterOutput
44
+
45
+ /** Get the underlying classifier */
46
+ classifier: RequestClassifier
47
+
48
+ /** Get the underlying guard */
49
+ guard: MainAgentGuard
50
+
51
+ /** Generate the system prompt fragment for the main agent */
52
+ generateSystemPrompt: () => string
53
+
54
+ /** Check if an operation is allowed under current guard state */
55
+ isOperationAllowed: (operation: string) => boolean
56
+ }
57
+
58
+ /**
59
+ * Create a router with the given configuration.
60
+ *
61
+ * Wires together:
62
+ * - RequestClassifier: analyzes user input, determines intent
63
+ * - MainAgentGuard: enforces thin-dispatcher constraints
64
+ */
65
+ export function createRouter(config: RouterConfig): Router {
66
+ const classifier = new RequestClassifier(config)
67
+ const guard = new MainAgentGuard(config)
68
+
69
+ function route(input: RouterInput): RouterOutput {
70
+ const context: ClassificationContext = {
71
+ planFileExists: input.planFileExists,
72
+ isExecutingPlan: input.isExecutingPlan,
73
+ hasUncommittedChanges: input.hasUncommittedChanges,
74
+ currentBranch: input.currentBranch,
75
+ mentionedFiles: input.mentionedFiles,
76
+ }
77
+
78
+ const result = classifier.classify(input.request, context)
79
+
80
+ // Activate embargo after routing (unless trivial edit)
81
+ if (result.classification.type !== 'quick-task') {
82
+ guard.activateEmbargo()
83
+ }
84
+
85
+ return {
86
+ classification: result.classification,
87
+ routing: result.routing,
88
+ confidence: result.confidence,
89
+ matchedRule: result.matchedRule,
90
+ }
91
+ }
92
+
93
+ function generateSystemPrompt(): string {
94
+ return guard.generateSystemPromptFragment()
95
+ }
96
+
97
+ function isOperationAllowed(operation: string): boolean {
98
+ return guard.checkOperation(operation as Parameters<typeof guard.checkOperation>[0]).allowed
99
+ }
100
+
101
+ return {
102
+ route,
103
+ classifier,
104
+ guard,
105
+ generateSystemPrompt,
106
+ isOperationAllowed,
107
+ }
108
+ }