@kolisachint/hoocode-agent 0.4.13 → 0.4.15
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/CHANGELOG.md +26 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +8 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts +107 -0
- package/dist/core/agent-frontmatter.d.ts.map +1 -0
- package/dist/core/agent-frontmatter.js +189 -0
- package/dist/core/agent-frontmatter.js.map +1 -0
- package/dist/core/agent-registry.d.ts +52 -0
- package/dist/core/agent-registry.d.ts.map +1 -0
- package/dist/core/agent-registry.js +131 -0
- package/dist/core/agent-registry.js.map +1 -0
- package/dist/core/dispatch-evaluator.d.ts +17 -0
- package/dist/core/dispatch-evaluator.d.ts.map +1 -1
- package/dist/core/dispatch-evaluator.js +44 -10
- package/dist/core/dispatch-evaluator.js.map +1 -1
- package/dist/core/lifeguard.d.ts.map +1 -1
- package/dist/core/lifeguard.js +5 -5
- package/dist/core/lifeguard.js.map +1 -1
- package/dist/core/output-verifier.d.ts.map +1 -1
- package/dist/core/output-verifier.js +2 -2
- package/dist/core/output-verifier.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +54 -3
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +152 -62
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/subagent-result.d.ts +11 -2
- package/dist/core/subagent-result.d.ts.map +1 -1
- package/dist/core/subagent-result.js +17 -4
- package/dist/core/subagent-result.js.map +1 -1
- package/dist/core/task-store.d.ts +12 -7
- package/dist/core/task-store.d.ts.map +1 -1
- package/dist/core/task-store.js +23 -15
- package/dist/core/task-store.js.map +1 -1
- package/dist/core/token-budget.d.ts.map +1 -1
- package/dist/core/token-budget.js +17 -14
- package/dist/core/token-budget.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +32 -15
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +236 -112
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +13 -5
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +31 -12
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +4 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +12 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts +2 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +29 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent registry: data-driven loading of subagent definitions.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the hardcoded `SubagentMode` enum + `MODE_TOOLS` map with frontmatter
|
|
5
|
+
* `.md` files (see agent-frontmatter.ts). Definitions are discovered from, in
|
|
6
|
+
* increasing order of precedence:
|
|
7
|
+
*
|
|
8
|
+
* 1. builtin embedded templates (EMBEDDED_AGENT_PROMPTS)
|
|
9
|
+
* 2. claude-user ~/.claude/agents/*.md (D7 native import)
|
|
10
|
+
* 3. user ~/.hoocode/agent/agents/*.md
|
|
11
|
+
* 4. claude-project <cwd>/.claude/agents/*.md (D7 native import)
|
|
12
|
+
* 5. project <cwd>/.hoocode/agents/*.md
|
|
13
|
+
*
|
|
14
|
+
* Higher-precedence sources override lower ones by name. Overrides are recorded
|
|
15
|
+
* as collision diagnostics. Loading never throws; problems surface as
|
|
16
|
+
* diagnostics, matching skills.ts.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join, resolve } from "path";
|
|
21
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
|
22
|
+
import { EMBEDDED_AGENT_PROMPTS } from "../init-templates.generated.js";
|
|
23
|
+
import { parseAgentDefinition } from "./agent-frontmatter.js";
|
|
24
|
+
/** Registry of agent definitions keyed by name. */
|
|
25
|
+
export class AgentRegistry {
|
|
26
|
+
agents = new Map();
|
|
27
|
+
diagnostics = [];
|
|
28
|
+
/**
|
|
29
|
+
* Add or override a definition. Later registrations win (used both by the
|
|
30
|
+
* loader for precedence and as an escape hatch for programmatic agents).
|
|
31
|
+
* Overriding an existing name records a collision diagnostic.
|
|
32
|
+
*/
|
|
33
|
+
register(def) {
|
|
34
|
+
const existing = this.agents.get(def.name);
|
|
35
|
+
if (existing) {
|
|
36
|
+
this.diagnostics.push({
|
|
37
|
+
type: "collision",
|
|
38
|
+
message: `agent "${def.name}" from ${def.source} overrides ${existing.source}`,
|
|
39
|
+
path: def.filePath,
|
|
40
|
+
collision: {
|
|
41
|
+
resourceType: "skill",
|
|
42
|
+
name: def.name,
|
|
43
|
+
winnerPath: def.filePath ?? `<${def.source}>`,
|
|
44
|
+
loserPath: existing.filePath ?? `<${existing.source}>`,
|
|
45
|
+
winnerSource: def.source,
|
|
46
|
+
loserSource: existing.source,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
this.agents.set(def.name, def);
|
|
51
|
+
}
|
|
52
|
+
get(name) {
|
|
53
|
+
return this.agents.get(name);
|
|
54
|
+
}
|
|
55
|
+
has(name) {
|
|
56
|
+
return this.agents.has(name);
|
|
57
|
+
}
|
|
58
|
+
list() {
|
|
59
|
+
return Array.from(this.agents.values());
|
|
60
|
+
}
|
|
61
|
+
/** Diagnostics accumulated during loading/registration. */
|
|
62
|
+
getDiagnostics() {
|
|
63
|
+
return this.diagnostics;
|
|
64
|
+
}
|
|
65
|
+
/** Append externally-produced diagnostics (e.g. from a parse step). */
|
|
66
|
+
addDiagnostics(diagnostics) {
|
|
67
|
+
this.diagnostics.push(...diagnostics);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Load and register every built-in (embedded) agent definition. */
|
|
71
|
+
function registerBuiltins(registry) {
|
|
72
|
+
for (const [key, raw] of Object.entries(EMBEDDED_AGENT_PROMPTS)) {
|
|
73
|
+
const { agent, diagnostics } = parseAgentDefinition(raw, { source: "builtin", fallbackName: key });
|
|
74
|
+
registry.addDiagnostics(diagnostics);
|
|
75
|
+
if (agent)
|
|
76
|
+
registry.register(agent);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Load flat `*.md` agent files from a directory. Non-`.md` entries and
|
|
80
|
+
* subdirectories are skipped (so runtime dispatch dirs are ignored). */
|
|
81
|
+
function registerDir(registry, dir, source) {
|
|
82
|
+
if (!existsSync(dir))
|
|
83
|
+
return;
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = readdirSync(dir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (entry.startsWith(".") || !entry.endsWith(".md"))
|
|
93
|
+
continue;
|
|
94
|
+
const filePath = join(dir, entry);
|
|
95
|
+
try {
|
|
96
|
+
if (!statSync(filePath).isFile())
|
|
97
|
+
continue;
|
|
98
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
99
|
+
const { agent, diagnostics } = parseAgentDefinition(raw, { source, filePath });
|
|
100
|
+
registry.addDiagnostics(diagnostics);
|
|
101
|
+
if (agent)
|
|
102
|
+
registry.register(agent);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : "failed to read agent file";
|
|
106
|
+
registry.addDiagnostics([{ type: "warning", message, path: filePath }]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Build an AgentRegistry from all configured locations, applying precedence.
|
|
112
|
+
*/
|
|
113
|
+
export function loadAgentRegistry(options) {
|
|
114
|
+
const { cwd, includeBuiltins = true, includeClaude = true } = options;
|
|
115
|
+
const userAgentDir = options.agentDir ?? getAgentDir();
|
|
116
|
+
const registry = new AgentRegistry();
|
|
117
|
+
// Lowest precedence first; later sources override earlier ones by name.
|
|
118
|
+
if (includeBuiltins) {
|
|
119
|
+
registerBuiltins(registry);
|
|
120
|
+
}
|
|
121
|
+
if (includeClaude) {
|
|
122
|
+
registerDir(registry, join(homedir(), ".claude", "agents"), "claude-user");
|
|
123
|
+
}
|
|
124
|
+
registerDir(registry, join(userAgentDir, "agents"), "user");
|
|
125
|
+
if (includeClaude) {
|
|
126
|
+
registerDir(registry, resolve(cwd, ".claude", "agents"), "claude-project");
|
|
127
|
+
}
|
|
128
|
+
registerDir(registry, resolve(cwd, CONFIG_DIR_NAME, "agents"), "project");
|
|
129
|
+
return registry;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=agent-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-registry.js","sourceRoot":"","sources":["../../src/core/agent-registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Agent registry: data-driven loading of subagent definitions.\n *\n * Replaces the hardcoded `SubagentMode` enum + `MODE_TOOLS` map with frontmatter\n * `.md` files (see agent-frontmatter.ts). Definitions are discovered from, in\n * increasing order of precedence:\n *\n * 1. builtin embedded templates (EMBEDDED_AGENT_PROMPTS)\n * 2. claude-user ~/.claude/agents/*.md (D7 native import)\n * 3. user ~/.hoocode/agent/agents/*.md\n * 4. claude-project <cwd>/.claude/agents/*.md (D7 native import)\n * 5. project <cwd>/.hoocode/agents/*.md\n *\n * Higher-precedence sources override lower ones by name. Overrides are recorded\n * as collision diagnostics. Loading never throws; problems surface as\n * diagnostics, matching skills.ts.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join, resolve } from \"path\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\nimport { EMBEDDED_AGENT_PROMPTS } from \"../init-templates.generated.js\";\nimport { type AgentDefinition, type AgentSource, parseAgentDefinition } from \"./agent-frontmatter.js\";\nimport type { ResourceDiagnostic } from \"./diagnostics.js\";\n\n/** Registry of agent definitions keyed by name. */\nexport class AgentRegistry {\n\tprivate agents = new Map<string, AgentDefinition>();\n\tprivate diagnostics: ResourceDiagnostic[] = [];\n\n\t/**\n\t * Add or override a definition. Later registrations win (used both by the\n\t * loader for precedence and as an escape hatch for programmatic agents).\n\t * Overriding an existing name records a collision diagnostic.\n\t */\n\tregister(def: AgentDefinition): void {\n\t\tconst existing = this.agents.get(def.name);\n\t\tif (existing) {\n\t\t\tthis.diagnostics.push({\n\t\t\t\ttype: \"collision\",\n\t\t\t\tmessage: `agent \"${def.name}\" from ${def.source} overrides ${existing.source}`,\n\t\t\t\tpath: def.filePath,\n\t\t\t\tcollision: {\n\t\t\t\t\tresourceType: \"skill\",\n\t\t\t\t\tname: def.name,\n\t\t\t\t\twinnerPath: def.filePath ?? `<${def.source}>`,\n\t\t\t\t\tloserPath: existing.filePath ?? `<${existing.source}>`,\n\t\t\t\t\twinnerSource: def.source,\n\t\t\t\t\tloserSource: existing.source,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t\tthis.agents.set(def.name, def);\n\t}\n\n\tget(name: string): AgentDefinition | undefined {\n\t\treturn this.agents.get(name);\n\t}\n\n\thas(name: string): boolean {\n\t\treturn this.agents.has(name);\n\t}\n\n\tlist(): AgentDefinition[] {\n\t\treturn Array.from(this.agents.values());\n\t}\n\n\t/** Diagnostics accumulated during loading/registration. */\n\tgetDiagnostics(): ResourceDiagnostic[] {\n\t\treturn this.diagnostics;\n\t}\n\n\t/** Append externally-produced diagnostics (e.g. from a parse step). */\n\taddDiagnostics(diagnostics: ResourceDiagnostic[]): void {\n\t\tthis.diagnostics.push(...diagnostics);\n\t}\n}\n\n/** Load and register every built-in (embedded) agent definition. */\nfunction registerBuiltins(registry: AgentRegistry): void {\n\tfor (const [key, raw] of Object.entries(EMBEDDED_AGENT_PROMPTS)) {\n\t\tconst { agent, diagnostics } = parseAgentDefinition(raw, { source: \"builtin\", fallbackName: key });\n\t\tregistry.addDiagnostics(diagnostics);\n\t\tif (agent) registry.register(agent);\n\t}\n}\n\n/** Load flat `*.md` agent files from a directory. Non-`.md` entries and\n * subdirectories are skipped (so runtime dispatch dirs are ignored). */\nfunction registerDir(registry: AgentRegistry, dir: string, source: AgentSource): void {\n\tif (!existsSync(dir)) return;\n\tlet entries: string[];\n\ttry {\n\t\tentries = readdirSync(dir);\n\t} catch {\n\t\treturn;\n\t}\n\tfor (const entry of entries) {\n\t\tif (entry.startsWith(\".\") || !entry.endsWith(\".md\")) continue;\n\t\tconst filePath = join(dir, entry);\n\t\ttry {\n\t\t\tif (!statSync(filePath).isFile()) continue;\n\t\t\tconst raw = readFileSync(filePath, \"utf-8\");\n\t\t\tconst { agent, diagnostics } = parseAgentDefinition(raw, { source, filePath });\n\t\t\tregistry.addDiagnostics(diagnostics);\n\t\t\tif (agent) registry.register(agent);\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : \"failed to read agent file\";\n\t\t\tregistry.addDiagnostics([{ type: \"warning\", message, path: filePath }]);\n\t\t}\n\t}\n}\n\nexport interface LoadAgentRegistryOptions {\n\t/** Working directory for project-local agents. */\n\tcwd: string;\n\t/** User agent config directory (parent of `agents/`). Defaults to getAgentDir(). */\n\tagentDir?: string;\n\t/** Include embedded built-in agents. Defaults to true. */\n\tincludeBuiltins?: boolean;\n\t/** Discover `.claude/agents/` directories for native Claude Code import (D7). Defaults to true. */\n\tincludeClaude?: boolean;\n}\n\n/**\n * Build an AgentRegistry from all configured locations, applying precedence.\n */\nexport function loadAgentRegistry(options: LoadAgentRegistryOptions): AgentRegistry {\n\tconst { cwd, includeBuiltins = true, includeClaude = true } = options;\n\tconst userAgentDir = options.agentDir ?? getAgentDir();\n\tconst registry = new AgentRegistry();\n\n\t// Lowest precedence first; later sources override earlier ones by name.\n\tif (includeBuiltins) {\n\t\tregisterBuiltins(registry);\n\t}\n\tif (includeClaude) {\n\t\tregisterDir(registry, join(homedir(), \".claude\", \"agents\"), \"claude-user\");\n\t}\n\tregisterDir(registry, join(userAgentDir, \"agents\"), \"user\");\n\tif (includeClaude) {\n\t\tregisterDir(registry, resolve(cwd, \".claude\", \"agents\"), \"claude-project\");\n\t}\n\tregisterDir(registry, resolve(cwd, CONFIG_DIR_NAME, \"agents\"), \"project\");\n\n\treturn registry;\n}\n"]}
|
|
@@ -13,12 +13,29 @@ export interface TaskAnalysis {
|
|
|
13
13
|
estimated_complexity: "low" | "medium" | "high";
|
|
14
14
|
parallelizable: boolean;
|
|
15
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;
|
|
16
21
|
}
|
|
17
22
|
export interface Subtask {
|
|
18
23
|
agent_type: AgentType;
|
|
19
24
|
prompt: string;
|
|
20
25
|
estimated_files: number;
|
|
21
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
|
+
};
|
|
22
39
|
export declare class DispatchEvaluator {
|
|
23
40
|
evaluate(task: string): TaskAnalysis;
|
|
24
41
|
shouldSplit(task: string): {
|
|
@@ -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;CACzB;AAED,MAAM,WAAW,OAAO;IACvB,UAAU,EAAE,SAAS,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;CACxB;AA8OD,qBAAa,iBAAiB;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAsCnC;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}\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\nfunction detectAgentType(task: string): AgentType | null {\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\tlet best: AgentType | null = null;\n\tlet bestScore = 0;\n\tfor (const [type, score] of Object.entries(scores)) {\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbest = type as AgentType;\n\t\t}\n\t}\n\treturn bestScore > 0 ? best : null;\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};\n\t\t}\n\n\t\tconst agentType = detectAgentType(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};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: agentType,\n\t\t\treason: `${agentType ?? \"general\"} 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};\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;;;;;;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"]}
|
|
@@ -98,7 +98,14 @@ function countMatches(text, keywords) {
|
|
|
98
98
|
const lower = text.toLowerCase();
|
|
99
99
|
return keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);
|
|
100
100
|
}
|
|
101
|
-
|
|
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) {
|
|
102
109
|
const lower = task.toLowerCase();
|
|
103
110
|
const scores = {
|
|
104
111
|
explore: countMatches(task, EXPLORE_KEYWORDS),
|
|
@@ -119,15 +126,35 @@ function detectAgentType(task) {
|
|
|
119
126
|
if (scores.review > 0 && lower.includes("security")) {
|
|
120
127
|
scores.review += 2;
|
|
121
128
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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];
|
|
127
151
|
best = type;
|
|
128
152
|
}
|
|
129
153
|
}
|
|
130
|
-
return bestScore > 0 ? best : null;
|
|
154
|
+
return { agent_type: bestScore > 0 ? best : null, confidence: bestScore / total };
|
|
155
|
+
}
|
|
156
|
+
function detectAgentType(task) {
|
|
157
|
+
return classifyWithConfidence(task).agent_type;
|
|
131
158
|
}
|
|
132
159
|
function estimateComplexity(task) {
|
|
133
160
|
const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
|
|
@@ -227,9 +254,10 @@ export class DispatchEvaluator {
|
|
|
227
254
|
estimated_complexity: "low",
|
|
228
255
|
parallelizable: false,
|
|
229
256
|
context_needed: [],
|
|
257
|
+
confidence: 0,
|
|
230
258
|
};
|
|
231
259
|
}
|
|
232
|
-
const agentType =
|
|
260
|
+
const { agent_type: agentType, confidence } = classifyWithConfidence(task);
|
|
233
261
|
const complexity = estimateComplexity(task);
|
|
234
262
|
const inline = canHandleInline(task);
|
|
235
263
|
const subtasks = extractSubtasks(task);
|
|
@@ -242,15 +270,21 @@ export class DispatchEvaluator {
|
|
|
242
270
|
estimated_complexity: complexity,
|
|
243
271
|
parallelizable: false,
|
|
244
272
|
context_needed: [],
|
|
273
|
+
confidence,
|
|
245
274
|
};
|
|
246
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";
|
|
247
280
|
return {
|
|
248
281
|
should_delegate: true,
|
|
249
|
-
agent_type:
|
|
250
|
-
reason: `${
|
|
282
|
+
agent_type: delegateType,
|
|
283
|
+
reason: `${delegateType} task with ${complexity} complexity requires isolated subagent`,
|
|
251
284
|
estimated_complexity: complexity,
|
|
252
285
|
parallelizable,
|
|
253
286
|
context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
|
|
287
|
+
confidence,
|
|
254
288
|
};
|
|
255
289
|
}
|
|
256
290
|
shouldSplit(task) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch-evaluator.js","sourceRoot":"","sources":["../../src/core/dispatch-evaluator.ts"],"names":[],"mappings":"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","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}\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\nfunction detectAgentType(task: string): AgentType | null {\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\tlet best: AgentType | null = null;\n\tlet bestScore = 0;\n\tfor (const [type, score] of Object.entries(scores)) {\n\t\tif (score > bestScore) {\n\t\t\tbestScore = score;\n\t\t\tbest = type as AgentType;\n\t\t}\n\t}\n\treturn bestScore > 0 ? best : null;\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};\n\t\t}\n\n\t\tconst agentType = detectAgentType(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};\n\t\t}\n\n\t\treturn {\n\t\t\tshould_delegate: true,\n\t\t\tagent_type: agentType,\n\t\t\treason: `${agentType ?? \"general\"} 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};\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":"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","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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifeguard.d.ts","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;IAClD,OAAO,CAAC,SAAS,CAAuC;IACxD,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,qBAAqB,CAAC,CAAa;IAE3C,YAAY,GAAG,EAAE,MAAM,EAMtB;IAED;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAgBrE;IAED,+CAA+C;IAC/C,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIrC;IAED,0DAA0D;IAC1D,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE9C;IAED,qDAAqD;IACrD,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErC;IAED,iDAAiD;IACjD,OAAO,IAAI,IAAI,CA2Bd;IAED,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,IAAI;CAoBZ","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {
|
|
1
|
+
{"version":3,"file":"lifeguard.d.ts","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;CACtB;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,YAAY;IAClD,OAAO,CAAC,SAAS,CAAuC;IACxD,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,qBAAqB,CAAC,CAAa;IAE3C,YAAY,GAAG,EAAE,MAAM,EAMtB;IAED;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAgBrE;IAED,+CAA+C;IAC/C,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIrC;IAED,0DAA0D;IAC1D,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE9C;IAED,qDAAqD;IACrD,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErC;IAED,iDAAiD;IACjD,OAAO,IAAI,IAAI,CA2Bd;IAED,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,IAAI;CAoBZ","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getDispatchRoot } from \"../config.js\";\n\nconst TIMEOUTS_MS: Record<string, number> = {\n\texplore: 5 * 60 * 1000,\n\tedit: 10 * 60 * 1000,\n\ttest: 10 * 60 * 1000,\n\treview: 8 * 60 * 1000,\n\tdoc: 5 * 60 * 1000,\n};\n\nconst HEARTBEAT_MISS_THRESHOLD_MS = 60000;\nconst PARENT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface LifeguardProcess {\n\tpid: number;\n\ttask_id: string;\n\tagent_type: string;\n\tprocess: ChildProcess;\n}\n\n/**\n * Monitors running subagent processes for heartbeats, hard timeouts,\n * and parent-exit cleanup. Emits \"stalled\" and \"timeout\" events when\n * processes are terminated.\n */\nexport class SubagentLifeguard extends EventEmitter {\n\tprivate processes = new Map<string, LifeguardProcess>();\n\tprivate lastHeartbeat = new Map<string, number>();\n\tprivate timeouts = new Map<string, NodeJS.Timeout>();\n\tprivate checkInterval: NodeJS.Timeout | null = null;\n\tprivate disposed = false;\n\tprivate readonly cwd: string;\n\tprivate parentShutdownHandler?: () => void;\n\n\tconstructor(cwd: string) {\n\t\tsuper();\n\t\tthis.cwd = cwd;\n\t\tthis.setupParentExitHandlers();\n\t\tthis.sweepOldAgents();\n\t\tthis.checkInterval = setInterval(() => this.checkHeartbeats(), 5000);\n\t}\n\n\t/**\n\t * Begin monitoring a child process. The process must emit a\n\t * `{\"ping\":true}` JSON line on stdout every 30 seconds.\n\t */\n\tmonitor(task_id: string, agent_type: string, proc: ChildProcess): void {\n\t\tif (this.disposed) return;\n\n\t\tconst pid = proc.pid ?? 0;\n\t\tthis.processes.set(task_id, { pid, task_id, agent_type, process: proc });\n\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\n\t\tconst timeoutMs = TIMEOUTS_MS[agent_type] ?? TIMEOUTS_MS.explore;\n\t\tconst timeout = setTimeout(() => {\n\t\t\tthis.handleTimeout(task_id);\n\t\t}, timeoutMs);\n\t\tthis.timeouts.set(task_id, timeout);\n\n\t\tproc.once(\"exit\", () => {\n\t\t\tthis.untrack(task_id);\n\t\t});\n\t}\n\n\t/** Record a heartbeat for a monitored task. */\n\trecordHeartbeat(task_id: string): void {\n\t\tif (this.processes.has(task_id)) {\n\t\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\t\t}\n\t}\n\n\t/** Get the last recorded heartbeat timestamp, or null. */\n\tlastHeartbeatAt(task_id: string): number | null {\n\t\treturn this.lastHeartbeat.get(task_id) ?? null;\n\t}\n\n\t/** True if the task is currently being monitored. */\n\tisMonitoring(task_id: string): boolean {\n\t\treturn this.processes.has(task_id);\n\t}\n\n\t/** Kill all monitored processes and clean up. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tif (this.checkInterval) {\n\t\t\tclearInterval(this.checkInterval);\n\t\t\tthis.checkInterval = null;\n\t\t}\n\n\t\tfor (const timeout of this.timeouts.values()) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t\tthis.timeouts.clear();\n\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t}\n\t\t}\n\t\tthis.processes.clear();\n\t\tthis.lastHeartbeat.clear();\n\t\tthis.removeAllListeners();\n\n\t\tif (this.parentShutdownHandler) {\n\t\t\tprocess.removeListener(\"SIGINT\", this.parentShutdownHandler);\n\t\t\tprocess.removeListener(\"SIGTERM\", this.parentShutdownHandler);\n\t\t}\n\t}\n\n\tprivate checkHeartbeats(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [task_id] of this.processes) {\n\t\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\t\tif (last === undefined) continue;\n\t\t\tif (now - last > HEARTBEAT_MISS_THRESHOLD_MS) {\n\t\t\t\tthis.handleStalled(task_id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleStalled(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"stalled\", { task_id, pid: monitored.pid });\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate handleTimeout(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"timeout\", { task_id, pid: monitored.pid });\n\t\tthis.timeouts.delete(task_id);\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate untrack(task_id: string): void {\n\t\tconst timeout = this.timeouts.get(task_id);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis.timeouts.delete(task_id);\n\t\t}\n\t\tthis.processes.delete(task_id);\n\t\tthis.lastHeartbeat.delete(task_id);\n\t}\n\n\tprivate setupParentExitHandlers(): void {\n\t\tconst shutdown = () => this.gracefulShutdown();\n\t\tthis.parentShutdownHandler = shutdown;\n\t\tprocess.setMaxListeners(Math.max(process.getMaxListeners(), 20));\n\t\tprocess.once(\"SIGINT\", shutdown);\n\t\tprocess.once(\"SIGTERM\", shutdown);\n\t}\n\n\tprivate gracefulShutdown(): void {\n\t\t// SIGTERM all children\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\n\t\t// SIGKILL after grace period\n\t\tsetTimeout(() => {\n\t\t\tfor (const monitored of this.processes.values()) {\n\t\t\t\tif (!monitored.process.killed) {\n\t\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t\t}\n\t\t\t}\n\t\t}, PARENT_SHUTDOWN_GRACE_MS).unref();\n\t}\n\n\tprivate sweepOldAgents(): void {\n\t\tconst dispatchDir = getDispatchRoot(this.cwd);\n\t\tif (!existsSync(dispatchDir)) return;\n\n\t\tconst now = Date.now();\n\t\tconst cutoff = 24 * 60 * 60 * 1000; // 24 hours\n\n\t\tfor (const entry of readdirSync(dispatchDir)) {\n\t\t\tconst entryPath = join(dispatchDir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (!stats.isDirectory()) continue;\n\n\t\t\t\tif (now - stats.mtimeMs > cutoff) {\n\t\t\t\t\tconst hasRunningPid = this.hasRunningPid(entryPath);\n\t\t\t\t\tif (!hasRunningPid) {\n\t\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors for individual entries\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hasRunningPid(dir: string): boolean {\n\t\tconst pidFile = join(dir, \"pid\");\n\t\tif (!existsSync(pidFile)) return false;\n\n\t\ttry {\n\t\t\tconst pid = Number.parseInt(readFileSync(pidFile, \"utf-8\"), 10);\n\t\t\tif (Number.isNaN(pid)) return false;\n\t\t\tprocess.kill(pid, 0); // Check if process exists\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rmrf(dir: string): void {\n\t\tfor (const entry of readdirSync(dir)) {\n\t\t\tconst entryPath = join(dir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (stats.isDirectory()) {\n\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t} else {\n\t\t\t\t\tunlinkSync(entryPath);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\trmdirSync(dir);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n}\n"]}
|
package/dist/core/lifeguard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { getDispatchRoot } from "../config.js";
|
|
5
5
|
const TIMEOUTS_MS = {
|
|
6
6
|
explore: 5 * 60 * 1000,
|
|
7
7
|
edit: 10 * 60 * 1000,
|
|
@@ -155,13 +155,13 @@ export class SubagentLifeguard extends EventEmitter {
|
|
|
155
155
|
}, PARENT_SHUTDOWN_GRACE_MS).unref();
|
|
156
156
|
}
|
|
157
157
|
sweepOldAgents() {
|
|
158
|
-
const
|
|
159
|
-
if (!existsSync(
|
|
158
|
+
const dispatchDir = getDispatchRoot(this.cwd);
|
|
159
|
+
if (!existsSync(dispatchDir))
|
|
160
160
|
return;
|
|
161
161
|
const now = Date.now();
|
|
162
162
|
const cutoff = 24 * 60 * 60 * 1000; // 24 hours
|
|
163
|
-
for (const entry of readdirSync(
|
|
164
|
-
const entryPath = join(
|
|
163
|
+
for (const entry of readdirSync(dispatchDir)) {
|
|
164
|
+
const entryPath = join(dispatchDir, entry);
|
|
165
165
|
try {
|
|
166
166
|
const stats = statSync(entryPath);
|
|
167
167
|
if (!stats.isDirectory())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifeguard.js","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } from \"../config.js\";\n\nconst TIMEOUTS_MS: Record<string, number> = {\n\texplore: 5 * 60 * 1000,\n\tedit: 10 * 60 * 1000,\n\ttest: 10 * 60 * 1000,\n\treview: 8 * 60 * 1000,\n\tdoc: 5 * 60 * 1000,\n};\n\nconst HEARTBEAT_MISS_THRESHOLD_MS = 60000;\nconst PARENT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface LifeguardProcess {\n\tpid: number;\n\ttask_id: string;\n\tagent_type: string;\n\tprocess: ChildProcess;\n}\n\n/**\n * Monitors running subagent processes for heartbeats, hard timeouts,\n * and parent-exit cleanup. Emits \"stalled\" and \"timeout\" events when\n * processes are terminated.\n */\nexport class SubagentLifeguard extends EventEmitter {\n\tprivate processes = new Map<string, LifeguardProcess>();\n\tprivate lastHeartbeat = new Map<string, number>();\n\tprivate timeouts = new Map<string, NodeJS.Timeout>();\n\tprivate checkInterval: NodeJS.Timeout | null = null;\n\tprivate disposed = false;\n\tprivate readonly cwd: string;\n\tprivate parentShutdownHandler?: () => void;\n\n\tconstructor(cwd: string) {\n\t\tsuper();\n\t\tthis.cwd = cwd;\n\t\tthis.setupParentExitHandlers();\n\t\tthis.sweepOldAgents();\n\t\tthis.checkInterval = setInterval(() => this.checkHeartbeats(), 5000);\n\t}\n\n\t/**\n\t * Begin monitoring a child process. The process must emit a\n\t * `{\"ping\":true}` JSON line on stdout every 30 seconds.\n\t */\n\tmonitor(task_id: string, agent_type: string, proc: ChildProcess): void {\n\t\tif (this.disposed) return;\n\n\t\tconst pid = proc.pid ?? 0;\n\t\tthis.processes.set(task_id, { pid, task_id, agent_type, process: proc });\n\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\n\t\tconst timeoutMs = TIMEOUTS_MS[agent_type] ?? TIMEOUTS_MS.explore;\n\t\tconst timeout = setTimeout(() => {\n\t\t\tthis.handleTimeout(task_id);\n\t\t}, timeoutMs);\n\t\tthis.timeouts.set(task_id, timeout);\n\n\t\tproc.once(\"exit\", () => {\n\t\t\tthis.untrack(task_id);\n\t\t});\n\t}\n\n\t/** Record a heartbeat for a monitored task. */\n\trecordHeartbeat(task_id: string): void {\n\t\tif (this.processes.has(task_id)) {\n\t\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\t\t}\n\t}\n\n\t/** Get the last recorded heartbeat timestamp, or null. */\n\tlastHeartbeatAt(task_id: string): number | null {\n\t\treturn this.lastHeartbeat.get(task_id) ?? null;\n\t}\n\n\t/** True if the task is currently being monitored. */\n\tisMonitoring(task_id: string): boolean {\n\t\treturn this.processes.has(task_id);\n\t}\n\n\t/** Kill all monitored processes and clean up. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tif (this.checkInterval) {\n\t\t\tclearInterval(this.checkInterval);\n\t\t\tthis.checkInterval = null;\n\t\t}\n\n\t\tfor (const timeout of this.timeouts.values()) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t\tthis.timeouts.clear();\n\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t}\n\t\t}\n\t\tthis.processes.clear();\n\t\tthis.lastHeartbeat.clear();\n\t\tthis.removeAllListeners();\n\n\t\tif (this.parentShutdownHandler) {\n\t\t\tprocess.removeListener(\"SIGINT\", this.parentShutdownHandler);\n\t\t\tprocess.removeListener(\"SIGTERM\", this.parentShutdownHandler);\n\t\t}\n\t}\n\n\tprivate checkHeartbeats(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [task_id] of this.processes) {\n\t\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\t\tif (last === undefined) continue;\n\t\t\tif (now - last > HEARTBEAT_MISS_THRESHOLD_MS) {\n\t\t\t\tthis.handleStalled(task_id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleStalled(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"stalled\", { task_id, pid: monitored.pid });\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate handleTimeout(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"timeout\", { task_id, pid: monitored.pid });\n\t\tthis.timeouts.delete(task_id);\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate untrack(task_id: string): void {\n\t\tconst timeout = this.timeouts.get(task_id);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis.timeouts.delete(task_id);\n\t\t}\n\t\tthis.processes.delete(task_id);\n\t\tthis.lastHeartbeat.delete(task_id);\n\t}\n\n\tprivate setupParentExitHandlers(): void {\n\t\tconst shutdown = () => this.gracefulShutdown();\n\t\tthis.parentShutdownHandler = shutdown;\n\t\tprocess.setMaxListeners(Math.max(process.getMaxListeners(), 20));\n\t\tprocess.once(\"SIGINT\", shutdown);\n\t\tprocess.once(\"SIGTERM\", shutdown);\n\t}\n\n\tprivate gracefulShutdown(): void {\n\t\t// SIGTERM all children\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\n\t\t// SIGKILL after grace period\n\t\tsetTimeout(() => {\n\t\t\tfor (const monitored of this.processes.values()) {\n\t\t\t\tif (!monitored.process.killed) {\n\t\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t\t}\n\t\t\t}\n\t\t}, PARENT_SHUTDOWN_GRACE_MS).unref();\n\t}\n\n\tprivate sweepOldAgents(): void {\n\t\tconst agentsDir = join(this.cwd, CONFIG_DIR_NAME, \"agents\");\n\t\tif (!existsSync(agentsDir)) return;\n\n\t\tconst now = Date.now();\n\t\tconst cutoff = 24 * 60 * 60 * 1000; // 24 hours\n\n\t\tfor (const entry of readdirSync(agentsDir)) {\n\t\t\tconst entryPath = join(agentsDir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (!stats.isDirectory()) continue;\n\n\t\t\t\tif (now - stats.mtimeMs > cutoff) {\n\t\t\t\t\tconst hasRunningPid = this.hasRunningPid(entryPath);\n\t\t\t\t\tif (!hasRunningPid) {\n\t\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors for individual entries\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hasRunningPid(dir: string): boolean {\n\t\tconst pidFile = join(dir, \"pid\");\n\t\tif (!existsSync(pidFile)) return false;\n\n\t\ttry {\n\t\t\tconst pid = Number.parseInt(readFileSync(pidFile, \"utf-8\"), 10);\n\t\t\tif (Number.isNaN(pid)) return false;\n\t\t\tprocess.kill(pid, 0); // Check if process exists\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rmrf(dir: string): void {\n\t\tfor (const entry of readdirSync(dir)) {\n\t\t\tconst entryPath = join(dir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (stats.isDirectory()) {\n\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t} else {\n\t\t\t\t\tunlinkSync(entryPath);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\trmdirSync(dir);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lifeguard.js","sourceRoot":"","sources":["../../src/core/lifeguard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ChildProcess } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport { existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getDispatchRoot } from \"../config.js\";\n\nconst TIMEOUTS_MS: Record<string, number> = {\n\texplore: 5 * 60 * 1000,\n\tedit: 10 * 60 * 1000,\n\ttest: 10 * 60 * 1000,\n\treview: 8 * 60 * 1000,\n\tdoc: 5 * 60 * 1000,\n};\n\nconst HEARTBEAT_MISS_THRESHOLD_MS = 60000;\nconst PARENT_SHUTDOWN_GRACE_MS = 5000;\n\nexport interface LifeguardProcess {\n\tpid: number;\n\ttask_id: string;\n\tagent_type: string;\n\tprocess: ChildProcess;\n}\n\n/**\n * Monitors running subagent processes for heartbeats, hard timeouts,\n * and parent-exit cleanup. Emits \"stalled\" and \"timeout\" events when\n * processes are terminated.\n */\nexport class SubagentLifeguard extends EventEmitter {\n\tprivate processes = new Map<string, LifeguardProcess>();\n\tprivate lastHeartbeat = new Map<string, number>();\n\tprivate timeouts = new Map<string, NodeJS.Timeout>();\n\tprivate checkInterval: NodeJS.Timeout | null = null;\n\tprivate disposed = false;\n\tprivate readonly cwd: string;\n\tprivate parentShutdownHandler?: () => void;\n\n\tconstructor(cwd: string) {\n\t\tsuper();\n\t\tthis.cwd = cwd;\n\t\tthis.setupParentExitHandlers();\n\t\tthis.sweepOldAgents();\n\t\tthis.checkInterval = setInterval(() => this.checkHeartbeats(), 5000);\n\t}\n\n\t/**\n\t * Begin monitoring a child process. The process must emit a\n\t * `{\"ping\":true}` JSON line on stdout every 30 seconds.\n\t */\n\tmonitor(task_id: string, agent_type: string, proc: ChildProcess): void {\n\t\tif (this.disposed) return;\n\n\t\tconst pid = proc.pid ?? 0;\n\t\tthis.processes.set(task_id, { pid, task_id, agent_type, process: proc });\n\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\n\t\tconst timeoutMs = TIMEOUTS_MS[agent_type] ?? TIMEOUTS_MS.explore;\n\t\tconst timeout = setTimeout(() => {\n\t\t\tthis.handleTimeout(task_id);\n\t\t}, timeoutMs);\n\t\tthis.timeouts.set(task_id, timeout);\n\n\t\tproc.once(\"exit\", () => {\n\t\t\tthis.untrack(task_id);\n\t\t});\n\t}\n\n\t/** Record a heartbeat for a monitored task. */\n\trecordHeartbeat(task_id: string): void {\n\t\tif (this.processes.has(task_id)) {\n\t\t\tthis.lastHeartbeat.set(task_id, Date.now());\n\t\t}\n\t}\n\n\t/** Get the last recorded heartbeat timestamp, or null. */\n\tlastHeartbeatAt(task_id: string): number | null {\n\t\treturn this.lastHeartbeat.get(task_id) ?? null;\n\t}\n\n\t/** True if the task is currently being monitored. */\n\tisMonitoring(task_id: string): boolean {\n\t\treturn this.processes.has(task_id);\n\t}\n\n\t/** Kill all monitored processes and clean up. */\n\tdispose(): void {\n\t\tif (this.disposed) return;\n\t\tthis.disposed = true;\n\n\t\tif (this.checkInterval) {\n\t\t\tclearInterval(this.checkInterval);\n\t\t\tthis.checkInterval = null;\n\t\t}\n\n\t\tfor (const timeout of this.timeouts.values()) {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t\tthis.timeouts.clear();\n\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t}\n\t\t}\n\t\tthis.processes.clear();\n\t\tthis.lastHeartbeat.clear();\n\t\tthis.removeAllListeners();\n\n\t\tif (this.parentShutdownHandler) {\n\t\t\tprocess.removeListener(\"SIGINT\", this.parentShutdownHandler);\n\t\t\tprocess.removeListener(\"SIGTERM\", this.parentShutdownHandler);\n\t\t}\n\t}\n\n\tprivate checkHeartbeats(): void {\n\t\tconst now = Date.now();\n\t\tfor (const [task_id] of this.processes) {\n\t\t\tconst last = this.lastHeartbeat.get(task_id);\n\t\t\tif (last === undefined) continue;\n\t\t\tif (now - last > HEARTBEAT_MISS_THRESHOLD_MS) {\n\t\t\t\tthis.handleStalled(task_id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleStalled(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"stalled\", { task_id, pid: monitored.pid });\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate handleTimeout(task_id: string): void {\n\t\tconst monitored = this.processes.get(task_id);\n\t\tif (!monitored) return;\n\n\t\tif (!monitored.process.killed) {\n\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t}\n\n\t\tthis.emit(\"timeout\", { task_id, pid: monitored.pid });\n\t\tthis.timeouts.delete(task_id);\n\t\t// Process exit handler will call untrack()\n\t}\n\n\tprivate untrack(task_id: string): void {\n\t\tconst timeout = this.timeouts.get(task_id);\n\t\tif (timeout) {\n\t\t\tclearTimeout(timeout);\n\t\t\tthis.timeouts.delete(task_id);\n\t\t}\n\t\tthis.processes.delete(task_id);\n\t\tthis.lastHeartbeat.delete(task_id);\n\t}\n\n\tprivate setupParentExitHandlers(): void {\n\t\tconst shutdown = () => this.gracefulShutdown();\n\t\tthis.parentShutdownHandler = shutdown;\n\t\tprocess.setMaxListeners(Math.max(process.getMaxListeners(), 20));\n\t\tprocess.once(\"SIGINT\", shutdown);\n\t\tprocess.once(\"SIGTERM\", shutdown);\n\t}\n\n\tprivate gracefulShutdown(): void {\n\t\t// SIGTERM all children\n\t\tfor (const monitored of this.processes.values()) {\n\t\t\tif (!monitored.process.killed) {\n\t\t\t\tmonitored.process.kill(\"SIGTERM\");\n\t\t\t}\n\t\t}\n\n\t\t// SIGKILL after grace period\n\t\tsetTimeout(() => {\n\t\t\tfor (const monitored of this.processes.values()) {\n\t\t\t\tif (!monitored.process.killed) {\n\t\t\t\t\tmonitored.process.kill(\"SIGKILL\");\n\t\t\t\t}\n\t\t\t}\n\t\t}, PARENT_SHUTDOWN_GRACE_MS).unref();\n\t}\n\n\tprivate sweepOldAgents(): void {\n\t\tconst dispatchDir = getDispatchRoot(this.cwd);\n\t\tif (!existsSync(dispatchDir)) return;\n\n\t\tconst now = Date.now();\n\t\tconst cutoff = 24 * 60 * 60 * 1000; // 24 hours\n\n\t\tfor (const entry of readdirSync(dispatchDir)) {\n\t\t\tconst entryPath = join(dispatchDir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (!stats.isDirectory()) continue;\n\n\t\t\t\tif (now - stats.mtimeMs > cutoff) {\n\t\t\t\t\tconst hasRunningPid = this.hasRunningPid(entryPath);\n\t\t\t\t\tif (!hasRunningPid) {\n\t\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore errors for individual entries\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate hasRunningPid(dir: string): boolean {\n\t\tconst pidFile = join(dir, \"pid\");\n\t\tif (!existsSync(pidFile)) return false;\n\n\t\ttry {\n\t\t\tconst pid = Number.parseInt(readFileSync(pidFile, \"utf-8\"), 10);\n\t\t\tif (Number.isNaN(pid)) return false;\n\t\t\tprocess.kill(pid, 0); // Check if process exists\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate rmrf(dir: string): void {\n\t\tfor (const entry of readdirSync(dir)) {\n\t\t\tconst entryPath = join(dir, entry);\n\t\t\ttry {\n\t\t\t\tconst stats = statSync(entryPath);\n\t\t\t\tif (stats.isDirectory()) {\n\t\t\t\t\tthis.rmrf(entryPath);\n\t\t\t\t} else {\n\t\t\t\t\tunlinkSync(entryPath);\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\trmdirSync(dir);\n\t\t} catch {\n\t\t\t// Ignore\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output-verifier.d.ts","sourceRoot":"","sources":["../../src/core/output-verifier.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;GAGG;AACH,qBAAa,cAAc;IACd,OAAO,CAAC,QAAQ,CAAC,UAAU;IAAvC,YAA6B,UAAU,GAAE,MAAsB,EAAI;IAEnE;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAiGxD;CACD","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport {
|
|
1
|
+
{"version":3,"file":"output-verifier.d.ts","sourceRoot":"","sources":["../../src/core/output-verifier.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;GAGG;AACH,qBAAa,cAAc;IACd,OAAO,CAAC,QAAQ,CAAC,UAAU;IAAvC,YAA6B,UAAU,GAAE,MAAsB,EAAI;IAEnE;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAiGxD;CACD","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { getDispatchTaskDir } from \"../config.js\";\n\nexport interface VerificationResult {\n\tvalid: boolean;\n\treason?: string;\n}\n\nconst VALID_STATUSES = [\"complete\", \"partial\", \"failed\"] as const;\n\ntype Status = (typeof VALID_STATUSES)[number];\n\n/**\n * Verifies that a subagent's result.json exists, matches the expected schema,\n * and meets quality thresholds (non-empty summary, confidence >= 0.5).\n */\nexport class OutputVerifier {\n\tconstructor(private readonly defaultCwd: string = process.cwd()) {}\n\n\t/**\n\t * Verify the output for a given task.\n\t * @param task_id The task identifier.\n\t * @param cwd Optional working directory override (defaults to constructor value).\n\t */\n\tverify(task_id: string, cwd?: string): VerificationResult {\n\t\tconst base = cwd ?? this.defaultCwd;\n\t\tconst path = join(getDispatchTaskDir(base, task_id), \"result.json\");\n\n\t\tif (!existsSync(path)) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `result.json not found for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\tlet raw: string;\n\t\ttry {\n\t\t\traw = readFileSync(path, \"utf-8\");\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Cannot read result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(raw);\n\t\t} catch {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Invalid JSON in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\tif (!parsed || typeof parsed !== \"object\") {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `result.json is not an object for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\tconst result = parsed as Record<string, unknown>;\n\n\t\t// summary\n\t\tif (typeof result.summary !== \"string\") {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Missing or invalid 'summary' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\t\tif (result.summary.trim().length === 0) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Empty 'summary' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\t// files_changed\n\t\tif (!Array.isArray(result.files_changed)) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Missing or invalid 'files_changed' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\t\tif (!result.files_changed.every((f) => typeof f === \"string\")) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Non-string entries in 'files_changed' for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\t// confidence\n\t\tif (typeof result.confidence !== \"number\") {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Missing or invalid 'confidence' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\t\tif (result.confidence < 0.5) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Confidence ${result.confidence} below threshold (0.5) for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\t// status\n\t\tif (typeof result.status !== \"string\") {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Missing or invalid 'status' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\t\tif (!VALID_STATUSES.includes(result.status as Status)) {\n\t\t\treturn {\n\t\t\t\tvalid: false,\n\t\t\t\treason: `Invalid status '${result.status}' in result.json for task ${task_id}`,\n\t\t\t};\n\t\t}\n\n\t\treturn { valid: true };\n\t}\n}\n"]}
|