@kinqs/brainrouter-cli 0.3.6 → 0.3.8

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 (129) hide show
  1. package/README.md +29 -52
  2. package/agents/architect.json +18 -0
  3. package/agents/explorer.json +18 -0
  4. package/agents/reviewer.json +18 -0
  5. package/agents/verifier.json +18 -0
  6. package/agents/worker.json +18 -0
  7. package/changelog/0.2.0.md +15 -0
  8. package/changelog/0.3.0.md +20 -0
  9. package/changelog/0.3.1.md +22 -0
  10. package/changelog/0.3.2.md +15 -0
  11. package/changelog/0.3.3.md +19 -0
  12. package/changelog/0.3.4.md +20 -0
  13. package/changelog/0.3.5.md +9 -0
  14. package/changelog/0.3.6.md +9 -0
  15. package/changelog/0.3.7.md +20 -0
  16. package/changelog/0.3.8.md +30 -0
  17. package/changelog/README.md +41 -0
  18. package/dist/agent/agent.d.ts +34 -1
  19. package/dist/agent/agent.js +372 -79
  20. package/dist/agent/toolCallRecovery.d.ts +57 -0
  21. package/dist/agent/toolCallRecovery.js +130 -0
  22. package/dist/agent/toolSafety.d.ts +17 -0
  23. package/dist/agent/toolSafety.js +102 -0
  24. package/dist/cli/banner.d.ts +20 -0
  25. package/dist/cli/banner.js +47 -14
  26. package/dist/cli/cliPrompt.d.ts +40 -3
  27. package/dist/cli/cliPrompt.js +117 -25
  28. package/dist/cli/commands/_context.d.ts +3 -1
  29. package/dist/cli/commands/_helpers.d.ts +1 -1
  30. package/dist/cli/commands/config.d.ts +46 -0
  31. package/dist/cli/commands/config.js +1042 -0
  32. package/dist/cli/commands/init.d.ts +20 -0
  33. package/dist/cli/commands/init.js +64 -0
  34. package/dist/cli/commands/login.d.ts +13 -0
  35. package/dist/cli/commands/login.js +179 -0
  36. package/dist/cli/commands/mcp.d.ts +13 -11
  37. package/dist/cli/commands/mcp.js +261 -74
  38. package/dist/cli/commands/mcpInstall.d.ts +20 -0
  39. package/dist/cli/commands/mcpInstall.js +87 -0
  40. package/dist/cli/commands/orchestration.js +51 -0
  41. package/dist/cli/commands/releaseNotes.d.ts +24 -0
  42. package/dist/cli/commands/releaseNotes.js +109 -0
  43. package/dist/cli/commands/schedule.d.ts +18 -0
  44. package/dist/cli/commands/schedule.js +189 -0
  45. package/dist/cli/commands/ui.js +119 -60
  46. package/dist/cli/commands/workflow.d.ts +2 -0
  47. package/dist/cli/commands/workflow.js +54 -8
  48. package/dist/cli/ink/ChatApp.d.ts +206 -0
  49. package/dist/cli/ink/ChatApp.js +493 -0
  50. package/dist/cli/ink/Frame.d.ts +26 -0
  51. package/dist/cli/ink/Frame.js +5 -0
  52. package/dist/cli/ink/Picker.d.ts +71 -0
  53. package/dist/cli/ink/Picker.js +168 -0
  54. package/dist/cli/ink/SlashPalette.d.ts +51 -0
  55. package/dist/cli/ink/SlashPalette.js +136 -0
  56. package/dist/cli/ink/TextField.d.ts +34 -0
  57. package/dist/cli/ink/TextField.js +47 -0
  58. package/dist/cli/ink/WizardApp.d.ts +7 -0
  59. package/dist/cli/ink/WizardApp.js +422 -0
  60. package/dist/cli/ink/ambientChat.d.ts +34 -0
  61. package/dist/cli/ink/ambientChat.js +7 -0
  62. package/dist/cli/ink/consoleCapture.d.ts +11 -0
  63. package/dist/cli/ink/consoleCapture.js +33 -0
  64. package/dist/cli/ink/markdownRender.d.ts +41 -0
  65. package/dist/cli/ink/markdownRender.js +278 -0
  66. package/dist/cli/ink/renderWithResizeClear.d.ts +14 -0
  67. package/dist/cli/ink/renderWithResizeClear.js +33 -0
  68. package/dist/cli/ink/runChat.d.ts +34 -0
  69. package/dist/cli/ink/runChat.js +682 -0
  70. package/dist/cli/ink/runPicker.d.ts +31 -0
  71. package/dist/cli/ink/runPicker.js +139 -0
  72. package/dist/cli/ink/runSlashPalette.d.ts +23 -0
  73. package/dist/cli/ink/runSlashPalette.js +33 -0
  74. package/dist/cli/ink/runWizard.d.ts +22 -0
  75. package/dist/cli/ink/runWizard.js +133 -0
  76. package/dist/cli/ink/stdinHandoff.d.ts +51 -0
  77. package/dist/cli/ink/stdinHandoff.js +78 -0
  78. package/dist/cli/ink/toolFormat.d.ts +75 -0
  79. package/dist/cli/ink/toolFormat.js +206 -0
  80. package/dist/cli/ink/useTerminalSize.d.ts +35 -0
  81. package/dist/cli/ink/useTerminalSize.js +26 -0
  82. package/dist/cli/repl.d.ts +25 -3
  83. package/dist/cli/repl.js +52 -714
  84. package/dist/cli/slashSuggest.d.ts +32 -0
  85. package/dist/cli/slashSuggest.js +146 -0
  86. package/dist/cli/wizard/modelsApi.d.ts +72 -0
  87. package/dist/cli/wizard/modelsApi.js +166 -0
  88. package/dist/cli/wizard/picker.d.ts +202 -0
  89. package/dist/cli/wizard/picker.js +547 -0
  90. package/dist/cli/wizard/providers.d.ts +86 -0
  91. package/dist/cli/wizard/providers.js +190 -0
  92. package/dist/cli/wizard/runner.d.ts +13 -0
  93. package/dist/cli/wizard/runner.js +488 -0
  94. package/dist/cli/wizard/types.d.ts +122 -0
  95. package/dist/cli/wizard/types.js +109 -0
  96. package/dist/config/config.d.ts +13 -1
  97. package/dist/config/config.js +45 -3
  98. package/dist/index.js +157 -206
  99. package/dist/memory/briefing.d.ts +1 -1
  100. package/dist/memory/briefing.js +4 -4
  101. package/dist/memory/consolidation.d.ts +1 -1
  102. package/dist/orchestration/agentRegistry.d.ts +36 -0
  103. package/dist/orchestration/agentRegistry.js +64 -0
  104. package/dist/orchestration/orchestrator.d.ts +7 -0
  105. package/dist/orchestration/orchestrator.js +2 -0
  106. package/dist/orchestration/tools.d.ts +105 -3
  107. package/dist/orchestration/tools.js +167 -8
  108. package/dist/prompt/skillCatalog.d.ts +11 -0
  109. package/dist/prompt/skillCatalog.js +134 -0
  110. package/dist/prompt/skillRunner.d.ts +2 -2
  111. package/dist/prompt/skillRunner.js +2 -31
  112. package/dist/prompt/systemPrompt.js +7 -2
  113. package/dist/runtime/anthropicAdapter.d.ts +100 -0
  114. package/dist/runtime/anthropicAdapter.js +293 -0
  115. package/dist/runtime/cronParser.d.ts +23 -0
  116. package/dist/runtime/cronParser.js +122 -0
  117. package/dist/runtime/mcpClient.js +14 -11
  118. package/dist/runtime/mcpPool.d.ts +170 -0
  119. package/dist/runtime/mcpPool.js +442 -0
  120. package/dist/runtime/mcpUtils.d.ts +17 -1
  121. package/dist/runtime/mcpUtils.js +23 -0
  122. package/dist/runtime/scheduleTicker.d.ts +33 -0
  123. package/dist/runtime/scheduleTicker.js +99 -0
  124. package/dist/runtime/vendorSnippets.d.ts +45 -0
  125. package/dist/runtime/vendorSnippets.js +153 -0
  126. package/dist/state/scheduleStore.d.ts +37 -0
  127. package/dist/state/scheduleStore.js +64 -0
  128. package/package.json +14 -5
  129. package/.env.example +0 -116
@@ -1,4 +1,4 @@
1
- import type { McpClientWrapper } from '../runtime/mcpClient.js';
1
+ import type { McpClientPool as McpClientWrapper } from '../runtime/mcpPool.js';
2
2
  /**
3
3
  * Filesystem memory consolidation — the human-readable companion to the
4
4
  * cognitive memory database.
@@ -0,0 +1,36 @@
1
+ export type Tier = 'chat' | 'reasoning' | 'worker';
2
+ export type AccessMode = 'read' | 'write' | 'shell';
3
+ export interface AgentDefinition {
4
+ id: string;
5
+ displayName: string;
6
+ whenToUse: string;
7
+ prompt: string;
8
+ model: string | null;
9
+ effort: string | null;
10
+ defaultAccess: AccessMode;
11
+ toolScope: {
12
+ local: string[];
13
+ mcp: string[];
14
+ };
15
+ disallowedTools: string[];
16
+ maxIterations: number;
17
+ timeoutMs: number;
18
+ maxResultChars: number;
19
+ subagents: string[];
20
+ delegateName: string;
21
+ tier: Tier;
22
+ outputContract: unknown;
23
+ }
24
+ export type DefinitionSource = 'builtin' | 'user' | 'workspace';
25
+ export interface LoadedDefinition {
26
+ def: AgentDefinition;
27
+ source: DefinitionSource;
28
+ filePath: string;
29
+ }
30
+ /**
31
+ * Load all agent definitions from three tiers (builtin → user-global → workspace).
32
+ * Same `id` from a higher-priority source wins; distinct ids coexist.
33
+ */
34
+ export declare function loadRegistry(workspaceRoot?: string): LoadedDefinition[];
35
+ export declare function findById(id: string, workspaceRoot?: string): LoadedDefinition | undefined;
36
+ export declare function listAll(workspaceRoot?: string): LoadedDefinition[];
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+ // Resolved at import time from dist/orchestration/agentRegistry.js → ../../agents
6
+ const BUILTIN_AGENTS_DIR = fileURLToPath(new URL('../../agents', import.meta.url));
7
+ function getUserAgentsDir() {
8
+ const home = process.env.BRAINROUTER_HOME ?? path.join(os.homedir(), '.config', 'brainrouter');
9
+ return path.join(home, 'agents');
10
+ }
11
+ function getWorkspaceAgentsDir(workspaceRoot) {
12
+ return path.join(workspaceRoot, '.brainrouter', 'agents');
13
+ }
14
+ function loadFromDir(dir, source) {
15
+ if (!fs.existsSync(dir))
16
+ return [];
17
+ let entries;
18
+ try {
19
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ const results = [];
25
+ for (const entry of entries) {
26
+ const filePath = path.join(dir, entry);
27
+ try {
28
+ const raw = fs.readFileSync(filePath, 'utf-8');
29
+ const def = JSON.parse(raw);
30
+ if (!def.id || typeof def.id !== 'string') {
31
+ console.error(`[agentRegistry] Skipping ${filePath}: missing or invalid "id" field.`);
32
+ continue;
33
+ }
34
+ results.push({ def, source, filePath });
35
+ }
36
+ catch (err) {
37
+ console.error(`[agentRegistry] Skipping ${filePath}: ${err.message}`);
38
+ }
39
+ }
40
+ return results;
41
+ }
42
+ /**
43
+ * Load all agent definitions from three tiers (builtin → user-global → workspace).
44
+ * Same `id` from a higher-priority source wins; distinct ids coexist.
45
+ */
46
+ export function loadRegistry(workspaceRoot) {
47
+ const builtin = loadFromDir(BUILTIN_AGENTS_DIR, 'builtin');
48
+ const user = loadFromDir(getUserAgentsDir(), 'user');
49
+ const workspace = workspaceRoot
50
+ ? loadFromDir(getWorkspaceAgentsDir(workspaceRoot), 'workspace')
51
+ : [];
52
+ // Precedence: builtin first (lowest), workspace last (highest).
53
+ const merged = new Map();
54
+ for (const loaded of [...builtin, ...user, ...workspace]) {
55
+ merged.set(loaded.def.id, loaded);
56
+ }
57
+ return Array.from(merged.values());
58
+ }
59
+ export function findById(id, workspaceRoot) {
60
+ return loadRegistry(workspaceRoot).find((l) => l.def.id === id);
61
+ }
62
+ export function listAll(workspaceRoot) {
63
+ return loadRegistry(workspaceRoot);
64
+ }
@@ -1,4 +1,5 @@
1
1
  import { type AccessMode } from './roles.js';
2
+ import type { Tier } from './agentRegistry.js';
2
3
  export type ChildStatus = 'pending' | 'running' | 'completed' | 'failed' | 'stale' | 'closed';
3
4
  export interface ChildSessionRecord {
4
5
  id: string;
@@ -14,6 +15,10 @@ export interface ChildSessionRecord {
14
15
  pid: number;
15
16
  finalOutput?: string;
16
17
  error?: string;
18
+ /** Agent tier from the definition (reasoning | worker). Undefined for legacy records. */
19
+ tier?: Tier;
20
+ /** Nesting depth in the spawn chain; 0 = direct child of the chat root. */
21
+ depth?: number;
17
22
  /** LLM usage attributable to this child (filled when the child completes). */
18
23
  usage?: {
19
24
  promptTokens: number;
@@ -30,6 +35,8 @@ export declare function createSession(workspaceRoot: string, input: {
30
35
  parentSessionKey: string;
31
36
  access?: AccessMode;
32
37
  label?: string;
38
+ tier?: Tier;
39
+ depth?: number;
33
40
  }): ChildSessionRecord;
34
41
  export declare function updateSession(workspaceRoot: string, id: string, patch: Partial<ChildSessionRecord>): ChildSessionRecord;
35
42
  export declare function reconcileStale(workspaceRoot: string): number;
@@ -28,6 +28,8 @@ export function createSession(workspaceRoot, input) {
28
28
  startedAt: now,
29
29
  updatedAt: now,
30
30
  pid: process.pid,
31
+ tier: input.tier,
32
+ depth: input.depth,
31
33
  };
32
34
  const data = readFile(workspaceRoot);
33
35
  data.sessions.push(record);
@@ -1,6 +1,7 @@
1
- import type { McpClientWrapper } from '../runtime/mcpClient.js';
1
+ import type { McpClientPool as McpClientWrapper } from '../runtime/mcpPool.js';
2
2
  import type { LLMConfig } from '../config/config.js';
3
3
  import { type AccessMode } from './roles.js';
4
+ import { type Tier } from './agentRegistry.js';
4
5
  export interface OrchestrationContext {
5
6
  workspaceRoot: string;
6
7
  parentSessionKey: string;
@@ -21,18 +22,35 @@ export interface OrchestrationContext {
21
22
  parentSpanId?: string;
22
23
  /** Parent agent_id so children can be grouped via attribute even without trace links. */
23
24
  parentAgentId?: string;
25
+ /** Parent agent tier — used for hierarchy checks (worker cannot spawn; reasoning can only spawn workers). */
26
+ parentTier?: Tier;
27
+ /** Current spawn-chain depth (0 = direct child of chat root). */
28
+ depth?: number;
24
29
  mcpClient: McpClientWrapper;
25
30
  llmConfig: LLMConfig;
26
31
  launchCwd: string;
27
32
  /** Called when a child output got offloaded — chars beyond preview that didn't land in parent context. */
28
33
  recordOffload?: (charsAvoided: number) => void;
29
- /** Called when the child agent emits a tool call, for live observability. */
30
- onChildToolEvent?: (event: {
34
+ /**
35
+ * Paired child tool lifecycle callbacks. Fire from the child agent's
36
+ * onToolStart / onToolEnd so the parent's REPL can render explicit
37
+ * "child began X" / "child finished X" rows in the scrollback — without
38
+ * these, long child runs look like the parent has frozen (roadmap §3).
39
+ */
40
+ onChildToolStart?: (event: {
41
+ childId: string;
42
+ role: string;
43
+ tool: string;
44
+ args: Record<string, any>;
45
+ }) => void;
46
+ onChildToolEnd?: (event: {
31
47
  childId: string;
32
48
  role: string;
33
49
  tool: string;
34
50
  ok: boolean;
35
51
  summary: string;
52
+ preview?: string;
53
+ durationMs: number;
36
54
  }) => void;
37
55
  /**
38
56
  * Called when a child agent's runTurn ends — success, fail, or empty answer.
@@ -76,6 +94,10 @@ export declare function createSpawnAgentTool(): {
76
94
  type: string;
77
95
  description: string;
78
96
  };
97
+ agentId: {
98
+ type: string;
99
+ description: string;
100
+ };
79
101
  prompt: {
80
102
  type: string;
81
103
  description: string;
@@ -108,6 +130,86 @@ export declare function createSpawnAgentTool(): {
108
130
  required: string[];
109
131
  };
110
132
  };
133
+ export declare function createTaskAgentTool(): {
134
+ name: string;
135
+ description: string;
136
+ inputSchema: {
137
+ type: string;
138
+ properties: {
139
+ role: {
140
+ type: string;
141
+ description: string;
142
+ };
143
+ agentId: {
144
+ type: string;
145
+ description: string;
146
+ };
147
+ prompt: {
148
+ type: string;
149
+ description: string;
150
+ };
151
+ label: {
152
+ type: string;
153
+ description: string;
154
+ };
155
+ access: {
156
+ type: string;
157
+ enum: string[];
158
+ description: string;
159
+ };
160
+ timeoutMs: {
161
+ type: string;
162
+ description: string;
163
+ };
164
+ seedRecordIds: {
165
+ type: string;
166
+ items: {
167
+ type: string;
168
+ };
169
+ description: string;
170
+ };
171
+ };
172
+ required: string[];
173
+ };
174
+ };
175
+ export declare function createDelegateAgentTool(): {
176
+ name: string;
177
+ description: string;
178
+ inputSchema: {
179
+ type: string;
180
+ properties: {
181
+ role: {
182
+ type: string;
183
+ description: string;
184
+ };
185
+ agentId: {
186
+ type: string;
187
+ description: string;
188
+ };
189
+ prompt: {
190
+ type: string;
191
+ description: string;
192
+ };
193
+ label: {
194
+ type: string;
195
+ description: string;
196
+ };
197
+ access: {
198
+ type: string;
199
+ enum: string[];
200
+ description: string;
201
+ };
202
+ seedRecordIds: {
203
+ type: string;
204
+ items: {
205
+ type: string;
206
+ };
207
+ description: string;
208
+ };
209
+ };
210
+ required: string[];
211
+ };
212
+ };
111
213
  export declare function createListAgentsTool(): {
112
214
  name: string;
113
215
  description: string;
@@ -1,6 +1,7 @@
1
1
  import { Agent } from '../agent/agent.js';
2
2
  import { createSession, formatSessionSummary, getSession, listSessions, updateSession, } from './orchestrator.js';
3
3
  import { buildRolePrompt, resolveRole } from './roles.js';
4
+ import { findById, listAll } from './agentRegistry.js';
4
5
  import { buildSystemPrompt, loadWorkspaceInstructionSummary } from '../prompt/systemPrompt.js';
5
6
  import { readTranscriptEntries } from '../state/sessionStore.js';
6
7
  import { callMcpTool, childSessionKey } from '../runtime/mcpUtils.js';
@@ -54,7 +55,16 @@ export function extractChildPreview(output, maxChars) {
54
55
  const tail = maxChars - head - 6; // 6 chars for the `\n...\n` divider
55
56
  return output.slice(0, head) + '\n…\n' + output.slice(-tail);
56
57
  }
58
+ // Default wait/timeout for foreground delegation. Mirrors wait_agent's
59
+ // historical 120 s default so task_agent and spawn_agent({ wait: true })
60
+ // behave identically when no explicit timeoutMs is passed. Only used inside
61
+ // handleTaskAgent (call-time); kept out of the schema-creator bodies to
62
+ // avoid the ESM circular-import TDZ between tools.ts and agent.ts (agent.ts
63
+ // constructs the LOCAL_TOOLS array eagerly at module load).
64
+ const DEFAULT_TASK_AGENT_TIMEOUT_MS = 120_000;
57
65
  const ORCHESTRATION_TOOL_NAMES = new Set([
66
+ 'task_agent',
67
+ 'delegate_agent',
58
68
  'spawn_agent',
59
69
  'spawn_agents',
60
70
  'list_agents',
@@ -100,11 +110,12 @@ export function trackedPromiseFor(id) {
100
110
  export function createSpawnAgentTool() {
101
111
  return {
102
112
  name: 'spawn_agent',
103
- description: 'Spawn a child agent with a specific role (explorer, architect, reviewer, worker, verifier) and a bounded prompt. Returns the child agent id immediately; the child runs in the background.',
113
+ description: 'Spawn a child agent and a bounded prompt. Returns the child agent id immediately; the child runs in the background. Specify the agent via `role` (legacy: explorer/architect/reviewer/worker/verifier) or `agentId` (registry id, e.g. a custom workspace definition).',
104
114
  inputSchema: {
105
115
  type: 'object',
106
116
  properties: {
107
- role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier.' },
117
+ role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
118
+ agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
108
119
  prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
109
120
  label: { type: 'string', description: 'Optional short label for the child run.' },
110
121
  access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
@@ -116,7 +127,55 @@ export function createSpawnAgentTool() {
116
127
  description: 'Optional BrainRouter memory record IDs that the parent already recalled. The child agent is told to build on these instead of re-discovering them.',
117
128
  },
118
129
  },
119
- required: ['role', 'prompt'],
130
+ required: ['prompt'],
131
+ },
132
+ };
133
+ }
134
+ export function createTaskAgentTool() {
135
+ return {
136
+ name: 'task_agent',
137
+ description: 'Run one foreground child agent for a bounded task and wait for its result. ' +
138
+ 'Returns the completed child output, failure, or an explicit timeout envelope. Prefer this over spawn_agent({ wait: true }) for foreground delegation.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
143
+ agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
144
+ prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
145
+ label: { type: 'string', description: 'Optional short label for the child run.' },
146
+ access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
147
+ timeoutMs: { type: 'integer', description: 'Optional timeout in milliseconds. Default 120000.' },
148
+ seedRecordIds: {
149
+ type: 'array',
150
+ items: { type: 'string' },
151
+ description: 'Optional BrainRouter memory record IDs that the parent already recalled.',
152
+ },
153
+ },
154
+ required: ['prompt'],
155
+ },
156
+ };
157
+ }
158
+ export function createDelegateAgentTool() {
159
+ return {
160
+ name: 'delegate_agent',
161
+ description: 'Start one background child agent and keep working in the parent turn. ' +
162
+ 'Non-blocking — there is no `timeoutMs`; the child runs until it finishes or is cancelled. ' +
163
+ 'Returns a running child id plus a reminder to continue useful work; call wait_agent later when the result is needed.',
164
+ inputSchema: {
165
+ type: 'object',
166
+ properties: {
167
+ role: { type: 'string', description: 'One of: explorer, architect, reviewer, worker, verifier. Prefer agentId for custom definitions.' },
168
+ agentId: { type: 'string', description: 'Registry id of the agent definition. Takes precedence over role when both are provided.' },
169
+ prompt: { type: 'string', description: 'The bounded task prompt for the child agent.' },
170
+ label: { type: 'string', description: 'Optional short label for the child run.' },
171
+ access: { type: 'string', enum: ['read', 'write', 'shell'], description: 'Override the role default access mode. Default: role default.' },
172
+ seedRecordIds: {
173
+ type: 'array',
174
+ items: { type: 'string' },
175
+ description: 'Optional BrainRouter memory record IDs that the parent already recalled.',
176
+ },
177
+ },
178
+ required: ['prompt'],
120
179
  },
121
180
  };
122
181
  }
@@ -223,6 +282,10 @@ export function createRouteAgentTool() {
223
282
  }
224
283
  export async function executeOrchestrationTool(name, args, ctx) {
225
284
  switch (name) {
285
+ case 'task_agent':
286
+ return await handleTaskAgent(args, ctx);
287
+ case 'delegate_agent':
288
+ return await handleDelegateAgent(args, ctx);
226
289
  case 'spawn_agent':
227
290
  return await handleSpawn(args, ctx);
228
291
  case 'spawn_agents':
@@ -243,6 +306,31 @@ export async function executeOrchestrationTool(name, args, ctx) {
243
306
  throw new Error(`Unknown orchestration tool: ${name}`);
244
307
  }
245
308
  }
309
+ async function handleTaskAgent(args, ctx) {
310
+ return await handleSpawn({ ...args, wait: true, timeoutMs: args?.timeoutMs ?? DEFAULT_TASK_AGENT_TIMEOUT_MS }, ctx);
311
+ }
312
+ async function handleDelegateAgent(args, ctx) {
313
+ const spawned = await handleSpawn({ ...args, wait: false }, ctx);
314
+ let parsed;
315
+ try {
316
+ const value = JSON.parse(spawned);
317
+ if (value && typeof value === 'object' && !Array.isArray(value))
318
+ parsed = value;
319
+ }
320
+ catch {
321
+ // not JSON; fall through to verbatim propagation
322
+ }
323
+ // If handleSpawn returned an error string or a non-object payload (no id to
324
+ // attach next-step semantics to), propagate it verbatim — wrapping it in
325
+ // { raw, nextAction } would hide the failure from the model and prevent the
326
+ // child-drain guardrail from finding a child id to wait on.
327
+ if (!parsed || typeof parsed.id !== 'string')
328
+ return spawned;
329
+ return JSON.stringify({
330
+ ...parsed,
331
+ nextAction: 'continue working in the parent turn; call wait_agent when this child output is needed',
332
+ }, null, 2);
333
+ }
246
334
  async function handleSpawnBatch(args, ctx) {
247
335
  const list = Array.isArray(args?.agents) ? args.agents : [];
248
336
  if (list.length === 0)
@@ -297,10 +385,47 @@ function explainRoute(task, role) {
297
385
  }
298
386
  }
299
387
  async function handleSpawn(args, ctx) {
300
- const role = resolveRole(String(args.role));
388
+ // Resolve agent definition via agentId (registry) or role (legacy).
389
+ let role;
390
+ let childTier;
391
+ if (typeof args.agentId === 'string' && args.agentId.trim()) {
392
+ const loaded = findById(args.agentId.trim(), ctx.workspaceRoot);
393
+ if (!loaded) {
394
+ const known = listAll(ctx.workspaceRoot).map((l) => l.def.id).join(', ');
395
+ throw new Error(`Unknown agentId "${args.agentId}". Known agents: ${known}.`);
396
+ }
397
+ role = {
398
+ name: loaded.def.id,
399
+ description: loaded.def.whenToUse,
400
+ defaultAccess: loaded.def.defaultAccess,
401
+ promptOverlay: loaded.def.prompt,
402
+ };
403
+ childTier = loaded.def.tier;
404
+ }
405
+ else {
406
+ const roleName = String(args.role ?? '');
407
+ if (!roleName.trim())
408
+ throw new Error('spawn_agent requires either "agentId" or "role".');
409
+ role = resolveRole(roleName);
410
+ childTier = findById(role.name, ctx.workspaceRoot)?.def.tier;
411
+ }
301
412
  const prompt = String(args.prompt ?? '');
302
413
  if (!prompt.trim())
303
414
  throw new Error('spawn_agent requires a non-empty prompt.');
415
+ // P1.2 — spawn hierarchy checks.
416
+ const rawMaxDepth = parseInt(process.env.BRAINROUTER_MAX_SPAWN_DEPTH ?? '3', 10);
417
+ const maxDepth = Number.isFinite(rawMaxDepth) && rawMaxDepth > 0 ? rawMaxDepth : 3;
418
+ const currentDepth = ctx.depth ?? 0;
419
+ const parentTier = ctx.parentTier;
420
+ if (parentTier === 'worker') {
421
+ throw new Error('Tier "worker" cannot delegate — ask the parent agent to spawn instead.');
422
+ }
423
+ if (parentTier === 'reasoning' && childTier && (childTier === 'chat' || childTier === 'reasoning')) {
424
+ throw new Error(`Tier "reasoning" cannot spawn a "${childTier}" agent — only "worker" children are allowed.`);
425
+ }
426
+ if (currentDepth >= maxDepth) {
427
+ throw new Error(`Spawn depth cap reached (${currentDepth}/${maxDepth}). Reduce agent nesting or raise BRAINROUTER_MAX_SPAWN_DEPTH.`);
428
+ }
304
429
  const requested = args.access ?? role.defaultAccess;
305
430
  const access = clampAccess(ctx.parentAccessMode ?? 'shell', requested);
306
431
  const record = createSession(ctx.workspaceRoot, {
@@ -309,6 +434,8 @@ async function handleSpawn(args, ctx) {
309
434
  parentSessionKey: ctx.parentSessionKey,
310
435
  access,
311
436
  label: typeof args.label === 'string' ? args.label : undefined,
437
+ tier: childTier,
438
+ depth: currentDepth + 1,
312
439
  });
313
440
  const childKey = childSessionKey(ctx.parentSessionKey, record.id);
314
441
  const seededIds = Array.isArray(args.seedRecordIds)
@@ -346,22 +473,41 @@ async function handleSpawn(args, ctx) {
346
473
  // dispatching spawn_agent tool span instead of starting a fresh tree.
347
474
  parentTraceId: ctx.parentTraceId,
348
475
  parentSpanId: ctx.parentSpanId,
476
+ // Propagate tier and depth so grandchildren can enforce hierarchy caps.
477
+ tier: childTier,
478
+ agentDepth: currentDepth + 1,
349
479
  });
350
480
  if (ctx.parentAgentId)
351
481
  childAgent.setParentAgentId(ctx.parentAgentId);
352
482
  updateSession(ctx.workspaceRoot, record.id, { status: 'running' });
353
483
  const promise = (async () => {
354
484
  try {
485
+ // Track per-tool start times so the paired onChildToolEnd carries a
486
+ // real duration — the REPL renders this on the child's end row.
487
+ const childToolStarts = new Map();
355
488
  const output = await childAgent.runTurn(prompt, {
356
489
  onStatusUpdate: () => { },
357
- onToolStart: () => { },
490
+ onToolStart: (tool, args) => {
491
+ childToolStarts.set(tool, Date.now());
492
+ ctx.onChildToolStart?.({
493
+ childId: record.id,
494
+ role: role.name,
495
+ tool,
496
+ args: args ?? {},
497
+ });
498
+ },
358
499
  onToolEnd: (tool, result) => {
359
- ctx.onChildToolEvent?.({
500
+ const startedAt = childToolStarts.get(tool);
501
+ childToolStarts.delete(tool);
502
+ const durationMs = startedAt ? Date.now() - startedAt : 0;
503
+ ctx.onChildToolEnd?.({
360
504
  childId: record.id,
361
505
  role: role.name,
362
506
  tool,
363
507
  ok: result.success,
364
508
  summary: result.summary,
509
+ preview: result.preview,
510
+ durationMs,
365
511
  });
366
512
  },
367
513
  });
@@ -462,12 +608,25 @@ async function handleWait(args, ctx) {
462
608
  const promise = runningPromises.get(id);
463
609
  if (promise) {
464
610
  let timedOut = false;
611
+ let timeout;
465
612
  await Promise.race([
466
613
  promise,
467
- new Promise((resolve) => setTimeout(() => { timedOut = true; resolve(); }, timeoutMs)),
614
+ new Promise((resolve) => {
615
+ timeout = setTimeout(() => { timedOut = true; resolve(); }, timeoutMs);
616
+ }),
468
617
  ]);
618
+ if (timeout)
619
+ clearTimeout(timeout);
469
620
  if (timedOut) {
470
- return JSON.stringify({ id, status: 'timeout' }, null, 2);
621
+ const record = getSession(ctx.workspaceRoot, id);
622
+ return JSON.stringify({
623
+ id,
624
+ status: 'timeout',
625
+ childStatus: record?.status ?? 'unknown',
626
+ role: record?.role,
627
+ label: record?.label,
628
+ summary: record ? formatSessionSummary(record) : `No child session with id ${id}.`,
629
+ }, null, 2);
471
630
  }
472
631
  }
473
632
  const record = getSession(ctx.workspaceRoot, id);
@@ -0,0 +1,11 @@
1
+ export interface SkillListItem {
2
+ name: string;
3
+ scope?: string;
4
+ category?: string;
5
+ description?: string;
6
+ source?: 'mcp' | 'filesystem';
7
+ }
8
+ export declare function listFilesystemSkills(workspaceRoot: string): SkillListItem[];
9
+ export declare function mergeSkillLists(primary: SkillListItem[], fallback: SkillListItem[]): SkillListItem[];
10
+ export declare function sortSkills(a: SkillListItem, b: SkillListItem): number;
11
+ export declare function skillSearchRoots(workspaceRoot: string): string[];