@kolisachint/hoocode-agent 0.4.8 → 0.4.10

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 +2 -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/slash-commands.d.ts.map +1 -1
  7. package/dist/core/slash-commands.js +1 -0
  8. package/dist/core/slash-commands.js.map +1 -1
  9. package/dist/core/subagent-pool.d.ts +35 -0
  10. package/dist/core/subagent-pool.d.ts.map +1 -1
  11. package/dist/core/subagent-pool.js +147 -7
  12. package/dist/core/subagent-pool.js.map +1 -1
  13. package/dist/core/subagent.d.ts +9 -1
  14. package/dist/core/subagent.d.ts.map +1 -1
  15. package/dist/core/subagent.js +23 -2
  16. package/dist/core/subagent.js.map +1 -1
  17. package/dist/core/task-store.d.ts +9 -1
  18. package/dist/core/task-store.d.ts.map +1 -1
  19. package/dist/core/task-store.js +2 -0
  20. package/dist/core/task-store.js.map +1 -1
  21. package/dist/core/tools/subagent.d.ts +5 -1
  22. package/dist/core/tools/subagent.d.ts.map +1 -1
  23. package/dist/core/tools/subagent.js +49 -8
  24. package/dist/core/tools/subagent.js.map +1 -1
  25. package/dist/extensions/core/hoo-core.d.ts +5 -2
  26. package/dist/extensions/core/hoo-core.d.ts.map +1 -1
  27. package/dist/extensions/core/hoo-core.js +3 -1
  28. package/dist/extensions/core/hoo-core.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 +4 -2
  46. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  47. package/dist/modes/interactive/components/task-panel.js +69 -6
  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 +59 -0
  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
@@ -34,6 +34,8 @@ class TaskStore {
34
34
  task.status = patch.status;
35
35
  if (patch.subagentMode !== undefined)
36
36
  task.subagentMode = patch.subagentMode;
37
+ if (patch.usage !== undefined)
38
+ task.usage = patch.usage;
37
39
  task.updatedAt = Date.now();
38
40
  this.emit();
39
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n}\n\nexport interface CreateTaskOptions {\n\tsubagentMode?: string;\n}\n\nexport type TaskPatch = Partial<Pick<Task, \"title\" | \"status\" | \"subagentMode\">>;\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Remove all finished tasks (done or failed), keeping pending/in_progress ones.\n\t *\n\t * Called when the main agent moves on after a parallel subagent spawn: finished\n\t * tasks stay visible (with their final status) until that point, then retire.\n\t */\n\tretireFinished(): void {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.status !== \"done\" && t.status !== \"failed\");\n\t\tif (this.tasks.length !== before) this.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\t/** Clear all tasks. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
1
+ {"version":3,"file":"task-store.js","sourceRoot":"","sources":["../../src/core/task-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Minimal in-process task store.\n *\n * Tracks short-lived tasks (e.g. subagent delegations) so the TUI task panel can\n * display active work. It is a process-level singleton because the tool that\n * creates tasks and the footer that renders them live in the same process and\n * there is no cross-process boundary to cross.\n */\n\nexport type TaskStatus = \"pending\" | \"in_progress\" | \"done\" | \"failed\";\n\nexport interface Task {\n\treadonly id: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Subagent mode when this task is owned by a subagent (e.g. \"explore\"). */\n\tsubagentMode?: string;\n\treadonly createdAt: number;\n\tupdatedAt: number;\n\t/** Token and cost usage attributed to this task (e.g. from a subagent session). */\n\tusage?: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\tcost: number;\n\t};\n}\n\nexport interface CreateTaskOptions {\n\tsubagentMode?: string;\n}\n\nexport type TaskPatch = Partial<Pick<Task, \"title\" | \"status\" | \"subagentMode\" | \"usage\">>;\n\ntype Listener = () => void;\n\nclass TaskStore {\n\tprivate tasks: Task[] = [];\n\tprivate nextId = 1;\n\tprivate readonly listeners = new Set<Listener>();\n\n\tcreate(title: string, options: CreateTaskOptions = {}): Task {\n\t\tconst now = Date.now();\n\t\tconst task: Task = {\n\t\t\tid: this.nextId++,\n\t\t\ttitle: title.trim() || \"(untitled task)\",\n\t\t\tstatus: \"pending\",\n\t\t\tsubagentMode: options.subagentMode,\n\t\t\tcreatedAt: now,\n\t\t\tupdatedAt: now,\n\t\t};\n\t\tthis.tasks.push(task);\n\t\tthis.emit();\n\t\treturn task;\n\t}\n\n\tupdate(id: number, patch: TaskPatch): void {\n\t\tconst task = this.tasks.find((t) => t.id === id);\n\t\tif (!task) return;\n\t\tif (patch.title !== undefined) task.title = patch.title;\n\t\tif (patch.status !== undefined) task.status = patch.status;\n\t\tif (patch.subagentMode !== undefined) task.subagentMode = patch.subagentMode;\n\t\tif (patch.usage !== undefined) task.usage = patch.usage;\n\t\ttask.updatedAt = Date.now();\n\t\tthis.emit();\n\t}\n\n\tremove(id: number): void {\n\t\tconst idx = this.tasks.findIndex((t) => t.id === id);\n\t\tif (idx === -1) return;\n\t\tthis.tasks.splice(idx, 1);\n\t\tthis.emit();\n\t}\n\n\t/**\n\t * Remove all finished tasks (done or failed), keeping pending/in_progress ones.\n\t *\n\t * Called when the main agent moves on after a parallel subagent spawn: finished\n\t * tasks stay visible (with their final status) until that point, then retire.\n\t */\n\tretireFinished(): void {\n\t\tconst before = this.tasks.length;\n\t\tthis.tasks = this.tasks.filter((t) => t.status !== \"done\" && t.status !== \"failed\");\n\t\tif (this.tasks.length !== before) this.emit();\n\t}\n\n\tlist(): readonly Task[] {\n\t\treturn this.tasks;\n\t}\n\n\tsubscribe(listener: Listener): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\t/** Clear all tasks. Intended for test isolation only. */\n\tclear(): void {\n\t\tthis.tasks = [];\n\t\tthis.nextId = 1;\n\t\tthis.emit();\n\t}\n\n\tprivate emit(): void {\n\t\tfor (const listener of this.listeners) {\n\t\t\tlistener();\n\t\t}\n\t}\n}\n\n/** Shared, process-wide task store. */\nexport const taskStore = new TaskStore();\n"]}
@@ -8,7 +8,7 @@
8
8
  */
9
9
  /** System prompt appendix for the main session when subagent tooling is enabled.
10
10
  * Instructs the parent agent on when and how to delegate effectively. */
11
- export declare const SUBAGENT_MAIN_PROMPT = "You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the `context` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.";
11
+ export declare const SUBAGENT_MAIN_PROMPT = "You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n- doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the `context` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.\n\nDispatch evaluator:\n- The dispatch evaluator determines if a subagent is needed. Do not spawn subagents directly unless the user explicitly requests it.\n- For simple single-file changes (<50 lines, read-only or trivial edit), handle them inline.\n- Use force=true to bypass evaluation when you are certain a subagent is required.";
12
12
  import type { ToolDefinition } from "../extensions/types.js";
13
13
  import { type SubagentMode } from "../subagent.js";
14
14
  export interface SubagentToolDetails {
@@ -16,7 +16,11 @@ export interface SubagentToolDetails {
16
16
  ok: boolean;
17
17
  error?: string;
18
18
  taskId: number;
19
+ /** True when the evaluator handled the task inline instead of delegating. */
20
+ inline?: boolean;
19
21
  }
22
+ /** Quick check: should this task go to a subagent? */
23
+ export declare function isSubagentRecommended(task: string): boolean;
20
24
  /** Create the subagent tool definition. Registered as a customTool when enabled. */
21
25
  export declare function createSubagentToolDefinition(): ToolDefinition;
22
26
  //# sourceMappingURL=subagent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;0EAC0E;AAC1E,eAAO,MAAM,oBAAoB,m8CAoBiF,CAAC;AAInH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA6BhE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CACf;AAQD,oFAAoF;AACpF,wBAAgB,4BAA4B,IAAI,cAAc,CAiE7D","sourcesContent":["/**\n * Subagent tool: delegate a focused task to a fresh, isolated agent loop.\n *\n * The tool registers a task in the shared task store (visible in the task panel),\n * runs the subagent to completion, and returns ONLY the subagent's final\n * answer. It is an optional, opt-in tool (enabled via --subagent or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\n/** System prompt appendix for the main session when subagent tooling is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport const SUBAGENT_MAIN_PROMPT = `You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the \\`context\\` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.`;\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { runSubagent, type SubagentMode } from \"../subagent.js\";\nimport { taskStore } from \"../task-store.js\";\n\nconst subagentParams = Type.Object({\n\ttask: Type.String({\n\t\tdescription:\n\t\t\t\"The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.\",\n\t}),\n\tcontext: Type.String({\n\t\tdescription:\n\t\t\t'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass \"\" if none.',\n\t}),\n\tmode: Type.Union(\n\t\t[\n\t\t\tType.Literal(\"explore\"),\n\t\t\tType.Literal(\"edit\"),\n\t\t\tType.Literal(\"test\"),\n\t\t\tType.Literal(\"fix\"),\n\t\t\tType.Literal(\"review\"),\n\t\t],\n\t\t{\n\t\t\tdescription:\n\t\t\t\t\"explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review.\",\n\t\t},\n\t),\n});\n\ntype SubagentParams = Static<typeof subagentParams>;\n\nexport interface SubagentToolDetails {\n\tmode: SubagentMode;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n}\n\n/** First line of the task, trimmed to a short summary for the task list/footer. */\nfunction summarize(task: string): string {\n\tconst firstLine = task.trim().split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine || \"(task)\";\n}\n\n/** Create the subagent tool definition. Registered as a customTool when enabled. */\nexport function createSubagentToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof subagentParams, SubagentToolDetails>({\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Pass everything it needs via `context`. The subagent returns only its final answer.\",\n\t\t\t\"Modes: explore, edit, test, fix, review.\",\n\t\t\t\"WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.\",\n\t\t\t\"(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.\",\n\t\t\t\"(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\",\n\t\t\t\"(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.\",\n\t\t\t\"Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.\",\n\t\t].join(\" \"),\n\t\tpromptSnippet: \"delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review)\",\n\t\tparameters: subagentParams,\n\n\t\tasync execute(_toolCallId, params: SubagentParams, signal, _onUpdate, ctx) {\n\t\t\tconst mode = params.mode as SubagentMode;\n\t\t\tconst summary = summarize(params.task);\n\n\t\t\tconst task = taskStore.create(summary, { subagentMode: mode });\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst result = await runSubagent({\n\t\t\t\t\ttask: params.task,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tmode,\n\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\tmodel: ctx.model,\n\t\t\t\t\tmodelRegistry: ctx.modelRegistry,\n\t\t\t\t\tsignal: signal ?? ctx.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\t// Signal failure by throwing: the agent loop derives a tool's error\n\t\t\t\t\t// state from a thrown error, not from a returned flag.\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\tthrow new Error(`Subagent (${mode}) failed: ${result.error ?? \"unknown error\"}`);\n\t\t\t\t}\n\n\t\t\t\t// Leave the task in the store with its final status. It stays visible in\n\t\t\t\t// the task panel until the main agent moves on (retireFinished is called\n\t\t\t\t// when the main agent starts its next turn).\n\t\t\t\ttaskStore.update(task.id, { status: \"done\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: result.answer || \"(subagent returned no output)\" }],\n\t\t\t\t\tdetails: { mode, ok: true, taskId: task.id },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst mode = args.mode ?? \"explore\";\n\t\t\tconst preview = summarize(args.task ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"subagent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${mode}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"subagent.d.ts","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;0EAC0E;AAC1E,eAAO,MAAM,oBAAoB,82DA0BkD,CAAC;AAKpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmChE,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAQD,sDAAsD;AACtD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3D;AAOD,oFAAoF;AACpF,wBAAgB,4BAA4B,IAAI,cAAc,CAuF7D","sourcesContent":["/**\n * Subagent tool: delegate a focused task to a fresh, isolated agent loop.\n *\n * The tool registers a task in the shared task store (visible in the task panel),\n * runs the subagent to completion, and returns ONLY the subagent's final\n * answer. It is an optional, opt-in tool (enabled via --subagent or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\n/** System prompt appendix for the main session when subagent tooling is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport const SUBAGENT_MAIN_PROMPT = `You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n- doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the \\`context\\` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.\n\nDispatch evaluator:\n- The dispatch evaluator determines if a subagent is needed. Do not spawn subagents directly unless the user explicitly requests it.\n- For simple single-file changes (<50 lines, read-only or trivial edit), handle them inline.\n- Use force=true to bypass evaluation when you are certain a subagent is required.`;\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { type AgentType, DispatchEvaluator } from \"../dispatch-evaluator.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { runSubagent, type SubagentMode } from \"../subagent.js\";\nimport { taskStore } from \"../task-store.js\";\n\nconst subagentParams = Type.Object({\n\ttask: Type.String({\n\t\tdescription:\n\t\t\t\"The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.\",\n\t}),\n\tcontext: Type.String({\n\t\tdescription:\n\t\t\t'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass \"\" if none.',\n\t}),\n\tmode: Type.Union(\n\t\t[\n\t\t\tType.Literal(\"explore\"),\n\t\t\tType.Literal(\"edit\"),\n\t\t\tType.Literal(\"test\"),\n\t\t\tType.Literal(\"fix\"),\n\t\t\tType.Literal(\"review\"),\n\t\t\tType.Literal(\"doc\"),\n\t\t],\n\t\t{\n\t\t\tdescription:\n\t\t\t\t\"explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review. doc: write documentation.\",\n\t\t},\n\t),\n\tforce: Type.Boolean({\n\t\tdescription:\n\t\t\t\"Bypass dispatch evaluation and spawn the subagent directly. Use when you are certain a subagent is required.\",\n\t\tdefault: false,\n\t}),\n});\n\ntype SubagentParams = Static<typeof subagentParams>;\n\nexport interface SubagentToolDetails {\n\tmode: SubagentMode;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** True when the evaluator handled the task inline instead of delegating. */\n\tinline?: boolean;\n}\n\n/** First line of the task, trimmed to a short summary for the task list/footer. */\nfunction summarize(task: string): string {\n\tconst firstLine = task.trim().split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine || \"(task)\";\n}\n\n/** Quick check: should this task go to a subagent? */\nexport function isSubagentRecommended(task: string): boolean {\n\tconst evaluator = new DispatchEvaluator();\n\treturn evaluator.evaluate(task).should_delegate;\n}\n\n/** Map evaluator AgentType to SubagentMode (they are compatible strings). */\nfunction toSubagentMode(agentType: AgentType | null): SubagentMode {\n\treturn (agentType as SubagentMode) ?? \"explore\";\n}\n\n/** Create the subagent tool definition. Registered as a customTool when enabled. */\nexport function createSubagentToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof subagentParams, SubagentToolDetails>({\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Pass everything it needs via `context`. The subagent returns only its final answer.\",\n\t\t\t\"Modes: explore, edit, test, fix, review, doc.\",\n\t\t\t\"WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.\",\n\t\t\t\"(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.\",\n\t\t\t\"(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs).\",\n\t\t\t\"(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.\",\n\t\t\t\"Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.\",\n\t\t\t\"Use force=true to bypass dispatch evaluation when you are certain a subagent is required.\",\n\t\t].join(\" \"),\n\t\tpromptSnippet: \"delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review/doc)\",\n\t\tparameters: subagentParams,\n\n\t\tasync execute(_toolCallId, params: SubagentParams, signal, _onUpdate, ctx) {\n\t\t\tconst forcedMode = params.mode as SubagentMode;\n\n\t\t\t// Dispatch evaluation\n\t\t\tif (!params.force) {\n\t\t\t\tconst evaluator = new DispatchEvaluator();\n\t\t\t\tconst analysis = evaluator.evaluate(params.task);\n\t\t\t\tif (!analysis.should_delegate) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `Task is simple enough for inline handling. Reason: ${analysis.reason}. Use force=true for subagent override.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { mode: forcedMode, ok: true, taskId: 0, inline: true },\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst mode = params.force\n\t\t\t\t? forcedMode\n\t\t\t\t: toSubagentMode(new DispatchEvaluator().evaluate(params.task).agent_type);\n\t\t\tconst summary = summarize(params.task);\n\n\t\t\tconst task = taskStore.create(summary, { subagentMode: mode });\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst result = await runSubagent({\n\t\t\t\t\ttask: params.task,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tmode,\n\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\tmodel: ctx.model,\n\t\t\t\t\tmodelRegistry: ctx.modelRegistry,\n\t\t\t\t\tsignal: signal ?? ctx.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\t// Signal failure by throwing: the agent loop derives a tool's error\n\t\t\t\t\t// state from a thrown error, not from a returned flag.\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\", usage: result.usage });\n\t\t\t\t\tthrow new Error(`Subagent (${mode}) failed: ${result.error ?? \"unknown error\"}`);\n\t\t\t\t}\n\n\t\t\t\t// Leave the task in the store with its final status. It stays visible in\n\t\t\t\t// the task panel until the main agent moves on (retireFinished is called\n\t\t\t\t// when the main agent starts its next turn).\n\t\t\t\ttaskStore.update(task.id, { status: \"done\", usage: result.usage });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: result.answer || \"(subagent returned no output)\" }],\n\t\t\t\t\tdetails: { mode, ok: true, taskId: task.id },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst mode = args.mode ?? \"explore\";\n\t\t\tconst preview = summarize(args.task ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"subagent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${mode}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
@@ -16,6 +16,7 @@ Available subagent modes:
16
16
  - test: run tests and report (read, bash, grep, find, ls).
17
17
  - fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).
18
18
  - review: read-only code review (read, grep, find, ls, bash).
19
+ - doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).
19
20
 
20
21
  When to delegate:
21
22
  1. The work is self-contained and you only need the final result, not intermediate steps.
@@ -28,9 +29,15 @@ Guidelines:
28
29
  - Pass all necessary context (files, constraints, prior findings) via the \`context\` parameter.
29
30
  - Do NOT delegate tasks that require tight back-and-forth with your current reasoning.
30
31
  - Do NOT delegate edits to files you are actively reasoning about.
31
- - The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.`;
32
+ - The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.
33
+
34
+ Dispatch evaluator:
35
+ - The dispatch evaluator determines if a subagent is needed. Do not spawn subagents directly unless the user explicitly requests it.
36
+ - For simple single-file changes (<50 lines, read-only or trivial edit), handle them inline.
37
+ - Use force=true to bypass evaluation when you are certain a subagent is required.`;
32
38
  import { Text } from "@kolisachint/hoocode-tui";
33
39
  import { Type } from "typebox";
40
+ import { DispatchEvaluator } from "../dispatch-evaluator.js";
34
41
  import { defineTool } from "../extensions/types.js";
35
42
  import { runSubagent } from "../subagent.js";
36
43
  import { taskStore } from "../task-store.js";
@@ -47,8 +54,13 @@ const subagentParams = Type.Object({
47
54
  Type.Literal("test"),
48
55
  Type.Literal("fix"),
49
56
  Type.Literal("review"),
57
+ Type.Literal("doc"),
50
58
  ], {
51
- description: "explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review.",
59
+ description: "explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review. doc: write documentation.",
60
+ }),
61
+ force: Type.Boolean({
62
+ description: "Bypass dispatch evaluation and spawn the subagent directly. Use when you are certain a subagent is required.",
63
+ default: false,
52
64
  }),
53
65
  });
54
66
  /** First line of the task, trimmed to a short summary for the task list/footer. */
@@ -56,6 +68,15 @@ function summarize(task) {
56
68
  const firstLine = task.trim().split("\n")[0] ?? "";
57
69
  return firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine || "(task)";
58
70
  }
71
+ /** Quick check: should this task go to a subagent? */
72
+ export function isSubagentRecommended(task) {
73
+ const evaluator = new DispatchEvaluator();
74
+ return evaluator.evaluate(task).should_delegate;
75
+ }
76
+ /** Map evaluator AgentType to SubagentMode (they are compatible strings). */
77
+ function toSubagentMode(agentType) {
78
+ return agentType ?? "explore";
79
+ }
59
80
  /** Create the subagent tool definition. Registered as a customTool when enabled. */
60
81
  export function createSubagentToolDefinition() {
61
82
  return defineTool({
@@ -64,17 +85,37 @@ export function createSubagentToolDefinition() {
64
85
  description: [
65
86
  "Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).",
66
87
  "Pass everything it needs via `context`. The subagent returns only its final answer.",
67
- "Modes: explore, edit, test, fix, review.",
88
+ "Modes: explore, edit, test, fix, review, doc.",
68
89
  "WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.",
69
90
  "(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.",
70
- "(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).",
91
+ "(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs).",
71
92
  "(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.",
72
93
  "Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.",
94
+ "Use force=true to bypass dispatch evaluation when you are certain a subagent is required.",
73
95
  ].join(" "),
74
- promptSnippet: "delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review)",
96
+ promptSnippet: "delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review/doc)",
75
97
  parameters: subagentParams,
76
98
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
77
- const mode = params.mode;
99
+ const forcedMode = params.mode;
100
+ // Dispatch evaluation
101
+ if (!params.force) {
102
+ const evaluator = new DispatchEvaluator();
103
+ const analysis = evaluator.evaluate(params.task);
104
+ if (!analysis.should_delegate) {
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text",
109
+ text: `Task is simple enough for inline handling. Reason: ${analysis.reason}. Use force=true for subagent override.`,
110
+ },
111
+ ],
112
+ details: { mode: forcedMode, ok: true, taskId: 0, inline: true },
113
+ };
114
+ }
115
+ }
116
+ const mode = params.force
117
+ ? forcedMode
118
+ : toSubagentMode(new DispatchEvaluator().evaluate(params.task).agent_type);
78
119
  const summary = summarize(params.task);
79
120
  const task = taskStore.create(summary, { subagentMode: mode });
80
121
  taskStore.update(task.id, { status: "in_progress" });
@@ -91,13 +132,13 @@ export function createSubagentToolDefinition() {
91
132
  if (!result.ok) {
92
133
  // Signal failure by throwing: the agent loop derives a tool's error
93
134
  // state from a thrown error, not from a returned flag.
94
- taskStore.update(task.id, { status: "failed" });
135
+ taskStore.update(task.id, { status: "failed", usage: result.usage });
95
136
  throw new Error(`Subagent (${mode}) failed: ${result.error ?? "unknown error"}`);
96
137
  }
97
138
  // Leave the task in the store with its final status. It stays visible in
98
139
  // the task panel until the main agent moves on (retireFinished is called
99
140
  // when the main agent starts its next turn).
100
- taskStore.update(task.id, { status: "done" });
141
+ taskStore.update(task.id, { status: "done", usage: result.usage });
101
142
  return {
102
143
  content: [{ type: "text", text: result.answer || "(subagent returned no output)" }],
103
144
  details: { mode, ok: true, taskId: task.id },
@@ -1 +1 @@
1
- {"version":3,"file":"subagent.js","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subagent tool: delegate a focused task to a fresh, isolated agent loop.\n *\n * The tool registers a task in the shared task store (visible in the task panel),\n * runs the subagent to completion, and returns ONLY the subagent's final\n * answer. It is an optional, opt-in tool (enabled via --subagent or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\n/** System prompt appendix for the main session when subagent tooling is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport const SUBAGENT_MAIN_PROMPT = `You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the \\`context\\` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.`;\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { runSubagent, type SubagentMode } from \"../subagent.js\";\nimport { taskStore } from \"../task-store.js\";\n\nconst subagentParams = Type.Object({\n\ttask: Type.String({\n\t\tdescription:\n\t\t\t\"The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.\",\n\t}),\n\tcontext: Type.String({\n\t\tdescription:\n\t\t\t'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass \"\" if none.',\n\t}),\n\tmode: Type.Union(\n\t\t[\n\t\t\tType.Literal(\"explore\"),\n\t\t\tType.Literal(\"edit\"),\n\t\t\tType.Literal(\"test\"),\n\t\t\tType.Literal(\"fix\"),\n\t\t\tType.Literal(\"review\"),\n\t\t],\n\t\t{\n\t\t\tdescription:\n\t\t\t\t\"explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review.\",\n\t\t},\n\t),\n});\n\ntype SubagentParams = Static<typeof subagentParams>;\n\nexport interface SubagentToolDetails {\n\tmode: SubagentMode;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n}\n\n/** First line of the task, trimmed to a short summary for the task list/footer. */\nfunction summarize(task: string): string {\n\tconst firstLine = task.trim().split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine || \"(task)\";\n}\n\n/** Create the subagent tool definition. Registered as a customTool when enabled. */\nexport function createSubagentToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof subagentParams, SubagentToolDetails>({\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Pass everything it needs via `context`. The subagent returns only its final answer.\",\n\t\t\t\"Modes: explore, edit, test, fix, review.\",\n\t\t\t\"WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.\",\n\t\t\t\"(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.\",\n\t\t\t\"(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\",\n\t\t\t\"(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.\",\n\t\t\t\"Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.\",\n\t\t].join(\" \"),\n\t\tpromptSnippet: \"delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review)\",\n\t\tparameters: subagentParams,\n\n\t\tasync execute(_toolCallId, params: SubagentParams, signal, _onUpdate, ctx) {\n\t\t\tconst mode = params.mode as SubagentMode;\n\t\t\tconst summary = summarize(params.task);\n\n\t\t\tconst task = taskStore.create(summary, { subagentMode: mode });\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst result = await runSubagent({\n\t\t\t\t\ttask: params.task,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tmode,\n\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\tmodel: ctx.model,\n\t\t\t\t\tmodelRegistry: ctx.modelRegistry,\n\t\t\t\t\tsignal: signal ?? ctx.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\t// Signal failure by throwing: the agent loop derives a tool's error\n\t\t\t\t\t// state from a thrown error, not from a returned flag.\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\tthrow new Error(`Subagent (${mode}) failed: ${result.error ?? \"unknown error\"}`);\n\t\t\t\t}\n\n\t\t\t\t// Leave the task in the store with its final status. It stays visible in\n\t\t\t\t// the task panel until the main agent moves on (retireFinished is called\n\t\t\t\t// when the main agent starts its next turn).\n\t\t\t\ttaskStore.update(task.id, { status: \"done\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: result.answer || \"(subagent returned no output)\" }],\n\t\t\t\t\tdetails: { mode, ok: true, taskId: task.id },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst mode = args.mode ?? \"explore\";\n\t\t\tconst preview = summarize(args.task ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"subagent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${mode}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
1
+ {"version":3,"file":"subagent.js","sourceRoot":"","sources":["../../../src/core/tools/subagent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Subagent tool: delegate a focused task to a fresh, isolated agent loop.\n *\n * The tool registers a task in the shared task store (visible in the task panel),\n * runs the subagent to completion, and returns ONLY the subagent's final\n * answer. It is an optional, opt-in tool (enabled via --subagent or the\n * `enableSubagent` setting); see buildSessionOptions in main.ts.\n */\n\n/** System prompt appendix for the main session when subagent tooling is enabled.\n * Instructs the parent agent on when and how to delegate effectively. */\nexport const SUBAGENT_MAIN_PROMPT = `You have access to the **subagent** tool. Use it to delegate self-contained tasks to isolated subagent loops that run with their own context and return only their final answer.\n\nAvailable subagent modes:\n- explore: read-only investigation (read, grep, find, ls, bash).\n- edit: make a focused code change (read, edit, write, grep, find, ls, bash).\n- test: run tests and report (read, bash, grep, find, ls).\n- fix: diagnose and fix a failure (read, edit, write, bash, grep, find, ls).\n- review: read-only code review (read, grep, find, ls, bash).\n- doc: write documentation, README, or comments (read, write, edit, grep, find, ls, bash).\n\nWhen to delegate:\n1. The work is self-contained and you only need the final result, not intermediate steps.\n2. You want to investigate or edit something in parallel without losing your current context or reasoning chain.\n3. The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug).\n4. You need to run a long command or test suite and wait for its output without blocking your own reasoning.\n\nGuidelines:\n- Make every task specific and self-contained. The subagent cannot see this conversation.\n- Pass all necessary context (files, constraints, prior findings) via the \\`context\\` parameter.\n- Do NOT delegate tasks that require tight back-and-forth with your current reasoning.\n- Do NOT delegate edits to files you are actively reasoning about.\n- The subagent returns ONLY its final answer. Intermediate reasoning, tool calls, and output are hidden from you.\n\nDispatch evaluator:\n- The dispatch evaluator determines if a subagent is needed. Do not spawn subagents directly unless the user explicitly requests it.\n- For simple single-file changes (<50 lines, read-only or trivial edit), handle them inline.\n- Use force=true to bypass evaluation when you are certain a subagent is required.`;\n\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { type AgentType, DispatchEvaluator } from \"../dispatch-evaluator.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { defineTool } from \"../extensions/types.js\";\nimport { runSubagent, type SubagentMode } from \"../subagent.js\";\nimport { taskStore } from \"../task-store.js\";\n\nconst subagentParams = Type.Object({\n\ttask: Type.String({\n\t\tdescription:\n\t\t\t\"The task to delegate. Make it specific and self-contained; the subagent cannot see this conversation.\",\n\t}),\n\tcontext: Type.String({\n\t\tdescription:\n\t\t\t'Context distilled from the conversation the subagent needs (files, constraints, prior findings). Pass \"\" if none.',\n\t}),\n\tmode: Type.Union(\n\t\t[\n\t\t\tType.Literal(\"explore\"),\n\t\t\tType.Literal(\"edit\"),\n\t\t\tType.Literal(\"test\"),\n\t\t\tType.Literal(\"fix\"),\n\t\t\tType.Literal(\"review\"),\n\t\t\tType.Literal(\"doc\"),\n\t\t],\n\t\t{\n\t\t\tdescription:\n\t\t\t\t\"explore: read-only investigation. edit: make a focused code change. test: run tests and report. fix: diagnose and fix a failure. review: read-only code review. doc: write documentation.\",\n\t\t},\n\t),\n\tforce: Type.Boolean({\n\t\tdescription:\n\t\t\t\"Bypass dispatch evaluation and spawn the subagent directly. Use when you are certain a subagent is required.\",\n\t\tdefault: false,\n\t}),\n});\n\ntype SubagentParams = Static<typeof subagentParams>;\n\nexport interface SubagentToolDetails {\n\tmode: SubagentMode;\n\tok: boolean;\n\terror?: string;\n\ttaskId: number;\n\t/** True when the evaluator handled the task inline instead of delegating. */\n\tinline?: boolean;\n}\n\n/** First line of the task, trimmed to a short summary for the task list/footer. */\nfunction summarize(task: string): string {\n\tconst firstLine = task.trim().split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.length > 60 ? `${firstLine.slice(0, 57)}...` : firstLine || \"(task)\";\n}\n\n/** Quick check: should this task go to a subagent? */\nexport function isSubagentRecommended(task: string): boolean {\n\tconst evaluator = new DispatchEvaluator();\n\treturn evaluator.evaluate(task).should_delegate;\n}\n\n/** Map evaluator AgentType to SubagentMode (they are compatible strings). */\nfunction toSubagentMode(agentType: AgentType | null): SubagentMode {\n\treturn (agentType as SubagentMode) ?? \"explore\";\n}\n\n/** Create the subagent tool definition. Registered as a customTool when enabled. */\nexport function createSubagentToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof subagentParams, SubagentToolDetails>({\n\t\tname: \"subagent\",\n\t\tlabel: \"Subagent\",\n\t\tdescription: [\n\t\t\t\"Delegate a focused task to a subagent that runs in a fresh, isolated context (it cannot see this conversation).\",\n\t\t\t\"Pass everything it needs via `context`. The subagent returns only its final answer.\",\n\t\t\t\"Modes: explore, edit, test, fix, review, doc.\",\n\t\t\t\"WHEN TO USE: (1) The work is self-contained and you do not need to see intermediate steps — only the final result.\",\n\t\t\t\"(2) You want to investigate or edit something in parallel without losing your current context or reasoning chain.\",\n\t\t\t\"(3) The task is a discrete unit (explore one module, run one test file, review one PR, fix one isolated bug, write docs).\",\n\t\t\t\"(4) You need to run a long command or test suite and wait for its output without blocking your own reasoning.\",\n\t\t\t\"Do NOT use for tasks that require tight back-and-forth with your current reasoning or that change files you are actively reasoning about.\",\n\t\t\t\"Use force=true to bypass dispatch evaluation when you are certain a subagent is required.\",\n\t\t].join(\" \"),\n\t\tpromptSnippet: \"delegate a self-contained task to an isolated subagent (modes: explore/edit/test/fix/review/doc)\",\n\t\tparameters: subagentParams,\n\n\t\tasync execute(_toolCallId, params: SubagentParams, signal, _onUpdate, ctx) {\n\t\t\tconst forcedMode = params.mode as SubagentMode;\n\n\t\t\t// Dispatch evaluation\n\t\t\tif (!params.force) {\n\t\t\t\tconst evaluator = new DispatchEvaluator();\n\t\t\t\tconst analysis = evaluator.evaluate(params.task);\n\t\t\t\tif (!analysis.should_delegate) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `Task is simple enough for inline handling. Reason: ${analysis.reason}. Use force=true for subagent override.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: { mode: forcedMode, ok: true, taskId: 0, inline: true },\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst mode = params.force\n\t\t\t\t? forcedMode\n\t\t\t\t: toSubagentMode(new DispatchEvaluator().evaluate(params.task).agent_type);\n\t\t\tconst summary = summarize(params.task);\n\n\t\t\tconst task = taskStore.create(summary, { subagentMode: mode });\n\t\t\ttaskStore.update(task.id, { status: \"in_progress\" });\n\t\t\ttry {\n\t\t\t\tconst result = await runSubagent({\n\t\t\t\t\ttask: params.task,\n\t\t\t\t\tcontext: params.context,\n\t\t\t\t\tmode,\n\t\t\t\t\tcwd: ctx.cwd,\n\t\t\t\t\tmodel: ctx.model,\n\t\t\t\t\tmodelRegistry: ctx.modelRegistry,\n\t\t\t\t\tsignal: signal ?? ctx.signal,\n\t\t\t\t});\n\n\t\t\t\tif (!result.ok) {\n\t\t\t\t\t// Signal failure by throwing: the agent loop derives a tool's error\n\t\t\t\t\t// state from a thrown error, not from a returned flag.\n\t\t\t\t\ttaskStore.update(task.id, { status: \"failed\", usage: result.usage });\n\t\t\t\t\tthrow new Error(`Subagent (${mode}) failed: ${result.error ?? \"unknown error\"}`);\n\t\t\t\t}\n\n\t\t\t\t// Leave the task in the store with its final status. It stays visible in\n\t\t\t\t// the task panel until the main agent moves on (retireFinished is called\n\t\t\t\t// when the main agent starts its next turn).\n\t\t\t\ttaskStore.update(task.id, { status: \"done\", usage: result.usage });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: result.answer || \"(subagent returned no output)\" }],\n\t\t\t\t\tdetails: { mode, ok: true, taskId: task.id },\n\t\t\t\t};\n\t\t\t} catch (error) {\n\t\t\t\ttaskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\trenderCall(args, theme) {\n\t\t\tconst mode = args.mode ?? \"explore\";\n\t\t\tconst preview = summarize(args.task ?? \"\");\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"subagent \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${mode}]`) +\n\t\t\t\ttheme.fg(\"dim\", ` ${preview}`);\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t});\n}\n"]}
@@ -103,6 +103,9 @@ export declare function parsePlanSections(planContent: string): PlanSections;
103
103
  */
104
104
  export declare function buildApproveMessage(sections: PlanSections): string;
105
105
  export declare function setupMode(pi: ExtensionAPI): void;
106
- export default function hooCore(pi: ExtensionAPI): void;
107
- export {};
106
+ declare function hooCore(pi: ExtensionAPI): void;
107
+ declare namespace hooCore {
108
+ var displayName: string;
109
+ }
110
+ export default hooCore;
108
111
  //# sourceMappingURL=hoo-core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,KAAK,EAKX,YAAY,EAMZ,MAAM,gCAAgC,CAAC;AA2DxC,UAAU,UAAU;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,8FAA8F;IAC9F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,SAAS;IACzB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,CA8B7E;AAuBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAUvD;AAyCD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiD1D;AAgBD,MAAM,WAAW,eAAe;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAuGD,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4FrD;AAmCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAGnH;AAMD,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CA2BnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAMD,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8MhD;AAMD,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAItD","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension\n *\n * A. Permission Gate — prompts before bash/write/edit; checks modes.{mode}.auto_allow\n * from the merged (global + project) config; persists \"always\"\n * choices back to the global config\n * B. MCP Server Loader — discovers ~/.hoocode/mcp-servers and ./.hoocode/mcp-servers JSON\n * configs, connects via JSON-RPC 2.0, registers server tools\n * C. Mode — resolves active mode (ask/plan/build/debug), loads the mode's\n * system prompt, filters active tools, and exposes /mode, /plan,\n * and /approve commands\n *\n * Config merge order (lowest → highest priority):\n * 1. ~/.hoocode/agent/hoo-config.json (global defaults)\n * 2. ./.hoocode/config.json (project overrides — scalars win; arrays union)\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type Static, Type } from \"typebox\";\nimport { getHooCodeDir } from \"../../config.js\";\nimport type {\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\n\n// ============================================================================\n// Fallback defaults for mode prompts\n// ============================================================================\n\nconst MODE_DEFAULTS: Record<string, string> = {\n\task: `You are in ASK mode — read-only Q&A.\nAnswer questions about the codebase. Trace logic, compare approaches, explain patterns.\nYou may read any file but NEVER write, edit, or execute commands.\nIf asked to make changes, refuse and suggest switching to /mode build.\nCite specific file paths and line numbers in your answers.`,\n\n\tplan: `You are in PLAN mode — exploration and planning.\nExplore the codebase thoroughly. Understand the current structure.\nDraft a complete plan with sections: Goal, Files to modify, New files, Tests, Verification.\nWrite the plan to {{PLAN_PATH}}.\nWhen the plan is complete, tell the user to run /approve to execute it.`,\n\n\tbuild: `You are in BUILD mode — careful implementation.\nRead files before editing them. Show diffs before non-trivial changes.\nAsk for confirmation before destructive operations (delete, reformat).\nRun tests after every logical unit of work.\nPrefer the smallest change that achieves the goal.\nFollow existing code patterns and conventions.`,\n\n\tdebug: `You are in DEBUG mode — root cause analysis.\nGather evidence: read files, check logs, reproduce the issue.\nTrace the call path from entry to failure point.\nState the root cause in one sentence.\nDescribe the fix precisely but do NOT apply it.\nTo fix, switch to /mode build.`,\n};\n\n// ============================================================================\n// Shared paths\n// ============================================================================\n\nconst HOOCODE_DIR = getHooCodeDir();\nconst GLOBAL_CONFIG_PATH = join(HOOCODE_DIR, \"agent\", \"hoo-config.json\");\n\n/**\n * Per-session plan file path. Keying on sessionId lets concurrent or resumed\n * plan sessions keep distinct plans instead of clobbering each other.\n */\nfunction getPlanPath(cwd: string, sessionId: string): string {\n\treturn join(cwd, \".hoocode\", \"plans\", `${sessionId}.md`);\n}\n\n/** Legacy single-file plan location, retained as a read-only fallback for /approve. */\nfunction getLegacyPlanPath(cwd: string): string {\n\treturn join(cwd, \".hoocode\", \"plan.md\");\n}\n\n// ============================================================================\n// Config types\n// ============================================================================\n\ninterface ModeConfig {\n\t/** Tool names that bypass the permission gate in this mode */\n\tauto_allow?: string[];\n\t/** Tool names available in this mode (if set, only these tools are active) */\n\tenabled_tools?: string[];\n\t/** Allowed write paths in this mode (glob patterns, only applies if write/edit is enabled) */\n\tallowed_write_paths?: string[];\n}\n\nexport interface HooConfig {\n\t/** Manually-pinned active mode (overrides default \"build\") */\n\tactive_mode?: string;\n\t/** Per-mode configuration keyed by mode name */\n\tmodes?: Record<string, ModeConfig>;\n\t/** Extra directories to search for `{name}/system.md` mode files (after project + user). */\n\tmode_paths?: string[];\n}\n\n// ============================================================================\n// Config I/O and merging\n// ============================================================================\n\nfunction readConfig(): HooConfig {\n\ttry {\n\t\treturn JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, \"utf8\")) as HooConfig;\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nfunction writeConfig(config: HooConfig): void {\n\tconst dir = join(HOOCODE_DIR, \"agent\");\n\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\twriteFileSync(GLOBAL_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\n}\n\n/**\n * Deep-merges a project-local config on top of the global config.\n *\n * Merge rules:\n * - active_mode: project wins if set\n * - modes[x].auto_allow: union of global + project arrays\n * - modes[x].allowed_write_paths: union of global + project arrays\n * - modes[x].enabled_tools: project wins if set, else falls back to global\n * - mode_paths: project list is prepended so project paths are searched first\n */\nexport function mergeConfigs(global: HooConfig, project: HooConfig): HooConfig {\n\tconst merged: HooConfig = { ...global };\n\n\tif (project.active_mode !== undefined) merged.active_mode = project.active_mode;\n\n\tif (project.modes) {\n\t\tmerged.modes = { ...(global.modes ?? {}) };\n\t\tfor (const [mode, projectCfg] of Object.entries(project.modes)) {\n\t\t\tconst globalCfg = global.modes?.[mode] ?? {};\n\t\t\tmerged.modes[mode] = {\n\t\t\t\t...globalCfg,\n\t\t\t\t...projectCfg,\n\t\t\t\t// Union both auto_allow lists so project can extend, not just replace\n\t\t\t\tauto_allow: Array.from(new Set([...(globalCfg.auto_allow ?? []), ...(projectCfg.auto_allow ?? [])])),\n\t\t\t\t// Union allowed_write_paths so project can extend\n\t\t\t\tallowed_write_paths: Array.from(\n\t\t\t\t\tnew Set([...(globalCfg.allowed_write_paths ?? []), ...(projectCfg.allowed_write_paths ?? [])]),\n\t\t\t\t),\n\t\t\t\t// enabled_tools: project wins if set, else falls back to global\n\t\t\t\tenabled_tools: projectCfg.enabled_tools ?? globalCfg.enabled_tools,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (project.mode_paths || global.mode_paths) {\n\t\t// Project paths first so they're searched before global paths\n\t\tmerged.mode_paths = dedupePaths([...(project.mode_paths ?? []), ...(global.mode_paths ?? [])]);\n\t}\n\n\treturn merged;\n}\n\nfunction dedupePaths(paths: string[]): string[] {\n\tconst seen = new Set<string>();\n\tconst out: string[] = [];\n\tfor (const p of paths) {\n\t\tif (!seen.has(p)) {\n\t\t\tseen.add(p);\n\t\t\tout.push(p);\n\t\t}\n\t}\n\treturn out;\n}\n\nfunction mergeSearchPaths(...sources: (string[] | undefined)[]): string[] {\n\tconst merged: string[] = [];\n\tfor (const source of sources) {\n\t\tif (!source) continue;\n\t\tmerged.push(...source);\n\t}\n\treturn dedupePaths(merged);\n}\n\n/**\n * Reads the global config and optionally overlays the project-local config at\n * `./.hoocode/config.json`. Project values win on all scalar fields; arrays are\n * unioned (see mergeConfigs for full rules).\n */\nexport function readMergedConfig(cwd: string): HooConfig {\n\tconst global = readConfig();\n\tconst projectPath = join(cwd, \".hoocode\", \"config.json\");\n\tif (!existsSync(projectPath)) return global;\n\ttry {\n\t\tconst project = JSON.parse(readFileSync(projectPath, \"utf8\")) as HooConfig;\n\t\treturn mergeConfigs(global, project);\n\t} catch {\n\t\treturn global;\n\t}\n}\n\n// ============================================================================\n// A. Permission Gate\n// ============================================================================\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\"]);\n\n/**\n * Checks if a file path matches any of the allowed patterns.\n * Supports glob patterns with * and exact paths.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[]): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tfor (const pattern of allowedPatterns) {\n\t\t// Exact match\n\t\tif (pattern === filePath) return true;\n\t\t// Glob pattern matching for *\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, \".*\")}$`);\n\t\t\tif (regex.test(filePath)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\tconst p = (event.input as { file_path?: string }).file_path ?? \"(unknown)\";\n\t\treturn `edit ${p}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\tconst p = (event.input as { file_path?: string }).file_path ?? \"(unknown)\";\n\t\treturn `write ${p}`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\t// Use the merged config so project-local auto_allow entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\tconst filePath = (event.input as { file_path?: string }).file_path ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n\n// ============================================================================\n// B. MCP Server Loader\n// ============================================================================\n\ninterface McpToolDef {\n\tname: string;\n\tdescription: string;\n\tinputSchema?: {\n\t\ttype?: string;\n\t\tproperties?: Record<string, { type?: string; description?: string }>;\n\t\trequired?: string[];\n\t};\n}\n\nexport interface McpServerConfig {\n\t/** Unique server identifier used as prefix for registered tool names */\n\tname: string;\n\t/** Executable to spawn */\n\tcommand: string;\n\t/** Optional arguments passed to the command */\n\targs?: string[];\n\t/** Optional extra environment variables for the server process */\n\tenv?: Record<string, string>;\n}\n\ninterface McpConnection {\n\trpc(method: string, params?: unknown): Promise<unknown>;\n\tterminate(): void;\n}\n\nconst mcpConnections = new Map<string, McpConnection>();\n\nfunction spawnMcpServer(config: McpServerConfig): McpConnection {\n\tconst proc: ChildProcess = spawn(config.command, config.args ?? [], {\n\t\tenv: { ...process.env, ...(config.env ?? {}) },\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t});\n\n\tlet nextId = 1;\n\tconst pending = new Map<number, { resolve: (r: unknown) => void; reject: (e: Error) => void }>();\n\n\tconst rl = createInterface({ input: proc.stdout! });\n\trl.on(\"line\", (line) => {\n\t\tif (!line.trim()) return;\n\t\ttry {\n\t\t\tconst msg = JSON.parse(line) as {\n\t\t\t\tid?: number;\n\t\t\t\tresult?: unknown;\n\t\t\t\terror?: { message: string };\n\t\t\t};\n\t\t\tif (msg.id === undefined) return;\n\t\t\tconst cb = pending.get(msg.id);\n\t\t\tif (!cb) return;\n\t\t\tpending.delete(msg.id);\n\t\t\tif (msg.error) cb.reject(new Error(msg.error.message));\n\t\t\telse cb.resolve(msg.result);\n\t\t} catch {\n\t\t\t// ignore non-JSON server startup output\n\t\t}\n\t});\n\n\tproc.on(\"exit\", () => {\n\t\tfor (const cb of pending.values()) cb.reject(new Error(`MCP server \"${config.name}\" exited unexpectedly`));\n\t\tpending.clear();\n\t\tmcpConnections.delete(config.name);\n\t});\n\n\tfunction rpc(method: string, params?: unknown): Promise<unknown> {\n\t\tconst id = nextId++;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tpending.set(id, { resolve, reject });\n\t\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, method, params })}\\n`);\n\t\t});\n\t}\n\n\treturn {\n\t\trpc,\n\t\tterminate: () => {\n\t\t\trl.close();\n\t\t\tproc.kill();\n\t\t},\n\t};\n}\n\nasync function connectMcpServer(config: McpServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tmcpConnections.get(config.name)?.terminate();\n\n\tconst conn = spawnMcpServer(config);\n\tmcpConnections.set(config.name, conn);\n\n\tawait conn.rpc(\"initialize\", {\n\t\tprotocolVersion: \"2024-11-05\",\n\t\tcapabilities: { tools: {} },\n\t\tclientInfo: { name: \"hoocode\", version: \"1.0.0\" },\n\t});\n\n\tconst toolsResult = (await conn.rpc(\"tools/list\", {})) as {\n\t\ttools?: McpToolDef[];\n\t};\n\treturn { conn, tools: toolsResult.tools ?? [] };\n}\n\nfunction buildMcpSchema(tool: McpToolDef): ReturnType<typeof Type.Object> {\n\tconst props = tool.inputSchema?.properties ?? {};\n\tconst required = new Set(tool.inputSchema?.required ?? []);\n\tconst shape: Record<string, ReturnType<typeof Type.String>> = {};\n\n\tfor (const [key, prop] of Object.entries(props)) {\n\t\tlet field: ReturnType<typeof Type.String>;\n\t\tswitch (prop.type) {\n\t\t\tcase \"number\":\n\t\t\tcase \"integer\":\n\t\t\t\tfield = Type.Number({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tcase \"boolean\":\n\t\t\t\tfield = Type.Boolean({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfield = Type.String({ description: prop.description });\n\t\t}\n\t\tshape[key] = required.has(key) ? field : (Type.Optional(field) as unknown as ReturnType<typeof Type.String>);\n\t}\n\n\treturn Type.Object(shape);\n}\n\nexport function setupMcpLoader(pi: ExtensionAPI): void {\n\tpi.on(\"session_start\", async (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tconst searchDirs = [join(HOOCODE_DIR, \"mcp-servers\"), join(ctx.cwd, \".hoocode\", \"mcp-servers\")];\n\n\t\tfor (const dir of searchDirs) {\n\t\t\tif (!existsSync(dir)) continue;\n\n\t\t\tlet files: string[];\n\t\t\ttry {\n\t\t\t\tfiles = (await readdir(dir)).filter((f) => f.endsWith(\".json\"));\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const file of files) {\n\t\t\t\tconst cfgPath = join(dir, file);\n\t\t\t\tlet serverConfig: McpServerConfig;\n\n\t\t\t\ttry {\n\t\t\t\t\tserverConfig = JSON.parse(readFileSync(cfgPath, \"utf8\")) as McpServerConfig;\n\t\t\t\t\tif (!serverConfig.name || !serverConfig.command) {\n\t\t\t\t\t\tctx.ui.notify(`MCP: config \"${file}\" is missing required \"name\" or \"command\"`, \"warning\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to parse \"${file}\": ${String(err)}`, \"error\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { tools } = await connectMcpServer(serverConfig);\n\n\t\t\t\t\tfor (const tool of tools) {\n\t\t\t\t\t\tconst toolName = `mcp_${serverConfig.name}_${tool.name}`;\n\t\t\t\t\t\tconst schema = buildMcpSchema(tool);\n\t\t\t\t\t\tconst capturedServer = serverConfig.name;\n\t\t\t\t\t\tconst capturedTool = tool.name;\n\n\t\t\t\t\t\tpi.registerTool({\n\t\t\t\t\t\t\tname: toolName,\n\t\t\t\t\t\t\tlabel: `[MCP] ${serverConfig.name} › ${tool.name}`,\n\t\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\t\tparameters: schema,\n\t\t\t\t\t\t\tasync execute(\n\t\t\t\t\t\t\t\t_toolCallId: string,\n\t\t\t\t\t\t\t\tparams: Static<typeof schema>,\n\t\t\t\t\t\t\t\tsignal: AbortSignal,\n\t\t\t\t\t\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t\t\t\t\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\t\t\t\t\t\tconst activeConn = mcpConnections.get(capturedServer);\n\t\t\t\t\t\t\t\tif (!activeConn) {\n\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\ttext: `MCP server \"${capturedServer}\" is not connected`,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\t\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")));\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\t\t\t\t\tactiveConn.rpc(\"tools/call\", {\n\t\t\t\t\t\t\t\t\t\tname: capturedTool,\n\t\t\t\t\t\t\t\t\t\targuments: params,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\tabortPromise,\n\t\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`MCP: connected \"${serverConfig.name}\" (${tools.length} tool${tools.length === 1 ? \"\" : \"s\"})`,\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to connect \"${serverConfig.name}\": ${String(err)}`, \"error\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n}\n\n// ============================================================================\n// C. Mode System\n// ============================================================================\n\nconst DEFAULT_MODE = \"build\";\n\nfunction tryReadFile(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst text = readFileSync(path, \"utf8\").trim();\n\t\treturn text || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Walks search dirs in precedence order and returns the first existing\n * `modes/{name}/system.md` content. Order: project → user → externalDirs.\n */\nfunction resolveModeFile(name: string, cwd: string, externalDirs: string[]): string | undefined {\n\tconst candidates: string[] = [\n\t\tjoin(cwd, \".hoocode\", \"modes\", name, \"system.md\"),\n\t\tjoin(HOOCODE_DIR, \"modes\", name, \"system.md\"),\n\t\t...externalDirs.map((dir) => join(dir, name, \"system.md\")),\n\t];\n\tfor (const candidate of candidates) {\n\t\tconst content = tryReadFile(candidate);\n\t\tif (content !== undefined) return content;\n\t}\n\treturn undefined;\n}\n\n/**\n * Returns the system prompt for the active mode.\n *\n * Search order (first hit wins):\n * - `./.hoocode/modes/{mode}/system.md`\n * - `~/.hoocode/modes/{mode}/system.md`\n * - each of `externalDirs` in declared order (config + CLI + extension contributions)\n * - built-in MODE_DEFAULTS for the four known modes\n */\nexport function buildSystemPrompt(mode: string, cwd: string, options?: { modePaths?: string[] }): string | undefined {\n\tconst modePaths = options?.modePaths ?? [];\n\treturn resolveModeFile(mode, cwd, modePaths) ?? MODE_DEFAULTS[mode];\n}\n\n// ============================================================================\n// Plan file: section parsing and step-by-step execution message\n// ============================================================================\n\nexport interface PlanSections {\n\tgoal?: string;\n\tfilesToModify?: string;\n\tnewFiles?: string;\n\ttests?: string;\n\tverification?: string;\n\t/** Original full text, used as fallback if no sections parsed */\n\traw: string;\n}\n\n/**\n * Parses `.hoocode/plan.md` into named sections.\n *\n * Recognises both ATX headings (`## Goal`) and bold labels (`**Goal**`).\n * Section names matched (case-insensitive): Goal, Files to modify, New files,\n * Tests, Verification.\n */\nexport function parsePlanSections(planContent: string): PlanSections {\n\tconst result: PlanSections = { raw: planContent };\n\n\t// Match `## Heading text` or `**Heading text**` followed by content until\n\t// the next heading of the same style.\n\tconst sectionPattern =\n\t\t/^(?:#{1,3}\\s+(.+?)|(?:\\*\\*(.+?)\\*\\*))\\s*\\n([\\s\\S]*?)(?=(?:^#{1,3}\\s+|\\*\\*[^*\\n]+\\*\\*\\s*\\n)|$)/gm;\n\n\tfor (const match of planContent.matchAll(sectionPattern)) {\n\t\tconst heading = (match[1] ?? match[2] ?? \"\").toLowerCase().trim();\n\t\tconst content = match[3].trim();\n\t\tif (!content) continue;\n\n\t\tif (/^goal/.test(heading)) {\n\t\t\tresult.goal = content;\n\t\t} else if (/files?\\s+to\\s+modif|^modif/.test(heading)) {\n\t\t\tresult.filesToModify = content;\n\t\t} else if (/new\\s+files?/.test(heading)) {\n\t\t\tresult.newFiles = content;\n\t\t} else if (/^tests?/.test(heading)) {\n\t\t\tresult.tests = content;\n\t\t} else if (/^verif/.test(heading)) {\n\t\t\tresult.verification = content;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Builds the user message sent to the agent when `/approve` is run.\n *\n * If the plan has recognisable sections, each is presented as a numbered step\n * so the agent works through them sequentially. Otherwise the raw plan is used.\n *\n * Execution order:\n * 1. Modify existing files\n * 2. Create new files\n * 3. Update / add tests\n * 4. Run verification commands\n */\nexport function buildApproveMessage(sections: PlanSections): string {\n\tconst steps: string[] = [];\n\n\tif (sections.goal) {\n\t\tsteps.push(`**Goal:** ${sections.goal}`);\n\t}\n\tif (sections.filesToModify) {\n\t\tsteps.push(`**Step 1 — Modify existing files:**\\n${sections.filesToModify}`);\n\t}\n\tif (sections.newFiles) {\n\t\tsteps.push(`**Step 2 — Create new files:**\\n${sections.newFiles}`);\n\t}\n\tif (sections.tests) {\n\t\tsteps.push(`**Step 3 — Update tests:**\\n${sections.tests}`);\n\t}\n\tif (sections.verification) {\n\t\tsteps.push(`**Step 4 — Verify:**\\n${sections.verification}`);\n\t}\n\n\tif (steps.length === 0) {\n\t\treturn `Execute the following plan:\\n\\n${sections.raw}`;\n\t}\n\n\treturn `Execute this plan step by step. Complete each step fully before moving to the next.\\n\\n${steps.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// C. setupMode\n// ============================================================================\n\nexport function setupMode(pi: ExtensionAPI): void {\n\tlet cachedMode = DEFAULT_MODE;\n\tlet cachedSystemPrompt: string | undefined;\n\tlet cachedPlanPath: string | undefined;\n\n\t// ── session_start ─────────────────────────────────────────────────────────\n\t// Config resolution order:\n\t// 1. Read global config (~/.hoocode/agent/hoo-config.json)\n\t// 2. Read project config (./.hoocode/config.json) if present\n\t// 3. Merge — project scalars win; arrays are unioned\n\t// 4. Re-resolve active_mode from the merged result\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\t// Steps 1–3: merge global + project configs\n\t\tconst config = readMergedConfig(ctx.cwd);\n\n\t\t// Step 4: resolve mode from the merged config\n\t\tcachedMode = config.active_mode ?? DEFAULT_MODE;\n\t\t// External search dirs come from two channels:\n\t\t// - HooConfig.mode_paths (config-declared)\n\t\t// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.getModeSearchPaths());\n\t\tconst rawSystemPrompt = buildSystemPrompt(cachedMode, ctx.cwd, { modePaths });\n\n\t\t// Per-session plan path so concurrent sessions don't overwrite each other.\n\t\t// The `{{PLAN_PATH}}` token in plan-mode templates is substituted here.\n\t\tcachedPlanPath = getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\tconst relPlanPath = relative(ctx.cwd, cachedPlanPath) || cachedPlanPath;\n\t\tcachedSystemPrompt = rawSystemPrompt?.replace(/\\{\\{PLAN_PATH\\}\\}/g, relPlanPath);\n\n\t\t// Update footer with active mode\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.setMode(cachedMode);\n\t\t}\n\n\t\t// Apply tool filter from mode enabled_tools\n\t\tconst modeCfg = config.modes?.[cachedMode];\n\t\tif (modeCfg?.enabled_tools && modeCfg.enabled_tools.length > 0) {\n\t\t\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined => {\n\t\tif (!cachedSystemPrompt) return;\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\n<!-- hoo-core: mode=${cachedMode} -->\\n${cachedSystemPrompt}`,\n\t\t};\n\t});\n\n\t// ── /mode command ─────────────────────────────────────────────────────────\n\n\tconst KNOWN_MODES = [\"ask\", \"plan\", \"build\", \"debug\"];\n\n\tpi.registerCommand(\"mode\", {\n\t\tdescription: \"Switch active mode. Usage: /mode <ask|plan|build|debug>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tKNOWN_MODES.filter((m) => m.startsWith(prefix)).map((m) => ({ value: m, label: m })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst name = args.trim();\n\t\t\tif (!name) {\n\t\t\t\tctx.ui.notify(`Active mode: ${cachedMode}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = name === DEFAULT_MODE ? undefined : name;\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"${name}\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /plan command (shorthand for /mode plan) ──────────────────────────────\n\n\tpi.registerCommand(\"plan\", {\n\t\tdescription: \"Switch to plan mode. Shorthand for /mode plan.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"plan\";\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"plan\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /approve command ──────────────────────────────────────────────────────\n\t// Reads .hoocode/plan.md, parses it into named sections (Goal, Files to\n\t// modify, New files, Tests, Verification), switches to build mode, then\n\t// injects a step-by-step execution message into the new session.\n\n\tpi.registerCommand(\"approve\", {\n\t\tdescription: \"Approve the current plan and switch to build mode to execute it.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tif (cachedMode !== \"plan\") {\n\t\t\t\tctx.ui.notify(`/approve is only available in plan mode (current mode: \"${cachedMode}\")`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Prefer the per-session plan file, fall back to the legacy single file.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst candidatePaths = [sessionPlanPath, getLegacyPlanPath(ctx.cwd)];\n\t\t\tlet approveMessage: string | undefined;\n\n\t\t\tfor (const planPath of candidatePaths) {\n\t\t\t\tif (!existsSync(planPath)) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst raw = readFileSync(planPath, \"utf8\").trim();\n\t\t\t\t\tif (raw) {\n\t\t\t\t\t\tconst sections = parsePlanSections(raw);\n\t\t\t\t\t\tapproveMessage = buildApproveMessage(sections);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, planPath) || planPath}`, \"error\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Switch global config to build mode\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"build\";\n\t\t\twriteConfig(config);\n\n\t\t\tif (approveMessage) {\n\t\t\t\t// Open a new build-mode session and deliver the parsed plan as the\n\t\t\t\t// first user message so the agent starts executing immediately\n\t\t\t\tawait ctx.newSession({\n\t\t\t\t\twithSession: async (replacedCtx) => {\n\t\t\t\t\t\tawait replacedCtx.sendUserMessage(approveMessage!, { deliverAs: \"followUp\" });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(`Switched to build mode. No ${relPlan} found — describe what to build.`, \"info\");\n\t\t\t\tawait ctx.reload();\n\t\t\t}\n\t\t},\n\t});\n\n\t// ── /cost command ─────────────────────────────────────────────────────────\n\t// Walks every assistant message in the current session and sums tokens + cost,\n\t// then prints a session total followed by a per-model breakdown.\n\t// Per-tool attribution is intentionally not shown — tokens aren't tracked\n\t// per-tool, and any heuristic would be misleading.\n\n\tpi.registerCommand(\"cost\", {\n\t\tdescription: \"Show session token and cost totals, broken down by model.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\ttype Totals = { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number };\n\t\t\tconst empty = (): Totals => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });\n\t\t\tconst total = empty();\n\t\t\tconst perModel = new Map<string, Totals>();\n\t\t\tlet assistantTurns = 0;\n\n\t\t\tfor (const entry of ctx.sessionManager.getEntries()) {\n\t\t\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\t\t\tconst u = entry.message.usage;\n\t\t\t\tif (!u) continue;\n\t\t\t\tassistantTurns++;\n\t\t\t\ttotal.input += u.input;\n\t\t\t\ttotal.output += u.output;\n\t\t\t\ttotal.cacheRead += u.cacheRead;\n\t\t\t\ttotal.cacheWrite += u.cacheWrite;\n\t\t\t\ttotal.cost += u.cost.total;\n\n\t\t\t\tconst key = `${entry.message.provider}/${entry.message.model}`;\n\t\t\t\tconst t = perModel.get(key) ?? empty();\n\t\t\t\tt.input += u.input;\n\t\t\t\tt.output += u.output;\n\t\t\t\tt.cacheRead += u.cacheRead;\n\t\t\t\tt.cacheWrite += u.cacheWrite;\n\t\t\t\tt.cost += u.cost.total;\n\t\t\t\tperModel.set(key, t);\n\t\t\t}\n\n\t\t\tif (assistantTurns === 0) {\n\t\t\t\tctx.ui.notify(\"No assistant turns yet — nothing to cost.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst fmt = (n: number) => n.toLocaleString();\n\t\t\tconst fmtCost = (n: number) => `$${n.toFixed(4)}`;\n\t\t\tconst lines: string[] = [];\n\t\t\tlines.push(`Session totals (${assistantTurns} assistant turn${assistantTurns === 1 ? \"\" : \"s\"})`);\n\t\t\tlines.push(` Input ${fmt(total.input)}`);\n\t\t\tlines.push(` Output ${fmt(total.output)}`);\n\t\t\tlines.push(` Cache read ${fmt(total.cacheRead)}`);\n\t\t\tlines.push(` Cache write ${fmt(total.cacheWrite)}`);\n\t\t\tlines.push(` Cost ${fmtCost(total.cost)}`);\n\n\t\t\tif (perModel.size > 1) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(\"By model:\");\n\t\t\t\tconst sorted = [...perModel.entries()].sort((a, b) => b[1].cost - a[1].cost);\n\t\t\t\tfor (const [key, t] of sorted) {\n\t\t\t\t\tlines.push(` ${key}: ${fmt(t.input)} in / ${fmt(t.output)} out ${fmtCost(t.cost)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t},\n\t});\n}\n\n// ============================================================================\n// Extension entry point\n// ============================================================================\n\nexport default function hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n}\n"]}
1
+ {"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,KAAK,EAKX,YAAY,EAMZ,MAAM,gCAAgC,CAAC;AA2DxC,UAAU,UAAU;IACnB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,8FAA8F;IAC9F,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,SAAS;IACzB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,4FAA4F;IAC5F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,CA8B7E;AAuBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAUvD;AAyCD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiD1D;AAgBD,MAAM,WAAW,eAAe;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAuGD,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4FrD;AAmCD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAGnH;AAMD,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CA2BnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAMD,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA8MhD;AAMD,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAIvC;;;;AAGD,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension\n *\n * A. Permission Gate — prompts before bash/write/edit; checks modes.{mode}.auto_allow\n * from the merged (global + project) config; persists \"always\"\n * choices back to the global config\n * B. MCP Server Loader — discovers ~/.hoocode/mcp-servers and ./.hoocode/mcp-servers JSON\n * configs, connects via JSON-RPC 2.0, registers server tools\n * C. Mode — resolves active mode (ask/plan/build/debug), loads the mode's\n * system prompt, filters active tools, and exposes /mode, /plan,\n * and /approve commands\n *\n * Config merge order (lowest → highest priority):\n * 1. ~/.hoocode/agent/hoo-config.json (global defaults)\n * 2. ./.hoocode/config.json (project overrides — scalars win; arrays union)\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type Static, Type } from \"typebox\";\nimport { getHooCodeDir } from \"../../config.js\";\nimport type {\n\tAgentToolResult,\n\tAgentToolUpdateCallback,\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n\tToolCallEvent,\n\tToolCallEventResult,\n} from \"../../core/extensions/types.js\";\nimport { isToolCallEventType } from \"../../core/extensions/types.js\";\n\n// ============================================================================\n// Fallback defaults for mode prompts\n// ============================================================================\n\nconst MODE_DEFAULTS: Record<string, string> = {\n\task: `You are in ASK mode — read-only Q&A.\nAnswer questions about the codebase. Trace logic, compare approaches, explain patterns.\nYou may read any file but NEVER write, edit, or execute commands.\nIf asked to make changes, refuse and suggest switching to /mode build.\nCite specific file paths and line numbers in your answers.`,\n\n\tplan: `You are in PLAN mode — exploration and planning.\nExplore the codebase thoroughly. Understand the current structure.\nDraft a complete plan with sections: Goal, Files to modify, New files, Tests, Verification.\nWrite the plan to {{PLAN_PATH}}.\nWhen the plan is complete, tell the user to run /approve to execute it.`,\n\n\tbuild: `You are in BUILD mode — careful implementation.\nRead files before editing them. Show diffs before non-trivial changes.\nAsk for confirmation before destructive operations (delete, reformat).\nRun tests after every logical unit of work.\nPrefer the smallest change that achieves the goal.\nFollow existing code patterns and conventions.`,\n\n\tdebug: `You are in DEBUG mode — root cause analysis.\nGather evidence: read files, check logs, reproduce the issue.\nTrace the call path from entry to failure point.\nState the root cause in one sentence.\nDescribe the fix precisely but do NOT apply it.\nTo fix, switch to /mode build.`,\n};\n\n// ============================================================================\n// Shared paths\n// ============================================================================\n\nconst HOOCODE_DIR = getHooCodeDir();\nconst GLOBAL_CONFIG_PATH = join(HOOCODE_DIR, \"agent\", \"hoo-config.json\");\n\n/**\n * Per-session plan file path. Keying on sessionId lets concurrent or resumed\n * plan sessions keep distinct plans instead of clobbering each other.\n */\nfunction getPlanPath(cwd: string, sessionId: string): string {\n\treturn join(cwd, \".hoocode\", \"plans\", `${sessionId}.md`);\n}\n\n/** Legacy single-file plan location, retained as a read-only fallback for /approve. */\nfunction getLegacyPlanPath(cwd: string): string {\n\treturn join(cwd, \".hoocode\", \"plan.md\");\n}\n\n// ============================================================================\n// Config types\n// ============================================================================\n\ninterface ModeConfig {\n\t/** Tool names that bypass the permission gate in this mode */\n\tauto_allow?: string[];\n\t/** Tool names available in this mode (if set, only these tools are active) */\n\tenabled_tools?: string[];\n\t/** Allowed write paths in this mode (glob patterns, only applies if write/edit is enabled) */\n\tallowed_write_paths?: string[];\n}\n\nexport interface HooConfig {\n\t/** Manually-pinned active mode (overrides default \"build\") */\n\tactive_mode?: string;\n\t/** Per-mode configuration keyed by mode name */\n\tmodes?: Record<string, ModeConfig>;\n\t/** Extra directories to search for `{name}/system.md` mode files (after project + user). */\n\tmode_paths?: string[];\n}\n\n// ============================================================================\n// Config I/O and merging\n// ============================================================================\n\nfunction readConfig(): HooConfig {\n\ttry {\n\t\treturn JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, \"utf8\")) as HooConfig;\n\t} catch {\n\t\treturn {};\n\t}\n}\n\nfunction writeConfig(config: HooConfig): void {\n\tconst dir = join(HOOCODE_DIR, \"agent\");\n\tif (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n\twriteFileSync(GLOBAL_CONFIG_PATH, `${JSON.stringify(config, null, 2)}\\n`, \"utf8\");\n}\n\n/**\n * Deep-merges a project-local config on top of the global config.\n *\n * Merge rules:\n * - active_mode: project wins if set\n * - modes[x].auto_allow: union of global + project arrays\n * - modes[x].allowed_write_paths: union of global + project arrays\n * - modes[x].enabled_tools: project wins if set, else falls back to global\n * - mode_paths: project list is prepended so project paths are searched first\n */\nexport function mergeConfigs(global: HooConfig, project: HooConfig): HooConfig {\n\tconst merged: HooConfig = { ...global };\n\n\tif (project.active_mode !== undefined) merged.active_mode = project.active_mode;\n\n\tif (project.modes) {\n\t\tmerged.modes = { ...(global.modes ?? {}) };\n\t\tfor (const [mode, projectCfg] of Object.entries(project.modes)) {\n\t\t\tconst globalCfg = global.modes?.[mode] ?? {};\n\t\t\tmerged.modes[mode] = {\n\t\t\t\t...globalCfg,\n\t\t\t\t...projectCfg,\n\t\t\t\t// Union both auto_allow lists so project can extend, not just replace\n\t\t\t\tauto_allow: Array.from(new Set([...(globalCfg.auto_allow ?? []), ...(projectCfg.auto_allow ?? [])])),\n\t\t\t\t// Union allowed_write_paths so project can extend\n\t\t\t\tallowed_write_paths: Array.from(\n\t\t\t\t\tnew Set([...(globalCfg.allowed_write_paths ?? []), ...(projectCfg.allowed_write_paths ?? [])]),\n\t\t\t\t),\n\t\t\t\t// enabled_tools: project wins if set, else falls back to global\n\t\t\t\tenabled_tools: projectCfg.enabled_tools ?? globalCfg.enabled_tools,\n\t\t\t};\n\t\t}\n\t}\n\n\tif (project.mode_paths || global.mode_paths) {\n\t\t// Project paths first so they're searched before global paths\n\t\tmerged.mode_paths = dedupePaths([...(project.mode_paths ?? []), ...(global.mode_paths ?? [])]);\n\t}\n\n\treturn merged;\n}\n\nfunction dedupePaths(paths: string[]): string[] {\n\tconst seen = new Set<string>();\n\tconst out: string[] = [];\n\tfor (const p of paths) {\n\t\tif (!seen.has(p)) {\n\t\t\tseen.add(p);\n\t\t\tout.push(p);\n\t\t}\n\t}\n\treturn out;\n}\n\nfunction mergeSearchPaths(...sources: (string[] | undefined)[]): string[] {\n\tconst merged: string[] = [];\n\tfor (const source of sources) {\n\t\tif (!source) continue;\n\t\tmerged.push(...source);\n\t}\n\treturn dedupePaths(merged);\n}\n\n/**\n * Reads the global config and optionally overlays the project-local config at\n * `./.hoocode/config.json`. Project values win on all scalar fields; arrays are\n * unioned (see mergeConfigs for full rules).\n */\nexport function readMergedConfig(cwd: string): HooConfig {\n\tconst global = readConfig();\n\tconst projectPath = join(cwd, \".hoocode\", \"config.json\");\n\tif (!existsSync(projectPath)) return global;\n\ttry {\n\t\tconst project = JSON.parse(readFileSync(projectPath, \"utf8\")) as HooConfig;\n\t\treturn mergeConfigs(global, project);\n\t} catch {\n\t\treturn global;\n\t}\n}\n\n// ============================================================================\n// A. Permission Gate\n// ============================================================================\n\nconst GATED_TOOLS = new Set([\"bash\", \"write\", \"edit\"]);\n\n/**\n * Checks if a file path matches any of the allowed patterns.\n * Supports glob patterns with * and exact paths.\n */\nfunction matchesAllowedPath(filePath: string, allowedPatterns: string[]): boolean {\n\tif (allowedPatterns.length === 0) return true;\n\tfor (const pattern of allowedPatterns) {\n\t\t// Exact match\n\t\tif (pattern === filePath) return true;\n\t\t// Glob pattern matching for *\n\t\tif (pattern.includes(\"*\")) {\n\t\t\tconst regex = new RegExp(`^${pattern.replace(/\\*/g, \".*\")}$`);\n\t\t\tif (regex.test(filePath)) return true;\n\t\t}\n\t}\n\treturn false;\n}\n\nfunction describeTool(event: ToolCallEvent): string {\n\tif (isToolCallEventType(\"bash\", event)) {\n\t\treturn `$ ${event.input.command.replace(/\\s+/g, \" \").slice(0, 100)}`;\n\t}\n\tif (isToolCallEventType(\"edit\", event)) {\n\t\tconst p = (event.input as { file_path?: string }).file_path ?? \"(unknown)\";\n\t\treturn `edit ${p}`;\n\t}\n\tif (isToolCallEventType(\"write\", event)) {\n\t\tconst p = (event.input as { file_path?: string }).file_path ?? \"(unknown)\";\n\t\treturn `write ${p}`;\n\t}\n\treturn event.toolName;\n}\n\nexport function setupPermissionGate(pi: ExtensionAPI): void {\n\tpi.on(\"tool_call\", async (event: ToolCallEvent, ctx: ExtensionContext): Promise<ToolCallEventResult | undefined> => {\n\t\tif (!GATED_TOOLS.has(event.toolName) || !ctx.hasUI) return;\n\n\t\t// Use the merged config so project-local auto_allow entries are respected\n\t\tconst config = readMergedConfig(ctx.cwd);\n\t\tconst mode = config.active_mode ?? \"build\";\n\t\tconst modeCfg = config.modes?.[mode];\n\t\tconst autoAllow = modeCfg?.auto_allow ?? [];\n\n\t\t// Check allowed_write_paths for write/edit operations\n\t\tif ((event.toolName === \"write\" || event.toolName === \"edit\") && modeCfg?.allowed_write_paths) {\n\t\t\tconst filePath = (event.input as { file_path?: string }).file_path ?? \"\";\n\t\t\tif (!matchesAllowedPath(filePath, modeCfg.allowed_write_paths)) {\n\t\t\t\treturn {\n\t\t\t\t\tblock: true,\n\t\t\t\t\treason:\n\t\t\t\t\t\t`Mode \"${mode}\" only allows writes to: ${modeCfg.allowed_write_paths.join(\", \")}. ` +\n\t\t\t\t\t\t`Attempted to ${event.toolName}: ${filePath}. ` +\n\t\t\t\t\t\t`Switch to \"/mode build\" to modify source files.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tif (autoAllow.includes(event.toolName)) return;\n\n\t\tconst choice = await ctx.ui.select(`Allow: ${describeTool(event)}`, [\n\t\t\t\"Yes (once)\",\n\t\t\t\"No (block)\",\n\t\t\t\"Always (add to auto-allow for this mode)\",\n\t\t]);\n\n\t\tif (!choice || choice.startsWith(\"No\")) {\n\t\t\treturn { block: true, reason: \"Denied by permission gate\" };\n\t\t}\n\n\t\tif (choice.startsWith(\"Always\")) {\n\t\t\t// Write \"always\" choices to the global config only\n\t\t\tconst latest = readConfig();\n\t\t\tconst currentMode = latest.active_mode ?? \"build\";\n\t\t\tlatest.modes ??= {};\n\t\t\tlatest.modes[currentMode] ??= {};\n\t\t\tlatest.modes[currentMode].auto_allow = Array.from(\n\t\t\t\tnew Set([...(latest.modes[currentMode].auto_allow ?? []), event.toolName]),\n\t\t\t);\n\t\t\twriteConfig(latest);\n\t\t\tctx.ui.notify(`\"${event.toolName}\" added to auto-allow for mode \"${currentMode}\"`, \"info\");\n\t\t}\n\t});\n}\n\n// ============================================================================\n// B. MCP Server Loader\n// ============================================================================\n\ninterface McpToolDef {\n\tname: string;\n\tdescription: string;\n\tinputSchema?: {\n\t\ttype?: string;\n\t\tproperties?: Record<string, { type?: string; description?: string }>;\n\t\trequired?: string[];\n\t};\n}\n\nexport interface McpServerConfig {\n\t/** Unique server identifier used as prefix for registered tool names */\n\tname: string;\n\t/** Executable to spawn */\n\tcommand: string;\n\t/** Optional arguments passed to the command */\n\targs?: string[];\n\t/** Optional extra environment variables for the server process */\n\tenv?: Record<string, string>;\n}\n\ninterface McpConnection {\n\trpc(method: string, params?: unknown): Promise<unknown>;\n\tterminate(): void;\n}\n\nconst mcpConnections = new Map<string, McpConnection>();\n\nfunction spawnMcpServer(config: McpServerConfig): McpConnection {\n\tconst proc: ChildProcess = spawn(config.command, config.args ?? [], {\n\t\tenv: { ...process.env, ...(config.env ?? {}) },\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t});\n\n\tlet nextId = 1;\n\tconst pending = new Map<number, { resolve: (r: unknown) => void; reject: (e: Error) => void }>();\n\n\tconst rl = createInterface({ input: proc.stdout! });\n\trl.on(\"line\", (line) => {\n\t\tif (!line.trim()) return;\n\t\ttry {\n\t\t\tconst msg = JSON.parse(line) as {\n\t\t\t\tid?: number;\n\t\t\t\tresult?: unknown;\n\t\t\t\terror?: { message: string };\n\t\t\t};\n\t\t\tif (msg.id === undefined) return;\n\t\t\tconst cb = pending.get(msg.id);\n\t\t\tif (!cb) return;\n\t\t\tpending.delete(msg.id);\n\t\t\tif (msg.error) cb.reject(new Error(msg.error.message));\n\t\t\telse cb.resolve(msg.result);\n\t\t} catch {\n\t\t\t// ignore non-JSON server startup output\n\t\t}\n\t});\n\n\tproc.on(\"exit\", () => {\n\t\tfor (const cb of pending.values()) cb.reject(new Error(`MCP server \"${config.name}\" exited unexpectedly`));\n\t\tpending.clear();\n\t\tmcpConnections.delete(config.name);\n\t});\n\n\tfunction rpc(method: string, params?: unknown): Promise<unknown> {\n\t\tconst id = nextId++;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tpending.set(id, { resolve, reject });\n\t\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, method, params })}\\n`);\n\t\t});\n\t}\n\n\treturn {\n\t\trpc,\n\t\tterminate: () => {\n\t\t\trl.close();\n\t\t\tproc.kill();\n\t\t},\n\t};\n}\n\nasync function connectMcpServer(config: McpServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tmcpConnections.get(config.name)?.terminate();\n\n\tconst conn = spawnMcpServer(config);\n\tmcpConnections.set(config.name, conn);\n\n\tawait conn.rpc(\"initialize\", {\n\t\tprotocolVersion: \"2024-11-05\",\n\t\tcapabilities: { tools: {} },\n\t\tclientInfo: { name: \"hoocode\", version: \"1.0.0\" },\n\t});\n\n\tconst toolsResult = (await conn.rpc(\"tools/list\", {})) as {\n\t\ttools?: McpToolDef[];\n\t};\n\treturn { conn, tools: toolsResult.tools ?? [] };\n}\n\nfunction buildMcpSchema(tool: McpToolDef): ReturnType<typeof Type.Object> {\n\tconst props = tool.inputSchema?.properties ?? {};\n\tconst required = new Set(tool.inputSchema?.required ?? []);\n\tconst shape: Record<string, ReturnType<typeof Type.String>> = {};\n\n\tfor (const [key, prop] of Object.entries(props)) {\n\t\tlet field: ReturnType<typeof Type.String>;\n\t\tswitch (prop.type) {\n\t\t\tcase \"number\":\n\t\t\tcase \"integer\":\n\t\t\t\tfield = Type.Number({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tcase \"boolean\":\n\t\t\t\tfield = Type.Boolean({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfield = Type.String({ description: prop.description });\n\t\t}\n\t\tshape[key] = required.has(key) ? field : (Type.Optional(field) as unknown as ReturnType<typeof Type.String>);\n\t}\n\n\treturn Type.Object(shape);\n}\n\nexport function setupMcpLoader(pi: ExtensionAPI): void {\n\tpi.on(\"session_start\", async (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tconst searchDirs = [join(HOOCODE_DIR, \"mcp-servers\"), join(ctx.cwd, \".hoocode\", \"mcp-servers\")];\n\n\t\tfor (const dir of searchDirs) {\n\t\t\tif (!existsSync(dir)) continue;\n\n\t\t\tlet files: string[];\n\t\t\ttry {\n\t\t\t\tfiles = (await readdir(dir)).filter((f) => f.endsWith(\".json\"));\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const file of files) {\n\t\t\t\tconst cfgPath = join(dir, file);\n\t\t\t\tlet serverConfig: McpServerConfig;\n\n\t\t\t\ttry {\n\t\t\t\t\tserverConfig = JSON.parse(readFileSync(cfgPath, \"utf8\")) as McpServerConfig;\n\t\t\t\t\tif (!serverConfig.name || !serverConfig.command) {\n\t\t\t\t\t\tctx.ui.notify(`MCP: config \"${file}\" is missing required \"name\" or \"command\"`, \"warning\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to parse \"${file}\": ${String(err)}`, \"error\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst { tools } = await connectMcpServer(serverConfig);\n\n\t\t\t\t\tfor (const tool of tools) {\n\t\t\t\t\t\tconst toolName = `mcp_${serverConfig.name}_${tool.name}`;\n\t\t\t\t\t\tconst schema = buildMcpSchema(tool);\n\t\t\t\t\t\tconst capturedServer = serverConfig.name;\n\t\t\t\t\t\tconst capturedTool = tool.name;\n\n\t\t\t\t\t\tpi.registerTool({\n\t\t\t\t\t\t\tname: toolName,\n\t\t\t\t\t\t\tlabel: `[MCP] ${serverConfig.name} › ${tool.name}`,\n\t\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\t\tparameters: schema,\n\t\t\t\t\t\t\tasync execute(\n\t\t\t\t\t\t\t\t_toolCallId: string,\n\t\t\t\t\t\t\t\tparams: Static<typeof schema>,\n\t\t\t\t\t\t\t\tsignal: AbortSignal,\n\t\t\t\t\t\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t\t\t\t\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\t\t\t\t\t\tconst activeConn = mcpConnections.get(capturedServer);\n\t\t\t\t\t\t\t\tif (!activeConn) {\n\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\t\t\ttext: `MCP server \"${capturedServer}\" is not connected`,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\t\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")));\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\t\t\t\t\tactiveConn.rpc(\"tools/call\", {\n\t\t\t\t\t\t\t\t\t\tname: capturedTool,\n\t\t\t\t\t\t\t\t\t\targuments: params,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\tabortPromise,\n\t\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`MCP: connected \"${serverConfig.name}\" (${tools.length} tool${tools.length === 1 ? \"\" : \"s\"})`,\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to connect \"${serverConfig.name}\": ${String(err)}`, \"error\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n}\n\n// ============================================================================\n// C. Mode System\n// ============================================================================\n\nconst DEFAULT_MODE = \"build\";\n\nfunction tryReadFile(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst text = readFileSync(path, \"utf8\").trim();\n\t\treturn text || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Walks search dirs in precedence order and returns the first existing\n * `modes/{name}/system.md` content. Order: project → user → externalDirs.\n */\nfunction resolveModeFile(name: string, cwd: string, externalDirs: string[]): string | undefined {\n\tconst candidates: string[] = [\n\t\tjoin(cwd, \".hoocode\", \"modes\", name, \"system.md\"),\n\t\tjoin(HOOCODE_DIR, \"modes\", name, \"system.md\"),\n\t\t...externalDirs.map((dir) => join(dir, name, \"system.md\")),\n\t];\n\tfor (const candidate of candidates) {\n\t\tconst content = tryReadFile(candidate);\n\t\tif (content !== undefined) return content;\n\t}\n\treturn undefined;\n}\n\n/**\n * Returns the system prompt for the active mode.\n *\n * Search order (first hit wins):\n * - `./.hoocode/modes/{mode}/system.md`\n * - `~/.hoocode/modes/{mode}/system.md`\n * - each of `externalDirs` in declared order (config + CLI + extension contributions)\n * - built-in MODE_DEFAULTS for the four known modes\n */\nexport function buildSystemPrompt(mode: string, cwd: string, options?: { modePaths?: string[] }): string | undefined {\n\tconst modePaths = options?.modePaths ?? [];\n\treturn resolveModeFile(mode, cwd, modePaths) ?? MODE_DEFAULTS[mode];\n}\n\n// ============================================================================\n// Plan file: section parsing and step-by-step execution message\n// ============================================================================\n\nexport interface PlanSections {\n\tgoal?: string;\n\tfilesToModify?: string;\n\tnewFiles?: string;\n\ttests?: string;\n\tverification?: string;\n\t/** Original full text, used as fallback if no sections parsed */\n\traw: string;\n}\n\n/**\n * Parses `.hoocode/plan.md` into named sections.\n *\n * Recognises both ATX headings (`## Goal`) and bold labels (`**Goal**`).\n * Section names matched (case-insensitive): Goal, Files to modify, New files,\n * Tests, Verification.\n */\nexport function parsePlanSections(planContent: string): PlanSections {\n\tconst result: PlanSections = { raw: planContent };\n\n\t// Match `## Heading text` or `**Heading text**` followed by content until\n\t// the next heading of the same style.\n\tconst sectionPattern =\n\t\t/^(?:#{1,3}\\s+(.+?)|(?:\\*\\*(.+?)\\*\\*))\\s*\\n([\\s\\S]*?)(?=(?:^#{1,3}\\s+|\\*\\*[^*\\n]+\\*\\*\\s*\\n)|$)/gm;\n\n\tfor (const match of planContent.matchAll(sectionPattern)) {\n\t\tconst heading = (match[1] ?? match[2] ?? \"\").toLowerCase().trim();\n\t\tconst content = match[3].trim();\n\t\tif (!content) continue;\n\n\t\tif (/^goal/.test(heading)) {\n\t\t\tresult.goal = content;\n\t\t} else if (/files?\\s+to\\s+modif|^modif/.test(heading)) {\n\t\t\tresult.filesToModify = content;\n\t\t} else if (/new\\s+files?/.test(heading)) {\n\t\t\tresult.newFiles = content;\n\t\t} else if (/^tests?/.test(heading)) {\n\t\t\tresult.tests = content;\n\t\t} else if (/^verif/.test(heading)) {\n\t\t\tresult.verification = content;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Builds the user message sent to the agent when `/approve` is run.\n *\n * If the plan has recognisable sections, each is presented as a numbered step\n * so the agent works through them sequentially. Otherwise the raw plan is used.\n *\n * Execution order:\n * 1. Modify existing files\n * 2. Create new files\n * 3. Update / add tests\n * 4. Run verification commands\n */\nexport function buildApproveMessage(sections: PlanSections): string {\n\tconst steps: string[] = [];\n\n\tif (sections.goal) {\n\t\tsteps.push(`**Goal:** ${sections.goal}`);\n\t}\n\tif (sections.filesToModify) {\n\t\tsteps.push(`**Step 1 — Modify existing files:**\\n${sections.filesToModify}`);\n\t}\n\tif (sections.newFiles) {\n\t\tsteps.push(`**Step 2 — Create new files:**\\n${sections.newFiles}`);\n\t}\n\tif (sections.tests) {\n\t\tsteps.push(`**Step 3 — Update tests:**\\n${sections.tests}`);\n\t}\n\tif (sections.verification) {\n\t\tsteps.push(`**Step 4 — Verify:**\\n${sections.verification}`);\n\t}\n\n\tif (steps.length === 0) {\n\t\treturn `Execute the following plan:\\n\\n${sections.raw}`;\n\t}\n\n\treturn `Execute this plan step by step. Complete each step fully before moving to the next.\\n\\n${steps.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// C. setupMode\n// ============================================================================\n\nexport function setupMode(pi: ExtensionAPI): void {\n\tlet cachedMode = DEFAULT_MODE;\n\tlet cachedSystemPrompt: string | undefined;\n\tlet cachedPlanPath: string | undefined;\n\n\t// ── session_start ─────────────────────────────────────────────────────────\n\t// Config resolution order:\n\t// 1. Read global config (~/.hoocode/agent/hoo-config.json)\n\t// 2. Read project config (./.hoocode/config.json) if present\n\t// 3. Merge — project scalars win; arrays are unioned\n\t// 4. Re-resolve active_mode from the merged result\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\t// Steps 1–3: merge global + project configs\n\t\tconst config = readMergedConfig(ctx.cwd);\n\n\t\t// Step 4: resolve mode from the merged config\n\t\tcachedMode = config.active_mode ?? DEFAULT_MODE;\n\t\t// External search dirs come from two channels:\n\t\t// - HooConfig.mode_paths (config-declared)\n\t\t// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.getModeSearchPaths());\n\t\tconst rawSystemPrompt = buildSystemPrompt(cachedMode, ctx.cwd, { modePaths });\n\n\t\t// Per-session plan path so concurrent sessions don't overwrite each other.\n\t\t// The `{{PLAN_PATH}}` token in plan-mode templates is substituted here.\n\t\tcachedPlanPath = getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\tconst relPlanPath = relative(ctx.cwd, cachedPlanPath) || cachedPlanPath;\n\t\tcachedSystemPrompt = rawSystemPrompt?.replace(/\\{\\{PLAN_PATH\\}\\}/g, relPlanPath);\n\n\t\t// Update footer with active mode\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.setMode(cachedMode);\n\t\t}\n\n\t\t// Apply tool filter from mode enabled_tools\n\t\tconst modeCfg = config.modes?.[cachedMode];\n\t\tif (modeCfg?.enabled_tools && modeCfg.enabled_tools.length > 0) {\n\t\t\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined => {\n\t\tif (!cachedSystemPrompt) return;\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\n<!-- hoo-core: mode=${cachedMode} -->\\n${cachedSystemPrompt}`,\n\t\t};\n\t});\n\n\t// ── /mode command ─────────────────────────────────────────────────────────\n\n\tconst KNOWN_MODES = [\"ask\", \"plan\", \"build\", \"debug\"];\n\n\tpi.registerCommand(\"mode\", {\n\t\tdescription: \"Switch active mode. Usage: /mode <ask|plan|build|debug>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tKNOWN_MODES.filter((m) => m.startsWith(prefix)).map((m) => ({ value: m, label: m })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst name = args.trim();\n\t\t\tif (!name) {\n\t\t\t\tctx.ui.notify(`Active mode: ${cachedMode}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = name === DEFAULT_MODE ? undefined : name;\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"${name}\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /plan command (shorthand for /mode plan) ──────────────────────────────\n\n\tpi.registerCommand(\"plan\", {\n\t\tdescription: \"Switch to plan mode. Shorthand for /mode plan.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"plan\";\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"plan\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /approve command ──────────────────────────────────────────────────────\n\t// Reads .hoocode/plan.md, parses it into named sections (Goal, Files to\n\t// modify, New files, Tests, Verification), switches to build mode, then\n\t// injects a step-by-step execution message into the new session.\n\n\tpi.registerCommand(\"approve\", {\n\t\tdescription: \"Approve the current plan and switch to build mode to execute it.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tif (cachedMode !== \"plan\") {\n\t\t\t\tctx.ui.notify(`/approve is only available in plan mode (current mode: \"${cachedMode}\")`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Prefer the per-session plan file, fall back to the legacy single file.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst candidatePaths = [sessionPlanPath, getLegacyPlanPath(ctx.cwd)];\n\t\t\tlet approveMessage: string | undefined;\n\n\t\t\tfor (const planPath of candidatePaths) {\n\t\t\t\tif (!existsSync(planPath)) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst raw = readFileSync(planPath, \"utf8\").trim();\n\t\t\t\t\tif (raw) {\n\t\t\t\t\t\tconst sections = parsePlanSections(raw);\n\t\t\t\t\t\tapproveMessage = buildApproveMessage(sections);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, planPath) || planPath}`, \"error\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Switch global config to build mode\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"build\";\n\t\t\twriteConfig(config);\n\n\t\t\tif (approveMessage) {\n\t\t\t\t// Open a new build-mode session and deliver the parsed plan as the\n\t\t\t\t// first user message so the agent starts executing immediately\n\t\t\t\tawait ctx.newSession({\n\t\t\t\t\twithSession: async (replacedCtx) => {\n\t\t\t\t\t\tawait replacedCtx.sendUserMessage(approveMessage!, { deliverAs: \"followUp\" });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(`Switched to build mode. No ${relPlan} found — describe what to build.`, \"info\");\n\t\t\t\tawait ctx.reload();\n\t\t\t}\n\t\t},\n\t});\n\n\t// ── /cost command ─────────────────────────────────────────────────────────\n\t// Walks every assistant message in the current session and sums tokens + cost,\n\t// then prints a session total followed by a per-model breakdown.\n\t// Per-tool attribution is intentionally not shown — tokens aren't tracked\n\t// per-tool, and any heuristic would be misleading.\n\n\tpi.registerCommand(\"cost\", {\n\t\tdescription: \"Show session token and cost totals, broken down by model.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\ttype Totals = { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number };\n\t\t\tconst empty = (): Totals => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });\n\t\t\tconst total = empty();\n\t\t\tconst perModel = new Map<string, Totals>();\n\t\t\tlet assistantTurns = 0;\n\n\t\t\tfor (const entry of ctx.sessionManager.getEntries()) {\n\t\t\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\t\t\tconst u = entry.message.usage;\n\t\t\t\tif (!u) continue;\n\t\t\t\tassistantTurns++;\n\t\t\t\ttotal.input += u.input;\n\t\t\t\ttotal.output += u.output;\n\t\t\t\ttotal.cacheRead += u.cacheRead;\n\t\t\t\ttotal.cacheWrite += u.cacheWrite;\n\t\t\t\ttotal.cost += u.cost.total;\n\n\t\t\t\tconst key = `${entry.message.provider}/${entry.message.model}`;\n\t\t\t\tconst t = perModel.get(key) ?? empty();\n\t\t\t\tt.input += u.input;\n\t\t\t\tt.output += u.output;\n\t\t\t\tt.cacheRead += u.cacheRead;\n\t\t\t\tt.cacheWrite += u.cacheWrite;\n\t\t\t\tt.cost += u.cost.total;\n\t\t\t\tperModel.set(key, t);\n\t\t\t}\n\n\t\t\tif (assistantTurns === 0) {\n\t\t\t\tctx.ui.notify(\"No assistant turns yet — nothing to cost.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst fmt = (n: number) => n.toLocaleString();\n\t\t\tconst fmtCost = (n: number) => `$${n.toFixed(4)}`;\n\t\t\tconst lines: string[] = [];\n\t\t\tlines.push(`Session totals (${assistantTurns} assistant turn${assistantTurns === 1 ? \"\" : \"s\"})`);\n\t\t\tlines.push(` Input ${fmt(total.input)}`);\n\t\t\tlines.push(` Output ${fmt(total.output)}`);\n\t\t\tlines.push(` Cache read ${fmt(total.cacheRead)}`);\n\t\t\tlines.push(` Cache write ${fmt(total.cacheWrite)}`);\n\t\t\tlines.push(` Cost ${fmtCost(total.cost)}`);\n\n\t\t\tif (perModel.size > 1) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(\"By model:\");\n\t\t\t\tconst sorted = [...perModel.entries()].sort((a, b) => b[1].cost - a[1].cost);\n\t\t\t\tfor (const [key, t] of sorted) {\n\t\t\t\t\tlines.push(` ${key}: ${fmt(t.input)} in / ${fmt(t.output)} out ${fmtCost(t.cost)}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tctx.ui.notify(lines.join(\"\\n\"), \"info\");\n\t\t},\n\t});\n}\n\n// ============================================================================\n// Extension entry point\n// ============================================================================\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
@@ -702,9 +702,11 @@ export function setupMode(pi) {
702
702
  // ============================================================================
703
703
  // Extension entry point
704
704
  // ============================================================================
705
- export default function hooCore(pi) {
705
+ function hooCore(pi) {
706
706
  setupPermissionGate(pi);
707
707
  setupMcpLoader(pi);
708
708
  setupMode(pi);
709
709
  }
710
+ hooCore.displayName = "hoo-core";
711
+ export default hooCore;
710
712
  //# sourceMappingURL=hoo-core.js.map