brainclaw 0.29.2 → 1.5.4

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 (197) hide show
  1. package/LICENSE +21 -74
  2. package/README.md +199 -176
  3. package/dist/brainclaw-vscode.vsix +0 -0
  4. package/dist/cli.js +710 -25
  5. package/dist/commands/accept.js +3 -0
  6. package/dist/commands/add-step.js +11 -26
  7. package/dist/commands/agent-board.js +70 -3
  8. package/dist/commands/audit.js +19 -0
  9. package/dist/commands/check-policy.js +54 -0
  10. package/dist/commands/check-security-mcp.js +145 -0
  11. package/dist/commands/check-security.js +106 -0
  12. package/dist/commands/claim-resource.js +1 -0
  13. package/dist/commands/codev.js +672 -0
  14. package/dist/commands/compact.js +74 -0
  15. package/dist/commands/complete-step.js +16 -26
  16. package/dist/commands/constraint.js +8 -20
  17. package/dist/commands/decision.js +9 -20
  18. package/dist/commands/delete-plan.js +10 -12
  19. package/dist/commands/delete-step.js +16 -0
  20. package/dist/commands/dispatch.js +163 -0
  21. package/dist/commands/doctor.js +1122 -49
  22. package/dist/commands/enable-agent.js +1 -0
  23. package/dist/commands/export.js +280 -22
  24. package/dist/commands/handoff.js +33 -0
  25. package/dist/commands/harvest.js +189 -0
  26. package/dist/commands/hooks.js +82 -25
  27. package/dist/commands/inbox.js +169 -0
  28. package/dist/commands/init.js +38 -31
  29. package/dist/commands/install-hooks.js +71 -44
  30. package/dist/commands/link.js +89 -0
  31. package/dist/commands/list-claims.js +48 -3
  32. package/dist/commands/list-plans.js +129 -25
  33. package/dist/commands/loops-handlers.js +409 -0
  34. package/dist/commands/mcp-read-handlers.js +1628 -0
  35. package/dist/commands/mcp-schemas.generated.js +269 -0
  36. package/dist/commands/mcp.js +4224 -1501
  37. package/dist/commands/plan-resource.js +64 -0
  38. package/dist/commands/plan.js +12 -26
  39. package/dist/commands/prune.js +37 -2
  40. package/dist/commands/reflect.js +20 -7
  41. package/dist/commands/release-claim.js +11 -6
  42. package/dist/commands/release-notes.js +170 -0
  43. package/dist/commands/repair.js +210 -0
  44. package/dist/commands/run-profile.js +57 -0
  45. package/dist/commands/sequence.js +113 -0
  46. package/dist/commands/session-end.js +423 -14
  47. package/dist/commands/session-start.js +214 -41
  48. package/dist/commands/setup-security.js +103 -0
  49. package/dist/commands/setup.js +42 -4
  50. package/dist/commands/stale.js +109 -0
  51. package/dist/commands/switch.js +100 -2
  52. package/dist/commands/trap.js +14 -31
  53. package/dist/commands/update-handoff.js +63 -4
  54. package/dist/commands/update-plan.js +21 -28
  55. package/dist/commands/update-step.js +37 -0
  56. package/dist/commands/upgrade.js +313 -6
  57. package/dist/commands/usage.js +102 -0
  58. package/dist/commands/version.js +20 -0
  59. package/dist/commands/who.js +33 -5
  60. package/dist/commands/worktree.js +105 -0
  61. package/dist/core/actions.js +315 -0
  62. package/dist/core/agent-capability.js +610 -17
  63. package/dist/core/agent-context.js +7 -1
  64. package/dist/core/agent-files.js +1169 -85
  65. package/dist/core/agent-integrations.js +160 -5
  66. package/dist/core/agent-inventory.js +2 -0
  67. package/dist/core/agent-profiles.js +93 -0
  68. package/dist/core/agent-registry.js +162 -30
  69. package/dist/core/agentrun-reconciler.js +345 -0
  70. package/dist/core/agentruns.js +424 -0
  71. package/dist/core/ai-agent-detection.js +31 -10
  72. package/dist/core/archival.js +77 -0
  73. package/dist/core/assignment-sweeper.js +82 -0
  74. package/dist/core/assignments.js +367 -0
  75. package/dist/core/audit.js +30 -0
  76. package/dist/core/brainclaw-version.js +94 -2
  77. package/dist/core/candidates.js +93 -2
  78. package/dist/core/claims.js +419 -0
  79. package/dist/core/codev-metrics.js +77 -0
  80. package/dist/core/codev-personas.js +31 -0
  81. package/dist/core/codev-plan-gen.js +35 -0
  82. package/dist/core/codev-prompts.js +74 -0
  83. package/dist/core/codev-responses.js +62 -0
  84. package/dist/core/codev-rounds.js +218 -0
  85. package/dist/core/config.js +4 -0
  86. package/dist/core/context.js +381 -34
  87. package/dist/core/coordination.js +201 -6
  88. package/dist/core/cross-project.js +230 -16
  89. package/dist/core/default-profiles/doctor.yaml +11 -0
  90. package/dist/core/default-profiles/janitor.yaml +11 -0
  91. package/dist/core/default-profiles/onboarder.yaml +11 -0
  92. package/dist/core/default-profiles/reviewer.yaml +13 -0
  93. package/dist/core/dispatcher.js +1189 -0
  94. package/dist/core/duplicates.js +2 -2
  95. package/dist/core/entity-operations.js +450 -0
  96. package/dist/core/entity-registry.js +344 -0
  97. package/dist/core/events.js +106 -2
  98. package/dist/core/execution-adapters.js +154 -0
  99. package/dist/core/execution-context.js +63 -0
  100. package/dist/core/execution-profile.js +270 -0
  101. package/dist/core/execution.js +255 -0
  102. package/dist/core/facade-schema.js +81 -0
  103. package/dist/core/federation-cloud.js +99 -0
  104. package/dist/core/federation-message.js +52 -0
  105. package/dist/core/federation-transport.js +65 -0
  106. package/dist/core/gc-semantic.js +482 -0
  107. package/dist/core/governance.js +247 -0
  108. package/dist/core/guards.js +19 -0
  109. package/dist/core/ideation.js +72 -0
  110. package/dist/core/identity.js +110 -25
  111. package/dist/core/ids.js +6 -0
  112. package/dist/core/input-validation.js +2 -2
  113. package/dist/core/instruction-templates.js +344 -136
  114. package/dist/core/io.js +90 -11
  115. package/dist/core/lock.js +6 -2
  116. package/dist/core/loops/brief-assembly.js +213 -0
  117. package/dist/core/loops/facade-schema.js +148 -0
  118. package/dist/core/loops/index.js +7 -0
  119. package/dist/core/loops/iteration-engine.js +139 -0
  120. package/dist/core/loops/lock.js +385 -0
  121. package/dist/core/loops/store.js +201 -0
  122. package/dist/core/loops/types.js +403 -0
  123. package/dist/core/loops/verbs.js +534 -0
  124. package/dist/core/markdown.js +15 -3
  125. package/dist/core/memory-compactor.js +432 -0
  126. package/dist/core/memory-git.js +152 -8
  127. package/dist/core/messaging.js +278 -0
  128. package/dist/core/migration.js +32 -1
  129. package/dist/core/mutation-pipeline.js +4 -2
  130. package/dist/core/operations/memory-mutation.js +129 -0
  131. package/dist/core/operations/memory-write.js +78 -0
  132. package/dist/core/operations/plan.js +190 -0
  133. package/dist/core/policy.js +169 -0
  134. package/dist/core/reputation.js +9 -3
  135. package/dist/core/schema.js +491 -6
  136. package/dist/core/search.js +21 -2
  137. package/dist/core/security-cache.js +71 -0
  138. package/dist/core/security-guard.js +152 -0
  139. package/dist/core/security-scoring.js +86 -0
  140. package/dist/core/sequence.js +130 -0
  141. package/dist/core/socket-client.js +113 -0
  142. package/dist/core/staleness.js +246 -0
  143. package/dist/core/state.js +98 -22
  144. package/dist/core/store-resolution.js +43 -11
  145. package/dist/core/toml-writer.js +76 -0
  146. package/dist/core/upgrades/backup.js +232 -0
  147. package/dist/core/upgrades/health-check.js +169 -0
  148. package/dist/core/upgrades/patches/candidate-archive.js +145 -0
  149. package/dist/core/upgrades/patches/handoff-review-strip.js +128 -0
  150. package/dist/core/upgrades/patches/provenance-rollout.js +136 -0
  151. package/dist/core/upgrades/schema-version.js +97 -0
  152. package/dist/core/worktree.js +606 -0
  153. package/dist/facts.js +114 -0
  154. package/dist/facts.json +111 -0
  155. package/docs/architecture/project-refs.md +5 -1
  156. package/docs/cli.md +690 -43
  157. package/docs/concepts/ideation-loop.md +317 -0
  158. package/docs/concepts/loop-engine.md +456 -0
  159. package/docs/concepts/mcp-governance.md +268 -0
  160. package/docs/concepts/memory-staleness.md +122 -0
  161. package/docs/concepts/multi-agent-workflows.md +166 -0
  162. package/docs/concepts/plans-and-claims.md +31 -6
  163. package/docs/concepts/project-md-convention.md +35 -0
  164. package/docs/concepts/troubleshooting.md +220 -0
  165. package/docs/concepts/upgrade-cli.md +202 -0
  166. package/docs/concepts/upgrade-dogfood-procedure.md +114 -0
  167. package/docs/context-format-changelog.md +2 -2
  168. package/docs/context-format.md +2 -2
  169. package/docs/index.md +68 -0
  170. package/docs/integrations/agents.md +15 -16
  171. package/docs/integrations/cline.md +88 -0
  172. package/docs/integrations/codex.md +75 -23
  173. package/docs/integrations/continue.md +60 -0
  174. package/docs/integrations/copilot.md +67 -9
  175. package/docs/integrations/kilocode.md +72 -0
  176. package/docs/integrations/mcp.md +304 -21
  177. package/docs/integrations/mistral-vibe.md +122 -0
  178. package/docs/integrations/opencode.md +84 -0
  179. package/docs/integrations/overview.md +23 -8
  180. package/docs/integrations/roo.md +74 -0
  181. package/docs/integrations/windsurf.md +83 -0
  182. package/docs/mcp-schema-changelog.md +191 -1
  183. package/docs/playbooks/integration/index.md +121 -0
  184. package/docs/playbooks/productivity/index.md +102 -0
  185. package/docs/playbooks/team/index.md +122 -0
  186. package/docs/product/agent-first-model.md +184 -0
  187. package/docs/product/entity-model-audit.md +462 -0
  188. package/docs/product/positioning.md +10 -10
  189. package/docs/quickstart-existing-project.md +135 -0
  190. package/docs/quickstart.md +124 -37
  191. package/docs/release-maintenance.md +79 -0
  192. package/docs/review.md +2 -0
  193. package/docs/server-operations.md +118 -0
  194. package/package.json +21 -13
  195. package/dist/commands/claude-desktop-extension.js +0 -18
  196. package/dist/commands/diff.js +0 -99
  197. package/dist/core/claude-desktop-extension.js +0 -224
@@ -4,95 +4,645 @@
4
4
  * integration depth, and pressure level accordingly.
5
5
  *
6
6
  * Three profile tiers drive instruction file templates:
7
- * A (full) — MCP + hooks + auto-approve → lightweight instructions
8
- * B (standard) — MCP, no hooks → directive instructions with top traps
7
+ * A (full) — MCP + hooks → lightweight instructions (context via hooks/MCP)
8
+ * B (standard) — MCP, no hooks → working rules + architecture + top traps
9
9
  * C (limited) — no MCP → rich static content (plans, traps, decisions)
10
+ *
11
+ * Tier A agents (as of 2026-04): Claude Code, Copilot, Codex, Cursor,
12
+ * Windsurf (12 hooks!), Cline (macOS/Linux only).
13
+ * Note: Cline hooks don't work on Windows — but templateTier stays A
14
+ * because brainclaw generates hooks that gracefully degrade.
10
15
  */
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
19
+ /** Agent name aliases — maps common short names to canonical profile names. */
20
+ const AGENT_ALIASES = {
21
+ 'copilot': 'github-copilot',
22
+ 'gh-copilot': 'github-copilot',
23
+ 'gemini': 'antigravity',
24
+ 'mistral': 'mistral-vibe',
25
+ 'vibe': 'mistral-vibe',
26
+ };
27
+ /** Resolve an alias to its canonical agent name, or return the input unchanged. */
28
+ export function resolveAgentAlias(name) {
29
+ return AGENT_ALIASES[name] ?? name;
30
+ }
11
31
  const PROFILES = {
12
32
  // --- Code agents (interactive, IDE-driven) ---
13
33
  'claude-code': {
14
34
  name: 'claude-code', category: 'code-agent', workflowModel: 'interactive',
15
35
  hasMcp: true, hasHooks: true, hasAutoApprove: true, hasSkills: true, hasRules: true,
16
36
  instructionFile: 'CLAUDE.md', sharedInstructionFile: true, mcpConfigScope: 'both', templateTier: 'A',
37
+ role_capabilities: ['execute', 'coordinate', 'review', 'consult'],
38
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: true, inbox: true },
39
+ max_concurrent_tasks: 3,
40
+ // Claude CLI: -p is a flag (print mode), prompt is positional or via stdin.
41
+ // Use stdin_pipe to avoid shell quoting issues with long prompts.
42
+ prompt_delivery: { methods: ['stdin_pipe', 'inline_arg', 'inbox_structured'], preferred: 'stdin_pipe', max_inline_length: 4000 },
43
+ execution_env: { surface: 'cli' },
44
+ invoke_template: 'claude -p --allowedTools "Edit,Write,Bash,Read,Glob,Grep" {prompt}',
45
+ invoke_binary: 'claude',
46
+ invoke_review_template: 'claude -p --allowedTools "Read,Glob,Grep" {prompt}',
47
+ invoke_consult_template: 'claude -p --allowedTools "Read,Glob,Grep" {prompt}',
48
+ },
49
+ 'claude-sonnet': {
50
+ name: 'claude-sonnet', category: 'code-agent', workflowModel: 'interactive',
51
+ hasMcp: true, hasHooks: true, hasAutoApprove: true, hasSkills: true, hasRules: true,
52
+ instructionFile: 'CLAUDE.md', sharedInstructionFile: true, mcpConfigScope: 'both', templateTier: 'A',
53
+ role_capabilities: ['execute', 'coordinate', 'review', 'consult'],
54
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: true, inbox: true },
55
+ max_concurrent_tasks: 6,
56
+ prompt_delivery: { methods: ['stdin_pipe', 'inline_arg', 'inbox_structured'], preferred: 'stdin_pipe', max_inline_length: 4000 },
57
+ execution_env: { surface: 'cli' },
58
+ invoke_template: 'claude --model sonnet -p --allowedTools "Edit,Write,Bash,Read,Glob,Grep" {prompt}',
59
+ invoke_binary: 'claude',
60
+ invoke_review_template: 'claude --model sonnet -p --allowedTools "Read,Glob,Grep" {prompt}',
61
+ invoke_consult_template: 'claude --model sonnet -p --allowedTools "Read,Glob,Grep" {prompt}',
17
62
  },
18
63
  cursor: {
19
64
  name: 'cursor', category: 'code-agent', workflowModel: 'interactive',
20
- hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
21
- instructionFile: '.cursor/rules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'machine', templateTier: 'B',
65
+ hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
66
+ instructionFile: '.cursor/rules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'machine', templateTier: 'A',
67
+ role_capabilities: ['execute', 'review'],
68
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: false, canSpawnOtherCli: false, inbox: false },
69
+ prompt_delivery: { methods: ['inbox_structured'], preferred: 'inbox_structured' },
70
+ max_concurrent_tasks: 1,
71
+ execution_env: { surface: 'ide' },
22
72
  },
23
73
  windsurf: {
24
74
  name: 'windsurf', category: 'code-agent', workflowModel: 'interactive',
25
- hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
26
- instructionFile: '.windsurfrules', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'B',
75
+ hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
76
+ instructionFile: '.windsurfrules', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
77
+ role_capabilities: ['execute', 'review'],
78
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: false, canSpawnOtherCli: false, inbox: false },
79
+ max_concurrent_tasks: 1,
80
+ prompt_delivery: { methods: ['inbox_structured'], preferred: 'inbox_structured' },
81
+ execution_env: { surface: 'ide' },
27
82
  },
28
83
  cline: {
29
84
  name: 'cline', category: 'code-agent', workflowModel: 'interactive',
30
- hasMcp: true, hasHooks: false, hasAutoApprove: true, hasSkills: false, hasRules: true,
31
- instructionFile: '.clinerules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'project', templateTier: 'B',
85
+ hasMcp: true, hasHooks: true, hasAutoApprove: true, hasSkills: true, hasRules: true,
86
+ instructionFile: '.clinerules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'project', templateTier: 'A',
87
+ role_capabilities: ['execute', 'review'],
88
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
89
+ max_concurrent_tasks: 3,
90
+ prompt_delivery: { methods: ['inline_arg', 'inbox_structured'], preferred: 'inline_arg', max_inline_length: 8000 },
91
+ execution_env: { surface: 'extension' },
92
+ invoke_template: 'cline -y "{prompt}"',
93
+ invoke_binary: 'cline',
94
+ invoke_review_template: 'cline -y "{prompt}"',
32
95
  },
33
96
  roo: {
34
97
  name: 'roo', category: 'code-agent', workflowModel: 'interactive',
35
98
  hasMcp: true, hasHooks: false, hasAutoApprove: true, hasSkills: false, hasRules: true,
36
99
  instructionFile: '.roo/rules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'project', templateTier: 'B',
100
+ role_capabilities: ['execute', 'review'],
101
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
102
+ max_concurrent_tasks: 2,
103
+ prompt_delivery: { methods: ['inline_arg', 'inbox_structured'], preferred: 'inline_arg', max_inline_length: 8000 },
104
+ execution_env: { surface: 'extension' },
105
+ invoke_template: 'roo -y "{prompt}"',
106
+ invoke_binary: 'roo',
107
+ invoke_review_template: 'roo -y "{prompt}"',
37
108
  },
38
109
  continue: {
39
110
  name: 'continue', category: 'code-agent', workflowModel: 'interactive',
40
111
  hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
41
112
  instructionFile: '.continue/rules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'both', templateTier: 'B',
113
+ role_capabilities: ['execute', 'consult'],
114
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
115
+ max_concurrent_tasks: 2,
116
+ prompt_delivery: { methods: ['inline_arg', 'inbox_structured'], preferred: 'inline_arg', max_inline_length: 8000 },
117
+ execution_env: { surface: 'extension' },
118
+ invoke_template: 'cn --auto "{prompt}"',
119
+ invoke_binary: 'cn',
120
+ invoke_review_template: 'cn --auto --readonly "{prompt}"',
42
121
  },
43
122
  opencode: {
44
123
  name: 'opencode', category: 'code-agent', workflowModel: 'interactive',
45
124
  hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
46
125
  instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'project', templateTier: 'B',
126
+ role_capabilities: ['execute', 'review'],
127
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
128
+ max_concurrent_tasks: 2,
129
+ prompt_delivery: { methods: ['inline_arg', 'temp_file'], preferred: 'inline_arg', max_inline_length: 8000 },
130
+ execution_env: { surface: 'cli' },
131
+ invoke_template: 'opencode "{prompt}"',
132
+ invoke_binary: 'opencode',
133
+ invoke_review_template: 'opencode "{prompt}"',
47
134
  },
135
+ // Sandbox note: when running under --sandbox workspace-write, Codex cannot reach
136
+ // the main project store via MCP. Use filesystem-direct writes instead:
137
+ // write candidates to .brainclaw/coordination/inbox/cnd_<id>.json in the active
138
+ // worktree. The coordinator then syncs them via bclaw_harvest_candidates.
48
139
  codex: {
49
140
  name: 'codex', category: 'code-agent', workflowModel: 'task-based',
50
- hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
51
- instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'B',
141
+ hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
142
+ instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'A',
143
+ role_capabilities: ['execute', 'review'],
144
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
145
+ max_concurrent_tasks: 5,
146
+ // pln#475: prefer stdin_pipe to avoid Windows cmd.exe arg-parsing breaking
147
+ // long prompts. codex.cmd resolves through cmd shell, where embedded
148
+ // backticks/`#` chars made codex CLI raise "unexpected argument" (trp#59).
149
+ // Codex CLI reads stdin when the [PROMPT] arg is omitted (`codex exec`
150
+ // with no positional). The execution adapter pipes promptText to stdin.
151
+ // inline_arg stays as a fallback for short prompts on POSIX.
152
+ prompt_delivery: { methods: ['stdin_pipe', 'inline_arg'], preferred: 'stdin_pipe' },
153
+ execution_env: { surface: 'cli' },
154
+ invoke_template: 'codex exec -c approval_policy="never" --sandbox workspace-write "{prompt}"',
155
+ invoke_binary: 'codex',
156
+ // Review runs need shell access for git/grep/rg and filesystem reads of
157
+ // the whole repo. Older templates forced --sandbox read-only on reviews,
158
+ // but that blocks PowerShell exec on Windows and forced reviewers to
159
+ // fall back to GitHub connectors — which fail for local-only commits.
160
+ // Aligning with the regular spawn template (workspace-write) is the
161
+ // accepted pattern per agent_spawn_inventory memory.
162
+ invoke_review_template: 'codex exec -c approval_policy="never" --sandbox workspace-write "{prompt}"',
52
163
  },
53
164
  antigravity: {
54
165
  name: 'antigravity', category: 'code-agent', workflowModel: 'interactive',
55
166
  hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: false, hasRules: true,
56
167
  instructionFile: 'GEMINI.md', sharedInstructionFile: true, mcpConfigScope: 'machine', templateTier: 'B',
168
+ role_capabilities: ['execute', 'consult'],
169
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
170
+ max_concurrent_tasks: 2,
171
+ prompt_delivery: { methods: ['inline_arg'], preferred: 'inline_arg', max_inline_length: 8000 },
172
+ execution_env: { surface: 'cli' },
173
+ invoke_template: 'gemini -p "{prompt}"',
174
+ invoke_binary: 'gemini',
175
+ invoke_review_template: 'gemini -p "{prompt}"',
57
176
  },
58
177
  'github-copilot': {
59
178
  name: 'github-copilot', category: 'code-agent', workflowModel: 'interactive',
60
- hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: true,
61
- instructionFile: '.github/copilot-instructions.md', sharedInstructionFile: true, mcpConfigScope: 'none', templateTier: 'C',
179
+ hasMcp: true, hasHooks: true, hasAutoApprove: false, hasSkills: true, hasRules: true,
180
+ instructionFile: '.github/copilot-instructions.md', sharedInstructionFile: true, mcpConfigScope: 'project', templateTier: 'A',
181
+ // Copilot CLI 1.0.35+ supports headless spawn via --allow-all + --no-ask-user and
182
+ // per-session MCP via --additional-mcp-config (validated spike pln#440, 2026-04-24
183
+ // on Windows: non-interactive prompt, file write, and MCP bclaw_create write path).
184
+ role_capabilities: ['execute', 'review', 'consult'],
185
+ runtime: { mcp_direct: true, hooks: true, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: true },
186
+ max_concurrent_tasks: 1,
187
+ prompt_delivery: { methods: ['inline_arg', 'inbox_structured'], preferred: 'inline_arg', max_inline_length: 4000 },
188
+ execution_env: { surface: 'cli' },
189
+ invoke_template: 'copilot -p "{prompt}" --allow-all --no-ask-user',
190
+ invoke_binary: 'copilot',
191
+ invoke_review_template: 'copilot -p "{prompt}" --allow-all --no-ask-user',
192
+ },
193
+ kilocode: {
194
+ name: 'kilocode', category: 'code-agent', workflowModel: 'interactive',
195
+ hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: true,
196
+ instructionFile: '.kilo/rules/brainclaw.md', sharedInstructionFile: false, mcpConfigScope: 'project', templateTier: 'B',
197
+ role_capabilities: ['execute', 'review', 'consult'],
198
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
199
+ max_concurrent_tasks: 2,
200
+ prompt_delivery: { methods: ['inline_arg', 'temp_file'], preferred: 'inline_arg', max_inline_length: 8000 },
201
+ execution_env: { surface: 'cli' },
202
+ invoke_template: 'kilo run --auto "{prompt}"',
203
+ invoke_binary: 'kilo',
204
+ invoke_review_template: 'kilo run --auto "{prompt}"',
205
+ invoke_consult_template: 'kilo run --auto "{prompt}"',
206
+ },
207
+ // Mistral Vibe (pln#489) — Tier B: MCP via TOML config + skills + CLI spawn,
208
+ // but no hooks (BeforePrompt feature request #531 still open) and no native
209
+ // rules file equivalent to CLAUDE.md. Reuses AGENTS.md as the static
210
+ // instruction surface and the universal .agents/skills/brainclaw/SKILL.md
211
+ // for skill discovery (auto-discovered by Mistral Vibe alongside .vibe/skills/).
212
+ // Strategic value: EU/FR data sovereignty (Mistral Paris-based, not subject
213
+ // to US CLOUD Act; Apache 2.0 open-source CLI; open-weight models). Caveats:
214
+ // CLI freezes documented on current version, Windows Git Bash unsupported
215
+ // (issue #135), max_concurrent_tasks set conservatively to 2.
216
+ 'mistral-vibe': {
217
+ name: 'mistral-vibe', category: 'code-agent', workflowModel: 'task-based',
218
+ hasMcp: true, hasHooks: false, hasAutoApprove: true, hasSkills: true, hasRules: false,
219
+ instructionFile: 'AGENTS.md', sharedInstructionFile: true, mcpConfigScope: 'both', templateTier: 'B',
220
+ role_capabilities: ['execute', 'review', 'consult'],
221
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
222
+ max_concurrent_tasks: 2,
223
+ prompt_delivery: { methods: ['inline_arg', 'stdin_pipe'], preferred: 'inline_arg', max_inline_length: 8000 },
224
+ execution_env: { surface: 'cli' },
225
+ invoke_template: 'vibe --prompt "{prompt}" --auto-approve --max-turns 5',
226
+ invoke_binary: 'vibe',
227
+ invoke_review_template: 'vibe --prompt "{prompt}" --auto-approve --max-turns 5',
228
+ invoke_consult_template: 'vibe --prompt "{prompt}" --auto-approve --max-turns 3',
62
229
  },
63
230
  // --- Autonomous agents (headless, task-based or scheduled) ---
64
231
  openclaw: {
65
232
  name: 'openclaw', category: 'autonomous-agent', workflowModel: 'task-based',
66
- hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
67
- instructionFile: 'skills/openclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'none', templateTier: 'C',
233
+ hasMcp: true, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
234
+ instructionFile: 'skills/openclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'machine', templateTier: 'B',
235
+ role_capabilities: ['execute', 'coordinate'],
236
+ runtime: { mcp_direct: true, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: true, inbox: true },
237
+ max_concurrent_tasks: 1,
238
+ prompt_delivery: { methods: ['temp_file', 'inbox_structured'], preferred: 'temp_file' },
239
+ execution_env: { surface: 'cli' },
240
+ invoke_template: 'openclaw run --auto "{prompt}"',
241
+ invoke_binary: 'openclaw',
68
242
  },
69
243
  nanoclaw: {
70
244
  name: 'nanoclaw', category: 'autonomous-agent', workflowModel: 'task-based',
71
245
  hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
72
246
  instructionFile: 'skills/nanoclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'none', templateTier: 'C',
247
+ role_capabilities: ['execute'],
248
+ runtime: { mcp_direct: false, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
249
+ max_concurrent_tasks: 1,
250
+ prompt_delivery: { methods: ['inline_arg', 'stdin_pipe'], preferred: 'inline_arg', max_inline_length: 2000 },
251
+ execution_env: { surface: 'cli' },
73
252
  },
74
253
  nemoclaw: {
75
254
  name: 'nemoclaw', category: 'autonomous-agent', workflowModel: 'task-based',
76
255
  hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
77
256
  instructionFile: 'skills/nemoclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'none', templateTier: 'C',
257
+ role_capabilities: ['execute'],
258
+ runtime: { mcp_direct: false, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
259
+ max_concurrent_tasks: 1,
260
+ prompt_delivery: { methods: ['inline_arg', 'stdin_pipe'], preferred: 'inline_arg', max_inline_length: 2000 },
261
+ execution_env: { surface: 'cli' },
78
262
  },
79
263
  picoclaw: {
80
264
  name: 'picoclaw', category: 'autonomous-agent', workflowModel: 'scheduled',
81
265
  hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
82
266
  instructionFile: 'skills/picoclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'none', templateTier: 'C',
267
+ role_capabilities: ['execute'],
268
+ runtime: { mcp_direct: false, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
269
+ max_concurrent_tasks: 1,
270
+ prompt_delivery: { methods: ['inline_arg'], preferred: 'inline_arg', max_inline_length: 1000 },
271
+ execution_env: { surface: 'cli' },
83
272
  },
84
273
  zeroclaw: {
85
274
  name: 'zeroclaw', category: 'autonomous-agent', workflowModel: 'task-based',
86
275
  hasMcp: false, hasHooks: false, hasAutoApprove: false, hasSkills: true, hasRules: false,
87
276
  instructionFile: 'skills/zeroclaw/SKILL.md', sharedInstructionFile: false, mcpConfigScope: 'none', templateTier: 'C',
277
+ role_capabilities: ['execute'],
278
+ runtime: { mcp_direct: false, hooks: false, canBeSpawnedCli: true, canSpawnOtherCli: false, inbox: false },
279
+ max_concurrent_tasks: 1,
280
+ prompt_delivery: { methods: ['inline_arg', 'stdin_pipe'], preferred: 'stdin_pipe', max_inline_length: 1000 },
281
+ execution_env: { surface: 'cli' },
88
282
  },
89
283
  };
284
+ /**
285
+ * Default capability profiles for all known brainclaw-supported agents.
286
+ * Use `registerCapabilityProfile` to add custom agent profiles at runtime.
287
+ */
288
+ export const DEFAULT_CAPABILITY_PROFILES = PROFILES;
289
+ // ── Custom profile registry (for user-defined / custom agents) ─────────────
290
+ const _customProfiles = new Map();
291
+ /**
292
+ * Register a custom agent capability profile at runtime.
293
+ * Custom profiles take precedence over DEFAULT_CAPABILITY_PROFILES on lookup.
294
+ */
295
+ export function registerCapabilityProfile(profile) {
296
+ _customProfiles.set(profile.name, profile);
297
+ }
298
+ /**
299
+ * Get the capability profile for an agent by name.
300
+ * Checks custom registry first, then DEFAULT_CAPABILITY_PROFILES.
301
+ * Returns undefined for completely unknown agents.
302
+ */
303
+ export function getCapabilityProfile(name) {
304
+ const resolved = resolveAgentAlias(name);
305
+ return _customProfiles.get(resolved) ?? PROFILES[resolved];
306
+ }
307
+ /**
308
+ * Escape a string for safe use as a double-quoted shell argument.
309
+ * Escapes characters that have special meaning inside double-quotes
310
+ * on the target platform.
311
+ */
312
+ function escapeForDoubleQuote(s, isWin32) {
313
+ if (isWin32) {
314
+ // On Windows cmd/PowerShell, escape internal double-quotes by doubling them.
315
+ return s.replace(/"/g, '""');
316
+ }
317
+ // On POSIX shells, escape backslash, dollar, backtick, and double-quote.
318
+ return s.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/`/g, '\\`').replace(/"/g, '\\"');
319
+ }
320
+ /**
321
+ * Build a complete shell argument string for embedding inside double-quotes.
322
+ * Returns the string wrapped in double-quotes.
323
+ */
324
+ function quoteArg(s, isWin32) {
325
+ return `"${escapeForDoubleQuote(s, isWin32)}"`;
326
+ }
327
+ /**
328
+ * Generate a short, stable filename suffix from an arbitrary string.
329
+ * Uses a simple djb2-style hash — no crypto dependency needed.
330
+ */
331
+ function shortHash(s) {
332
+ let h = 5381;
333
+ for (let i = 0; i < s.length; i++) {
334
+ h = ((h << 5) + h) ^ s.charCodeAt(i);
335
+ h = h >>> 0; // keep unsigned 32-bit
336
+ }
337
+ return h.toString(16).padStart(8, '0');
338
+ }
339
+ /**
340
+ * Parse a raw template string (e.g. `claude -p "{prompt}" --allowedTools "..."`)
341
+ * into [executable, ...rawArgs], keeping the `{prompt}` token as-is.
342
+ *
343
+ * This is a simple shell-word splitter that handles:
344
+ * - unquoted tokens
345
+ * - double-quoted tokens (with \" escapes)
346
+ * - single-quoted tokens (no escape processing)
347
+ * It does NOT handle variable expansion, redirections, or compound commands.
348
+ */
349
+ function parseTemplateString(template) {
350
+ const tokens = [];
351
+ let i = 0;
352
+ const len = template.length;
353
+ while (i < len) {
354
+ // Skip whitespace between tokens
355
+ while (i < len && /\s/.test(template[i]))
356
+ i++;
357
+ if (i >= len)
358
+ break;
359
+ let token = '';
360
+ while (i < len && !/\s/.test(template[i])) {
361
+ const ch = template[i];
362
+ if (ch === '"') {
363
+ // Consume double-quoted segment
364
+ i++;
365
+ while (i < len && template[i] !== '"') {
366
+ if (template[i] === '\\' && i + 1 < len) {
367
+ i++; // skip backslash, keep next char literal
368
+ }
369
+ token += template[i];
370
+ i++;
371
+ }
372
+ if (i < len)
373
+ i++; // consume closing "
374
+ }
375
+ else if (ch === "'") {
376
+ // Consume single-quoted segment (no escaping)
377
+ i++;
378
+ while (i < len && template[i] !== "'") {
379
+ token += template[i];
380
+ i++;
381
+ }
382
+ if (i < len)
383
+ i++; // consume closing '
384
+ }
385
+ else {
386
+ token += ch;
387
+ i++;
388
+ }
389
+ }
390
+ if (token.length > 0)
391
+ tokens.push(token);
392
+ }
393
+ return tokens;
394
+ }
395
+ /**
396
+ * Build a structured, ready-to-run invoke command for an agent.
397
+ *
398
+ * Resolution order:
399
+ * 1. Look up profile via `getCapabilityProfile`.
400
+ * 2. Return undefined if the agent has no CLI invoke template.
401
+ * 3. Apply mode fallback chain (same as `getDefaultInvokeTemplate`).
402
+ * 4. Determine prompt delivery method:
403
+ * - If `prompt_delivery.preferred` is 'temp_file' OR prompt exceeds
404
+ * `max_inline_length`, use 'temp_file'.
405
+ * - If `prompt_delivery.preferred` is 'stdin_pipe', use 'stdin_pipe'.
406
+ * - Otherwise use 'inline_arg'.
407
+ * 5. Parse the resolved template into [executable, ...args].
408
+ * 6. Interpolate `{prompt}` token in the args list.
409
+ * 7. Build a platform-aware `bashCommand` string.
410
+ *
411
+ * @param name Agent name (matches AgentName or custom profile name).
412
+ * @param prompt The actual prompt text to deliver to the agent.
413
+ * @param options Optional overrides for mode, platform, and temp file path.
414
+ * @returns Structured InvokeCommand, or undefined if the agent is not CLI-spawnable.
415
+ */
416
+ export function buildInvokeCommand(name, prompt, options = {}) {
417
+ const profile = getCapabilityProfile(name);
418
+ if (!profile?.invoke_template || !profile?.invoke_binary)
419
+ return undefined;
420
+ if (!profile.runtime.canBeSpawnedCli)
421
+ return undefined;
422
+ const mode = options.mode ?? 'worker';
423
+ const isWin32 = (options.platform ?? process.platform) === 'win32';
424
+ // ── 1. Resolve the template string using the mode fallback chain ──────────
425
+ let templateStr;
426
+ switch (mode) {
427
+ case 'consult':
428
+ templateStr =
429
+ profile.invoke_consult_template ??
430
+ profile.invoke_review_template ??
431
+ profile.invoke_template;
432
+ break;
433
+ case 'reviewer':
434
+ templateStr = profile.invoke_review_template ?? profile.invoke_template;
435
+ break;
436
+ default:
437
+ templateStr = profile.invoke_template;
438
+ }
439
+ // ── 2. Determine prompt delivery method ──────────────────────────────────
440
+ const preferredDelivery = profile.prompt_delivery.preferred;
441
+ const maxInline = profile.prompt_delivery.max_inline_length;
442
+ let promptDelivery;
443
+ if (preferredDelivery === 'stdin_pipe') {
444
+ promptDelivery = 'stdin_pipe';
445
+ }
446
+ else if (preferredDelivery === 'temp_file' ||
447
+ (maxInline !== undefined && prompt.length > maxInline)) {
448
+ promptDelivery = 'temp_file';
449
+ }
450
+ else {
451
+ promptDelivery = 'inline_arg';
452
+ }
453
+ // ── 3. Resolve the prompt value to embed in the command ──────────────────
454
+ let embeddedPrompt;
455
+ let tempFilePath;
456
+ if (promptDelivery === 'temp_file') {
457
+ tempFilePath =
458
+ options.tempFilePath ??
459
+ path.join(os.tmpdir(), `bclaw_prompt_${shortHash(prompt)}.md`);
460
+ embeddedPrompt = tempFilePath;
461
+ }
462
+ else if (promptDelivery === 'stdin_pipe') {
463
+ // stdin_pipe: the {prompt} placeholder in the template (if present) is
464
+ // replaced with an empty string; the actual prompt is piped via stdin.
465
+ embeddedPrompt = '';
466
+ }
467
+ else {
468
+ embeddedPrompt = prompt;
469
+ }
470
+ // ── 4. Parse the template and interpolate {prompt} ───────────────────────
471
+ const rawTokens = parseTemplateString(templateStr);
472
+ if (rawTokens.length === 0)
473
+ return undefined;
474
+ const executable = rawTokens[0];
475
+ const interpolatedTokens = rawTokens.slice(1).map((tok) => tok === '{prompt}' ? embeddedPrompt : tok);
476
+ // ── 5. Build the args array ───────────────────────────────────────────────
477
+ // The args are the interpolated values; they are passed to execFile/spawn
478
+ // without further shell quoting. The bashCommand is built separately.
479
+ // Filter out empty strings from stdin_pipe delivery where {prompt} resolves to ''.
480
+ const args = interpolatedTokens.filter((tok) => tok !== '');
481
+ // ── 6. Build the platform-aware bashCommand string ───────────────────────
482
+ //
483
+ // Strategy:
484
+ // - For 'inline_arg': embed the prompt inline, double-quoted.
485
+ // - For 'temp_file' on POSIX: write prompt to temp file with `cat >`,
486
+ // then run the command; the {prompt} slot holds the file path.
487
+ // - For 'temp_file' on Windows: assume the caller has written the file;
488
+ // just run the command with the path. No `cat` pipes.
489
+ // - For 'stdin_pipe' on POSIX: use a heredoc to pipe prompt into the
490
+ // command via stdin.
491
+ // - For 'stdin_pipe' on Windows: omit stdin piping (use inbox fallback).
492
+ let bashCommand;
493
+ if (promptDelivery === 'temp_file') {
494
+ if (isWin32) {
495
+ // Caller writes the file; bashCommand is just the command invocation.
496
+ const cmdParts = [executable, ...interpolatedTokens.map((t) => quoteArg(t, isWin32))];
497
+ bashCommand = cmdParts.join(' ');
498
+ }
499
+ else {
500
+ // POSIX: write prompt to temp file, then run command.
501
+ const escapedPromptForHeredoc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
502
+ const writeStep = `printf '%s' '${escapedPromptForHeredoc}' > ${tempFilePath}`;
503
+ const cmdParts = [executable, ...interpolatedTokens.map((t) => quoteArg(t, isWin32))];
504
+ bashCommand = `${writeStep} && ${cmdParts.join(' ')}`;
505
+ }
506
+ }
507
+ else if (promptDelivery === 'stdin_pipe') {
508
+ if (isWin32) {
509
+ // Windows: no heredoc; just run the command without piping.
510
+ const cmdParts = [executable, ...interpolatedTokens.filter(Boolean).map((t) => quoteArg(t, isWin32))];
511
+ bashCommand = cmdParts.join(' ');
512
+ }
513
+ else {
514
+ // POSIX: use heredoc to pipe prompt into stdin.
515
+ const escapedPromptForHeredoc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
516
+ const nonEmptyArgs = interpolatedTokens.filter(Boolean);
517
+ const cmdParts = [executable, ...nonEmptyArgs.map((t) => quoteArg(t, isWin32))];
518
+ bashCommand = `printf '%s' '${escapedPromptForHeredoc}' | ${cmdParts.join(' ')}`;
519
+ }
520
+ }
521
+ else {
522
+ // inline_arg: embed prompt directly, double-quoted.
523
+ const cmdParts = rawTokens.map((tok, idx) => {
524
+ if (idx === 0)
525
+ return tok; // executable — no quoting
526
+ return tok === '{prompt}' ? quoteArg(embeddedPrompt, isWin32) : tok;
527
+ });
528
+ bashCommand = cmdParts.join(' ');
529
+ }
530
+ return {
531
+ executable,
532
+ args,
533
+ promptDelivery,
534
+ shell: false,
535
+ bashCommand,
536
+ ...(tempFilePath !== undefined ? { env: { BCLAW_PROMPT_FILE: tempFilePath } } : {}),
537
+ promptText: prompt,
538
+ ...(tempFilePath !== undefined ? { tempFilePath } : {}),
539
+ };
540
+ }
541
+ /**
542
+ * Resolve the appropriate brief mode for an agent based on its capability profile.
543
+ *
544
+ * Resolution rules:
545
+ * 1. Agent is NOT canBeSpawnedCli (IDE-only) → 'task_card'
546
+ * 2. Agent is task-based AND has NO MCP access → 'compact'
547
+ * (brief skips the Protocol section because the agent cannot call
548
+ * bclaw_assignment_update / bclaw_release_claim anyway)
549
+ * 3. Otherwise → 'full'
550
+ *
551
+ * Rule 2 was previously `workflowModel === 'task-based'` regardless of MCP
552
+ * capability (pln#496 Phase 1.b). That forced codex (task-based + hasMcp:
553
+ * true) onto 'compact' mode, which strips the Protocol section that
554
+ * contains `bclaw_assignment_update(status: …)` lifecycle instructions.
555
+ * Empirically validated 2026-05-04: every codex review in May 2026
556
+ * silently stayed `run_running` forever because codex never received the
557
+ * 'when done, call bclaw_assignment_update(status: completed)' line —
558
+ * last 'successful' review (lop_950a51aef0bb8263) had assignment
559
+ * status='offered', completed_at=null. The hasMcp check fixes this for
560
+ * codex and mistral-vibe (both task-based + MCP) without changing
561
+ * behaviour for genuinely MCP-less agents (nanoclaw / nemoclaw /
562
+ * zeroclaw).
563
+ *
564
+ * Note: stdin_pipe as prompt delivery is an optimization used by several
565
+ * interactive agents (claude-code, cline prefer it for long prompts) and
566
+ * does NOT indicate a sandboxed runtime — use workflowModel as the
567
+ * discriminator instead.
568
+ *
569
+ * Falls back to 'full' for unknown agents.
570
+ */
571
+ export function resolveBriefMode(agentName) {
572
+ const profile = getCapabilityProfile(agentName);
573
+ if (!profile)
574
+ return 'full';
575
+ if (!profile.runtime.canBeSpawnedCli)
576
+ return 'task_card';
577
+ if (profile.workflowModel === 'task-based' && !profile.hasMcp)
578
+ return 'compact';
579
+ return 'full';
580
+ }
581
+ // ── getDefaultInvokeTemplate ───────────────────────────────────────────────
582
+ /**
583
+ * Get the default invoke template for an agent.
584
+ * Reads invoke_template / invoke_binary from the capability profile.
585
+ * Mode selects the appropriate template variant with fallback chain:
586
+ * - 'worker' (default): invoke_template
587
+ * - 'reviewer': invoke_review_template → invoke_template
588
+ * - 'consult': invoke_consult_template → invoke_review_template → invoke_template
589
+ * Returns undefined for IDE-only agents or unknown agents without a CLI template.
590
+ */
591
+ export function getDefaultInvokeTemplate(name, mode = 'worker') {
592
+ const profile = getCapabilityProfile(name);
593
+ if (!profile?.invoke_template || !profile?.invoke_binary)
594
+ return undefined;
595
+ let command;
596
+ switch (mode) {
597
+ case 'consult':
598
+ command = profile.invoke_consult_template ?? profile.invoke_review_template ?? profile.invoke_template;
599
+ break;
600
+ case 'reviewer':
601
+ command = profile.invoke_review_template ?? profile.invoke_template;
602
+ break;
603
+ default:
604
+ command = profile.invoke_template;
605
+ }
606
+ return {
607
+ command,
608
+ channel: 'spawn',
609
+ timeout: 600,
610
+ binary: profile.invoke_binary,
611
+ mode,
612
+ };
613
+ }
614
+ /**
615
+ * Get all agents (known + custom) that have an invoke template (CLI-spawnable).
616
+ */
617
+ export function getSpawnableAgents() {
618
+ const result = [];
619
+ const allProfiles = [
620
+ ...Object.values(PROFILES),
621
+ ..._customProfiles.values(),
622
+ ];
623
+ for (const profile of allProfiles) {
624
+ if (profile.runtime.canBeSpawnedCli && profile.invoke_template && profile.invoke_binary) {
625
+ result.push({
626
+ name: profile.name,
627
+ template: {
628
+ command: profile.invoke_template,
629
+ channel: 'spawn',
630
+ timeout: 600,
631
+ binary: profile.invoke_binary,
632
+ mode: 'worker',
633
+ },
634
+ });
635
+ }
636
+ }
637
+ return result;
638
+ }
90
639
  /**
91
640
  * Get the capability profile for a known agent.
92
641
  * Returns undefined for unknown agent names.
642
+ * @deprecated Prefer getCapabilityProfile — supports custom agents too.
93
643
  */
94
644
  export function getAgentCapabilityProfile(name) {
95
- return PROFILES[name];
645
+ return getCapabilityProfile(name);
96
646
  }
97
647
  /**
98
648
  * Get all known agent capability profiles.
@@ -110,14 +660,57 @@ export function getAgentsByTier(tier) {
110
660
  * Check if an agent name is a known brainclaw-supported agent.
111
661
  */
112
662
  export function isKnownAgent(name) {
113
- return name in PROFILES;
663
+ const resolved = resolveAgentAlias(name);
664
+ return resolved in PROFILES;
665
+ }
666
+ export function validateAgentForDispatch(name, options = {}) {
667
+ const profile = getCapabilityProfile(name);
668
+ if (!profile) {
669
+ return {
670
+ valid: false,
671
+ code: 'unknown_profile',
672
+ reason: `Unknown agent profile: '${name}'. Registered agents: ${Object.keys(PROFILES).join(', ')}.`,
673
+ };
674
+ }
675
+ if (options.requireSpawnable) {
676
+ if (!profile.runtime.canBeSpawnedCli) {
677
+ return {
678
+ valid: false,
679
+ code: 'not_spawnable',
680
+ reason: `Agent '${name}' has no CLI spawn support (runtime.canBeSpawnedCli=false). Use a worker-capable agent for dispatch.`,
681
+ profile,
682
+ };
683
+ }
684
+ const bin = profile.invoke_binary;
685
+ // In test mode we skip the PATH probe: CI runners don't have external
686
+ // agent CLIs (codex, copilot, …) installed, and tests set BRAINCLAW_NO_SPAWN
687
+ // so no real spawn ever happens. Probing PATH here used to make the
688
+ // bclaw-coordinate suite fail on CI while passing locally whenever the
689
+ // developer had the binaries installed. Profile-based checks
690
+ // (unknown_profile, not_spawnable) still apply — they're deterministic.
691
+ const skipBinaryProbe = process.env.BRAINCLAW_TEST_MODE === '1'
692
+ || process.env.BRAINCLAW_NO_SPAWN === '1';
693
+ if (bin && !skipBinaryProbe) {
694
+ const probe = process.platform === 'win32' ? 'where' : 'which';
695
+ const result = spawnSync(probe, [bin], { encoding: 'utf-8' });
696
+ if (result.status !== 0) {
697
+ return {
698
+ valid: false,
699
+ code: 'binary_missing',
700
+ reason: `Agent '${name}' declares invoke_binary='${bin}' but it is not on PATH. Install the agent CLI or update its profile.`,
701
+ profile,
702
+ };
703
+ }
704
+ }
705
+ }
706
+ return { valid: true, profile };
114
707
  }
115
708
  /**
116
709
  * Summarize which integration surfaces are available for a given agent.
117
710
  * Useful for setup UI to explain what brainclaw will configure.
118
711
  */
119
712
  export function describeAgentSurfaces(name) {
120
- const profile = getAgentCapabilityProfile(name);
713
+ const profile = getCapabilityProfile(name);
121
714
  if (!profile)
122
715
  return [];
123
716
  const surfaces = [];