@kodrunhq/opencode-autopilot 1.17.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.
- package/README.md +95 -13
- package/assets/commands/oc-doctor.md +17 -0
- package/assets/commands/oc-update-docs.md +1 -1
- package/bin/configure-tui.ts +1 -1
- package/package.json +1 -1
- package/src/agents/index.ts +0 -12
- package/src/agents/pipeline/index.ts +0 -4
- package/src/autonomy/completion.ts +52 -0
- package/src/autonomy/controller.ts +144 -0
- package/src/autonomy/index.ts +25 -0
- package/src/autonomy/injector.ts +49 -0
- package/src/autonomy/state.ts +91 -0
- package/src/autonomy/types.ts +30 -0
- package/src/autonomy/verification.ts +86 -0
- package/src/background/database.ts +170 -0
- package/src/background/executor.ts +174 -0
- package/src/background/index.ts +8 -0
- package/src/background/manager.ts +232 -0
- package/src/background/repository.ts +174 -0
- package/src/background/schema.ts +24 -0
- package/src/background/sdk-runner.ts +40 -0
- package/src/background/slot-manager.ts +41 -0
- package/src/background/state-machine.ts +19 -0
- package/src/config/v7.ts +3 -3
- package/src/config.ts +105 -21
- package/src/context/budget.ts +45 -0
- package/src/context/compaction-handler.ts +58 -0
- package/src/context/discovery.ts +94 -0
- package/src/context/index.ts +14 -0
- package/src/context/injector.ts +119 -0
- package/src/context/types.ts +24 -0
- package/src/health/checks.ts +214 -3
- package/src/health/index.ts +7 -1
- package/src/health/runner.ts +14 -2
- package/src/index.ts +113 -6
- package/src/installer.ts +13 -0
- package/src/kernel/index.ts +6 -0
- package/src/kernel/migrations.ts +50 -0
- package/src/kernel/retry.ts +49 -0
- package/src/kernel/schema.ts +9 -1
- package/src/kernel/transaction.ts +40 -12
- package/src/logging/forensic-writer.ts +6 -2
- package/src/logging/index.ts +2 -0
- package/src/mcp/index.ts +34 -0
- package/src/mcp/manager.ts +206 -0
- package/src/mcp/scope-filter.ts +44 -0
- package/src/mcp/types.ts +38 -0
- package/src/orchestrator/arena.ts +7 -1
- package/src/orchestrator/fallback/event-handler.ts +12 -1
- package/src/orchestrator/handlers/challenge.ts +8 -1
- package/src/orchestrator/handlers/plan.ts +8 -1
- package/src/orchestrator/handlers/recon.ts +8 -1
- package/src/orchestrator/handlers/types.ts +2 -2
- package/src/orchestrator/lesson-memory.ts +6 -1
- package/src/orchestrator/orchestration-logger.ts +15 -3
- package/src/orchestrator/skill-injection.ts +7 -1
- package/src/orchestrator/state.ts +6 -1
- package/src/recovery/classifier.ts +127 -0
- package/src/recovery/event-handler.ts +263 -0
- package/src/recovery/index.ts +20 -0
- package/src/recovery/orchestrator.ts +180 -0
- package/src/recovery/persistence.ts +87 -0
- package/src/recovery/strategies.ts +107 -0
- package/src/recovery/types.ts +31 -0
- package/src/registry/model-groups.ts +2 -19
- package/src/registry/resolver.ts +38 -9
- package/src/review/agent-catalog.ts +83 -251
- package/src/review/agents/architecture-verifier.ts +41 -0
- package/src/review/agents/code-hygiene-auditor.ts +40 -0
- package/src/review/agents/correctness-auditor.ts +41 -0
- package/src/review/agents/frontend-auditor.ts +39 -0
- package/src/review/agents/index.ts +15 -42
- package/src/review/agents/language-idioms-auditor.ts +39 -0
- package/src/review/agents/security-auditor.ts +12 -8
- package/src/review/stack-gate.ts +2 -6
- package/src/routing/categories.ts +111 -0
- package/src/routing/classifier.ts +152 -0
- package/src/routing/engine.ts +89 -0
- package/src/routing/index.ts +4 -0
- package/src/routing/types.ts +14 -0
- package/src/skills/adaptive-injector.ts +34 -3
- package/src/skills/loader.ts +4 -0
- package/src/tools/background.ts +196 -0
- package/src/tools/configure.ts +1 -1
- package/src/tools/delegate.ts +205 -0
- package/src/tools/loop.ts +94 -0
- package/src/tools/recover.ts +172 -0
- package/src/types/background.ts +51 -0
- package/src/types/mcp.ts +27 -0
- package/src/types/recovery.ts +49 -0
- package/src/types/routing.ts +39 -0
- package/src/ux/context-warnings.ts +81 -0
- package/src/ux/error-hints.ts +38 -0
- package/src/ux/index.ts +7 -0
- package/src/ux/notifications.ts +67 -0
- package/src/ux/progress.ts +77 -0
- package/src/ux/session-summary.ts +67 -0
- package/src/ux/task-status.ts +109 -0
- package/src/ux/types.ts +24 -0
- package/src/agents/db-specialist.ts +0 -295
- package/src/agents/devops.ts +0 -352
- package/src/agents/documenter.ts +0 -44
- package/src/agents/frontend-engineer.ts +0 -541
- package/src/agents/pipeline/oc-explorer.ts +0 -46
- package/src/agents/pipeline/oc-retrospector.ts +0 -42
- package/src/review/agents/auth-flow-verifier.ts +0 -47
- package/src/review/agents/concurrency-checker.ts +0 -47
- package/src/review/agents/dead-code-scanner.ts +0 -47
- package/src/review/agents/go-idioms-auditor.ts +0 -46
- package/src/review/agents/python-django-auditor.ts +0 -46
- package/src/review/agents/react-patterns-auditor.ts +0 -46
- package/src/review/agents/rust-safety-auditor.ts +0 -46
- package/src/review/agents/scope-intent-verifier.ts +0 -45
- package/src/review/agents/silent-failure-hunter.ts +0 -45
- package/src/review/agents/spec-checker.ts +0 -45
- package/src/review/agents/state-mgmt-auditor.ts +0 -46
- package/src/review/agents/type-soundness.ts +0 -46
- package/src/review/agents/wiring-inspector.ts +0 -46
|
@@ -1,80 +1,53 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
16
|
+
architectureVerifier,
|
|
17
|
+
codeHygieneAuditor,
|
|
25
18
|
codeQualityAuditor,
|
|
26
|
-
concurrencyChecker,
|
|
27
19
|
contractVerifier,
|
|
20
|
+
correctnessAuditor,
|
|
28
21
|
databaseAuditor,
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
deadCodeScanner,
|
|
64
|
-
specChecker,
|
|
44
|
+
architectureVerifier,
|
|
65
45
|
databaseAuditor,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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. **
|
|
18
|
-
4. **
|
|
19
|
-
5. **
|
|
20
|
-
6. **
|
|
21
|
-
7. **
|
|
22
|
-
8. **
|
|
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
|
|
package/src/review/stack-gate.ts
CHANGED
|
@@ -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
|
-
"
|
|
11
|
-
"
|
|
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,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
|
-
|
|
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
|
}
|