@kolisachint/hoocode-agent 0.4.9 → 0.4.11

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 (69) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core/dispatch-evaluator.d.ts +31 -0
  3. package/dist/core/dispatch-evaluator.d.ts.map +1 -0
  4. package/dist/core/dispatch-evaluator.js +300 -0
  5. package/dist/core/dispatch-evaluator.js.map +1 -0
  6. package/dist/core/lifeguard.d.ts +1 -0
  7. package/dist/core/lifeguard.d.ts.map +1 -1
  8. package/dist/core/lifeguard.js +6 -0
  9. package/dist/core/lifeguard.js.map +1 -1
  10. package/dist/core/slash-commands.d.ts.map +1 -1
  11. package/dist/core/slash-commands.js +1 -0
  12. package/dist/core/slash-commands.js.map +1 -1
  13. package/dist/core/subagent-pool.d.ts +35 -0
  14. package/dist/core/subagent-pool.d.ts.map +1 -1
  15. package/dist/core/subagent-pool.js +152 -11
  16. package/dist/core/subagent-pool.js.map +1 -1
  17. package/dist/core/subagent.d.ts +9 -1
  18. package/dist/core/subagent.d.ts.map +1 -1
  19. package/dist/core/subagent.js +23 -2
  20. package/dist/core/subagent.js.map +1 -1
  21. package/dist/core/task-store.d.ts +12 -3
  22. package/dist/core/task-store.d.ts.map +1 -1
  23. package/dist/core/task-store.js +5 -2
  24. package/dist/core/task-store.js.map +1 -1
  25. package/dist/core/tools/subagent.d.ts +5 -1
  26. package/dist/core/tools/subagent.d.ts.map +1 -1
  27. package/dist/core/tools/subagent.js +64 -13
  28. package/dist/core/tools/subagent.js.map +1 -1
  29. package/dist/init-templates.generated.d.ts +1 -0
  30. package/dist/init-templates.generated.d.ts.map +1 -1
  31. package/dist/init-templates.generated.js +8 -0
  32. package/dist/init-templates.generated.js.map +1 -1
  33. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  34. package/dist/modes/interactive/components/assistant-message.js +2 -2
  35. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  36. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  37. package/dist/modes/interactive/components/bash-execution.js +9 -14
  38. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  39. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  40. package/dist/modes/interactive/components/config-selector.js +1 -6
  41. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  42. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  43. package/dist/modes/interactive/components/model-selector.js +1 -5
  44. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  45. package/dist/modes/interactive/components/task-panel.d.ts +5 -2
  46. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  47. package/dist/modes/interactive/components/task-panel.js +103 -9
  48. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  49. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  50. package/dist/modes/interactive/components/tool-execution.js +12 -10
  51. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  52. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  53. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  54. package/dist/modes/interactive/interactive-mode.js +64 -3
  55. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  56. package/dist/modes/interactive/theme/dark.json +5 -5
  57. package/dist/modes/interactive/theme/light.json +5 -5
  58. package/docs/routing.md +57 -0
  59. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  60. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  61. package/examples/extensions/sandbox/package.json +1 -1
  62. package/examples/extensions/with-deps/package.json +1 -1
  63. package/package.json +4 -4
  64. package/templates/agents/doc.md +35 -0
  65. package/templates/agents/edit.md +37 -0
  66. package/templates/agents/explore.md +35 -0
  67. package/templates/agents/review.md +36 -0
  68. package/templates/agents/test.md +35 -0
  69. package/templates/subagent/doc.md +16 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.11] - 2026-05-30
4
+
5
+ ### Changed
6
+
7
+ - Task panel: subagent task titles are now limited to ~4–8 words so they stay legible in the pane.
8
+ - Task panel: finished tasks show combined token usage and elapsed time (`tokens · time`).
9
+ - Task panel: header shows a per-turn token + cost delta (`turn ↑in ↓out $cost`) summed across the turn's tasks.
10
+ - Task panel: the `[mode]` tag (e.g. `[explore]`) is no longer shown per row — the task title is the meaningful label.
11
+ - Finished subagent tasks now persist in the task panel until the next user message, instead of retiring when the main agent starts its next turn.
12
+
13
+ ## [0.4.10] - 2026-05-30
14
+
3
15
  ## [0.4.8] - 2026-05-30
4
16
 
5
17
  ### Added
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Deterministic subagent dispatch evaluator.
3
+ *
4
+ * Decides whether a task should be handled inline or delegated to a subagent,
5
+ * which subagent type to use, and whether a task should be split across
6
+ * multiple subagents. No LLM call — keyword + heuristic only.
7
+ */
8
+ export type AgentType = "explore" | "edit" | "test" | "review" | "doc";
9
+ export interface TaskAnalysis {
10
+ should_delegate: boolean;
11
+ agent_type: AgentType | null;
12
+ reason: string;
13
+ estimated_complexity: "low" | "medium" | "high";
14
+ parallelizable: boolean;
15
+ context_needed: string[];
16
+ }
17
+ export interface Subtask {
18
+ agent_type: AgentType;
19
+ prompt: string;
20
+ estimated_files: number;
21
+ }
22
+ export declare class DispatchEvaluator {
23
+ evaluate(task: string): TaskAnalysis;
24
+ shouldSplit(task: string): {
25
+ split: boolean;
26
+ subtasks: Subtask[];
27
+ };
28
+ canHandleInline(task: string): boolean;
29
+ getReason(analysis: TaskAnalysis): string;
30
+ }
31
+ //# sourceMappingURL=dispatch-evaluator.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Deterministic subagent dispatch evaluator.
3
+ *
4
+ * Decides whether a task should be handled inline or delegated to a subagent,
5
+ * which subagent type to use, and whether a task should be split across
6
+ * multiple subagents. No LLM call — keyword + heuristic only.
7
+ */
8
+ /* ------------------------------------------------------------------ */
9
+ // Keyword routing tables
10
+ const EXPLORE_KEYWORDS = [
11
+ "explore",
12
+ "understand",
13
+ "scout",
14
+ "investigate",
15
+ "trace",
16
+ "find",
17
+ "where",
18
+ "how does",
19
+ "what is",
20
+ "lookup",
21
+ "search",
22
+ "navigate",
23
+ "discover",
24
+ "map out",
25
+ "get familiar",
26
+ ];
27
+ const EDIT_KEYWORDS = [
28
+ "create",
29
+ "implement",
30
+ "refactor",
31
+ "add",
32
+ "build",
33
+ "change",
34
+ "update",
35
+ "modify",
36
+ "fix",
37
+ "repair",
38
+ "correct",
39
+ "migrate",
40
+ "rename",
41
+ "remove",
42
+ "delete",
43
+ "write",
44
+ ];
45
+ const TEST_KEYWORDS = [
46
+ "test",
47
+ "validate",
48
+ "assert",
49
+ "coverage",
50
+ "jest",
51
+ "vitest",
52
+ "mocha",
53
+ "pytest",
54
+ "unit test",
55
+ "integration test",
56
+ "e2e test",
57
+ "regression test",
58
+ ];
59
+ const REVIEW_KEYWORDS = [
60
+ "review",
61
+ "audit",
62
+ "critique",
63
+ "security",
64
+ "check",
65
+ "inspect",
66
+ "verify",
67
+ "assess",
68
+ "evaluate",
69
+ "analyze for",
70
+ "vulnerab",
71
+ "perf audit",
72
+ ];
73
+ const DOC_KEYWORDS = [
74
+ "readme",
75
+ "documentation",
76
+ "document",
77
+ "comment",
78
+ "explain",
79
+ "docs",
80
+ "guide",
81
+ "tutorial",
82
+ "changelog",
83
+ "api docs",
84
+ ];
85
+ const CROSS_DOMAIN_MARKERS = [
86
+ " and ",
87
+ " as well as ",
88
+ " plus ",
89
+ " then ",
90
+ " after that ",
91
+ " followed by ",
92
+ " in addition ",
93
+ " simultaneously ",
94
+ ];
95
+ /* ------------------------------------------------------------------ */
96
+ // Helpers
97
+ function countMatches(text, keywords) {
98
+ const lower = text.toLowerCase();
99
+ return keywords.reduce((count, kw) => count + (lower.includes(kw) ? 1 : 0), 0);
100
+ }
101
+ function detectAgentType(task) {
102
+ const lower = task.toLowerCase();
103
+ const scores = {
104
+ explore: countMatches(task, EXPLORE_KEYWORDS),
105
+ edit: countMatches(task, EDIT_KEYWORDS),
106
+ test: countMatches(task, TEST_KEYWORDS),
107
+ review: countMatches(task, REVIEW_KEYWORDS),
108
+ doc: countMatches(task, DOC_KEYWORDS),
109
+ };
110
+ // Boost doc when the task is clearly about documentation
111
+ if (scores.doc > 0 && (lower.includes("readme") || lower.includes("documentation") || lower.includes("document "))) {
112
+ scores.doc += 2;
113
+ }
114
+ // Boost test when the task is clearly about testing
115
+ if (scores.test > 0 && (lower.includes("test") || lower.includes("tests"))) {
116
+ scores.test += 2;
117
+ }
118
+ // Boost review for security-related tasks
119
+ if (scores.review > 0 && lower.includes("security")) {
120
+ scores.review += 2;
121
+ }
122
+ let best = null;
123
+ let bestScore = 0;
124
+ for (const [type, score] of Object.entries(scores)) {
125
+ if (score > bestScore) {
126
+ bestScore = score;
127
+ best = type;
128
+ }
129
+ }
130
+ return bestScore > 0 ? best : null;
131
+ }
132
+ function estimateComplexity(task) {
133
+ const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
134
+ const fileCount = fileMatches ? fileMatches.length : 0;
135
+ const lineMatch = task.match(/(\d+)\s*(lines?|loc)\b/i);
136
+ const lineCount = lineMatch ? Number.parseInt(lineMatch[1], 10) : 0;
137
+ const highScope = /\b(across|multiple|many|several|all files|rearchitect|redesign|migrate|restructure)\b/i.test(task);
138
+ const mediumScope = /\b(2|3|4|5)\s*files?\b/i.test(task) || /\b(few|some|couple)\b/i.test(task);
139
+ if (lineCount > 200 || fileCount >= 4 || highScope)
140
+ return "high";
141
+ if (lineCount > 50 || fileCount >= 2 || mediumScope)
142
+ return "medium";
143
+ return "low";
144
+ }
145
+ function canHandleInline(task) {
146
+ if (estimateComplexity(task) !== "low")
147
+ return false;
148
+ const fileMatches = task.match(/\b[\w/-]+\.(ts|js|tsx|jsx|py|go|rs|java|cpp|c|h|md|json|yaml|yml|toml)\b/g);
149
+ if (fileMatches && fileMatches.length > 1)
150
+ return false;
151
+ const hasCrossDomain = CROSS_DOMAIN_MARKERS.some((m) => task.toLowerCase().includes(m));
152
+ if (hasCrossDomain)
153
+ return false;
154
+ // Exploration: broad tasks delegate; simple lookups can be inline
155
+ const isExplore = detectAgentType(task) === "explore";
156
+ if (isExplore) {
157
+ const broadExplore = /\b(understand|investigate|trace|how does|how is|scout|map out|get familiar)\b/i.test(task);
158
+ return !broadExplore;
159
+ }
160
+ // Documentation tasks always delegate to the doc subagent
161
+ if (detectAgentType(task) === "doc") {
162
+ return false;
163
+ }
164
+ const isReadOnly = countMatches(task, EXPLORE_KEYWORDS) > 0 && countMatches(task, EDIT_KEYWORDS) === 0;
165
+ const isTrivialEdit = countMatches(task, EDIT_KEYWORDS) > 0 && !/\b(create|implement|build|refactor|migrate|restructure)\b/i.test(task);
166
+ return isReadOnly || isTrivialEdit;
167
+ }
168
+ function extractSubtasks(task) {
169
+ // Split on sentence boundaries and conjunctions, then classify each segment.
170
+ const segments = task
171
+ .split(/(?:[,;]|\.(?:\s+|$))\s*/)
172
+ .map((s) => s.trim())
173
+ .filter((s) => s.length > 10);
174
+ if (segments.length < 2) {
175
+ // No obvious sentence split — try cross-domain markers
176
+ const parts = [];
177
+ let remaining = task;
178
+ for (const marker of CROSS_DOMAIN_MARKERS) {
179
+ const idx = remaining.toLowerCase().indexOf(marker);
180
+ if (idx !== -1) {
181
+ parts.push(remaining.slice(0, idx).trim());
182
+ remaining = remaining.slice(idx + marker.length).trim();
183
+ }
184
+ }
185
+ if (parts.length > 0) {
186
+ parts.push(remaining);
187
+ return parts
188
+ .map((p) => {
189
+ const type = detectAgentType(p);
190
+ if (!type)
191
+ return null;
192
+ const est = estimateComplexity(p);
193
+ return {
194
+ agent_type: type,
195
+ prompt: p,
196
+ estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
197
+ };
198
+ })
199
+ .filter((s) => s !== null);
200
+ }
201
+ return [];
202
+ }
203
+ return segments
204
+ .map((segment) => {
205
+ const type = detectAgentType(segment);
206
+ if (!type)
207
+ return null;
208
+ const est = estimateComplexity(segment);
209
+ return {
210
+ agent_type: type,
211
+ prompt: segment,
212
+ estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
213
+ };
214
+ })
215
+ .filter((s) => s !== null);
216
+ }
217
+ /* ------------------------------------------------------------------ */
218
+ // Evaluator
219
+ export class DispatchEvaluator {
220
+ evaluate(task) {
221
+ const depth = Number.parseInt(process.env.HOOCODE_SUBAGENT_DEPTH ?? "0", 10);
222
+ if (depth >= 1) {
223
+ return {
224
+ should_delegate: false,
225
+ agent_type: null,
226
+ reason: "Subagents cannot spawn subagents",
227
+ estimated_complexity: "low",
228
+ parallelizable: false,
229
+ context_needed: [],
230
+ };
231
+ }
232
+ const agentType = detectAgentType(task);
233
+ const complexity = estimateComplexity(task);
234
+ const inline = canHandleInline(task);
235
+ const subtasks = extractSubtasks(task);
236
+ const parallelizable = subtasks.length > 1 || (complexity === "high" && subtasks.length > 0);
237
+ if (inline) {
238
+ return {
239
+ should_delegate: false,
240
+ agent_type: null,
241
+ reason: `Simple ${agentType ?? "task"} suitable for inline handling (<50 lines, 1 file, no cross-domain)`,
242
+ estimated_complexity: complexity,
243
+ parallelizable: false,
244
+ context_needed: [],
245
+ };
246
+ }
247
+ return {
248
+ should_delegate: true,
249
+ agent_type: agentType,
250
+ reason: `${agentType ?? "general"} task with ${complexity} complexity requires isolated subagent`,
251
+ estimated_complexity: complexity,
252
+ parallelizable,
253
+ context_needed: parallelizable ? subtasks.map((st) => st.prompt) : [task],
254
+ };
255
+ }
256
+ shouldSplit(task) {
257
+ const subtasks = extractSubtasks(task);
258
+ if (subtasks.length >= 2) {
259
+ return { split: true, subtasks };
260
+ }
261
+ // Check for explicit multi-domain keywords even when sentence splitting failed
262
+ const multiDomain = /\b(implement|write|create|refactor|fix|test|review|document|explore)\b.*\b(and|also|plus|then|followed by)\b.*\b(test|review|document|explore|implement|write|create|refactor|fix)\b/i.test(task);
263
+ if (!multiDomain)
264
+ return { split: false, subtasks: [] };
265
+ // Force split using cross-domain markers
266
+ const parts = [];
267
+ let remaining = task;
268
+ for (const marker of CROSS_DOMAIN_MARKERS) {
269
+ const idx = remaining.toLowerCase().indexOf(marker);
270
+ if (idx !== -1) {
271
+ parts.push(remaining.slice(0, idx).trim());
272
+ remaining = remaining.slice(idx + marker.length).trim();
273
+ }
274
+ }
275
+ if (parts.length === 0)
276
+ return { split: false, subtasks: [] };
277
+ parts.push(remaining);
278
+ const forcedSubtasks = parts
279
+ .map((p) => {
280
+ const type = detectAgentType(p);
281
+ if (!type)
282
+ return null;
283
+ const est = estimateComplexity(p);
284
+ return {
285
+ agent_type: type,
286
+ prompt: p,
287
+ estimated_files: est === "high" ? 4 : est === "medium" ? 2 : 1,
288
+ };
289
+ })
290
+ .filter((s) => s !== null);
291
+ return forcedSubtasks.length >= 2 ? { split: true, subtasks: forcedSubtasks } : { split: false, subtasks: [] };
292
+ }
293
+ canHandleInline(task) {
294
+ return canHandleInline(task);
295
+ }
296
+ getReason(analysis) {
297
+ return analysis.reason;
298
+ }
299
+ }
300
+ //# sourceMappingURL=dispatch-evaluator.js.map
@@ -0,0 +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"]}
@@ -18,6 +18,7 @@ export declare class SubagentLifeguard extends EventEmitter {
18
18
  private checkInterval;
19
19
  private disposed;
20
20
  private readonly cwd;
21
+ private parentShutdownHandler?;
21
22
  constructor(cwd: string);
22
23
  /**
23
24
  * Begin monitoring a child process. The process must emit a
@@ -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;IAE7B,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,CAsBd;IAED,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,aAAa;IAarB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,uBAAuB;IAO/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\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\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\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 { 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"]}
@@ -23,6 +23,7 @@ export class SubagentLifeguard extends EventEmitter {
23
23
  checkInterval = null;
24
24
  disposed = false;
25
25
  cwd;
26
+ parentShutdownHandler;
26
27
  constructor(cwd) {
27
28
  super();
28
29
  this.cwd = cwd;
@@ -84,6 +85,10 @@ export class SubagentLifeguard extends EventEmitter {
84
85
  this.processes.clear();
85
86
  this.lastHeartbeat.clear();
86
87
  this.removeAllListeners();
88
+ if (this.parentShutdownHandler) {
89
+ process.removeListener("SIGINT", this.parentShutdownHandler);
90
+ process.removeListener("SIGTERM", this.parentShutdownHandler);
91
+ }
87
92
  }
88
93
  checkHeartbeats() {
89
94
  const now = Date.now();
@@ -128,6 +133,7 @@ export class SubagentLifeguard extends EventEmitter {
128
133
  }
129
134
  setupParentExitHandlers() {
130
135
  const shutdown = () => this.gracefulShutdown();
136
+ this.parentShutdownHandler = shutdown;
131
137
  process.setMaxListeners(Math.max(process.getMaxListeners(), 20));
132
138
  process.once("SIGINT", shutdown);
133
139
  process.once("SIGTERM", shutdown);
@@ -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\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\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\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 { 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 +1 @@
1
- {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAsBrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
1
+ {"version":3,"file":"slash-commands.d.ts","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,sBAAsB,EAAE,aAAa,CAAC,mBAAmB,CAuBrE,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}
@@ -21,5 +21,6 @@ export const BUILTIN_SLASH_COMMANDS = [
21
21
  { name: "resume", description: "Resume a different session" },
22
22
  { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" },
23
23
  { name: "quit", description: `Quit ${APP_NAME}` },
24
+ { name: "subagent", description: "Spawn a subagent directly: /subagent <mode> <task>" },
24
25
  ];
25
26
  //# sourceMappingURL=slash-commands.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;CACjD,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n];\n"]}
1
+ {"version":3,"file":"slash-commands.js","sourceRoot":"","sources":["../../src/core/slash-commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiBxC,MAAM,CAAC,MAAM,sBAAsB,GAAuC;IACzE,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oBAAoB,EAAE;IACvD,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,kCAAkC,EAAE;IAClE,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,EAAE,0CAA0C,EAAE;IAClF,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,8DAA8D,EAAE;IAC/F,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+CAA+C,EAAE;IAChF,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uCAAuC,EAAE;IACvE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,0BAA0B,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE;IAC5D,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,6BAA6B,EAAE;IAC/D,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAC/E,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,uDAAuD,EAAE;IACvF,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,yCAAyC,EAAE;IACxE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,mCAAmC,EAAE;IACnE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,gCAAgC,EAAE;IACjE,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE;IACnD,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,sCAAsC,EAAE;IACxE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,4BAA4B,EAAE;IAC7D,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,6DAA6D,EAAE;IAC9F,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,QAAQ,EAAE,EAAE;IACjD,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,oDAAoD,EAAE;CACvF,CAAC","sourcesContent":["import { APP_NAME } from \"../config.js\";\nimport type { SourceInfo } from \"./source-info.js\";\n\nexport type SlashCommandSource = \"extension\" | \"prompt\" | \"skill\";\n\nexport interface SlashCommandInfo {\n\tname: string;\n\tdescription?: string;\n\tsource: SlashCommandSource;\n\tsourceInfo: SourceInfo;\n}\n\nexport interface BuiltinSlashCommand {\n\tname: string;\n\tdescription: string;\n}\n\nexport const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand> = [\n\t{ name: \"settings\", description: \"Open settings menu\" },\n\t{ name: \"model\", description: \"Select model (opens selector UI)\" },\n\t{ name: \"scoped-models\", description: \"Enable/disable models for Ctrl+P cycling\" },\n\t{ name: \"export\", description: \"Export session (HTML default, or specify path: .html/.jsonl)\" },\n\t{ name: \"import\", description: \"Import and resume a session from a JSONL file\" },\n\t{ name: \"share\", description: \"Share session as a secret GitHub gist\" },\n\t{ name: \"copy\", description: \"Copy last agent message to clipboard\" },\n\t{ name: \"name\", description: \"Set session display name\" },\n\t{ name: \"session\", description: \"Show session info and stats\" },\n\t{ name: \"changelog\", description: \"Show changelog entries\" },\n\t{ name: \"hotkeys\", description: \"Show all keyboard shortcuts\" },\n\t{ name: \"fork\", description: \"Create a new fork from a previous user message\" },\n\t{ name: \"clone\", description: \"Duplicate the current session at the current position\" },\n\t{ name: \"tree\", description: \"Navigate session tree (switch branches)\" },\n\t{ name: \"login\", description: \"Configure provider authentication\" },\n\t{ name: \"logout\", description: \"Remove provider authentication\" },\n\t{ name: \"new\", description: \"Start a new session\" },\n\t{ name: \"compact\", description: \"Manually compact the session context\" },\n\t{ name: \"resume\", description: \"Resume a different session\" },\n\t{ name: \"reload\", description: \"Reload keybindings, extensions, skills, prompts, and themes\" },\n\t{ name: \"quit\", description: `Quit ${APP_NAME}` },\n\t{ name: \"subagent\", description: \"Spawn a subagent directly: /subagent <mode> <task>\" },\n];\n"]}