dual-brain 0.2.30 → 0.3.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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/detect.mjs
DELETED
|
@@ -1,702 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// detect.mjs — Task detection for dual-brain. Self-contained, no internal imports.
|
|
3
|
-
// Exports: detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths, classifySpecialist
|
|
4
|
-
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
6
|
-
import { resolve, dirname } from 'path';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
import { execSync } from 'child_process';
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
|
|
12
|
-
// ─── Intent definitions ────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
const INTENTS = {
|
|
15
|
-
search: /\b(grep|find|locate|where is|where are|list|explore|read|look up|look for|check|what is|show me|display)\b/i,
|
|
16
|
-
explain: /\b(explain|walk me through|what does|how does|describe|summarize|understand|clarify)\b/i,
|
|
17
|
-
compare: /\b(compare|contrast|difference|versus|vs\.?|trade.?off|which is better|pros and cons|benchmark|performance)\b/i,
|
|
18
|
-
document: /\b(document|docs?|readme|jsdoc|typedoc|api docs|write docs|add docs|update docs)\b/i,
|
|
19
|
-
format: /\b(format|lint|prettier|style|indent|whitespace|typo|typos|comment[s]?|reformat)\b/i,
|
|
20
|
-
planning: /\b(plan|roadmap|strategy|prioritize|break down|decompose|prioritise)\b/i,
|
|
21
|
-
architecture: /\b(design|architect|architecture|propose|how should we|system design|system architecture)\b/i,
|
|
22
|
-
security: /\b(auth(?:enticat\w*)?|credential|secret|token|password|encrypt|permission[s]?|vulnerability|vulnerabilities|CVE|oauth|jwt|api.?key)\b/i,
|
|
23
|
-
review: /\b(review|audit|check for issues|evaluate|assess|inspect code|code review)\b/i,
|
|
24
|
-
debug: /\b(debug|investigate|why (is|does|isn't|doesn't)|trace|diagnose|figure out|broken|not working|failing|regression)\b/i,
|
|
25
|
-
test: /\b(test[s]?|spec[s]?|add test|fix test|test coverage|unit test|e2e|integration test|jest|vitest|mocha)\b/i,
|
|
26
|
-
refactor: /\b(refactor|restructure|reorganize|reorganise|extract|split|consolidate|clean up|cleanup|dedupe|dedup)\b/i,
|
|
27
|
-
edit: /\b(fix|add|update|modify|change|rename|move|replace|write|implement|create|remove|delete|insert)\b/i,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const INTENT_PRIORITY = [
|
|
31
|
-
'security', 'architecture', 'planning', 'compare', 'review',
|
|
32
|
-
'debug', 'refactor', 'test', 'explain', 'document', 'format', 'search', 'edit',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// ─── Risk patterns (file path based) ──────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const RISK_PATTERNS = [
|
|
38
|
-
{ level: 'critical', regex: /\b(auth|credential|secret|\.env|key[s]?|token[s]?|password|encrypt|certificate|cert[s]?|\.pem|\.key)\b/i, label: 'security-sensitive' },
|
|
39
|
-
{ level: 'high', regex: /\b(billing|payment|migration|deploy|ci[-/]cd|\.github\/workflows|security|permission|policy|schema\.prisma|schema\.sql|api[-_]?contract|openapi|swagger)\b/i, label: 'high-impact infrastructure' },
|
|
40
|
-
{ level: 'medium', regex: /\b(test|spec|\.test\.|\.spec\.|shared|util[s]?|lib\/|public[-_]?api|integrat|config|\.config\.)\b/i, label: 'shared/tested code' },
|
|
41
|
-
{ level: 'low', regex: /\b(readme|\.md$|docs?\/|comment|format|lint|\.prettierrc|local[-_]?script|internal[-_]?only|changelog)\b/i, label: 'docs/formatting' },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
// ─── Description-level risk keywords ──────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
const RISK_KEYWORDS = [
|
|
47
|
-
{ level: 'critical', regex: /\b(auth|secret|credential|token|password|encrypt|certificate|oauth|jwt|api.?key|vulnerability|CVE)\b/i },
|
|
48
|
-
{ level: 'high', regex: /\b(billing|payment|migration|deploy|ci.?cd|security|permission|policy|schema|openapi|swagger|production|prod)\b/i },
|
|
49
|
-
{ level: 'medium', regex: /\b(test|spec|config|shared|util|lib|integration|public.?api)\b/i },
|
|
50
|
-
{ level: 'low', regex: /\b(readme|docs?|comment|format|lint|changelog|typo|whitespace)\b/i },
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
const DESIGN_IMPACT_PATTERNS = [
|
|
54
|
-
/\bbin\/dual-brain\.mjs\b/,
|
|
55
|
-
/\bsrc\/(?:tui|profile|detect|decide|dispatch|session|health|index)\.mjs\b/,
|
|
56
|
-
/\bhooks\/(?:head-guard|enforce-tier|budget-balancer|dual-brain-think|dual-brain-review|wave-orchestrator)\.mjs\b/,
|
|
57
|
-
/\bVISION\.md\b/,
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
const LEVEL_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
61
|
-
|
|
62
|
-
// ─── Helpers / Exported functions ─────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function higherRisk(a, b) { return LEVEL_ORDER[a] >= LEVEL_ORDER[b] ? a : b; }
|
|
65
|
-
|
|
66
|
-
/** Extract file paths from free-form text. */
|
|
67
|
-
function extractPaths(text) {
|
|
68
|
-
if (!text) return [];
|
|
69
|
-
const matches = text.match(/(?:^|\s|["'`])([./~]?(?:[\w@.-]+\/)+[\w@.*-]+(?:\.\w+)?)/g);
|
|
70
|
-
if (!matches) return [];
|
|
71
|
-
return matches.map(m => m.trim().replace(/^["'`]/, ''));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Classify risk from an array of file paths. Returns { level, riskyFiles }. */
|
|
75
|
-
function classifyRisk(paths) {
|
|
76
|
-
if (!paths || paths.length === 0) {
|
|
77
|
-
return { level: 'low', riskyFiles: [] };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
let highestLevel = 'low';
|
|
81
|
-
const riskyFiles = [];
|
|
82
|
-
|
|
83
|
-
for (const p of paths) {
|
|
84
|
-
for (const pattern of RISK_PATTERNS) {
|
|
85
|
-
if (pattern.regex.test(p)) {
|
|
86
|
-
if (LEVEL_ORDER[pattern.level] > LEVEL_ORDER['low']) {
|
|
87
|
-
riskyFiles.push({ path: p, risk: pattern.level, reason: pattern.label });
|
|
88
|
-
}
|
|
89
|
-
if (LEVEL_ORDER[pattern.level] > LEVEL_ORDER[highestLevel]) {
|
|
90
|
-
highestLevel = pattern.level;
|
|
91
|
-
if (highestLevel === 'critical') break;
|
|
92
|
-
}
|
|
93
|
-
break; // use highest-priority match for this path
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (highestLevel === 'critical') break;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { level: highestLevel, riskyFiles };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Extract the dominant intent from a task description. */
|
|
103
|
-
function classifyIntent(prompt) {
|
|
104
|
-
for (const key of INTENT_PRIORITY) {
|
|
105
|
-
if (INTENTS[key].test(prompt)) return key;
|
|
106
|
-
}
|
|
107
|
-
return 'edit';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Determine complexity from description, file count, risk, intent, prior failures. */
|
|
111
|
-
function estimateComplexity({ prompt, fileCount = 0, risk = 'low', intent = 'edit', priorFailures = 0 }) {
|
|
112
|
-
const isAmbiguous = prompt.length > 120 || /\b(and also|as well as|plus|additionally|also)\b/i.test(prompt);
|
|
113
|
-
|
|
114
|
-
if (priorFailures >= 2 || intent === 'architecture' || risk === 'critical' || fileCount >= 6) {
|
|
115
|
-
return 'complex';
|
|
116
|
-
}
|
|
117
|
-
if (fileCount >= 3 || intent === 'refactor' || intent === 'debug' || risk === 'high' || isAmbiguous) {
|
|
118
|
-
return 'moderate';
|
|
119
|
-
}
|
|
120
|
-
if (fileCount <= 2 && (risk === 'low' || risk === 'medium')) {
|
|
121
|
-
if (intent === 'format' || (fileCount <= 1 && risk === 'low')) return 'trivial';
|
|
122
|
-
return 'simple';
|
|
123
|
-
}
|
|
124
|
-
return 'moderate';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Map intent + risk + complexity → tier (think / search / execute). */
|
|
128
|
-
function inferTier({ intent, risk, complexity, effort, specialistTierBias }) {
|
|
129
|
-
const thinkIntents = ['architecture', 'security', 'planning', 'compare', 'review'];
|
|
130
|
-
if (thinkIntents.includes(intent) || risk === 'critical') return 'think';
|
|
131
|
-
|
|
132
|
-
// Specialist tier_bias can elevate to think before general tier logic
|
|
133
|
-
if (specialistTierBias === 'think') return 'think';
|
|
134
|
-
|
|
135
|
-
const searchIntents = ['search', 'explain', 'format'];
|
|
136
|
-
if (searchIntents.includes(intent) && effort === 'low') return 'search';
|
|
137
|
-
|
|
138
|
-
return 'execute';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Whether this task likely requires writing/editing files. */
|
|
142
|
-
function requiresWrite(intent) {
|
|
143
|
-
const readOnly = ['search', 'explain', 'compare', 'review'];
|
|
144
|
-
return !readOnly.includes(intent);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Build a one-sentence explanation of the classification. */
|
|
148
|
-
function buildExplanation({ intent, risk, complexity, fileCount, priorFailures }) {
|
|
149
|
-
const parts = [];
|
|
150
|
-
|
|
151
|
-
const complexityWord = { trivial: 'Trivial', simple: 'Simple', moderate: 'Moderate', complex: 'Complex' }[complexity];
|
|
152
|
-
const riskWord = risk === 'low' ? 'low-risk' : `${risk}-risk`;
|
|
153
|
-
parts.push(`${complexityWord} ${riskWord} ${intent}`);
|
|
154
|
-
|
|
155
|
-
if (fileCount > 0) parts.push(`touching ${fileCount} file${fileCount === 1 ? '' : 's'}`);
|
|
156
|
-
if (priorFailures > 0) parts.push(`with ${priorFailures} prior failure${priorFailures === 1 ? '' : 's'} — elevated effort`);
|
|
157
|
-
|
|
158
|
-
return parts.join(' ') + '.';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ─── Reasoning depth classification ───────────────────────────────────────────
|
|
162
|
-
|
|
163
|
-
const ULTRA_UNCERTAINTY = /\b(not sure|maybe|should we|architect|design|trade-?off|approach)\b/i;
|
|
164
|
-
const ULTRA_DEEP_ANALYSIS = /\b(think about|analyze|analyse|evaluate|compare options)\b/i;
|
|
165
|
-
const HIGH_CROSS_CUTTING = /\b(refactor|rename across|update all|migration)\b/i;
|
|
166
|
-
const LOW_SIMPLE = /\b(grep|find|search|list|show|what is|where is)\b/i;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Classify the reasoning depth needed for a task.
|
|
170
|
-
* Returns { depth: 'low'|'medium'|'high'|'ultra', signals: string[] }
|
|
171
|
-
*/
|
|
172
|
-
function classifyReasoningDepth(prompt, files = [], priorOutcomes = []) {
|
|
173
|
-
const signals = [];
|
|
174
|
-
|
|
175
|
-
// Gather prior failure count from priorOutcomes array
|
|
176
|
-
const failures = priorOutcomes.filter(o => o && (o.failed || o.status === 'failed' || o.outcome === 'failed' || o.success === false)).length;
|
|
177
|
-
|
|
178
|
-
// File-based risk (reuse classifyRisk)
|
|
179
|
-
const { level: fileRisk } = classifyRisk(files);
|
|
180
|
-
|
|
181
|
-
// Keyword risk from prompt (reuse RISK_KEYWORDS)
|
|
182
|
-
let keywordRisk = 'low';
|
|
183
|
-
for (const { level, regex } of RISK_KEYWORDS) {
|
|
184
|
-
if (regex.test(prompt)) { keywordRisk = level; break; }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const risk = higherRisk(fileRisk, keywordRisk);
|
|
188
|
-
|
|
189
|
-
// Directory spread from files
|
|
190
|
-
const dirs = new Set(files.map(f => {
|
|
191
|
-
const parts = f.replace(/^\//, '').split('/');
|
|
192
|
-
return parts.length > 1 ? parts[0] : '.';
|
|
193
|
-
}));
|
|
194
|
-
const dirCount = dirs.size;
|
|
195
|
-
|
|
196
|
-
// ── Ultra signals ──────────────────────────────────────────────────────────
|
|
197
|
-
const ultraSignals = [];
|
|
198
|
-
|
|
199
|
-
if (ULTRA_UNCERTAINTY.test(prompt)) {
|
|
200
|
-
const match = prompt.match(ULTRA_UNCERTAINTY);
|
|
201
|
-
ultraSignals.push(`prompt contains '${match[0]}'`);
|
|
202
|
-
}
|
|
203
|
-
if (ULTRA_DEEP_ANALYSIS.test(prompt)) {
|
|
204
|
-
const match = prompt.match(ULTRA_DEEP_ANALYSIS);
|
|
205
|
-
ultraSignals.push(`prompt requests deep analysis ('${match[0]}')`);
|
|
206
|
-
}
|
|
207
|
-
if (risk === 'critical') {
|
|
208
|
-
ultraSignals.push('risk classified as critical');
|
|
209
|
-
}
|
|
210
|
-
if (failures >= 2) {
|
|
211
|
-
ultraSignals.push(`${failures} prior failures on similar task`);
|
|
212
|
-
}
|
|
213
|
-
if (fileRisk === 'critical') {
|
|
214
|
-
ultraSignals.push('files include auth/security/billing/migration patterns');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (ultraSignals.length > 0) {
|
|
218
|
-
return { depth: 'ultra', signals: ultraSignals };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── High signals ───────────────────────────────────────────────────────────
|
|
222
|
-
const highSignals = [];
|
|
223
|
-
|
|
224
|
-
if (risk === 'high') {
|
|
225
|
-
highSignals.push('risk classified as high');
|
|
226
|
-
}
|
|
227
|
-
if (files.length > 5) {
|
|
228
|
-
highSignals.push(`${files.length} files provided`);
|
|
229
|
-
}
|
|
230
|
-
if (failures === 1) {
|
|
231
|
-
highSignals.push('1 prior failure on similar task');
|
|
232
|
-
}
|
|
233
|
-
if (HIGH_CROSS_CUTTING.test(prompt)) {
|
|
234
|
-
const match = prompt.match(HIGH_CROSS_CUTTING);
|
|
235
|
-
highSignals.push(`prompt mentions cross-cutting concern ('${match[0]}')`);
|
|
236
|
-
}
|
|
237
|
-
if (dirCount >= 3) {
|
|
238
|
-
highSignals.push(`files span ${dirCount} directories`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (highSignals.length > 0) {
|
|
242
|
-
return { depth: 'high', signals: highSignals };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// ── Medium signals ─────────────────────────────────────────────────────────
|
|
246
|
-
const MEDIUM_IMPL = /\b(add|implement|build|create|fix|update)\b/i;
|
|
247
|
-
const mediumSignals = [];
|
|
248
|
-
|
|
249
|
-
if (risk === 'medium') {
|
|
250
|
-
mediumSignals.push('risk classified as medium');
|
|
251
|
-
}
|
|
252
|
-
if (files.length >= 2 && files.length <= 5) {
|
|
253
|
-
mediumSignals.push(`${files.length} files provided`);
|
|
254
|
-
}
|
|
255
|
-
if (MEDIUM_IMPL.test(prompt)) {
|
|
256
|
-
const match = prompt.match(MEDIUM_IMPL);
|
|
257
|
-
mediumSignals.push(`prompt contains implementation keyword ('${match[0]}')`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (mediumSignals.length > 0) {
|
|
261
|
-
return { depth: 'medium', signals: mediumSignals };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ── Low signals ────────────────────────────────────────────────────────────
|
|
265
|
-
const lowSignals = [];
|
|
266
|
-
|
|
267
|
-
if (risk === 'low') {
|
|
268
|
-
lowSignals.push('risk classified as low');
|
|
269
|
-
}
|
|
270
|
-
if (files.length <= 1) {
|
|
271
|
-
lowSignals.push(files.length === 0 ? 'no files provided' : '1 file provided');
|
|
272
|
-
}
|
|
273
|
-
if (LOW_SIMPLE.test(prompt)) {
|
|
274
|
-
const match = prompt.match(LOW_SIMPLE);
|
|
275
|
-
lowSignals.push(`prompt is a simple lookup ('${match[0]}')`);
|
|
276
|
-
}
|
|
277
|
-
if (failures === 0) {
|
|
278
|
-
lowSignals.push('no prior failures');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return { depth: 'low', signals: lowSignals.length > 0 ? lowSignals : ['no elevated signals detected'] };
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// ─── Plugin-aware detection helpers ───────────────────────────────────────────
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Known plugin service keywords → plugin IDs.
|
|
288
|
-
* Maps common service names and their aliases to Codex plugin directory names.
|
|
289
|
-
* Static map so detect.mjs stays self-contained (no I/O at classify time).
|
|
290
|
-
*/
|
|
291
|
-
const PLUGIN_KEYWORD_MAP = {
|
|
292
|
-
// Payments
|
|
293
|
-
stripe: 'stripe',
|
|
294
|
-
payment: 'stripe',
|
|
295
|
-
checkout: 'stripe',
|
|
296
|
-
subscription: 'stripe',
|
|
297
|
-
webhook: 'stripe',
|
|
298
|
-
// Collaboration / messaging
|
|
299
|
-
slack: 'slack',
|
|
300
|
-
teams: 'teams',
|
|
301
|
-
// Data / backend
|
|
302
|
-
supabase: 'supabase',
|
|
303
|
-
neondb: 'neon-postgres',
|
|
304
|
-
// Dev tools
|
|
305
|
-
github: 'github',
|
|
306
|
-
'pull request': 'github',
|
|
307
|
-
linear: 'linear',
|
|
308
|
-
jira: 'atlassian-rovo',
|
|
309
|
-
atlassian: 'atlassian-rovo',
|
|
310
|
-
// Comms / productivity
|
|
311
|
-
gmail: 'gmail',
|
|
312
|
-
outlook: 'outlook-email',
|
|
313
|
-
notion: 'notion',
|
|
314
|
-
'google calendar': 'google-calendar',
|
|
315
|
-
'google drive': 'google-drive',
|
|
316
|
-
// Monitoring / infra
|
|
317
|
-
sentry: 'sentry',
|
|
318
|
-
vercel: 'vercel',
|
|
319
|
-
netlify: 'netlify',
|
|
320
|
-
cloudflare: 'cloudflare',
|
|
321
|
-
// Analytics
|
|
322
|
-
amplitude: 'amplitude',
|
|
323
|
-
// Design
|
|
324
|
-
figma: 'figma',
|
|
325
|
-
canva: 'canva',
|
|
326
|
-
// CRM / sales
|
|
327
|
-
hubspot: 'hubspot',
|
|
328
|
-
pipedrive: 'pipedrive',
|
|
329
|
-
// Communication
|
|
330
|
-
sendgrid: 'sendgrid',
|
|
331
|
-
twilio: 'twilio-developer-kit',
|
|
332
|
-
// Storage
|
|
333
|
-
sharepoint: 'sharepoint',
|
|
334
|
-
box: 'box',
|
|
335
|
-
// AI / ML
|
|
336
|
-
openai: 'openai-developers',
|
|
337
|
-
'hugging face': 'hugging-face',
|
|
338
|
-
// Other
|
|
339
|
-
razorpay: 'razorpay',
|
|
340
|
-
render: 'render',
|
|
341
|
-
monday: 'monday-com',
|
|
342
|
-
asana: 'asana',
|
|
343
|
-
clickup: 'clickup',
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Detect Codex plugin IDs that match keywords in the prompt.
|
|
348
|
-
* Returns an array of matched plugin IDs (deduplicated, max 5).
|
|
349
|
-
* @param {string} prompt
|
|
350
|
-
* @returns {string[]}
|
|
351
|
-
*/
|
|
352
|
-
function detectSuggestedPlugins(prompt) {
|
|
353
|
-
if (!prompt) return [];
|
|
354
|
-
const lower = prompt.toLowerCase();
|
|
355
|
-
const matched = new Set();
|
|
356
|
-
|
|
357
|
-
// Check multi-word phrases first (longer matches take priority)
|
|
358
|
-
const sortedEntries = Object.entries(PLUGIN_KEYWORD_MAP).sort((a, b) => b[0].length - a[0].length);
|
|
359
|
-
for (const [keyword, pluginId] of sortedEntries) {
|
|
360
|
-
if (lower.includes(keyword)) {
|
|
361
|
-
matched.add(pluginId);
|
|
362
|
-
if (matched.size >= 5) break;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return [...matched];
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// ─── CI risk check ────────────────────────────────────────────────────────────
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Lightweight CI risk check: returns true if the current branch has a recent
|
|
373
|
-
* CI failure, indicating the task may touch already-broken code.
|
|
374
|
-
* Intentionally best-effort — any error returns false (never blocks detection).
|
|
375
|
-
* @param {string} [cwd]
|
|
376
|
-
* @returns {{ hasCIFailure: boolean, failedBranch: string|null }}
|
|
377
|
-
*/
|
|
378
|
-
function checkCIRisk(cwd) {
|
|
379
|
-
try {
|
|
380
|
-
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
381
|
-
cwd, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
382
|
-
}).trim();
|
|
383
|
-
|
|
384
|
-
const json = execSync(
|
|
385
|
-
'gh run list --limit 5 --json conclusion,headBranch 2>/dev/null',
|
|
386
|
-
{ cwd, encoding: 'utf8', timeout: 8000 }
|
|
387
|
-
);
|
|
388
|
-
const runs = JSON.parse(json);
|
|
389
|
-
const branchFailure = runs.find(
|
|
390
|
-
r => r.conclusion === 'failure' && r.headBranch === currentBranch
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
hasCIFailure: Boolean(branchFailure),
|
|
395
|
-
failedBranch: branchFailure ? currentBranch : null,
|
|
396
|
-
};
|
|
397
|
-
} catch {
|
|
398
|
-
return { hasCIFailure: false, failedBranch: null };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/** Main detection function. Input: { prompt, files?, priorFailures?, sessionContext? } */
|
|
403
|
-
function detectTask(input) {
|
|
404
|
-
const { prompt = '', files = [], sessionContext = null, headJudgment = null } = input;
|
|
405
|
-
let { priorFailures = 0 } = input;
|
|
406
|
-
|
|
407
|
-
// Session context: bump priorFailures if session history shows failures on similar tasks
|
|
408
|
-
let repeatedFailure = false;
|
|
409
|
-
if (sessionContext) {
|
|
410
|
-
const sessionFailures = Array.isArray(sessionContext.priorAttempts)
|
|
411
|
-
? sessionContext.priorAttempts.filter(a => a && (a.failed || a.status === 'failed')).length
|
|
412
|
-
: 0;
|
|
413
|
-
if (sessionFailures > 0) {
|
|
414
|
-
priorFailures = Math.max(priorFailures, sessionFailures);
|
|
415
|
-
}
|
|
416
|
-
// Flag repeated_failure if riskSignals contains failure indicators
|
|
417
|
-
const riskSignals = sessionContext.riskSignals ?? [];
|
|
418
|
-
if (riskSignals.some(s => s && (s.type === 'failure' || s.failed || /fail/i.test(String(s))))) {
|
|
419
|
-
repeatedFailure = true;
|
|
420
|
-
}
|
|
421
|
-
if (sessionFailures >= 2) repeatedFailure = true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// 1. Intent
|
|
425
|
-
const intent = classifyIntent(prompt);
|
|
426
|
-
|
|
427
|
-
// 2. Paths and risk
|
|
428
|
-
const extractedPaths = extractPaths(prompt);
|
|
429
|
-
const allPaths = [...files, ...extractedPaths];
|
|
430
|
-
const { level: pathRiskLevel, riskyFiles } = classifyRisk(allPaths);
|
|
431
|
-
const designImpact = allPaths.some(p => DESIGN_IMPACT_PATTERNS.some(re => re.test(p)));
|
|
432
|
-
|
|
433
|
-
// 3. Keyword risk from description
|
|
434
|
-
let keywordRisk = 'low';
|
|
435
|
-
for (const { level, regex } of RISK_KEYWORDS) {
|
|
436
|
-
if (regex.test(prompt)) { keywordRisk = level; break; }
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
let risk = higherRisk(pathRiskLevel, keywordRisk);
|
|
440
|
-
|
|
441
|
-
// Session context: bump risk one level if prior session attempts failed on similar tasks
|
|
442
|
-
if (repeatedFailure && LEVEL_ORDER[risk] < LEVEL_ORDER['high']) {
|
|
443
|
-
const riskLevels = ['low', 'medium', 'high', 'critical'];
|
|
444
|
-
const currentIdx = riskLevels.indexOf(risk);
|
|
445
|
-
risk = riskLevels[Math.min(currentIdx + 1, riskLevels.length - 1)];
|
|
446
|
-
}
|
|
447
|
-
const fileCount = files.length;
|
|
448
|
-
|
|
449
|
-
// 4. Complexity
|
|
450
|
-
const complexity = estimateComplexity({ prompt, fileCount, risk, intent, priorFailures });
|
|
451
|
-
|
|
452
|
-
// 5. Effort
|
|
453
|
-
const effortOrder = ['low', 'medium', 'high', 'xhigh'];
|
|
454
|
-
function bumpEffort(e, n = 1) {
|
|
455
|
-
return effortOrder[Math.min(effortOrder.indexOf(e) + n, effortOrder.length - 1)];
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
let effort = { trivial: 'low', simple: 'medium', moderate: 'high', complex: 'high' }[complexity];
|
|
459
|
-
if (risk === 'critical' && LEVEL_ORDER[effort] < LEVEL_ORDER['high']) effort = 'high';
|
|
460
|
-
if (priorFailures >= 2) {
|
|
461
|
-
effort = 'xhigh';
|
|
462
|
-
} else if (priorFailures === 1) {
|
|
463
|
-
effort = bumpEffort(effort, 1);
|
|
464
|
-
}
|
|
465
|
-
if (intent === 'format' || intent === 'search') {
|
|
466
|
-
if (LEVEL_ORDER[effort] > LEVEL_ORDER['medium']) effort = 'medium';
|
|
467
|
-
}
|
|
468
|
-
if ((intent === 'architecture' || intent === 'security') && LEVEL_ORDER[effort] < LEVEL_ORDER['high']) {
|
|
469
|
-
effort = 'high';
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// 6. Specialist
|
|
473
|
-
const specialistResult = classifySpecialist(prompt, files);
|
|
474
|
-
const specialistDef = SPECIALIST_DEFS[specialistResult.specialist] || null;
|
|
475
|
-
const specialistTierBias = specialistDef?.tier_bias || null;
|
|
476
|
-
|
|
477
|
-
// 7. Tier
|
|
478
|
-
const tier = inferTier({ intent, risk, complexity, effort, specialistTierBias });
|
|
479
|
-
|
|
480
|
-
// 8. Explanation
|
|
481
|
-
const explanation = buildExplanation({ intent, risk, complexity, fileCount, priorFailures });
|
|
482
|
-
|
|
483
|
-
// 9. Reasoning depth
|
|
484
|
-
const priorOutcomes = priorFailures > 0
|
|
485
|
-
? Array.from({ length: priorFailures }, () => ({ failed: true }))
|
|
486
|
-
: [];
|
|
487
|
-
const { depth: reasoningDepth, signals: reasoningSignals } = classifyReasoningDepth(prompt, files, priorOutcomes);
|
|
488
|
-
|
|
489
|
-
// 10. Suggested Codex plugins (keyword-based, static map — no I/O)
|
|
490
|
-
const suggestedPlugins = detectSuggestedPlugins(prompt);
|
|
491
|
-
|
|
492
|
-
// 11. CI risk — check if current branch has failing CI runs (best-effort, never throws)
|
|
493
|
-
const ciRiskResult = checkCIRisk(input.cwd || process.cwd());
|
|
494
|
-
|
|
495
|
-
// 12. Match specialized agent from registry (synchronous, best-effort)
|
|
496
|
-
const suggestedAgent = _matchAgentSync(intent, risk, specialistResult.specialist || '');
|
|
497
|
-
|
|
498
|
-
// HEAD judgment override: when HEAD's cognitive pipeline has already assessed
|
|
499
|
-
// the situation, use its risk/depth as authoritative and reconcile differences.
|
|
500
|
-
let headOverrides = {};
|
|
501
|
-
if (headJudgment?.situation) {
|
|
502
|
-
const hj = headJudgment.situation;
|
|
503
|
-
const headRisk = hj.taskShape?.risk;
|
|
504
|
-
const headAmbiguity = hj.taskShape?.ambiguity;
|
|
505
|
-
|
|
506
|
-
// HEAD's risk takes precedence when it's higher (HEAD sees more signals)
|
|
507
|
-
if (headRisk && LEVEL_ORDER[headRisk] > LEVEL_ORDER[risk]) {
|
|
508
|
-
risk = headRisk;
|
|
509
|
-
headOverrides.riskElevatedBy = 'head-judgment';
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// HEAD's depth maps to reasoning depth
|
|
513
|
-
const headDepthMap = { reflexive: 'low', light: 'medium', full: 'high', deep: 'ultra' };
|
|
514
|
-
const headDepth = headDepthMap[headJudgment.depth];
|
|
515
|
-
if (headDepth) {
|
|
516
|
-
const depthOrder = { low: 0, medium: 1, high: 2, ultra: 3 };
|
|
517
|
-
if (depthOrder[headDepth] > depthOrder[reasoningDepth]) {
|
|
518
|
-
reasoningDepth = headDepth;
|
|
519
|
-
reasoningSignals.push(`HEAD assessed depth as ${headJudgment.depth}`);
|
|
520
|
-
headOverrides.depthElevatedBy = 'head-judgment';
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// HEAD's ambiguity signals complexity
|
|
525
|
-
if (headAmbiguity === 'high' && complexity !== 'complex') {
|
|
526
|
-
headOverrides.ambiguityWarning = 'HEAD detected high ambiguity';
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return {
|
|
531
|
-
intent,
|
|
532
|
-
risk,
|
|
533
|
-
complexity,
|
|
534
|
-
effort,
|
|
535
|
-
tier,
|
|
536
|
-
fileCount,
|
|
537
|
-
riskyFiles,
|
|
538
|
-
designImpact,
|
|
539
|
-
requiresWrite: requiresWrite(intent),
|
|
540
|
-
explanation,
|
|
541
|
-
specialist: specialistResult,
|
|
542
|
-
reasoningDepth,
|
|
543
|
-
reasoningSignals,
|
|
544
|
-
suggestedPlugins,
|
|
545
|
-
ciRisk: ciRiskResult,
|
|
546
|
-
suggestedAgent,
|
|
547
|
-
...(repeatedFailure && { repeatedFailure: true }),
|
|
548
|
-
...(Object.keys(headOverrides).length > 0 && { headOverrides }),
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// ─── Agent registry bridge (synchronous, injected) ───────────────────────────
|
|
553
|
-
//
|
|
554
|
-
// detect.mjs is synchronous by design. The ESM agent registry is loaded
|
|
555
|
-
// asynchronously by callers (pipeline, CLI) via primeAgentRegistry(), which
|
|
556
|
-
// caches the matchAgent function here so detectTask can call it synchronously.
|
|
557
|
-
|
|
558
|
-
let _matchAgentFn = null;
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Prime the agent registry so detectTask can match agents synchronously.
|
|
562
|
-
* Call this once at startup: await primeAgentRegistry()
|
|
563
|
-
*/
|
|
564
|
-
export async function primeAgentRegistry() {
|
|
565
|
-
try {
|
|
566
|
-
const { matchAgent } = await import('./agents/registry.mjs');
|
|
567
|
-
_matchAgentFn = matchAgent;
|
|
568
|
-
} catch {
|
|
569
|
-
// Registry unavailable — detectTask continues without agent matching
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Synchronously match a specialized agent from the primed registry.
|
|
575
|
-
* Returns the best match or null if not yet primed.
|
|
576
|
-
*/
|
|
577
|
-
function _matchAgentSync(intent, risk, taskType) {
|
|
578
|
-
try {
|
|
579
|
-
if (typeof _matchAgentFn !== 'function') return null;
|
|
580
|
-
const matches = _matchAgentFn(intent, risk, taskType);
|
|
581
|
-
return matches.length > 0 ? matches[0] : null;
|
|
582
|
-
} catch {
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// ─── Specialist registry ──────────────────────────────────────────────────────
|
|
588
|
-
|
|
589
|
-
const SPECIALIST_REGISTRY_PATH = resolve(__dirname, '../agents/specialists/registry.json');
|
|
590
|
-
|
|
591
|
-
const DEFAULT_SPECIALISTS = {
|
|
592
|
-
python: { triggers: { extensions: ['.py', '.pyx', '.pyi'], keywords: ['python', 'pip', 'pytest', 'django', 'flask', 'asyncio'] } },
|
|
593
|
-
typescript: { triggers: { extensions: ['.ts', '.tsx', '.mts'], keywords: ['typescript', 'tsc', 'generics', 'react', 'next', 'node'] } },
|
|
594
|
-
html: { triggers: { extensions: ['.html', '.css', '.scss', '.svg'], keywords: ['html', 'css', 'accessibility', 'a11y', 'aria', 'responsive', 'tailwind'] } },
|
|
595
|
-
linux: { triggers: { extensions: ['.sh', '.bash', '.conf', '.service', '.dockerfile'], keywords: ['linux', 'bash', 'shell', 'systemd', 'nginx', 'docker', 'ssh', 'deploy'] } },
|
|
596
|
-
security: { triggers: { extensions: [], keywords: ['auth', 'oauth', 'jwt', 'credential', 'secret', 'encrypt', 'vulnerability', 'vulnerabilities', 'audit', 'owasp', 'xss', 'csrf'] }, tier_bias: 'think' },
|
|
597
|
-
doctor: { triggers: { extensions: [], keywords: ['doctor', 'health', 'diagnose', 'diagnosis', 'checkup', 'drift', 'completeness', 'broken', 'regression', 'audit health', 'package health', 'health check', 'health report', 'health-manifest'] }, tier_bias: 'think' },
|
|
598
|
-
};
|
|
599
|
-
|
|
600
|
-
function loadSpecialistRegistry() {
|
|
601
|
-
try {
|
|
602
|
-
const raw = readFileSync(SPECIALIST_REGISTRY_PATH, 'utf8');
|
|
603
|
-
const parsed = JSON.parse(raw);
|
|
604
|
-
return parsed.specialists || DEFAULT_SPECIALISTS;
|
|
605
|
-
} catch {
|
|
606
|
-
return DEFAULT_SPECIALISTS;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const SPECIALIST_DEFS = loadSpecialistRegistry();
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Classify which specialist domain best matches the prompt and file list.
|
|
614
|
-
* Returns { specialist, confidence, triggers }.
|
|
615
|
-
*/
|
|
616
|
-
function classifySpecialist(prompt = '', files = []) {
|
|
617
|
-
const promptLower = prompt.toLowerCase();
|
|
618
|
-
const scores = {};
|
|
619
|
-
const matchedTriggers = {};
|
|
620
|
-
|
|
621
|
-
for (const [name, def] of Object.entries(SPECIALIST_DEFS)) {
|
|
622
|
-
const { extensions = [], keywords = [] } = def.triggers || {};
|
|
623
|
-
let score = 0;
|
|
624
|
-
const hits = [];
|
|
625
|
-
|
|
626
|
-
// +2 per matching file extension
|
|
627
|
-
for (const file of files) {
|
|
628
|
-
for (const ext of extensions) {
|
|
629
|
-
if (file.endsWith(ext)) {
|
|
630
|
-
score += 2;
|
|
631
|
-
hits.push(ext);
|
|
632
|
-
break; // count each file once per specialist
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// +1 per matching keyword in prompt
|
|
638
|
-
for (const kw of keywords) {
|
|
639
|
-
// Use word-boundary-aware match where possible
|
|
640
|
-
const re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
|
|
641
|
-
if (re.test(promptLower)) {
|
|
642
|
-
score += 1;
|
|
643
|
-
hits.push(kw);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
scores[name] = score;
|
|
648
|
-
matchedTriggers[name] = hits;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Find highest score
|
|
652
|
-
let best = null;
|
|
653
|
-
let bestScore = 0;
|
|
654
|
-
let bestExtCount = 0;
|
|
655
|
-
|
|
656
|
-
for (const [name, score] of Object.entries(scores)) {
|
|
657
|
-
if (score < 2) continue;
|
|
658
|
-
const extCount = matchedTriggers[name].filter(t => t.startsWith('.')).length;
|
|
659
|
-
if (
|
|
660
|
-
score > bestScore ||
|
|
661
|
-
(score === bestScore && extCount > bestExtCount)
|
|
662
|
-
) {
|
|
663
|
-
best = name;
|
|
664
|
-
bestScore = score;
|
|
665
|
-
bestExtCount = extCount;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (!best) {
|
|
670
|
-
return { specialist: 'generic', confidence: 'low', triggers: [] };
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const confidence = bestScore >= 4 ? 'high' : 'medium';
|
|
674
|
-
return { specialist: best, confidence, triggers: matchedTriggers[best] };
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
678
|
-
|
|
679
|
-
if (process.argv[1] && new URL(import.meta.url).pathname === process.argv[1]) {
|
|
680
|
-
const args = process.argv.slice(2);
|
|
681
|
-
const prompt = args.find(a => !a.startsWith('--')) || '';
|
|
682
|
-
|
|
683
|
-
if (!prompt) {
|
|
684
|
-
console.error('Usage: node src/detect.mjs "task description" [--files a,b]');
|
|
685
|
-
process.exit(1);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const filesFlag = args.find(a => a.startsWith('--files=')) ||
|
|
689
|
-
(args.includes('--files') ? args[args.indexOf('--files') + 1] : null);
|
|
690
|
-
const files = filesFlag
|
|
691
|
-
? filesFlag.replace(/^--files=/, '').split(',').map(f => f.trim()).filter(Boolean)
|
|
692
|
-
: [];
|
|
693
|
-
|
|
694
|
-
const failuresFlag = args.find(a => a.startsWith('--failures=')) ||
|
|
695
|
-
(args.includes('--failures') ? args[args.indexOf('--failures') + 1] : null);
|
|
696
|
-
const priorFailures = failuresFlag ? parseInt(failuresFlag.replace(/^--failures=/, ''), 10) : 0;
|
|
697
|
-
|
|
698
|
-
const result = detectTask({ prompt, files, priorFailures });
|
|
699
|
-
console.log(JSON.stringify(result, null, 2));
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths, classifySpecialist, classifyReasoningDepth, detectSuggestedPlugins };
|