@kodrunhq/opencode-autopilot 1.18.0 → 1.19.0

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 (110) hide show
  1. package/README.md +95 -13
  2. package/assets/commands/oc-update-docs.md +1 -1
  3. package/package.json +1 -1
  4. package/src/agents/index.ts +0 -12
  5. package/src/agents/pipeline/index.ts +0 -4
  6. package/src/autonomy/completion.ts +52 -0
  7. package/src/autonomy/controller.ts +144 -0
  8. package/src/autonomy/index.ts +25 -0
  9. package/src/autonomy/injector.ts +49 -0
  10. package/src/autonomy/state.ts +91 -0
  11. package/src/autonomy/types.ts +30 -0
  12. package/src/autonomy/verification.ts +86 -0
  13. package/src/background/database.ts +170 -0
  14. package/src/background/executor.ts +174 -0
  15. package/src/background/index.ts +8 -0
  16. package/src/background/manager.ts +232 -0
  17. package/src/background/repository.ts +174 -0
  18. package/src/background/schema.ts +24 -0
  19. package/src/background/sdk-runner.ts +40 -0
  20. package/src/background/slot-manager.ts +41 -0
  21. package/src/background/state-machine.ts +19 -0
  22. package/src/context/budget.ts +45 -0
  23. package/src/context/compaction-handler.ts +58 -0
  24. package/src/context/discovery.ts +94 -0
  25. package/src/context/index.ts +14 -0
  26. package/src/context/injector.ts +119 -0
  27. package/src/context/types.ts +24 -0
  28. package/src/health/checks.ts +145 -2
  29. package/src/health/index.ts +7 -1
  30. package/src/health/runner.ts +6 -0
  31. package/src/index.ts +113 -6
  32. package/src/installer.ts +13 -0
  33. package/src/kernel/index.ts +6 -0
  34. package/src/kernel/migrations.ts +50 -0
  35. package/src/kernel/retry.ts +49 -0
  36. package/src/kernel/schema.ts +9 -1
  37. package/src/kernel/transaction.ts +40 -12
  38. package/src/logging/forensic-writer.ts +6 -2
  39. package/src/logging/index.ts +2 -0
  40. package/src/mcp/index.ts +34 -0
  41. package/src/mcp/manager.ts +206 -0
  42. package/src/mcp/scope-filter.ts +44 -0
  43. package/src/mcp/types.ts +38 -0
  44. package/src/orchestrator/arena.ts +7 -1
  45. package/src/orchestrator/fallback/event-handler.ts +12 -1
  46. package/src/orchestrator/handlers/challenge.ts +8 -1
  47. package/src/orchestrator/handlers/plan.ts +8 -1
  48. package/src/orchestrator/handlers/recon.ts +8 -1
  49. package/src/orchestrator/handlers/types.ts +2 -2
  50. package/src/orchestrator/lesson-memory.ts +6 -1
  51. package/src/orchestrator/orchestration-logger.ts +15 -3
  52. package/src/orchestrator/skill-injection.ts +7 -1
  53. package/src/orchestrator/state.ts +6 -1
  54. package/src/recovery/classifier.ts +127 -0
  55. package/src/recovery/event-handler.ts +263 -0
  56. package/src/recovery/index.ts +20 -0
  57. package/src/recovery/orchestrator.ts +180 -0
  58. package/src/recovery/persistence.ts +87 -0
  59. package/src/recovery/strategies.ts +107 -0
  60. package/src/recovery/types.ts +31 -0
  61. package/src/registry/model-groups.ts +2 -19
  62. package/src/registry/resolver.ts +38 -9
  63. package/src/review/agent-catalog.ts +83 -251
  64. package/src/review/agents/architecture-verifier.ts +41 -0
  65. package/src/review/agents/code-hygiene-auditor.ts +40 -0
  66. package/src/review/agents/correctness-auditor.ts +41 -0
  67. package/src/review/agents/frontend-auditor.ts +39 -0
  68. package/src/review/agents/index.ts +15 -42
  69. package/src/review/agents/language-idioms-auditor.ts +39 -0
  70. package/src/review/agents/security-auditor.ts +12 -8
  71. package/src/review/stack-gate.ts +2 -6
  72. package/src/routing/categories.ts +111 -0
  73. package/src/routing/classifier.ts +152 -0
  74. package/src/routing/engine.ts +89 -0
  75. package/src/routing/index.ts +4 -0
  76. package/src/routing/types.ts +14 -0
  77. package/src/skills/adaptive-injector.ts +34 -3
  78. package/src/skills/loader.ts +4 -0
  79. package/src/tools/background.ts +196 -0
  80. package/src/tools/delegate.ts +205 -0
  81. package/src/tools/loop.ts +94 -0
  82. package/src/tools/recover.ts +172 -0
  83. package/src/types/recovery.ts +10 -0
  84. package/src/ux/context-warnings.ts +81 -0
  85. package/src/ux/error-hints.ts +38 -0
  86. package/src/ux/index.ts +7 -0
  87. package/src/ux/notifications.ts +67 -0
  88. package/src/ux/progress.ts +77 -0
  89. package/src/ux/session-summary.ts +67 -0
  90. package/src/ux/task-status.ts +109 -0
  91. package/src/ux/types.ts +24 -0
  92. package/src/agents/db-specialist.ts +0 -295
  93. package/src/agents/devops.ts +0 -352
  94. package/src/agents/documenter.ts +0 -44
  95. package/src/agents/frontend-engineer.ts +0 -541
  96. package/src/agents/pipeline/oc-explorer.ts +0 -46
  97. package/src/agents/pipeline/oc-retrospector.ts +0 -42
  98. package/src/review/agents/auth-flow-verifier.ts +0 -47
  99. package/src/review/agents/concurrency-checker.ts +0 -47
  100. package/src/review/agents/dead-code-scanner.ts +0 -47
  101. package/src/review/agents/go-idioms-auditor.ts +0 -46
  102. package/src/review/agents/python-django-auditor.ts +0 -46
  103. package/src/review/agents/react-patterns-auditor.ts +0 -46
  104. package/src/review/agents/rust-safety-auditor.ts +0 -46
  105. package/src/review/agents/scope-intent-verifier.ts +0 -45
  106. package/src/review/agents/silent-failure-hunter.ts +0 -45
  107. package/src/review/agents/spec-checker.ts +0 -45
  108. package/src/review/agents/state-mgmt-auditor.ts +0 -46
  109. package/src/review/agents/type-soundness.ts +0 -46
  110. package/src/review/agents/wiring-inspector.ts +0 -46
@@ -1,80 +1,53 @@
1
- import { authFlowVerifier } from "./auth-flow-verifier";
1
+ import { architectureVerifier } from "./architecture-verifier";
2
+ import { codeHygieneAuditor } from "./code-hygiene-auditor";
2
3
  import { codeQualityAuditor } from "./code-quality-auditor";
3
- import { concurrencyChecker } from "./concurrency-checker";
4
4
  import { contractVerifier } from "./contract-verifier";
5
+ import { correctnessAuditor } from "./correctness-auditor";
5
6
  import { databaseAuditor } from "./database-auditor";
6
- import { deadCodeScanner } from "./dead-code-scanner";
7
- import { goIdiomsAuditor } from "./go-idioms-auditor";
7
+ import { frontendAuditor } from "./frontend-auditor";
8
+ import { languageIdiomsAuditor } from "./language-idioms-auditor";
8
9
  import { logicAuditor } from "./logic-auditor";
9
10
  import { productThinker } from "./product-thinker";
10
- import { pythonDjangoAuditor } from "./python-django-auditor";
11
- import { reactPatternsAuditor } from "./react-patterns-auditor";
12
11
  import { redTeam } from "./red-team";
13
- import { rustSafetyAuditor } from "./rust-safety-auditor";
14
- import { scopeIntentVerifier } from "./scope-intent-verifier";
15
12
  import { securityAuditor } from "./security-auditor";
16
- import { silentFailureHunter } from "./silent-failure-hunter";
17
- import { specChecker } from "./spec-checker";
18
- import { stateMgmtAuditor } from "./state-mgmt-auditor";
19
13
  import { testInterrogator } from "./test-interrogator";
20
- import { typeSoundness } from "./type-soundness";
21
- import { wiringInspector } from "./wiring-inspector";
22
14
 
23
15
  export {
24
- authFlowVerifier,
16
+ architectureVerifier,
17
+ codeHygieneAuditor,
25
18
  codeQualityAuditor,
26
- concurrencyChecker,
27
19
  contractVerifier,
20
+ correctnessAuditor,
28
21
  databaseAuditor,
29
- deadCodeScanner,
30
- goIdiomsAuditor,
22
+ frontendAuditor,
23
+ languageIdiomsAuditor,
31
24
  logicAuditor,
32
25
  productThinker,
33
- pythonDjangoAuditor,
34
- reactPatternsAuditor,
35
26
  redTeam,
36
- rustSafetyAuditor,
37
- scopeIntentVerifier,
38
27
  securityAuditor,
39
- silentFailureHunter,
40
- specChecker,
41
- stateMgmtAuditor,
42
28
  testInterrogator,
43
- typeSoundness,
44
- wiringInspector,
45
29
  };
46
30
 
47
- /** The 6 universal specialist agents (Stage 1 & 2 reviews). */
48
31
  export const REVIEW_AGENTS = Object.freeze([
49
32
  logicAuditor,
50
33
  securityAuditor,
51
34
  codeQualityAuditor,
52
35
  testInterrogator,
53
- silentFailureHunter,
36
+ codeHygieneAuditor,
54
37
  contractVerifier,
55
38
  ] as const);
56
39
 
57
40
  /** Stage 3 agents: adversarial red team + product completeness. */
58
41
  export const STAGE3_AGENTS = Object.freeze([redTeam, productThinker] as const);
59
42
 
60
- /** The 13 specialized agents added for stack-aware review. */
61
43
  export const SPECIALIZED_AGENTS = Object.freeze([
62
- wiringInspector,
63
- deadCodeScanner,
64
- specChecker,
44
+ architectureVerifier,
65
45
  databaseAuditor,
66
- authFlowVerifier,
67
- typeSoundness,
68
- stateMgmtAuditor,
69
- concurrencyChecker,
70
- scopeIntentVerifier,
71
- reactPatternsAuditor,
72
- goIdiomsAuditor,
73
- pythonDjangoAuditor,
74
- rustSafetyAuditor,
46
+ correctnessAuditor,
47
+ frontendAuditor,
48
+ languageIdiomsAuditor,
75
49
  ] as const);
76
50
 
77
- /** All 21 review agents combined (6 universal + 13 specialized + 2 sequenced). */
78
51
  export const ALL_REVIEW_AGENTS = Object.freeze([
79
52
  ...REVIEW_AGENTS,
80
53
  ...SPECIALIZED_AGENTS,
@@ -0,0 +1,39 @@
1
+ import type { ReviewAgent } from "../types";
2
+
3
+ export const languageIdiomsAuditor: Readonly<ReviewAgent> = Object.freeze({
4
+ name: "language-idioms-auditor",
5
+ description:
6
+ "Audits Go idioms, Python and Django or FastAPI patterns, and Rust safety conventions for language-specific bug classes.",
7
+ relevantStacks: ["go", "django", "fastapi", "rust"] as readonly string[],
8
+ severityFocus: ["CRITICAL", "HIGH"] as const,
9
+ prompt: `You are the Language Idioms Auditor. You verify that Go, Python web frameworks, and Rust code respect the language-specific safety and correctness rules that general reviewers often miss.
10
+
11
+ ## Instructions
12
+
13
+ Check each category systematically for the stacks present in the diff:
14
+
15
+ 1. **Go Idioms** -- Flag defer-in-loop, goroutine leaks, nil-interface traps, error shadowing with :=, and context misuse or ignored cancellation.
16
+ 2. **Python/Django/FastAPI Patterns** -- Flag N+1 ORM access in templates or handlers, unsafe ModelForm field exposure, missing CSRF protection for cookie-based auth, mutable default arguments, and lazy-evaluation traps.
17
+ 3. **Rust Safety** -- Flag unsafe blocks without real safety justification, unwrap/expect in non-test code, questionable lifetime assumptions, Send/Sync misuse, and mem::forget or manual resource leaks.
18
+ 4. **Language-Specific Resource Lifecycles** -- Verify cleanup and ownership rules match the idioms of the language instead of relying on accidental runtime behavior.
19
+
20
+ Explain the mechanism behind each issue: why this is a Go, Python-web, or Rust trap rather than a generic style preference.
21
+
22
+ Do not comment on general code style -- only language- and framework-specific correctness and safety issues.
23
+
24
+ ## Diff
25
+ {{DIFF}}
26
+
27
+ ## Prior Findings (for cross-verification)
28
+ {{PRIOR_FINDINGS}}
29
+
30
+ ## Project Memory (false positive suppression)
31
+ {{MEMORY}}
32
+
33
+ ## Output
34
+ For each finding, output a JSON object:
35
+ {"severity": "CRITICAL|HIGH|MEDIUM|LOW", "domain": "language-idioms", "title": "short title", "file": "path/to/file.ts", "line": 42, "agent": "language-idioms-auditor", "source": "phase1", "evidence": "what was found", "problem": "why it is an issue", "fix": "how to fix it"}
36
+
37
+ If no findings: {"findings": []}
38
+ Wrap all findings in: {"findings": [...]}`,
39
+ });
@@ -3,10 +3,10 @@ import type { ReviewAgent } from "../types";
3
3
  export const securityAuditor: Readonly<ReviewAgent> = Object.freeze({
4
4
  name: "security-auditor",
5
5
  description:
6
- "Audits OWASP vulnerabilities, hardcoded secrets, injection vectors, and cryptographic correctness.",
6
+ "Audits OWASP vulnerabilities, authentication and authorization flows, hardcoded secrets, injection vectors, and cryptographic correctness.",
7
7
  relevantStacks: [] as readonly string[],
8
8
  severityFocus: ["CRITICAL", "HIGH"] as const,
9
- prompt: `You are the Security Auditor. You scan for security vulnerabilities and secure coding violations. Every finding must include a concrete exploit scenario.
9
+ prompt: `You are the Security Auditor. You scan for security vulnerabilities, broken auth flows, and secure coding violations. Every finding must include a concrete exploit scenario.
10
10
 
11
11
  ## Instructions
12
12
 
@@ -14,12 +14,16 @@ Check each category systematically against the changed code:
14
14
 
15
15
  1. **Hardcoded Secrets** -- Scan for API keys, passwords, tokens, connection strings, or private keys in source code. Check .env files committed to version control. Flag any string that looks like a credential.
16
16
  2. **Injection Vulnerabilities** -- Trace every user input from entry point to use. Check for SQL injection (string concatenation in queries), command injection (unsanitized shell input), XSS (unescaped HTML output), and template injection.
17
- 3. **Authentication & Authorization** -- Verify auth middleware/guards on every protected endpoint. Check that authorization is enforced server-side, not just in UI routing. Flag endpoints missing auth checks.
18
- 4. **CSRF Protection** -- Verify anti-CSRF tokens on state-changing endpoints. Check SameSite cookie attributes. Flag forms that POST without CSRF protection.
19
- 5. **Sensitive Data Exposure** -- Check that passwords, tokens, PII, and credentials are never logged, included in error messages, or returned in API responses.
20
- 6. **Cryptographic Correctness** -- Flag MD5/SHA1 for password hashing, weak random number generation (Math.random for security), missing TLS configuration.
21
- 7. **SSRF** -- Verify that user-supplied URLs are validated against an allowlist before server-side fetching.
22
- 8. **Rate Limiting** -- Check that public and auth endpoints have rate limiting to prevent brute force and abuse.
17
+ 3. **Route Protection** -- For every route or endpoint that accesses user data, modifies state, or returns sensitive information, verify an auth guard (middleware, decorator, or check) is present. Flag any protected resource accessible without authentication.
18
+ 4. **Token Validation** -- For every token check (JWT verification, session lookup, API key validation), verify the validation is complete: signature check, expiry check, issuer check, and audience check where applicable. Flag partial validation.
19
+ 5. **Privilege Escalation** -- Trace every operation that uses a user ID or role. Verify the ID comes from the authenticated session, not from request parameters. Flag any path where a user could access or modify another user's data by changing an ID in the request.
20
+ 6. **Session Fixation & Token Expiry** -- Verify session IDs are regenerated after login and that access tokens have finite TTLs that are enforced. Flag reused sessions, missing expiry checks, or tokens with no expiration.
21
+ 7. **Password Storage** -- Verify passwords are hashed with bcrypt, scrypt, or argon2 before storage. Flag plaintext password storage, MD5/SHA1 hashing, or missing salt.
22
+ 8. **CSRF Protection** -- Verify anti-CSRF tokens on state-changing endpoints. Check SameSite cookie attributes. Flag forms that POST without CSRF protection.
23
+ 9. **Sensitive Data Exposure** -- Check that passwords, tokens, PII, and credentials are never logged, included in error messages, or returned in API responses.
24
+ 10. **Cryptographic Correctness** -- Flag MD5/SHA1 for password hashing, weak random number generation (Math.random for security), and missing TLS configuration.
25
+ 11. **SSRF** -- Verify that user-supplied URLs are validated against an allowlist before server-side fetching.
26
+ 12. **Rate Limiting** -- Check that public and auth endpoints have rate limiting to prevent brute force and abuse.
23
27
 
24
28
  For each finding, describe the exploit: "An attacker could [action] because [vulnerability], resulting in [impact]."
25
29
 
@@ -7,12 +7,8 @@ import type { AgentDefinition } from "./types";
7
7
  * If listed, at least ONE of the required tags must be present in the project.
8
8
  */
9
9
  export const STACK_GATE_RULES: Readonly<Record<string, readonly string[]>> = Object.freeze({
10
- "react-patterns-auditor": Object.freeze(["react", "nextjs"]),
11
- "go-idioms-auditor": Object.freeze(["go"]),
12
- "python-django-auditor": Object.freeze(["django", "fastapi"]),
13
- "rust-safety-auditor": Object.freeze(["rust"]),
14
- "state-mgmt-auditor": Object.freeze(["react", "vue", "svelte", "angular"]),
15
- "type-soundness": Object.freeze(["typescript", "kotlin", "rust", "go"]),
10
+ "frontend-auditor": Object.freeze(["react", "nextjs", "vue", "svelte", "angular"]),
11
+ "language-idioms-auditor": Object.freeze(["go", "django", "fastapi", "rust"]),
16
12
  });
17
13
 
18
14
  /**
@@ -0,0 +1,111 @@
1
+ import type { Category } from "../types/routing";
2
+ import type { CategoryDefinition } from "./types";
3
+
4
+ function freezeCategoryDefinition(definition: CategoryDefinition): CategoryDefinition {
5
+ return Object.freeze({
6
+ ...definition,
7
+ skills: Object.freeze([...definition.skills]),
8
+ keywords: Object.freeze([...definition.keywords]),
9
+ filePatterns: Object.freeze([...definition.filePatterns]),
10
+ });
11
+ }
12
+
13
+ const ALL_CATEGORIES: readonly CategoryDefinition[] = Object.freeze([
14
+ freezeCategoryDefinition({
15
+ category: "quick",
16
+ description: "Small, low-risk tasks with minimal complexity.",
17
+ modelGroup: "utilities",
18
+ skills: [],
19
+ maxIterations: 1,
20
+ timeoutSeconds: 60,
21
+ keywords: ["fix typo", "rename", "simple", "trivial", "single file"],
22
+ filePatterns: [],
23
+ }),
24
+ freezeCategoryDefinition({
25
+ category: "visual-engineering",
26
+ description: "UI, UX, styling, and frontend presentation work.",
27
+ modelGroup: "builders",
28
+ skills: ["frontend-design", "frontend-ui-ux"],
29
+ maxIterations: 5,
30
+ timeoutSeconds: 300,
31
+ keywords: [
32
+ "ui",
33
+ "ux",
34
+ "css",
35
+ "styling",
36
+ "animation",
37
+ "layout",
38
+ "design",
39
+ "dark mode",
40
+ "responsive",
41
+ ],
42
+ filePatterns: [".css", ".scss", ".tsx", ".jsx", ".vue", ".svelte"],
43
+ }),
44
+ freezeCategoryDefinition({
45
+ category: "ultrabrain",
46
+ description: "Logic-heavy, performance-sensitive, or algorithmic work.",
47
+ modelGroup: "architects",
48
+ skills: [],
49
+ maxIterations: 10,
50
+ timeoutSeconds: 600,
51
+ keywords: ["algorithm", "architecture", "complex", "optimize", "performance", "logic-heavy"],
52
+ filePatterns: [],
53
+ }),
54
+ freezeCategoryDefinition({
55
+ category: "artistry",
56
+ description: "Creative, novel, or unconventional solutioning.",
57
+ modelGroup: "architects",
58
+ skills: [],
59
+ maxIterations: 8,
60
+ timeoutSeconds: 600,
61
+ keywords: ["creative", "unconventional", "novel", "innovative"],
62
+ filePatterns: [],
63
+ }),
64
+ freezeCategoryDefinition({
65
+ category: "writing",
66
+ description: "Documentation, changelogs, and technical writing tasks.",
67
+ modelGroup: "communicators",
68
+ skills: ["coding-standards"],
69
+ maxIterations: 3,
70
+ timeoutSeconds: 180,
71
+ keywords: ["documentation", "readme", "changelog", "write docs", "technical writing"],
72
+ filePatterns: [],
73
+ }),
74
+ freezeCategoryDefinition({
75
+ category: "unspecified-low",
76
+ description: "General work with unclear category and moderate complexity.",
77
+ modelGroup: "utilities",
78
+ skills: [],
79
+ maxIterations: 3,
80
+ timeoutSeconds: 120,
81
+ keywords: [],
82
+ filePatterns: [],
83
+ }),
84
+ freezeCategoryDefinition({
85
+ category: "unspecified-high",
86
+ description: "General work with unclear category but high likely complexity.",
87
+ modelGroup: "builders",
88
+ skills: [],
89
+ maxIterations: 8,
90
+ timeoutSeconds: 300,
91
+ keywords: [],
92
+ filePatterns: [],
93
+ }),
94
+ ] satisfies readonly CategoryDefinition[]);
95
+
96
+ export const CATEGORY_DEFINITIONS: ReadonlyMap<Category, CategoryDefinition> = Object.freeze(
97
+ new Map(ALL_CATEGORIES.map((definition) => [definition.category, definition] as const)),
98
+ );
99
+
100
+ export function getCategoryDefinition(category: Category): CategoryDefinition {
101
+ const definition = CATEGORY_DEFINITIONS.get(category);
102
+ if (!definition) {
103
+ throw new Error(`Unknown routing category: ${category}`);
104
+ }
105
+
106
+ return definition;
107
+ }
108
+
109
+ export function getAllCategories(): readonly CategoryDefinition[] {
110
+ return ALL_CATEGORIES;
111
+ }
@@ -0,0 +1,152 @@
1
+ import type { Category } from "../types/routing";
2
+ import { getAllCategories } from "./categories";
3
+
4
+ export interface ClassificationResult {
5
+ readonly category: Category;
6
+ readonly confidence: number;
7
+ readonly reasoning: string;
8
+ }
9
+
10
+ interface ScoredCategory {
11
+ readonly category: Category;
12
+ readonly score: number;
13
+ readonly matches: readonly string[];
14
+ }
15
+
16
+ const COMPLEXITY_SIGNALS: readonly string[] = Object.freeze([
17
+ "implement",
18
+ "authentication",
19
+ "jwt",
20
+ "refresh token",
21
+ "oauth",
22
+ "integration",
23
+ "workflow",
24
+ "subsystem",
25
+ "pipeline",
26
+ "multi-step",
27
+ "end-to-end",
28
+ ]);
29
+
30
+ function clampConfidence(value: number): number {
31
+ return Math.max(0, Math.min(1, value));
32
+ }
33
+
34
+ function scoreFilePatternMatches(changedFiles: readonly string[]): ScoredCategory | null {
35
+ const lowerFiles = changedFiles.map((file) => file.toLowerCase());
36
+ let bestMatch: ScoredCategory | null = null;
37
+
38
+ for (const definition of getAllCategories()) {
39
+ if (definition.filePatterns.length === 0) {
40
+ continue;
41
+ }
42
+
43
+ const matches = definition.filePatterns.filter((pattern) =>
44
+ lowerFiles.some((file) => file.endsWith(pattern.toLowerCase())),
45
+ );
46
+
47
+ if (matches.length === 0) {
48
+ continue;
49
+ }
50
+
51
+ if (bestMatch === null || matches.length > bestMatch.score) {
52
+ bestMatch = {
53
+ category: definition.category,
54
+ score: matches.length,
55
+ matches: Object.freeze([...matches]),
56
+ };
57
+ }
58
+ }
59
+
60
+ return bestMatch;
61
+ }
62
+
63
+ function scoreKeywordMatches(description: string): ScoredCategory | null {
64
+ let bestMatch: ScoredCategory | null = null;
65
+
66
+ for (const definition of getAllCategories()) {
67
+ if (definition.keywords.length === 0) {
68
+ continue;
69
+ }
70
+
71
+ const matches = definition.keywords.filter((keyword) =>
72
+ description.includes(keyword.toLowerCase()),
73
+ );
74
+ if (matches.length === 0) {
75
+ continue;
76
+ }
77
+
78
+ if (bestMatch === null || matches.length > bestMatch.score) {
79
+ bestMatch = {
80
+ category: definition.category,
81
+ score: matches.length,
82
+ matches: Object.freeze([...matches]),
83
+ };
84
+ }
85
+ }
86
+
87
+ return bestMatch;
88
+ }
89
+
90
+ function classifyByHeuristic(description: string): ClassificationResult | null {
91
+ if (description.length < 20) {
92
+ return Object.freeze({
93
+ category: "quick",
94
+ confidence: 0.45,
95
+ reasoning: "Short task description suggests a quick utility task.",
96
+ });
97
+ }
98
+
99
+ const matchedSignals = COMPLEXITY_SIGNALS.filter((signal) => description.includes(signal));
100
+ if (description.length > 200 || matchedSignals.length > 0) {
101
+ const confidence = clampConfidence(
102
+ 0.58 + matchedSignals.length * 0.07 + (description.length > 200 ? 0.12 : 0),
103
+ );
104
+ const details =
105
+ matchedSignals.length > 0
106
+ ? `complexity signals: ${matchedSignals.join(", ")}`
107
+ : "long task description suggests high complexity";
108
+ return Object.freeze({
109
+ category: "unspecified-high",
110
+ confidence,
111
+ reasoning: `Heuristic classified task as unspecified-high based on ${details}.`,
112
+ });
113
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ export function classifyTask(
119
+ description: string,
120
+ changedFiles: readonly string[] = [],
121
+ ): ClassificationResult {
122
+ const normalizedDescription = description.trim().toLowerCase();
123
+
124
+ const filePatternMatch = scoreFilePatternMatches(changedFiles);
125
+ if (filePatternMatch !== null) {
126
+ return Object.freeze({
127
+ category: filePatternMatch.category,
128
+ confidence: clampConfidence(0.72 + filePatternMatch.score * 0.1),
129
+ reasoning: `Matched file patterns for ${filePatternMatch.category}: ${filePatternMatch.matches.join(", ")}.`,
130
+ });
131
+ }
132
+
133
+ const keywordMatch = scoreKeywordMatches(normalizedDescription);
134
+ if (keywordMatch !== null) {
135
+ return Object.freeze({
136
+ category: keywordMatch.category,
137
+ confidence: clampConfidence(0.55 + keywordMatch.score * 0.15),
138
+ reasoning: `Matched keywords for ${keywordMatch.category}: ${keywordMatch.matches.join(", ")}.`,
139
+ });
140
+ }
141
+
142
+ const heuristicMatch = classifyByHeuristic(normalizedDescription);
143
+ if (heuristicMatch !== null) {
144
+ return heuristicMatch;
145
+ }
146
+
147
+ return Object.freeze({
148
+ category: "unspecified-low",
149
+ confidence: 0.3,
150
+ reasoning: "No strong routing signals found; defaulted to unspecified-low.",
151
+ });
152
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ type Category,
3
+ CategoryConfigSchema,
4
+ type RoutingConfig,
5
+ type RoutingDecision,
6
+ } from "../types/routing";
7
+ import { getCategoryDefinition } from "./categories";
8
+ import { classifyTask } from "./classifier";
9
+
10
+ function buildDefaultCategoryConfig(category: Category) {
11
+ const definition = getCategoryDefinition(category);
12
+ return CategoryConfigSchema.parse({
13
+ enabled: true,
14
+ modelGroup: definition.modelGroup,
15
+ timeoutSeconds: definition.timeoutSeconds,
16
+ skills: [...definition.skills],
17
+ metadata: {
18
+ maxIterations: definition.maxIterations,
19
+ },
20
+ });
21
+ }
22
+
23
+ function buildAppliedConfig(category: Category, override?: RoutingConfig["categories"][string]) {
24
+ const baseConfig = buildDefaultCategoryConfig(category);
25
+ return CategoryConfigSchema.parse({
26
+ ...baseConfig,
27
+ ...override,
28
+ skills: override?.skills ?? baseConfig.skills,
29
+ metadata: {
30
+ ...baseConfig.metadata,
31
+ ...(override?.metadata ?? {}),
32
+ },
33
+ });
34
+ }
35
+
36
+ function resolveCategory(
37
+ classifiedCategory: Category,
38
+ config?: RoutingConfig,
39
+ ): { readonly category: Category; readonly fallbackReason: string | null } {
40
+ const override = config?.categories[classifiedCategory];
41
+ if (override?.enabled === false && classifiedCategory !== "unspecified-low") {
42
+ return Object.freeze({
43
+ category: "unspecified-low",
44
+ fallbackReason: `Category '${classifiedCategory}' is disabled by config; fell back to unspecified-low.`,
45
+ });
46
+ }
47
+
48
+ return Object.freeze({
49
+ category: classifiedCategory,
50
+ fallbackReason: null,
51
+ });
52
+ }
53
+
54
+ export function makeRoutingDecision(
55
+ description: string,
56
+ config?: RoutingConfig,
57
+ changedFiles: readonly string[] = [],
58
+ ): RoutingDecision {
59
+ if (config?.enabled === false) {
60
+ const fallbackCategory: Category = "unspecified-low";
61
+ const appliedConfig = buildAppliedConfig(
62
+ fallbackCategory,
63
+ config?.categories[fallbackCategory],
64
+ );
65
+ return Object.freeze({
66
+ category: fallbackCategory,
67
+ confidence: 0,
68
+ agentId: appliedConfig.agentId,
69
+ reasoning: "Routing is disabled globally.",
70
+ appliedConfig,
71
+ });
72
+ }
73
+
74
+ const classification = classifyTask(description, changedFiles);
75
+ const resolved = resolveCategory(classification.category, config);
76
+ const appliedConfig = buildAppliedConfig(
77
+ resolved.category,
78
+ config?.categories[resolved.category],
79
+ );
80
+ const reasoning = [classification.reasoning, resolved.fallbackReason].filter(Boolean).join(" ");
81
+
82
+ return Object.freeze({
83
+ category: resolved.category,
84
+ confidence: classification.confidence,
85
+ agentId: appliedConfig.agentId,
86
+ reasoning,
87
+ appliedConfig,
88
+ });
89
+ }
@@ -0,0 +1,4 @@
1
+ export { CATEGORY_DEFINITIONS, getAllCategories, getCategoryDefinition } from "./categories";
2
+ export { classifyTask } from "./classifier";
3
+ export { makeRoutingDecision } from "./engine";
4
+ export type { CategoryDefinition } from "./types";
@@ -0,0 +1,14 @@
1
+ import type { Category, CategoryConfig, RoutingDecision } from "../types/routing";
2
+
3
+ export interface CategoryDefinition {
4
+ readonly category: Category;
5
+ readonly description: string;
6
+ readonly modelGroup: string;
7
+ readonly skills: readonly string[];
8
+ readonly maxIterations: number;
9
+ readonly timeoutSeconds: number;
10
+ readonly keywords: readonly string[];
11
+ readonly filePatterns: readonly string[];
12
+ }
13
+
14
+ export type { Category, CategoryConfig, RoutingDecision };
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { access, readdir } from "node:fs/promises";
11
11
  import { join } from "node:path";
12
+ import { getLogger } from "../logging/domains";
13
+ import { getGlobalMcpManager } from "../mcp";
12
14
  import { sanitizeTemplateContent } from "../review/sanitize";
13
15
  import { isEnoentError } from "../utils/fs-helpers";
14
16
  import { resolveDependencyOrder } from "./dependency-resolver";
@@ -18,6 +20,26 @@ const DEFAULT_TOKEN_BUDGET = 8000;
18
20
  /** Rough estimate: 1 token ~ 4 chars */
19
21
  const CHARS_PER_TOKEN = 4;
20
22
 
23
+ const mcpLogger = getLogger("mcp", "skill-activation");
24
+
25
+ function activateMcpForSkills(skills: ReadonlyMap<string, LoadedSkill>, mcpEnabled: boolean): void {
26
+ if (!mcpEnabled) return;
27
+
28
+ const manager = getGlobalMcpManager();
29
+ if (!manager) return;
30
+
31
+ for (const [name, skill] of skills) {
32
+ if (skill.frontmatter.mcp) {
33
+ manager.startServer(name, skill.frontmatter.mcp).catch((error: unknown) => {
34
+ mcpLogger.warn("Failed to start MCP server for skill", {
35
+ skill: name,
36
+ error: error instanceof Error ? error.message : String(error),
37
+ });
38
+ });
39
+ }
40
+ }
41
+ }
42
+
21
43
  /**
22
44
  * Maps pipeline phases to the skill names relevant for that phase.
23
45
  * Skills not in the list for the current phase are excluded from injection,
@@ -34,6 +56,9 @@ export const PHASE_SKILL_MAP: Readonly<Record<string, readonly string[]>> = Obje
34
56
  EXPLORE: [],
35
57
  });
36
58
 
59
+ const MCP_SUPPORT_NOTE =
60
+ "Embedded MCP support is available for skills that declare an MCP server when plugin config.mcp.enabled is true.";
61
+
37
62
  export type SkillMode = "summary" | "full";
38
63
 
39
64
  /**
@@ -147,7 +172,8 @@ export function buildSkillSummary(skill: LoadedSkill): string {
147
172
  const { name, description } = skill.frontmatter;
148
173
  const safeName = sanitizeTemplateContent(name);
149
174
  const safeDesc = sanitizeTemplateContent((description ?? "").slice(0, 200));
150
- return `[Skill: ${safeName}]\n${safeDesc}`;
175
+ const mcpNote = skill.frontmatter.mcp ? `\n${MCP_SUPPORT_NOTE}` : "";
176
+ return `[Skill: ${safeName}]\n${safeDesc}${mcpNote}`;
151
177
  }
152
178
 
153
179
  /**
@@ -173,9 +199,12 @@ export function buildMultiSkillContext(
173
199
  skills: ReadonlyMap<string, LoadedSkill>,
174
200
  tokenBudget: number = DEFAULT_TOKEN_BUDGET,
175
201
  mode: SkillMode = "summary",
202
+ mcpEnabled = true,
176
203
  ): string {
177
204
  if (skills.size === 0) return "";
178
205
 
206
+ activateMcpForSkills(skills, mcpEnabled);
207
+
179
208
  // Resolve dependency order
180
209
  const depMap = new Map(
181
210
  [...skills.entries()].map(([name, skill]) => [name, { requires: skill.frontmatter.requires }]),
@@ -232,11 +261,13 @@ export function buildAdaptiveSkillContext(
232
261
  readonly phase?: string;
233
262
  readonly budget?: number;
234
263
  readonly mode?: SkillMode;
264
+ readonly mcpEnabled?: boolean;
235
265
  },
236
266
  ): string {
237
267
  const phase = options?.phase;
238
268
  const budget = options?.budget ?? DEFAULT_TOKEN_BUDGET;
239
269
  const mode = options?.mode ?? "summary";
270
+ const mcpEnabled = options?.mcpEnabled ?? true;
240
271
 
241
272
  if (phase !== undefined) {
242
273
  const allowedNames = PHASE_SKILL_MAP[phase] ?? [];
@@ -250,9 +281,9 @@ export function buildAdaptiveSkillContext(
250
281
  }
251
282
  }
252
283
 
253
- return buildMultiSkillContext(filtered, budget, mode);
284
+ return buildMultiSkillContext(filtered, budget, mode, mcpEnabled);
254
285
  }
255
286
 
256
287
  // No phase -- include all provided skills (caller already stack-filtered)
257
- return buildMultiSkillContext(skills, budget, mode);
288
+ return buildMultiSkillContext(skills, budget, mode, mcpEnabled);
258
289
  }