@kolisachint/hoocode-agent 0.4.14 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +8 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/config.d.ts +8 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +12 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-frontmatter.d.ts +107 -0
  11. package/dist/core/agent-frontmatter.d.ts.map +1 -0
  12. package/dist/core/agent-frontmatter.js +189 -0
  13. package/dist/core/agent-frontmatter.js.map +1 -0
  14. package/dist/core/agent-registry.d.ts +52 -0
  15. package/dist/core/agent-registry.d.ts.map +1 -0
  16. package/dist/core/agent-registry.js +131 -0
  17. package/dist/core/agent-registry.js.map +1 -0
  18. package/dist/core/lifeguard.d.ts.map +1 -1
  19. package/dist/core/lifeguard.js +5 -5
  20. package/dist/core/lifeguard.js.map +1 -1
  21. package/dist/core/output-verifier.d.ts.map +1 -1
  22. package/dist/core/output-verifier.js +2 -2
  23. package/dist/core/output-verifier.js.map +1 -1
  24. package/dist/core/subagent-pool.d.ts +54 -3
  25. package/dist/core/subagent-pool.d.ts.map +1 -1
  26. package/dist/core/subagent-pool.js +152 -62
  27. package/dist/core/subagent-pool.js.map +1 -1
  28. package/dist/core/subagent-result.d.ts +11 -2
  29. package/dist/core/subagent-result.d.ts.map +1 -1
  30. package/dist/core/subagent-result.js +17 -4
  31. package/dist/core/subagent-result.js.map +1 -1
  32. package/dist/core/token-budget.d.ts.map +1 -1
  33. package/dist/core/token-budget.js +2 -2
  34. package/dist/core/token-budget.js.map +1 -1
  35. package/dist/core/tools/subagent.d.ts +32 -15
  36. package/dist/core/tools/subagent.d.ts.map +1 -1
  37. package/dist/core/tools/subagent.js +235 -113
  38. package/dist/core/tools/subagent.js.map +1 -1
  39. package/dist/main.d.ts.map +1 -1
  40. package/dist/main.js +13 -5
  41. package/dist/main.js.map +1 -1
  42. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  43. package/dist/modes/interactive/interactive-mode.js +5 -2
  44. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  45. package/dist/modes/print-mode.d.ts +2 -0
  46. package/dist/modes/print-mode.d.ts.map +1 -1
  47. package/dist/modes/print-mode.js +29 -2
  48. package/dist/modes/print-mode.js.map +1 -1
  49. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  50. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  51. package/examples/extensions/sandbox/package.json +1 -1
  52. package/examples/extensions/with-deps/package.json +1 -1
  53. 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"]}
@@ -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 { 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.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"]}
@@ -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 { CONFIG_DIR_NAME } from "../config.js";
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 agentsDir = join(this.cwd, CONFIG_DIR_NAME, "agents");
159
- if (!existsSync(agentsDir))
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(agentsDir)) {
164
- const entryPath = join(agentsDir, entry);
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 { CONFIG_DIR_NAME } 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(base, CONFIG_DIR_NAME, \"agents\", 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"]}
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"]}
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { CONFIG_DIR_NAME } from "../config.js";
3
+ import { getDispatchTaskDir } from "../config.js";
4
4
  const VALID_STATUSES = ["complete", "partial", "failed"];
5
5
  /**
6
6
  * Verifies that a subagent's result.json exists, matches the expected schema,
@@ -18,7 +18,7 @@ export class OutputVerifier {
18
18
  */
19
19
  verify(task_id, cwd) {
20
20
  const base = cwd ?? this.defaultCwd;
21
- const path = join(base, CONFIG_DIR_NAME, "agents", task_id, "result.json");
21
+ const path = join(getDispatchTaskDir(base, task_id), "result.json");
22
22
  if (!existsSync(path)) {
23
23
  return {
24
24
  valid: false,
@@ -1 +1 @@
1
- {"version":3,"file":"output-verifier.js","sourceRoot":"","sources":["../../src/core/output-verifier.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { CONFIG_DIR_NAME } 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(base, CONFIG_DIR_NAME, \"agents\", 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"]}
1
+ {"version":3,"file":"output-verifier.js","sourceRoot":"","sources":["../../src/core/output-verifier.ts"],"names":[],"mappings":"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","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"]}
@@ -10,6 +10,12 @@ export interface SubagentPoolTask {
10
10
  cwd?: string;
11
11
  model?: string;
12
12
  provider?: string;
13
+ /**
14
+ * Explicit session file for the child to persist/continue. When omitted the
15
+ * child uses its own dispatch dir (`<dispatch>/<task_id>/session.jsonl`).
16
+ * Resume reuses the original task's session file to continue the transcript.
17
+ */
18
+ sessionFile?: string;
13
19
  }
14
20
  export interface SubagentSlot {
15
21
  pid: number;
@@ -46,14 +52,17 @@ export interface TaskResult {
46
52
  duration?: number;
47
53
  }
48
54
  export interface DispatchOptions {
49
- /** Skip evaluation and force this agent type (user/explicit override). */
50
- forceAgent?: SubagentMode;
55
+ /** Skip evaluation and force this agent type (user/explicit override).
56
+ * Accepts any registry-defined agent name, not just the built-in modes. */
57
+ forceAgent?: SubagentMode | string;
51
58
  /** Context distilled from the calling agent, passed to the subagent. */
52
59
  context?: string;
53
60
  /** Model id for the subagent (defaults to the child's configured default). */
54
61
  model?: string;
55
62
  /** Provider for the subagent. */
56
63
  provider?: string;
64
+ /** Explicit session file to persist/continue (used by resume). */
65
+ sessionFile?: string;
57
66
  }
58
67
  export interface SubagentPoolOptions {
59
68
  /** Path to the hoocode executable (or the runtime, e.g. node, when prefixArgs is set). */
@@ -69,6 +78,12 @@ export interface SubagentPoolOptions {
69
78
  /** Default token budget per task. Defaults to 0. */
70
79
  defaultTokenBudget?: number;
71
80
  }
81
+ /**
82
+ * Default hard cap on assistant turns for a spawned subagent when its definition
83
+ * does not set `maxTurns`. The token budget is advisory (it warns but never
84
+ * kills), so this turn cap is the guaranteed hard stop for every subagent.
85
+ */
86
+ export declare const DEFAULT_SUBAGENT_MAX_TURNS = 50;
72
87
  /**
73
88
  * Pool for running hoocode subagents as child processes with bounded concurrency,
74
89
  * FIFO queuing with priority support, and automatic slot refill.
@@ -78,7 +93,8 @@ export interface SubagentPoolOptions {
78
93
  * - "task_failed" – task failed (spawn error, bad exit code, verification failure)
79
94
  * - "task_stalled" – heartbeat missed for 60s, process was SIGKILLed
80
95
  * - "task_timeout" – hard timeout exceeded, process was SIGKILLed
81
- * - "budget_warning" – token usage crossed 80% threshold
96
+ * - "budget_warning" – token usage crossed 80% threshold (advisory)
97
+ * - "budget_exceeded" – token usage crossed 100% threshold (advisory; never kills)
82
98
  */
83
99
  export declare class SubagentPool extends EventEmitter {
84
100
  private readonly maxConcurrency;
@@ -95,11 +111,15 @@ export declare class SubagentPool extends EventEmitter {
95
111
  private verifier;
96
112
  private lifeguard;
97
113
  private disposed;
114
+ /** Lazily-loaded agent registry (frontmatter definitions) for this pool's cwd. */
115
+ private registry?;
98
116
  /** Tracks why a task was killed (stalled / timeout) before exit handler fires. */
99
117
  private killReasons;
100
118
  /** Persistent terminal status map, survives wait_for consumption. */
101
119
  private taskStatus;
102
120
  constructor(options: SubagentPoolOptions);
121
+ /** Lazily load the agent registry for this pool's cwd. */
122
+ private getRegistry;
103
123
  /** Priority value: higher numbers run first. */
104
124
  private priorityOf;
105
125
  /** Queue a task. It will run when a slot is free. */
@@ -122,6 +142,37 @@ export declare class SubagentPool extends EventEmitter {
122
142
  * `output.json`, and return the result.
123
143
  */
124
144
  dispatch(task: string, options?: DispatchOptions): Promise<TaskResult>;
145
+ /**
146
+ * Fire-and-forget dispatch for background agents. Spawns the subagent and
147
+ * returns its handle immediately; the caller polls get_status()/collect().
148
+ */
149
+ dispatchDetached(task: string, options?: DispatchOptions): {
150
+ handled_inline: boolean;
151
+ task_id?: string;
152
+ agent_type?: string;
153
+ reason?: string;
154
+ };
155
+ /**
156
+ * Evaluate, log, and spawn a task without waiting. Shared by dispatch()
157
+ * (blocking) and dispatchDetached() (background).
158
+ */
159
+ private beginDispatch;
160
+ /**
161
+ * Non-destructively read a completed task's result (for background polling).
162
+ * Returns undefined while the task is still running/queued, or if its result
163
+ * was already consumed via wait_for().
164
+ */
165
+ collect(task_id: string): SubagentResult | undefined;
166
+ /** Absolute path of the persisted session file for a task. */
167
+ getSessionFile(task_id: string, cwd?: string): string;
168
+ /**
169
+ * Resume a previously dispatched subagent, continuing its persisted session
170
+ * with a follow-up prompt. Recovers the original agent type from its dispatch
171
+ * log. Rejects if no resumable session exists for the task.
172
+ */
173
+ resume(task_id: string, prompt: string, options?: Omit<DispatchOptions, "forceAgent" | "sessionFile">): Promise<TaskResult>;
174
+ /** Recover the agent type a task was dispatched with, from its dispatch log. */
175
+ private readDispatchAgentType;
125
176
  /**
126
177
  * Dispatch a batch of subtasks concurrently.
127
178
  *