@kolisachint/hoocode-agent 0.4.16 → 0.4.17

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 (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/core/dispatch-evaluator.d.ts +9 -36
  3. package/dist/core/dispatch-evaluator.d.ts.map +1 -1
  4. package/dist/core/dispatch-evaluator.js +11 -302
  5. package/dist/core/dispatch-evaluator.js.map +1 -1
  6. package/dist/core/subagent-pool.d.ts +2 -13
  7. package/dist/core/subagent-pool.d.ts.map +1 -1
  8. package/dist/core/subagent-pool.js +6 -31
  9. package/dist/core/subagent-pool.js.map +1 -1
  10. package/dist/core/token-budget.d.ts +4 -0
  11. package/dist/core/token-budget.d.ts.map +1 -1
  12. package/dist/core/token-budget.js +4 -2
  13. package/dist/core/token-budget.js.map +1 -1
  14. package/dist/core/tools/subagent.d.ts +0 -3
  15. package/dist/core/tools/subagent.d.ts.map +1 -1
  16. package/dist/core/tools/subagent.js +0 -6
  17. package/dist/core/tools/subagent.js.map +1 -1
  18. package/dist/init-templates.generated.d.ts +0 -1
  19. package/dist/init-templates.generated.d.ts.map +1 -1
  20. package/dist/init-templates.generated.js +5 -13
  21. package/dist/init-templates.generated.js.map +1 -1
  22. package/docs/routing.md +48 -36
  23. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  24. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  25. package/examples/extensions/sandbox/package.json +1 -1
  26. package/examples/extensions/with-deps/package.json +1 -1
  27. package/package.json +4 -4
  28. package/templates/agents/doc.md +1 -0
  29. package/templates/agents/edit.md +1 -0
  30. package/templates/agents/explore.md +1 -0
  31. package/templates/agents/review.md +1 -0
  32. package/templates/agents/test.md +1 -0
  33. package/dist/core/subagent.d.ts +0 -14
  34. package/dist/core/subagent.d.ts.map +0 -1
  35. package/dist/core/subagent.js +0 -27
  36. package/dist/core/subagent.js.map +0 -1
  37. package/templates/subagent/doc.md +0 -16
  38. package/templates/subagent/edit.md +0 -22
  39. package/templates/subagent/explore.md +0 -21
  40. package/templates/subagent/fix.md +0 -22
  41. package/templates/subagent/review.md +0 -21
  42. package/templates/subagent/test.md +0 -20
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.17] - 2026-06-01
4
+
5
+ ### Changed
6
+
7
+ - Consolidated subagent prompts to a single source of truth. The duplicate `templates/subagent/**` prompt set (and the generated `EMBEDDED_SUBAGENT_PROMPTS` map) is removed; `templates/agents/**` (the frontmatter registry) is now the only prompt source. The built-in agent set is the canonical five: `explore`, `edit`, `test`, `review`, `doc`. The unreachable `fix` mode (never exposed by the Task tool, `/subagent`, or routing) was dropped.
8
+ - Trimmed `DispatchEvaluator` to its only live responsibilities: the nested-delegation depth guard and a complexity estimate for the dispatch log. Delegation is fully description-driven (the parent agent chooses the agent), so the dead keyword-routing/auto-split surface was removed. Updated `docs/routing.md` to match.
9
+ - Moved built-in subagent tool allowlists into agent frontmatter (`tools:` in `templates/agents/*.md`), making the agent registry the single source of truth for each agent's prompt, tools, and model. The hardcoded `SubagentMode` enum and `MODE_TOOLS` map are gone; `SubagentPool` reads the allowlist solely from the resolved definition.
10
+
11
+ ### Removed
12
+
13
+ - Removed unused exports: `isSubagentRecommended` (tools/subagent), `SubagentPool.dispatchBatch`, and the `DispatchEvaluator` routing helpers (`classifyWithConfidence`, `shouldSplit`, `canHandleInline`, `getReason`).
14
+ - Removed the `core/subagent.ts` module (`SubagentMode`, `SUBAGENT_MODES`, `MODE_TOOLS`); its role is now served by the frontmatter agent registry.
15
+
3
16
  ## [0.4.16] - 2026-06-01
4
17
 
5
18
  ### Fixed
@@ -1,48 +1,21 @@
1
1
  /**
2
- * Deterministic subagent dispatch evaluator.
2
+ * Subagent dispatch guard + lightweight task telemetry.
3
3
  *
4
- * Decides whether a task should be handled inline or delegated to a subagent,
5
- * which subagent type to use, and whether a task should be split across
6
- * multiple subagents. No LLM call — keyword + heuristic only.
4
+ * The parent agent selects which subagent to delegate to (via the Task tool),
5
+ * so this module performs NO keyword routing. It survives for two narrow
6
+ * responsibilities, both cheap and LLM-free:
7
+ * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn
8
+ * further subagents.
9
+ * 2. Complexity estimate — a heuristic recorded in the dispatch log for
10
+ * diagnostics only.
7
11
  */
8
- export type AgentType = "explore" | "edit" | "test" | "review" | "doc";
9
12
  export interface TaskAnalysis {
13
+ /** False only when the depth guard blocks delegation. */
10
14
  should_delegate: boolean;
11
- agent_type: AgentType | null;
12
15
  reason: string;
13
16
  estimated_complexity: "low" | "medium" | "high";
14
- parallelizable: boolean;
15
- context_needed: string[];
16
- /**
17
- * Normalized routing confidence in [0, 1]: the share of matched keyword
18
- * signal that pointed at the chosen agent type. 0 when no keywords matched.
19
- */
20
- confidence: number;
21
17
  }
22
- export interface Subtask {
23
- agent_type: AgentType;
24
- prompt: string;
25
- estimated_files: number;
26
- }
27
- /**
28
- * Classify a task to an agent type with a normalized confidence in [0, 1].
29
- *
30
- * confidence = winning score / total matched score across all agent types, i.e.
31
- * the share of keyword signal that agrees on the winner. 1.0 means every matched
32
- * keyword pointed at one type; lower values mean the signal was split. Returns a
33
- * null type with confidence 0 when nothing matched.
34
- */
35
- export declare function classifyWithConfidence(task: string): {
36
- agent_type: AgentType | null;
37
- confidence: number;
38
- };
39
18
  export declare class DispatchEvaluator {
40
19
  evaluate(task: string): TaskAnalysis;
41
- shouldSplit(task: string): {
42
- split: boolean;
43
- subtasks: Subtask[];
44
- };
45
- canHandleInline(task: string): boolean;
46
- getReason(analysis: TaskAnalysis): string;
47
20
  }
48
21
  //# sourceMappingURL=dispatch-evaluator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-evaluator.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEvE,MAAM,WAAW,YAAY;IAC5B,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAChD,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACvB,UAAU,EAAE,SAAS,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACxB;AA4ID;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAiBzG;AAwGD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CA8CnC;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,CAwCjE;IAED,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErC;IAED,SAAS,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAExC;CACD","sourcesContent":["/**\n * Deterministic subagent dispatch evaluator.\n *\n * Decides whether a task should be handled inline or delegated to a subagent,\n * which subagent type to use, and whether a task should be split across\n * multiple subagents. No LLM call — keyword + heuristic only.\n */\n\nexport type AgentType = \"explore\" | \"edit\" | \"test\" | \"review\" | \"doc\";\n\nexport interface TaskAnalysis {\n\tshould_delegate: boolean;\n\tagent_type: AgentType | null;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n\tparallelizable: boolean;\n\tcontext_needed: string[];\n\t/**\n\t * Normalized routing confidence in [0, 1]: the share of matched keyword\n\t * signal that pointed at the chosen agent type. 0 when no keywords matched.\n\t */\n\tconfidence: number;\n}\n\nexport interface Subtask {\n\tagent_type: AgentType;\n\tprompt: string;\n\testimated_files: number;\n}\n\n/* ------------------------------------------------------------------ */\n// Keyword routing tables\n\nconst EXPLORE_KEYWORDS = [\n\t\"explore\",\n\t\"understand\",\n\t\"scout\",\n\t\"investigate\",\n\t\"trace\",\n\t\"find\",\n\t\"where\",\n\t\"how does\",\n\t\"what is\",\n\t\"lookup\",\n\t\"search\",\n\t\"navigate\",\n\t\"discover\",\n\t\"map out\",\n\t\"get familiar\",\n];\n\nconst EDIT_KEYWORDS = [\n\t\"create\",\n\t\"implement\",\n\t\"refactor\",\n\t\"add\",\n\t\"build\",\n\t\"change\",\n\t\"update\",\n\t\"modify\",\n\t\"fix\",\n\t\"repair\",\n\t\"correct\",\n\t\"migrate\",\n\t\"rename\",\n\t\"remove\",\n\t\"delete\",\n\t\"write\",\n];\n\nconst TEST_KEYWORDS = [\n\t\"test\",\n\t\"validate\",\n\t\"assert\",\n\t\"coverage\",\n\t\"jest\",\n\t\"vitest\",\n\t\"mocha\",\n\t\"pytest\",\n\t\"unit test\",\n\t\"integration test\",\n\t\"e2e test\",\n\t\"regression test\",\n];\n\nconst REVIEW_KEYWORDS = [\n\t\"review\",\n\t\"audit\",\n\t\"critique\",\n\t\"security\",\n\t\"check\",\n\t\"inspect\",\n\t\"verify\",\n\t\"assess\",\n\t\"evaluate\",\n\t\"analyze for\",\n\t\"vulnerab\",\n\t\"perf audit\",\n];\n\nconst DOC_KEYWORDS = [\n\t\"readme\",\n\t\"documentation\",\n\t\"document\",\n\t\"comment\",\n\t\"explain\",\n\t\"docs\",\n\t\"guide\",\n\t\"tutorial\",\n\t\"changelog\",\n\t\"api docs\",\n];\n\nconst CROSS_DOMAIN_MARKERS = [\n\t\" and \",\n\t\" as well as \",\n\t\" plus \",\n\t\" then \",\n\t\" after that \",\n\t\" followed by \",\n\t\" in addition \",\n\t\" simultaneously \",\n];\n\n/* ------------------------------------------------------------------ */\n// Helpers\n\nfunction countMatches(text: string, keywords: readonly string[]): number {\n\tconst lower = text.toLowerCase();\n\treturn keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);\n}\n\n/**\n * Explicit, deterministic tie-break order. When two agent types match the same\n * number of keywords, the earliest type in this list wins. This mirrors the\n * previous (accidental) object-iteration order so routing stays stable, but the\n * order is now intentional and documented rather than incidental.\n */\nconst AGENT_PRIORITY: readonly AgentType[] = [\"explore\", \"edit\", \"test\", \"review\", \"doc\"];\n\nfunction scoreAgents(task: string): Record<AgentType, number> {\n\tconst lower = task.toLowerCase();\n\tconst scores: Record<AgentType, number> = {\n\t\texplore: countMatches(task, EXPLORE_KEYWORDS),\n\t\tedit: countMatches(task, EDIT_KEYWORDS),\n\t\ttest: countMatches(task, TEST_KEYWORDS),\n\t\treview: countMatches(task, REVIEW_KEYWORDS),\n\t\tdoc: countMatches(task, DOC_KEYWORDS),\n\t};\n\n\t// Boost doc when the task is clearly about documentation\n\tif (scores.doc > 0 && (lower.includes(\"readme\") || lower.includes(\"documentation\") || lower.includes(\"document \"))) {\n\t\tscores.doc += 2;\n\t}\n\n\t// Boost test when the task is clearly about testing\n\tif (scores.test > 0 && (lower.includes(\"test\") || lower.includes(\"tests\"))) {\n\t\tscores.test += 2;\n\t}\n\n\t// Boost review for security-related tasks\n\tif (scores.review > 0 && lower.includes(\"security\")) {\n\t\tscores.review += 2;\n\t}\n\n\treturn scores;\n}\n\n/**\n * Classify a task to an agent type with a normalized confidence in [0, 1].\n *\n * confidence = winning score / total matched score across all agent types, i.e.\n * the share of keyword signal that agrees on the winner. 1.0 means every matched\n * keyword pointed at one type; lower values mean the signal was split. Returns a\n * null type with confidence 0 when nothing matched.\n */\nexport function classifyWithConfidence(task: string): { agent_type: AgentType | null; confidence: number } {\n\tconst scores = scoreAgents(task);\n\n\tlet total = 0;\n\tfor (const type of AGENT_PRIORITY) total += scores[type];\n\tif (total === 0) return { agent_type: null, confidence: 0 };\n\n\tlet best: AgentType = AGENT_PRIORITY[0];\n\tlet bestScore = -1;\n\tfor (const type of AGENT_PRIORITY) {\n\t\tif (scores[type] > bestScore) {\n\t\t\tbestScore = scores[type];\n\t\t\tbest = type;\n\t\t}\n\t}\n\n\treturn { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\treturn classifyWithConfidence(task).agent_type;\n}\n\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nfunction canHandleInline(task: string): boolean {\n\tif (estimateComplexity(task) !== \"low\") return false;\n\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tif (fileMatches && fileMatches.length > 1) return false;\n\n\tconst hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));\n\tif (hasCrossDomain) return false;\n\n\t// Exploration: broad tasks delegate; simple lookups can be inline\n\tconst isExplore = detectAgentType(task) === \"explore\";\n\tif (isExplore) {\n\t\tconst broadExplore = /\\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\\b/i.test(task);\n\t\treturn !broadExplore;\n\t}\n\n\t// Documentation tasks always delegate to the doc subagent\n\tif (detectAgentType(task) === \"doc\") {\n\t\treturn false;\n\t}\n\n\tconst isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;\n\tconst isTrivialEdit =\n\t\tcountMatches(task, EDIT_KEYWORDS) > 0 && !/\\b(create|implement|build|refactor|migrate|restructure)\\b/i.test(task);\n\n\treturn isReadOnly || isTrivialEdit;\n}\n\nfunction extractSubtasks(task: string): Subtask[] {\n\t// Split on sentence boundaries and conjunctions, then classify each segment.\n\tconst segments = task\n\t\t.split(/(?:[,;]|\\.(?:\\s+|$))\\s*/)\n\t\t.map((s) => s.trim())\n\t\t.filter((s) => s.length > 10);\n\n\tif (segments.length < 2) {\n\t\t// No obvious sentence split — try cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length > 0) {\n\t\t\tparts.push(remaining);\n\t\t\treturn parts\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\t\tif (!type) return null;\n\t\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tagent_type: type,\n\t\t\t\t\t\tprompt: p,\n\t\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t.filter((s): s is Subtask => s !== null);\n\t\t}\n\t\treturn [];\n\t}\n\n\treturn segments\n\t\t.map((segment) => {\n\t\t\tconst type = detectAgentType(segment);\n\t\t\tif (!type) return null;\n\t\t\tconst est = estimateComplexity(segment);\n\t\t\treturn {\n\t\t\t\tagent_type: type,\n\t\t\t\tprompt: segment,\n\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t};\n\t\t})\n\t\t.filter((s): s is Subtask => s !== null);\n}\n\n/* ------------------------------------------------------------------ */\n// Evaluator\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t\tconfidence: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst { agent_type: agentType, confidence } = classifyWithConfidence(task);\n\t\tconst complexity = estimateComplexity(task);\n\t\tconst inline = canHandleInline(task);\n\t\tconst subtasks = extractSubtasks(task);\n\t\tconst parallelizable = subtasks.length > 1 || (complexity === \"high\" && subtasks.length > 0);\n\n\t\tif (inline) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: `Simple ${agentType ?? \"task\"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,\n\t\t\t\testimated_complexity: complexity,\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t\tconfidence,\n\t\t\t};\n\t\t}\n\n\t\t// When delegating, a missing keyword match defaults to explore: a fresh\n\t\t// read-only investigation is the safest start for an ambiguous task, and the\n\t\t// parent can re-delegate with a specific mode afterwards.\n\t\tconst delegateType: AgentType = agentType ?? \"explore\";\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: delegateType,\n\t\t\treason: `${delegateType} task with ${complexity} complexity requires isolated subagent`,\n\t\t\testimated_complexity: complexity,\n\t\t\tparallelizable,\n\t\t\tcontext_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],\n\t\t\tconfidence,\n\t\t};\n\t}\n\n\tshouldSplit(task: string): { split: boolean; subtasks: Subtask[] } {\n\t\tconst subtasks = extractSubtasks(task);\n\t\tif (subtasks.length >= 2) {\n\t\t\treturn { split: true, subtasks };\n\t\t}\n\n\t\t// Check for explicit multi-domain keywords even when sentence splitting failed\n\t\tconst multiDomain =\n\t\t\t/\\b(implement|write|create|refactor|fix|test|review|document|explore)\\b.*\\b(and|also|plus|then|followed by)\\b.*\\b(test|review|document|explore|implement|write|create|refactor|fix)\\b/i.test(\n\t\t\t\ttask,\n\t\t\t);\n\t\tif (!multiDomain) return { split: false, subtasks: [] };\n\n\t\t// Force split using cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length === 0) return { split: false, subtasks: [] };\n\t\tparts.push(remaining);\n\n\t\tconst forcedSubtasks = parts\n\t\t\t.map((p) => {\n\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\tif (!type) return null;\n\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\treturn {\n\t\t\t\t\tagent_type: type,\n\t\t\t\t\tprompt: p,\n\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s): s is Subtask => s !== null);\n\n\t\treturn forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };\n\t}\n\n\tcanHandleInline(task: string): boolean {\n\t\treturn canHandleInline(task);\n\t}\n\n\tgetReason(analysis: TaskAnalysis): string {\n\t\treturn analysis.reason;\n\t}\n}\n"]}
1
+ {"version":3,"file":"dispatch-evaluator.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,YAAY;IAC5B,yDAAyD;IACzD,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,oBAAoB,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAChD;AAoBD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAenC;CACD","sourcesContent":["/**\n * Subagent dispatch guard + lightweight task telemetry.\n *\n * The parent agent selects which subagent to delegate to (via the Task tool),\n * so this module performs NO keyword routing. It survives for two narrow\n * responsibilities, both cheap and LLM-free:\n * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn\n * further subagents.\n * 2. Complexity estimate — a heuristic recorded in the dispatch log for\n * diagnostics only.\n */\n\nexport interface TaskAnalysis {\n\t/** False only when the depth guard blocks delegation. */\n\tshould_delegate: boolean;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n}\n\n/** Heuristic complexity estimate from file/line/scope mentions in the task. */\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\treason: \"delegated to subagent\",\n\t\t\testimated_complexity: estimateComplexity(task),\n\t\t};\n\t}\n}\n"]}
@@ -1,161 +1,15 @@
1
1
  /**
2
- * Deterministic subagent dispatch evaluator.
2
+ * Subagent dispatch guard + lightweight task telemetry.
3
3
  *
4
- * Decides whether a task should be handled inline or delegated to a subagent,
5
- * which subagent type to use, and whether a task should be split across
6
- * multiple subagents. No LLM call — keyword + heuristic only.
4
+ * The parent agent selects which subagent to delegate to (via the Task tool),
5
+ * so this module performs NO keyword routing. It survives for two narrow
6
+ * responsibilities, both cheap and LLM-free:
7
+ * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn
8
+ * further subagents.
9
+ * 2. Complexity estimate — a heuristic recorded in the dispatch log for
10
+ * diagnostics only.
7
11
  */
8
- /* ------------------------------------------------------------------ */
9
- // Keyword routing tables
10
- const EXPLORE_KEYWORDS = [
11
- "explore",
12
- "understand",
13
- "scout",
14
- "investigate",
15
- "trace",
16
- "find",
17
- "where",
18
- "how does",
19
- "what is",
20
- "lookup",
21
- "search",
22
- "navigate",
23
- "discover",
24
- "map out",
25
- "get familiar",
26
- ];
27
- const EDIT_KEYWORDS = [
28
- "create",
29
- "implement",
30
- "refactor",
31
- "add",
32
- "build",
33
- "change",
34
- "update",
35
- "modify",
36
- "fix",
37
- "repair",
38
- "correct",
39
- "migrate",
40
- "rename",
41
- "remove",
42
- "delete",
43
- "write",
44
- ];
45
- const TEST_KEYWORDS = [
46
- "test",
47
- "validate",
48
- "assert",
49
- "coverage",
50
- "jest",
51
- "vitest",
52
- "mocha",
53
- "pytest",
54
- "unit test",
55
- "integration test",
56
- "e2e test",
57
- "regression test",
58
- ];
59
- const REVIEW_KEYWORDS = [
60
- "review",
61
- "audit",
62
- "critique",
63
- "security",
64
- "check",
65
- "inspect",
66
- "verify",
67
- "assess",
68
- "evaluate",
69
- "analyze for",
70
- "vulnerab",
71
- "perf audit",
72
- ];
73
- const DOC_KEYWORDS = [
74
- "readme",
75
- "documentation",
76
- "document",
77
- "comment",
78
- "explain",
79
- "docs",
80
- "guide",
81
- "tutorial",
82
- "changelog",
83
- "api docs",
84
- ];
85
- const CROSS_DOMAIN_MARKERS = [
86
- " and ",
87
- " as well as ",
88
- " plus ",
89
- " then ",
90
- " after that ",
91
- " followed by ",
92
- " in addition ",
93
- " simultaneously ",
94
- ];
95
- /* ------------------------------------------------------------------ */
96
- // Helpers
97
- function countMatches(text, keywords) {
98
- const lower = text.toLowerCase();
99
- return keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);
100
- }
101
- /**
102
- * Explicit, deterministic tie-break order. When two agent types match the same
103
- * number of keywords, the earliest type in this list wins. This mirrors the
104
- * previous (accidental) object-iteration order so routing stays stable, but the
105
- * order is now intentional and documented rather than incidental.
106
- */
107
- const AGENT_PRIORITY = ["explore", "edit", "test", "review", "doc"];
108
- function scoreAgents(task) {
109
- const lower = task.toLowerCase();
110
- const scores = {
111
- explore: countMatches(task, EXPLORE_KEYWORDS),
112
- edit: countMatches(task, EDIT_KEYWORDS),
113
- test: countMatches(task, TEST_KEYWORDS),
114
- review: countMatches(task, REVIEW_KEYWORDS),
115
- doc: countMatches(task, DOC_KEYWORDS),
116
- };
117
- // Boost doc when the task is clearly about documentation
118
- if (scores.doc > 0 && (lower.includes("readme") || lower.includes("documentation") || lower.includes("document "))) {
119
- scores.doc += 2;
120
- }
121
- // Boost test when the task is clearly about testing
122
- if (scores.test > 0 && (lower.includes("test") || lower.includes("tests"))) {
123
- scores.test += 2;
124
- }
125
- // Boost review for security-related tasks
126
- if (scores.review > 0 && lower.includes("security")) {
127
- scores.review += 2;
128
- }
129
- return scores;
130
- }
131
- /**
132
- * Classify a task to an agent type with a normalized confidence in [0, 1].
133
- *
134
- * confidence = winning score / total matched score across all agent types, i.e.
135
- * the share of keyword signal that agrees on the winner. 1.0 means every matched
136
- * keyword pointed at one type; lower values mean the signal was split. Returns a
137
- * null type with confidence 0 when nothing matched.
138
- */
139
- export function classifyWithConfidence(task) {
140
- const scores = scoreAgents(task);
141
- let total = 0;
142
- for (const type of AGENT_PRIORITY)
143
- total += scores[type];
144
- if (total === 0)
145
- return { agent_type: null, confidence: 0 };
146
- let best = AGENT_PRIORITY[0];
147
- let bestScore = -1;
148
- for (const type of AGENT_PRIORITY) {
149
- if (scores[type] > bestScore) {
150
- bestScore = scores[type];
151
- best = type;
152
- }
153
- }
154
- return { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };
155
- }
156
- function detectAgentType(task) {
157
- return classifyWithConfidence(task).agent_type;
158
- }
12
+ /** Heuristic complexity estimate from file/line/scope mentions in the task. */
159
13
  function estimateComplexity(task) {
160
14
  const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
161
15
  const fileCount = fileMatches ? fileMatches.length : 0;
@@ -169,166 +23,21 @@ function estimateComplexity(task) {
169
23
  return "medium";
170
24
  return "low";
171
25
  }
172
- function canHandleInline(task) {
173
- if (estimateComplexity(task) !== "low")
174
- return false;
175
- const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
176
- if (fileMatches && fileMatches.length > 1)
177
- return false;
178
- const hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));
179
- if (hasCrossDomain)
180
- return false;
181
- // Exploration: broad tasks delegate; simple lookups can be inline
182
- const isExplore = detectAgentType(task) === "explore";
183
- if (isExplore) {
184
- const broadExplore = /\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\b/i.test(task);
185
- return !broadExplore;
186
- }
187
- // Documentation tasks always delegate to the doc subagent
188
- if (detectAgentType(task) === "doc") {
189
- return false;
190
- }
191
- const isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;
192
- const isTrivialEdit = countMatches(task, EDIT_KEYWORDS) > 0 && !/\b(create|implement|build|refactor|migrate|restructure)\b/i.test(task);
193
- return isReadOnly || isTrivialEdit;
194
- }
195
- function extractSubtasks(task) {
196
- // Split on sentence boundaries and conjunctions, then classify each segment.
197
- const segments = task
198
- .split(/(?:[,;]|\.(?:\s+|$))\s*/)
199
- .map((s) => s.trim())
200
- .filter((s) => s.length > 10);
201
- if (segments.length < 2) {
202
- // No obvious sentence split — try cross-domain markers
203
- const parts = [];
204
- let remaining = task;
205
- for (const marker of CROSS_DOMAIN_MARKERS) {
206
- const idx = remaining.toLowerCase().indexOf(marker);
207
- if (idx !== -1) {
208
- parts.push(remaining.slice(0, idx).trim());
209
- remaining = remaining.slice(idx + marker.length).trim();
210
- }
211
- }
212
- if (parts.length > 0) {
213
- parts.push(remaining);
214
- return parts
215
- .map((p) => {
216
- const type = detectAgentType(p);
217
- if (!type)
218
- return null;
219
- const est = estimateComplexity(p);
220
- return {
221
- agent_type: type,
222
- prompt: p,
223
- estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
224
- };
225
- })
226
- .filter((s) => s !== null);
227
- }
228
- return [];
229
- }
230
- return segments
231
- .map((segment) => {
232
- const type = detectAgentType(segment);
233
- if (!type)
234
- return null;
235
- const est = estimateComplexity(segment);
236
- return {
237
- agent_type: type,
238
- prompt: segment,
239
- estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
240
- };
241
- })
242
- .filter((s) => s !== null);
243
- }
244
- /* ------------------------------------------------------------------ */
245
- // Evaluator
246
26
  export class DispatchEvaluator {
247
27
  evaluate(task) {
248
28
  const depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? "0", 10);
249
29
  if (depth >= 1) {
250
30
  return {
251
31
  should_delegate: false,
252
- agent_type: null,
253
32
  reason: "Subagents cannot spawn subagents",
254
33
  estimated_complexity: "low",
255
- parallelizable: false,
256
- context_needed: [],
257
- confidence: 0,
258
34
  };
259
35
  }
260
- const { agent_type: agentType, confidence } = classifyWithConfidence(task);
261
- const complexity = estimateComplexity(task);
262
- const inline = canHandleInline(task);
263
- const subtasks = extractSubtasks(task);
264
- const parallelizable = subtasks.length > 1 || (complexity === "high" && subtasks.length > 0);
265
- if (inline) {
266
- return {
267
- should_delegate: false,
268
- agent_type: null,
269
- reason: `Simple ${agentType ?? "task"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,
270
- estimated_complexity: complexity,
271
- parallelizable: false,
272
- context_needed: [],
273
- confidence,
274
- };
275
- }
276
- // When delegating, a missing keyword match defaults to explore: a fresh
277
- // read-only investigation is the safest start for an ambiguous task, and the
278
- // parent can re-delegate with a specific mode afterwards.
279
- const delegateType = agentType ?? "explore";
280
36
  return {
281
37
  should_delegate: true,
282
- agent_type: delegateType,
283
- reason: `${delegateType} task with ${complexity} complexity requires isolated subagent`,
284
- estimated_complexity: complexity,
285
- parallelizable,
286
- context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
287
- confidence,
38
+ reason: "delegated to subagent",
39
+ estimated_complexity: estimateComplexity(task),
288
40
  };
289
41
  }
290
- shouldSplit(task) {
291
- const subtasks = extractSubtasks(task);
292
- if (subtasks.length >= 2) {
293
- return { split: true, subtasks };
294
- }
295
- // Check for explicit multi-domain keywords even when sentence splitting failed
296
- const multiDomain = /\b(implement|write|create|refactor|fix|test|review|document|explore)\b.*\b(and|also|plus|then|followed by)\b.*\b(test|review|document|explore|implement|write|create|refactor|fix)\b/i.test(task);
297
- if (!multiDomain)
298
- return { split: false, subtasks: [] };
299
- // Force split using cross-domain markers
300
- const parts = [];
301
- let remaining = task;
302
- for (const marker of CROSS_DOMAIN_MARKERS) {
303
- const idx = remaining.toLowerCase().indexOf(marker);
304
- if (idx !== -1) {
305
- parts.push(remaining.slice(0, idx).trim());
306
- remaining = remaining.slice(idx + marker.length).trim();
307
- }
308
- }
309
- if (parts.length === 0)
310
- return { split: false, subtasks: [] };
311
- parts.push(remaining);
312
- const forcedSubtasks = parts
313
- .map((p) => {
314
- const type = detectAgentType(p);
315
- if (!type)
316
- return null;
317
- const est = estimateComplexity(p);
318
- return {
319
- agent_type: type,
320
- prompt: p,
321
- estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
322
- };
323
- })
324
- .filter((s) => s !== null);
325
- return forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };
326
- }
327
- canHandleInline(task) {
328
- return canHandleInline(task);
329
- }
330
- getReason(analysis) {
331
- return analysis.reason;
332
- }
333
42
  }
334
43
  //# sourceMappingURL=dispatch-evaluator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-evaluator.js","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,wEAAwE;AACxE,yBAAyB;AAEzB,MAAM,gBAAgB,GAAG;IACxB,SAAS;IACT,YAAY;IACZ,OAAO;IACP,aAAa;IACb,OAAO;IACP,MAAM;IACN,OAAO;IACP,UAAU;IACV,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,UAAU;IACV,SAAS;IACT,cAAc;CACd,CAAC;AAEF,MAAM,aAAa,GAAG;IACrB,QAAQ;IACR,WAAW;IACX,UAAU;IACV,KAAK;IACL,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,KAAK;IACL,QAAQ;IACR,SAAS;IACT,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,QAAQ;IACR,OAAO;CACP,CAAC;AAEF,MAAM,aAAa,GAAG;IACrB,MAAM;IACN,UAAU;IACV,QAAQ;IACR,UAAU;IACV,MAAM;IACN,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,WAAW;IACX,kBAAkB;IAClB,UAAU;IACV,iBAAiB;CACjB,CAAC;AAEF,MAAM,eAAe,GAAG;IACvB,QAAQ;IACR,OAAO;IACP,UAAU;IACV,UAAU;IACV,OAAO;IACP,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,aAAa;IACb,UAAU;IACV,YAAY;CACZ,CAAC;AAEF,MAAM,YAAY,GAAG;IACpB,QAAQ;IACR,eAAe;IACf,UAAU;IACV,SAAS;IACT,SAAS;IACT,MAAM;IACN,OAAO;IACP,UAAU;IACV,WAAW;IACX,UAAU;CACV,CAAC;AAEF,MAAM,oBAAoB,GAAG;IAC5B,OAAO;IACP,cAAc;IACd,QAAQ;IACR,QAAQ;IACR,cAAc;IACd,eAAe;IACf,eAAe;IACf,kBAAkB;CAClB,CAAC;AAEF,wEAAwE;AACxE,UAAU;AAEV,SAAS,YAAY,CAAC,IAAY,EAAE,QAA2B,EAAU;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAAA,CAC/E;AAED;;;;;GAKG;AACH,MAAM,cAAc,GAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;AAE1F,SAAS,WAAW,CAAC,IAAY,EAA6B;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,MAAM,MAAM,GAA8B;QACzC,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,gBAAgB,CAAC;QAC7C,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC;QACvC,IAAI,EAAE,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC;QACvC,MAAM,EAAE,YAAY,CAAC,IAAI,EAAE,eAAe,CAAC;QAC3C,GAAG,EAAE,YAAY,CAAC,IAAI,EAAE,YAAY,CAAC;KACrC,CAAC;IAEF,yDAAyD;IACzD,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;QACpH,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACjB,CAAC;IAED,oDAAoD;IACpD,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QAC5E,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;IAClB,CAAC;IAED,0CAA0C;IAC1C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QACrD,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IACpB,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY,EAAwD;IAC1G,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAEjC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,cAAc;QAAE,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAE5D,IAAI,IAAI,GAAc,cAAc,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC;IACnB,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QACnC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC;YAC9B,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YACzB,IAAI,GAAG,IAAI,CAAC;QACb,CAAC;IACF,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,GAAG,KAAK,EAAE,CAAC;AAAA,CAClF;AAED,SAAS,eAAe,CAAC,IAAY,EAAoB;IACxD,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC;AAAA,CAC/C;AAED,SAAS,kBAAkB,CAAC,IAAY,EAA6B;IACpE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC5G,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,SAAS,GAAG,wFAAwF,CAAC,IAAI,CAC9G,IAAI,CACJ,CAAC;IACF,MAAM,WAAW,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhG,IAAI,SAAS,GAAG,GAAG,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS;QAAE,OAAO,MAAM,CAAC;IAClE,IAAI,SAAS,GAAG,EAAE,IAAI,SAAS,IAAI,CAAC,IAAI,WAAW;QAAE,OAAO,QAAQ,CAAC;IACrE,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,eAAe,CAAC,IAAY,EAAW;IAC/C,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAErD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC5G,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAExD,MAAM,cAAc,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACxF,IAAI,cAAc;QAAE,OAAO,KAAK,CAAC;IAEjC,kEAAkE;IAClE,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;IACtD,IAAI,SAAS,EAAE,CAAC;QACf,MAAM,YAAY,GAAG,gFAAgF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjH,OAAO,CAAC,YAAY,CAAC;IACtB,CAAC;IAED,0DAA0D;IAC1D,IAAI,eAAe,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;QACrC,OAAO,KAAK,CAAC;IACd,CAAC;IAED,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,EAAE,gBAAgB,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IACvG,MAAM,aAAa,GAClB,YAAY,CAAC,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,4DAA4D,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEnH,OAAO,UAAU,IAAI,aAAa,CAAC;AAAA,CACnC;AAED,SAAS,eAAe,CAAC,IAAY,EAAa;IACjD,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,IAAI;SACnB,KAAK,CAAC,yBAAyB,CAAC;SAChC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAE/B,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,yDAAuD;QACvD,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,SAAS,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,MAAM,IAAI,oBAAoB,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACpD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBAChB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC3C,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YACzD,CAAC;QACF,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACtB,OAAO,KAAK;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACX,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;gBAChC,IAAI,CAAC,IAAI;oBAAE,OAAO,IAAI,CAAC;gBACvB,MAAM,GAAG,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;gBAClC,OAAO;oBACN,UAAU,EAAE,IAAI;oBAChB,MAAM,EAAE,CAAC;oBACT,eAAe,EAAE,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;iBAC9D,CAAC;YAAA,CACF,CAAC;iBACD,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,EAAE,CAAC;IACX,CAAC;IAED,OAAO,QAAQ;SACb,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,MAAM,GAAG,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACxC,OAAO;YACN,UAAU,EAAE,IAAI;YAChB,MAAM,EAAE,OAAO;YACf,eAAe,EAAE,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAC9D,CAAC;IAAA,CACF,CAAC;SACD,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;AAAA,CAC1C;AAED,wEAAwE;AACxE,YAAY;AAEZ,MAAM,OAAO,iBAAiB;IAC7B,QAAQ,CAAC,IAAY,EAAgB;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7E,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAChB,OAAO;gBACN,eAAe,EAAE,KAAK;gBACtB,UAAU,EAAE,IAAI;gBAChB,MAAM,EAAE,kCAAkC;gBAC1C,oBAAoB,EAAE,KAAK;gBAC3B,cAAc,EAAE,KAAK;gBACrB,cAAc,EAAE,EAAE;gBAClB,UAAU,EAAE,CAAC;aACb,CAAC;QACH,CAAC;QAED,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;QAC3E,MAAM,UAAU,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,UAAU,KAAK,MAAM,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAE7F,IAAI,MAAM,EAAE,CAAC;YACZ,OAAO;gBACN,eAAe,EAAE,KAAK;gBACtB,UAAU,EAAE,IAAI;gBAChB,MAAM,EAAE,UAAU,SAAS,IAAI,MAAM,oEAAoE;gBACzG,oBAAoB,EAAE,UAAU;gBAChC,cAAc,EAAE,KAAK;gBACrB,cAAc,EAAE,EAAE;gBAClB,UAAU;aACV,CAAC;QACH,CAAC;QAED,wEAAwE;QACxE,6EAA6E;QAC7E,0DAA0D;QAC1D,MAAM,YAAY,GAAc,SAAS,IAAI,SAAS,CAAC;QAEvD,OAAO;YACN,eAAe,EAAE,IAAI;YACrB,UAAU,EAAE,YAAY;YACxB,MAAM,EAAE,GAAG,YAAY,cAAc,UAAU,wCAAwC;YACvF,oBAAoB,EAAE,UAAU;YAChC,cAAc;YACd,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YACzE,UAAU;SACV,CAAC;IAAA,CACF;IAED,WAAW,CAAC,IAAY,EAA2C;QAClE,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QAClC,CAAC;QAED,+EAA+E;QAC/E,MAAM,WAAW,GAChB,uLAAuL,CAAC,IAAI,CAC3L,IAAI,CACJ,CAAC;QACH,IAAI,CAAC,WAAW;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAExD,yCAAyC;QACzC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,SAAS,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,MAAM,IAAI,oBAAoB,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACpD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBAChB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC3C,SAAS,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YACzD,CAAC;QACF,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;QAC9D,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAEtB,MAAM,cAAc,GAAG,KAAK;aAC1B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACX,MAAM,IAAI,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC;YAChC,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YACvB,MAAM,GAAG,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;YAClC,OAAO;gBACN,UAAU,EAAE,IAAI;gBAChB,MAAM,EAAE,CAAC;gBACT,eAAe,EAAE,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aAC9D,CAAC;QAAA,CACF,CAAC;aACD,MAAM,CAAC,CAAC,CAAC,EAAgB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QAE1C,OAAO,cAAc,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAAA,CAC/G;IAED,eAAe,CAAC,IAAY,EAAW;QACtC,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC;IAAA,CAC7B;IAED,SAAS,CAAC,QAAsB,EAAU;QACzC,OAAO,QAAQ,CAAC,MAAM,CAAC;IAAA,CACvB;CACD","sourcesContent":["/**\n * Deterministic subagent dispatch evaluator.\n *\n * Decides whether a task should be handled inline or delegated to a subagent,\n * which subagent type to use, and whether a task should be split across\n * multiple subagents. No LLM call — keyword + heuristic only.\n */\n\nexport type AgentType = \"explore\" | \"edit\" | \"test\" | \"review\" | \"doc\";\n\nexport interface TaskAnalysis {\n\tshould_delegate: boolean;\n\tagent_type: AgentType | null;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n\tparallelizable: boolean;\n\tcontext_needed: string[];\n\t/**\n\t * Normalized routing confidence in [0, 1]: the share of matched keyword\n\t * signal that pointed at the chosen agent type. 0 when no keywords matched.\n\t */\n\tconfidence: number;\n}\n\nexport interface Subtask {\n\tagent_type: AgentType;\n\tprompt: string;\n\testimated_files: number;\n}\n\n/* ------------------------------------------------------------------ */\n// Keyword routing tables\n\nconst EXPLORE_KEYWORDS = [\n\t\"explore\",\n\t\"understand\",\n\t\"scout\",\n\t\"investigate\",\n\t\"trace\",\n\t\"find\",\n\t\"where\",\n\t\"how does\",\n\t\"what is\",\n\t\"lookup\",\n\t\"search\",\n\t\"navigate\",\n\t\"discover\",\n\t\"map out\",\n\t\"get familiar\",\n];\n\nconst EDIT_KEYWORDS = [\n\t\"create\",\n\t\"implement\",\n\t\"refactor\",\n\t\"add\",\n\t\"build\",\n\t\"change\",\n\t\"update\",\n\t\"modify\",\n\t\"fix\",\n\t\"repair\",\n\t\"correct\",\n\t\"migrate\",\n\t\"rename\",\n\t\"remove\",\n\t\"delete\",\n\t\"write\",\n];\n\nconst TEST_KEYWORDS = [\n\t\"test\",\n\t\"validate\",\n\t\"assert\",\n\t\"coverage\",\n\t\"jest\",\n\t\"vitest\",\n\t\"mocha\",\n\t\"pytest\",\n\t\"unit test\",\n\t\"integration test\",\n\t\"e2e test\",\n\t\"regression test\",\n];\n\nconst REVIEW_KEYWORDS = [\n\t\"review\",\n\t\"audit\",\n\t\"critique\",\n\t\"security\",\n\t\"check\",\n\t\"inspect\",\n\t\"verify\",\n\t\"assess\",\n\t\"evaluate\",\n\t\"analyze for\",\n\t\"vulnerab\",\n\t\"perf audit\",\n];\n\nconst DOC_KEYWORDS = [\n\t\"readme\",\n\t\"documentation\",\n\t\"document\",\n\t\"comment\",\n\t\"explain\",\n\t\"docs\",\n\t\"guide\",\n\t\"tutorial\",\n\t\"changelog\",\n\t\"api docs\",\n];\n\nconst CROSS_DOMAIN_MARKERS = [\n\t\" and \",\n\t\" as well as \",\n\t\" plus \",\n\t\" then \",\n\t\" after that \",\n\t\" followed by \",\n\t\" in addition \",\n\t\" simultaneously \",\n];\n\n/* ------------------------------------------------------------------ */\n// Helpers\n\nfunction countMatches(text: string, keywords: readonly string[]): number {\n\tconst lower = text.toLowerCase();\n\treturn keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);\n}\n\n/**\n * Explicit, deterministic tie-break order. When two agent types match the same\n * number of keywords, the earliest type in this list wins. This mirrors the\n * previous (accidental) object-iteration order so routing stays stable, but the\n * order is now intentional and documented rather than incidental.\n */\nconst AGENT_PRIORITY: readonly AgentType[] = [\"explore\", \"edit\", \"test\", \"review\", \"doc\"];\n\nfunction scoreAgents(task: string): Record<AgentType, number> {\n\tconst lower = task.toLowerCase();\n\tconst scores: Record<AgentType, number> = {\n\t\texplore: countMatches(task, EXPLORE_KEYWORDS),\n\t\tedit: countMatches(task, EDIT_KEYWORDS),\n\t\ttest: countMatches(task, TEST_KEYWORDS),\n\t\treview: countMatches(task, REVIEW_KEYWORDS),\n\t\tdoc: countMatches(task, DOC_KEYWORDS),\n\t};\n\n\t// Boost doc when the task is clearly about documentation\n\tif (scores.doc > 0 && (lower.includes(\"readme\") || lower.includes(\"documentation\") || lower.includes(\"document \"))) {\n\t\tscores.doc += 2;\n\t}\n\n\t// Boost test when the task is clearly about testing\n\tif (scores.test > 0 && (lower.includes(\"test\") || lower.includes(\"tests\"))) {\n\t\tscores.test += 2;\n\t}\n\n\t// Boost review for security-related tasks\n\tif (scores.review > 0 && lower.includes(\"security\")) {\n\t\tscores.review += 2;\n\t}\n\n\treturn scores;\n}\n\n/**\n * Classify a task to an agent type with a normalized confidence in [0, 1].\n *\n * confidence = winning score / total matched score across all agent types, i.e.\n * the share of keyword signal that agrees on the winner. 1.0 means every matched\n * keyword pointed at one type; lower values mean the signal was split. Returns a\n * null type with confidence 0 when nothing matched.\n */\nexport function classifyWithConfidence(task: string): { agent_type: AgentType | null; confidence: number } {\n\tconst scores = scoreAgents(task);\n\n\tlet total = 0;\n\tfor (const type of AGENT_PRIORITY) total += scores[type];\n\tif (total === 0) return { agent_type: null, confidence: 0 };\n\n\tlet best: AgentType = AGENT_PRIORITY[0];\n\tlet bestScore = -1;\n\tfor (const type of AGENT_PRIORITY) {\n\t\tif (scores[type] > bestScore) {\n\t\t\tbestScore = scores[type];\n\t\t\tbest = type;\n\t\t}\n\t}\n\n\treturn { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };\n}\n\nfunction detectAgentType(task: string): AgentType | null {\n\treturn classifyWithConfidence(task).agent_type;\n}\n\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nfunction canHandleInline(task: string): boolean {\n\tif (estimateComplexity(task) !== \"low\") return false;\n\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tif (fileMatches && fileMatches.length > 1) return false;\n\n\tconst hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));\n\tif (hasCrossDomain) return false;\n\n\t// Exploration: broad tasks delegate; simple lookups can be inline\n\tconst isExplore = detectAgentType(task) === \"explore\";\n\tif (isExplore) {\n\t\tconst broadExplore = /\\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\\b/i.test(task);\n\t\treturn !broadExplore;\n\t}\n\n\t// Documentation tasks always delegate to the doc subagent\n\tif (detectAgentType(task) === \"doc\") {\n\t\treturn false;\n\t}\n\n\tconst isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;\n\tconst isTrivialEdit =\n\t\tcountMatches(task, EDIT_KEYWORDS) > 0 && !/\\b(create|implement|build|refactor|migrate|restructure)\\b/i.test(task);\n\n\treturn isReadOnly || isTrivialEdit;\n}\n\nfunction extractSubtasks(task: string): Subtask[] {\n\t// Split on sentence boundaries and conjunctions, then classify each segment.\n\tconst segments = task\n\t\t.split(/(?:[,;]|\\.(?:\\s+|$))\\s*/)\n\t\t.map((s) => s.trim())\n\t\t.filter((s) => s.length > 10);\n\n\tif (segments.length < 2) {\n\t\t// No obvious sentence split — try cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length > 0) {\n\t\t\tparts.push(remaining);\n\t\t\treturn parts\n\t\t\t\t.map((p) => {\n\t\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\t\tif (!type) return null;\n\t\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tagent_type: type,\n\t\t\t\t\t\tprompt: p,\n\t\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t\t};\n\t\t\t\t})\n\t\t\t\t.filter((s): s is Subtask => s !== null);\n\t\t}\n\t\treturn [];\n\t}\n\n\treturn segments\n\t\t.map((segment) => {\n\t\t\tconst type = detectAgentType(segment);\n\t\t\tif (!type) return null;\n\t\t\tconst est = estimateComplexity(segment);\n\t\t\treturn {\n\t\t\t\tagent_type: type,\n\t\t\t\tprompt: segment,\n\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t};\n\t\t})\n\t\t.filter((s): s is Subtask => s !== null);\n}\n\n/* ------------------------------------------------------------------ */\n// Evaluator\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t\tconfidence: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst { agent_type: agentType, confidence } = classifyWithConfidence(task);\n\t\tconst complexity = estimateComplexity(task);\n\t\tconst inline = canHandleInline(task);\n\t\tconst subtasks = extractSubtasks(task);\n\t\tconst parallelizable = subtasks.length > 1 || (complexity === \"high\" && subtasks.length > 0);\n\n\t\tif (inline) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\tagent_type: null,\n\t\t\t\treason: `Simple ${agentType ?? \"task\"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,\n\t\t\t\testimated_complexity: complexity,\n\t\t\t\tparallelizable: false,\n\t\t\t\tcontext_needed: [],\n\t\t\t\tconfidence,\n\t\t\t};\n\t\t}\n\n\t\t// When delegating, a missing keyword match defaults to explore: a fresh\n\t\t// read-only investigation is the safest start for an ambiguous task, and the\n\t\t// parent can re-delegate with a specific mode afterwards.\n\t\tconst delegateType: AgentType = agentType ?? \"explore\";\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: delegateType,\n\t\t\treason: `${delegateType} task with ${complexity} complexity requires isolated subagent`,\n\t\t\testimated_complexity: complexity,\n\t\t\tparallelizable,\n\t\t\tcontext_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],\n\t\t\tconfidence,\n\t\t};\n\t}\n\n\tshouldSplit(task: string): { split: boolean; subtasks: Subtask[] } {\n\t\tconst subtasks = extractSubtasks(task);\n\t\tif (subtasks.length >= 2) {\n\t\t\treturn { split: true, subtasks };\n\t\t}\n\n\t\t// Check for explicit multi-domain keywords even when sentence splitting failed\n\t\tconst multiDomain =\n\t\t\t/\\b(implement|write|create|refactor|fix|test|review|document|explore)\\b.*\\b(and|also|plus|then|followed by)\\b.*\\b(test|review|document|explore|implement|write|create|refactor|fix)\\b/i.test(\n\t\t\t\ttask,\n\t\t\t);\n\t\tif (!multiDomain) return { split: false, subtasks: [] };\n\n\t\t// Force split using cross-domain markers\n\t\tconst parts: string[] = [];\n\t\tlet remaining = task;\n\t\tfor (const marker of CROSS_DOMAIN_MARKERS) {\n\t\t\tconst idx = remaining.toLowerCase().indexOf(marker);\n\t\t\tif (idx !== -1) {\n\t\t\t\tparts.push(remaining.slice(0, idx).trim());\n\t\t\t\tremaining = remaining.slice(idx + marker.length).trim();\n\t\t\t}\n\t\t}\n\t\tif (parts.length === 0) return { split: false, subtasks: [] };\n\t\tparts.push(remaining);\n\n\t\tconst forcedSubtasks = parts\n\t\t\t.map((p) => {\n\t\t\t\tconst type = detectAgentType(p);\n\t\t\t\tif (!type) return null;\n\t\t\t\tconst est = estimateComplexity(p);\n\t\t\t\treturn {\n\t\t\t\t\tagent_type: type,\n\t\t\t\t\tprompt: p,\n\t\t\t\t\testimated_files: est === \"high\" ? 4 : est === \"medium\" ? 2 : 1,\n\t\t\t\t};\n\t\t\t})\n\t\t\t.filter((s): s is Subtask => s !== null);\n\n\t\treturn forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };\n\t}\n\n\tcanHandleInline(task: string): boolean {\n\t\treturn canHandleInline(task);\n\t}\n\n\tgetReason(analysis: TaskAnalysis): string {\n\t\treturn analysis.reason;\n\t}\n}\n"]}
1
+ {"version":3,"file":"dispatch-evaluator.js","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AASH,+EAA+E;AAC/E,SAAS,kBAAkB,CAAC,IAAY,EAA6B;IACpE,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC5G,MAAM,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEvD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,SAAS,GAAG,wFAAwF,CAAC,IAAI,CAC9G,IAAI,CACJ,CAAC;IACF,MAAM,WAAW,GAAG,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAEhG,IAAI,SAAS,GAAG,GAAG,IAAI,SAAS,IAAI,CAAC,IAAI,SAAS;QAAE,OAAO,MAAM,CAAC;IAClE,IAAI,SAAS,GAAG,EAAE,IAAI,SAAS,IAAI,CAAC,IAAI,WAAW;QAAE,OAAO,QAAQ,CAAC;IACrE,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,OAAO,iBAAiB;IAC7B,QAAQ,CAAC,IAAY,EAAgB;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QAC7E,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;YAChB,OAAO;gBACN,eAAe,EAAE,KAAK;gBACtB,MAAM,EAAE,kCAAkC;gBAC1C,oBAAoB,EAAE,KAAK;aAC3B,CAAC;QACH,CAAC;QAED,OAAO;YACN,eAAe,EAAE,IAAI;YACrB,MAAM,EAAE,uBAAuB;YAC/B,oBAAoB,EAAE,kBAAkB,CAAC,IAAI,CAAC;SAC9C,CAAC;IAAA,CACF;CACD","sourcesContent":["/**\n * Subagent dispatch guard + lightweight task telemetry.\n *\n * The parent agent selects which subagent to delegate to (via the Task tool),\n * so this module performs NO keyword routing. It survives for two narrow\n * responsibilities, both cheap and LLM-free:\n * 1. Depth guard — a subagent (HOOCODE_SUBAGENT_DEPTH>=1) must not spawn\n * further subagents.\n * 2. Complexity estimate — a heuristic recorded in the dispatch log for\n * diagnostics only.\n */\n\nexport interface TaskAnalysis {\n\t/** False only when the depth guard blocks delegation. */\n\tshould_delegate: boolean;\n\treason: string;\n\testimated_complexity: \"low\" | \"medium\" | \"high\";\n}\n\n/** Heuristic complexity estimate from file/line/scope mentions in the task. */\nfunction estimateComplexity(task: string): \"low\" | \"medium\" | \"high\" {\n\tconst fileMatches = task.match(/\\b[\\w/-]+\\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\\b/g);\n\tconst fileCount = fileMatches ? fileMatches.length : 0;\n\n\tconst lineMatch = task.match(/(\\d+)\\s*(lines?|loc)\\b/i);\n\tconst lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;\n\n\tconst highScope = /\\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\\b/i.test(\n\t\ttask,\n\t);\n\tconst mediumScope = /\\b(2|3|4|5)\\s*files?\\b/i.test(task) || /\\b(few|some|couple)\\b/i.test(task);\n\n\tif (lineCount > 200 || fileCount >= 4 || highScope) return \"high\";\n\tif (lineCount > 50 || fileCount >= 2 || mediumScope) return \"medium\";\n\treturn \"low\";\n}\n\nexport class DispatchEvaluator {\n\tevaluate(task: string): TaskAnalysis {\n\t\tconst depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? \"0\", 10);\n\t\tif (depth >= 1) {\n\t\t\treturn {\n\t\t\t\tshould_delegate: false,\n\t\t\t\treason: \"Subagents cannot spawn subagents\",\n\t\t\t\testimated_complexity: \"low\",\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\treason: \"delegated to subagent\",\n\t\t\testimated_complexity: estimateComplexity(task),\n\t\t};\n\t}\n}\n"]}
@@ -1,9 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
- import { type SubagentMode } from "./subagent.js";
4
3
  export interface SubagentPoolTask {
5
4
  task_id: string;
6
- agent_type: SubagentMode | string;
5
+ agent_type: string;
7
6
  task: string;
8
7
  context?: string;
9
8
  token_budget?: number;
@@ -54,7 +53,7 @@ export interface TaskResult {
54
53
  export interface DispatchOptions {
55
54
  /** Skip evaluation and force this agent type (user/explicit override).
56
55
  * Accepts any registry-defined agent name, not just the built-in modes. */
57
- forceAgent?: SubagentMode | string;
56
+ forceAgent?: string;
58
57
  /** Context distilled from the calling agent, passed to the subagent. */
59
58
  context?: string;
60
59
  /** Model id for the subagent (defaults to the child's configured default). */
@@ -173,16 +172,6 @@ export declare class SubagentPool extends EventEmitter {
173
172
  resume(task_id: string, prompt: string, options?: Omit<DispatchOptions, "forceAgent" | "sessionFile">): Promise<TaskResult>;
174
173
  /** Recover the agent type a task was dispatched with, from its dispatch log. */
175
174
  private readDispatchAgentType;
176
- /**
177
- * Dispatch a batch of subtasks concurrently.
178
- *
179
- * Spawns up to `maxConcurrency` at once; overflow is queued with FIFO.
180
- * Returns aggregated results in the same order as the input.
181
- */
182
- dispatchBatch(tasks: Array<{
183
- agent_type: SubagentMode;
184
- prompt: string;
185
- }>, shared?: Omit<DispatchOptions, "forceAgent">): Promise<TaskResult[]>;
186
175
  private writeDispatchLog;
187
176
  private writeOutputJson;
188
177
  /** Kill all running processes, clear the queue, and reject pending waiters. */