agent-tempo 1.2.0 → 1.4.0

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 (281) hide show
  1. package/CLAUDE.md +253 -219
  2. package/LICENSE +21 -21
  3. package/README.md +293 -289
  4. package/assets/icon-dark.svg +9 -9
  5. package/assets/icon.svg +9 -9
  6. package/assets/logo-dark.svg +11 -11
  7. package/assets/logo-light.svg +11 -11
  8. package/dashboard/README.md +91 -91
  9. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  10. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  11. package/dashboard/dist/index.html +20 -20
  12. package/dashboard/package.json +47 -47
  13. package/dist/activities/outbox.d.ts +30 -1
  14. package/dist/activities/outbox.js +96 -3
  15. package/dist/adapters/base.js +5 -0
  16. package/dist/adapters/copilot/adapter.js +12 -1
  17. package/dist/adapters/index.d.ts +1 -1
  18. package/dist/adapters/index.js +7 -0
  19. package/dist/adapters/pi/adapter.d.ts +2 -0
  20. package/dist/adapters/pi/adapter.js +43 -0
  21. package/dist/adapters/pi/index.d.ts +16 -0
  22. package/dist/adapters/pi/index.js +10 -0
  23. package/dist/cli/global-wrapper.d.ts +19 -0
  24. package/dist/cli/global-wrapper.js +169 -0
  25. package/dist/cli/help-text.js +97 -97
  26. package/dist/cli/startup.js +11 -0
  27. package/dist/cli/upgrade-command.js +81 -81
  28. package/dist/cli.js +12 -0
  29. package/dist/client/core.js +9 -2
  30. package/dist/client/interface.d.ts +6 -0
  31. package/dist/config.d.ts +79 -0
  32. package/dist/config.js +74 -0
  33. package/dist/daemon.js +37 -1
  34. package/dist/http/aggregate.d.ts +22 -1
  35. package/dist/http/aggregate.js +41 -0
  36. package/dist/http/auth.d.ts +94 -8
  37. package/dist/http/auth.js +93 -9
  38. package/dist/http/body.d.ts +4 -1
  39. package/dist/http/body.js +6 -3
  40. package/dist/http/event-bus.js +1 -0
  41. package/dist/http/event-types.d.ts +34 -2
  42. package/dist/http/event-types.js +1 -0
  43. package/dist/http/gate-audit.d.ts +12 -0
  44. package/dist/http/gate-audit.js +95 -0
  45. package/dist/http/gate-registry.d.ts +167 -0
  46. package/dist/http/gate-registry.js +163 -0
  47. package/dist/http/gate-routes.d.ts +48 -0
  48. package/dist/http/gate-routes.js +102 -0
  49. package/dist/http/ingest-registry.d.ts +30 -0
  50. package/dist/http/ingest-registry.js +108 -0
  51. package/dist/http/inner-loop-routes.d.ts +66 -0
  52. package/dist/http/inner-loop-routes.js +182 -0
  53. package/dist/http/inner-loop.d.ts +92 -0
  54. package/dist/http/inner-loop.js +155 -0
  55. package/dist/http/server.d.ts +38 -3
  56. package/dist/http/server.js +211 -6
  57. package/dist/http/snapshot.d.ts +6 -0
  58. package/dist/http/snapshot.js +6 -0
  59. package/dist/pi/cue-pump.d.ts +61 -0
  60. package/dist/pi/cue-pump.js +95 -0
  61. package/dist/pi/extension.d.ts +45 -0
  62. package/dist/pi/extension.js +407 -0
  63. package/dist/pi/gate-client.d.ts +54 -0
  64. package/dist/pi/gate-client.js +136 -0
  65. package/dist/pi/headless.d.ts +85 -0
  66. package/dist/pi/headless.js +224 -0
  67. package/dist/pi/index.d.ts +28 -0
  68. package/dist/pi/index.js +43 -0
  69. package/dist/pi/inner-loop-client.d.ts +67 -0
  70. package/dist/pi/inner-loop-client.js +164 -0
  71. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  72. package/dist/pi/inner-loop-publisher.js +236 -0
  73. package/dist/pi/lazy-proxy.d.ts +37 -0
  74. package/dist/pi/lazy-proxy.js +55 -0
  75. package/dist/pi/mission-control/actions.d.ts +48 -0
  76. package/dist/pi/mission-control/actions.js +98 -0
  77. package/dist/pi/mission-control/board.d.ts +53 -0
  78. package/dist/pi/mission-control/board.js +104 -0
  79. package/dist/pi/mission-control/extension.d.ts +44 -0
  80. package/dist/pi/mission-control/extension.js +251 -0
  81. package/dist/pi/mission-control/index.d.ts +15 -0
  82. package/dist/pi/mission-control/index.js +32 -0
  83. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  84. package/dist/pi/mission-control/inner-tail.js +76 -0
  85. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  86. package/dist/pi/mission-control/pi-ui.js +10 -0
  87. package/dist/pi/mission-control/render.d.ts +6 -0
  88. package/dist/pi/mission-control/render.js +95 -0
  89. package/dist/pi/phase-driver.d.ts +74 -0
  90. package/dist/pi/phase-driver.js +122 -0
  91. package/dist/pi/pi-types.d.ts +208 -0
  92. package/dist/pi/pi-types.js +21 -0
  93. package/dist/pi/probe.d.ts +80 -0
  94. package/dist/pi/probe.js +154 -0
  95. package/dist/pi/render-tools.d.ts +17 -0
  96. package/dist/pi/render-tools.js +51 -0
  97. package/dist/pi/reset-pump.d.ts +47 -0
  98. package/dist/pi/reset-pump.js +85 -0
  99. package/dist/pi/tool-capability.d.ts +60 -0
  100. package/dist/pi/tool-capability.js +156 -0
  101. package/dist/pi/workflow-client.d.ts +158 -0
  102. package/dist/pi/workflow-client.js +289 -0
  103. package/dist/pi/zod-to-typebox.d.ts +74 -0
  104. package/dist/pi/zod-to-typebox.js +191 -0
  105. package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
  106. package/dist/server-tools.d.ts +2 -0
  107. package/dist/server-tools.js +50 -46
  108. package/dist/server.js +4 -0
  109. package/dist/spawn.d.ts +55 -0
  110. package/dist/spawn.js +84 -12
  111. package/dist/tools/agent-types.d.ts +2 -2
  112. package/dist/tools/agent-types.js +22 -17
  113. package/dist/tools/attachment-info.d.ts +2 -2
  114. package/dist/tools/attachment-info.js +38 -33
  115. package/dist/tools/broadcast.d.ts +2 -2
  116. package/dist/tools/broadcast.js +69 -64
  117. package/dist/tools/cancel-stage.d.ts +2 -2
  118. package/dist/tools/cancel-stage.js +20 -15
  119. package/dist/tools/clear-state.d.ts +2 -2
  120. package/dist/tools/clear-state.js +25 -20
  121. package/dist/tools/coat-check-evict.d.ts +2 -2
  122. package/dist/tools/coat-check-evict.js +30 -25
  123. package/dist/tools/coat-check-get.d.ts +2 -2
  124. package/dist/tools/coat-check-get.js +39 -34
  125. package/dist/tools/coat-check-list.d.ts +2 -2
  126. package/dist/tools/coat-check-list.js +48 -43
  127. package/dist/tools/coat-check-put.d.ts +2 -2
  128. package/dist/tools/coat-check-put.js +41 -36
  129. package/dist/tools/cue.d.ts +2 -2
  130. package/dist/tools/cue.js +57 -52
  131. package/dist/tools/descriptor.d.ts +72 -0
  132. package/dist/tools/descriptor.js +39 -0
  133. package/dist/tools/destroy.d.ts +2 -2
  134. package/dist/tools/destroy.js +153 -148
  135. package/dist/tools/ensemble.d.ts +2 -2
  136. package/dist/tools/ensemble.js +71 -66
  137. package/dist/tools/evaluate-gate.d.ts +2 -2
  138. package/dist/tools/evaluate-gate.js +33 -27
  139. package/dist/tools/fetch-state.d.ts +2 -2
  140. package/dist/tools/fetch-state.js +43 -38
  141. package/dist/tools/gates.d.ts +2 -2
  142. package/dist/tools/gates.js +39 -34
  143. package/dist/tools/hosts.d.ts +2 -2
  144. package/dist/tools/hosts.js +25 -20
  145. package/dist/tools/listen.d.ts +2 -2
  146. package/dist/tools/listen.js +23 -18
  147. package/dist/tools/load-lineup.d.ts +2 -2
  148. package/dist/tools/load-lineup.js +324 -319
  149. package/dist/tools/migrate.d.ts +2 -2
  150. package/dist/tools/migrate.js +45 -40
  151. package/dist/tools/pause.d.ts +2 -2
  152. package/dist/tools/pause.js +34 -29
  153. package/dist/tools/play.d.ts +2 -2
  154. package/dist/tools/play.js +53 -48
  155. package/dist/tools/quality-gate.d.ts +2 -2
  156. package/dist/tools/quality-gate.js +26 -21
  157. package/dist/tools/recall.d.ts +2 -2
  158. package/dist/tools/recall.js +32 -27
  159. package/dist/tools/recruit.d.ts +2 -2
  160. package/dist/tools/recruit.js +325 -256
  161. package/dist/tools/release.d.ts +2 -2
  162. package/dist/tools/release.js +85 -80
  163. package/dist/tools/report.d.ts +2 -2
  164. package/dist/tools/report.js +28 -23
  165. package/dist/tools/reset.d.ts +3 -0
  166. package/dist/tools/reset.js +51 -0
  167. package/dist/tools/restart.d.ts +2 -2
  168. package/dist/tools/restart.js +51 -46
  169. package/dist/tools/restore.d.ts +2 -2
  170. package/dist/tools/restore.js +76 -71
  171. package/dist/tools/save-lineup.d.ts +2 -2
  172. package/dist/tools/save-lineup.js +32 -27
  173. package/dist/tools/save-state.d.ts +2 -2
  174. package/dist/tools/save-state.js +43 -38
  175. package/dist/tools/schedule.d.ts +2 -2
  176. package/dist/tools/schedule.js +133 -128
  177. package/dist/tools/schedules.d.ts +2 -2
  178. package/dist/tools/schedules.js +41 -36
  179. package/dist/tools/set-ensemble-description.d.ts +2 -2
  180. package/dist/tools/set-ensemble-description.js +26 -21
  181. package/dist/tools/set-name.d.ts +2 -2
  182. package/dist/tools/set-name.js +38 -33
  183. package/dist/tools/set-part.d.ts +2 -2
  184. package/dist/tools/set-part.js +20 -15
  185. package/dist/tools/shutdown.d.ts +2 -2
  186. package/dist/tools/shutdown.js +39 -34
  187. package/dist/tools/stage.d.ts +2 -2
  188. package/dist/tools/stage.js +28 -23
  189. package/dist/tools/stages.d.ts +2 -2
  190. package/dist/tools/stages.js +36 -31
  191. package/dist/tools/unschedule.d.ts +2 -2
  192. package/dist/tools/unschedule.js +30 -25
  193. package/dist/tools/who-am-i.d.ts +2 -2
  194. package/dist/tools/who-am-i.js +36 -31
  195. package/dist/tools/worktree.d.ts +2 -2
  196. package/dist/tools/worktree.js +134 -129
  197. package/dist/tui/index.js +6 -6
  198. package/dist/types.d.ts +47 -2
  199. package/dist/types.js +1 -1
  200. package/dist/utils/default-part.js +1 -0
  201. package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
  202. package/dist/utils/grpc-shutdown-guard.js +88 -0
  203. package/dist/utils/sdk-probe.d.ts +23 -0
  204. package/dist/utils/sdk-probe.js +46 -7
  205. package/dist/worker.d.ts +3 -1
  206. package/dist/worker.js +6 -2
  207. package/dist/workflows/session.js +70 -2
  208. package/dist/workflows/signals.d.ts +32 -2
  209. package/dist/workflows/signals.js +25 -2
  210. package/examples/agents/tempo-composer.md +56 -56
  211. package/examples/agents/tempo-conductor.md +117 -117
  212. package/examples/agents/tempo-critic.md +73 -73
  213. package/examples/agents/tempo-improv.md +74 -74
  214. package/examples/agents/tempo-liner.md +75 -75
  215. package/examples/agents/tempo-roadie.md +61 -61
  216. package/examples/agents/tempo-soloist.md +71 -71
  217. package/examples/agents/tempo-tuner.md +94 -94
  218. package/examples/ensembles/tempo-big-band.yaml +146 -146
  219. package/examples/ensembles/tempo-dev-team.yaml +58 -58
  220. package/examples/ensembles/tempo-headless-jam.yaml +77 -77
  221. package/examples/ensembles/tempo-jam-session.yaml +41 -41
  222. package/examples/ensembles/tempo-mock-jam.yaml +79 -79
  223. package/examples/ensembles/tempo-review-squad.yaml +32 -32
  224. package/package.json +176 -173
  225. package/packaging/launchd/com.agent.tempo.plist +46 -46
  226. package/packaging/systemd/agent-tempo.service +32 -32
  227. package/packaging/windows/install-task.ps1 +71 -71
  228. package/scenarios/conductor-recruit-mock.yaml +33 -33
  229. package/scenarios/echo-roundtrip.yaml +15 -15
  230. package/scenarios/multi-player-handoff.yaml +38 -38
  231. package/scenarios/recruit-cascade.yaml +38 -38
  232. package/scenarios/two-player-conversation.yaml +33 -33
  233. package/workflow-bundle.js +97 -6
  234. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  235. package/dist/activities/claude-stop.d.ts +0 -21
  236. package/dist/activities/claude-stop.js +0 -94
  237. package/dist/channel.d.ts +0 -3
  238. package/dist/channel.js +0 -48
  239. package/dist/copilot-bridge.d.ts +0 -22
  240. package/dist/copilot-bridge.js +0 -565
  241. package/dist/scripts/258-spotcheck.js +0 -303
  242. package/dist/tools/detach.d.ts +0 -4
  243. package/dist/tools/detach.js +0 -45
  244. package/dist/tools/encore.d.ts +0 -4
  245. package/dist/tools/encore.js +0 -31
  246. package/dist/tools/helpers.d.ts +0 -21
  247. package/dist/tools/helpers.js +0 -25
  248. package/dist/tools/pause-ensemble.d.ts +0 -4
  249. package/dist/tools/pause-ensemble.js +0 -58
  250. package/dist/tools/resume-ensemble.d.ts +0 -4
  251. package/dist/tools/resume-ensemble.js +0 -79
  252. package/dist/tools/stop.d.ts +0 -4
  253. package/dist/tools/stop.js +0 -29
  254. package/dist/tui/client.d.ts +0 -6
  255. package/dist/tui/client.js +0 -9
  256. package/dist/tui/components/ActivityLog.d.ts +0 -16
  257. package/dist/tui/components/ActivityLog.js +0 -36
  258. package/dist/tui/components/CommandOverlay.d.ts +0 -15
  259. package/dist/tui/components/CommandOverlay.js +0 -34
  260. package/dist/tui/components/ConductorChat.d.ts +0 -16
  261. package/dist/tui/components/ConductorChat.js +0 -32
  262. package/dist/tui/components/EnsembleListView.d.ts +0 -14
  263. package/dist/tui/components/EnsembleListView.js +0 -32
  264. package/dist/tui/components/EnsemblePanel.d.ts +0 -12
  265. package/dist/tui/components/EnsemblePanel.js +0 -40
  266. package/dist/tui/components/InputBar.d.ts +0 -13
  267. package/dist/tui/components/InputBar.js +0 -58
  268. package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
  269. package/dist/tui/components/ScheduleOverlay.js +0 -113
  270. package/dist/tui/components/TopBar.d.ts +0 -12
  271. package/dist/tui/components/TopBar.js +0 -15
  272. package/dist/tui/core-api.d.ts +0 -26
  273. package/dist/tui/core-api.js +0 -67
  274. package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
  275. package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
  276. package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
  277. package/dist/tui/hooks/useMaestroPoller.js +0 -36
  278. package/dist/tui/hooks/useSendCommand.d.ts +0 -7
  279. package/dist/tui/hooks/useSendCommand.js +0 -29
  280. package/dist/utils/bg-preflight.d.ts +0 -25
  281. package/dist/utils/bg-preflight.js +0 -154
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.registerRecruitTool = registerRecruitTool;
36
+ exports.buildRecruitTool = buildRecruitTool;
37
37
  exports.checkHostPreflight = checkHostPreflight;
38
38
  exports.nearestHostname = nearestHostname;
39
39
  const zod_1 = require("zod");
@@ -42,13 +42,20 @@ const config_1 = require("../config");
42
42
  const types_1 = require("../types");
43
43
  const resolve_1 = require("./resolve");
44
44
  const signals_1 = require("../workflows/signals");
45
- const helpers_1 = require("./helpers");
45
+ const descriptor_1 = require("./descriptor");
46
46
  const agent_types_1 = require("../ensemble/agent-types");
47
47
  const validation_1 = require("../utils/validation");
48
48
  const sdk_probe_1 = require("../utils/sdk-probe");
49
49
  const pre_flight_1 = require("../adapters/claude-code-headless/pre-flight");
50
50
  const types_2 = require("../adapters/claude-code-headless/types");
51
+ const probe_1 = require("../pi/probe");
51
52
  const toolLog = (...args) => console.error('[agent-tempo:recruit]', ...args);
53
+ /**
54
+ * True dynamic ESM import (survives tsc's commonjs downlevel) — used to load
55
+ * pi-ai's sessionless `getModel` for the Copilot model-index pre-flight gate.
56
+ */
57
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
58
+ const esmImport = new Function('s', 'return import(s)');
52
59
  /**
53
60
  * #449 Phase C — check whether the `opencode` binary is on PATH. Used by
54
61
  * the recruit pre-flight to fail fast with an actionable error before the
@@ -71,7 +78,7 @@ function hasOpencodeOnPath() {
71
78
  return false;
72
79
  }
73
80
  }
74
- function registerRecruitTool(server, client, config, getPlayerId, handle, ownAgentType = 'claude', deps = {}) {
81
+ function buildRecruitTool(client, config, getPlayerId, handle, ownAgentType = 'claude', deps = {}) {
75
82
  // Lazy default — only imports utils/hosts when actually called, so the
76
83
  // MCP server's module load graph doesn't drag the whole join layer
77
84
  // into every consumer at import time.
@@ -83,282 +90,344 @@ function registerRecruitTool(server, client, config, getPlayerId, handle, ownAge
83
90
  taskQueue: config.taskQueue,
84
91
  });
85
92
  });
86
- (0, helpers_1.defineTool)(server, 'recruit', `Start a new named session in a directory. Rejects if the name is already active. Supports Claude Code or Copilot CLI agents. Defaults to "${ownAgentType}" (same as this session).`, {
87
- workDir: zod_1.z.string().max(validation_1.PATH_MAX).describe('The working directory for the new session'),
88
- name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('Name for the new session'),
89
- conductor: zod_1.z.boolean().optional()
90
- .describe('Whether this session is a conductor (default: false)'),
91
- initialMessage: zod_1.z.string().max(validation_1.MESSAGE_MAX).optional()
92
- .describe('Optional task or message for the new session (sent after it sets its name)'),
93
- agent: zod_1.z.enum(types_1.AGENT_TYPES).optional()
94
- .describe(`Which agent to use (default: "${ownAgentType}", same as this session). "mock" requires dev mode (--dev). "claude-api" runs headless via the Anthropic Messages API — requires ANTHROPIC_API_KEY env var and the @anthropic-ai/sdk optional dependency installed; has access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but NOT file-edit or shell tools (use "claude" for those). "opencode" runs headless via a local opencode serve subprocess; multi-provider (Anthropic, OpenAI, Bedrock, Ollama, …) — requires the @opencode-ai/sdk optional dep and an opencode binary on PATH. opencode players ARE file-op-capable (file edits / shell / web search via OpenCode's built-in tools). "claude-code-headless" runs the official Claude Code CLI as a headless per-turn \`claude -p\` subprocess — requires the \`claude\` binary on PATH AND a logged-in Claude Code session (\`claude auth login\`); turns bill against the host's existing subscription extra-usage credits, NOT a Console API key. claude-code-headless players have full Claude Code tool access (Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch).`),
95
- model: zod_1.z.string().regex(/^[a-z0-9][a-z0-9-/.:_]*$/).optional()
96
- .describe('Model id. For "claude-api": bare Anthropic id (e.g. "claude-opus-4-7"). For "opencode": combined "provider/model" (e.g. "anthropic/claude-opus-4-7", "openai/gpt-4o", "ollama/llama3"). Falls back to AGENT_TEMPO_API_MODEL (claude-api) or AGENT_TEMPO_OPENCODE_MODEL (opencode), then a constants-pinned default. Ignored for claude / copilot / mock adapters.'),
97
- type: zod_1.z.string().optional()
98
- .describe('Agent type name — references a Claude Code agent definition (e.g., "tempo-soloist")'),
99
- systemPrompt: zod_1.z.string().optional()
100
- .describe('Path to a .md file to use as custom agent system prompt (--system-prompt)'),
101
- host: zod_1.z.string().optional()
102
- .describe('Target hostname for cross-machine recruiting. Omit for local spawn.'),
103
- force: zod_1.z.boolean().optional()
104
- .describe('Force-terminate any existing session with this name before recruiting. Use when a previous session is orphaned or stuck.'),
105
- mockMode: zod_1.z.enum(types_1.MOCK_MODES).optional()
106
- .describe('Dev-mode only (agent: "mock"). Mock adapter mode. Default: "echo". "silent" never replies (heartbeat-stale validation); "chaos" probabilistic fail/crash injection (env-tuned).'),
107
- mockScenario: zod_1.z.string().optional()
108
- .describe('Dev-mode only (agent: "mock", mockMode: "scripted"). Bare scenario name (resolved against shipped scenarios/) or absolute path to a scenario YAML.'),
109
- // #520 claude-code-headless adapter knobs. Both ignored for other adapters.
110
- permissionMode: zod_1.z.enum(types_2.CLAUDE_CODE_PERMISSION_MODES).optional()
111
- .describe('claude-code-headless only. Permission mode forwarded to `claude -p --permission-mode`. Default "acceptEdits" auto-approves writes + common fs commands. "bypassPermissions" / "dangerouslySkipPermissions" trades safety for speed in trusted contexts. "plan" plans without executing — not useful for headless players. Mutually exclusive with `dangerouslySkipPermissions`.'),
112
- dangerouslySkipPermissions: zod_1.z.boolean().optional()
113
- .describe('claude-code-headless only. When true, passes `--dangerously-skip-permissions` to `claude -p` instead of `--permission-mode`. Use only in sandboxed/trusted contexts. Mutually exclusive with `permissionMode`.'),
114
- }, async (args) => {
115
- const { workDir, name, initialMessage } = args;
116
- const isConductor = args.conductor === true;
117
- const agent = args.agent || ownAgentType;
118
- const model = args.model;
119
- const agentTypeName = args.type;
120
- const systemPrompt = args.systemPrompt;
121
- const host = args.host;
122
- const force = args.force === true;
123
- const mockMode = args.mockMode;
124
- const mockScenario = args.mockScenario;
125
- const permissionMode = args.permissionMode;
126
- const dangerouslySkipPermissions = args.dangerouslySkipPermissions === true;
127
- // ADR 0014 §7 gate 3 recruit-time rejection of `agent: 'mock'`
128
- // outside dev mode. Defense-in-depth: even if a hand-edited install
129
- // had `dist/adapters/mock/` present (gate 1 bypassed) AND somehow
130
- // got the registry to register the descriptor (gate 2 bypassed),
131
- // this rejects the request with a clear, actionable error.
132
- if (agent === 'mock' && !(0, config_1.isDevMode)()) {
133
- return (0, helpers_1.fail)(`agent: "mock" is only available in dev mode. Restart agent-tempo with --dev (or set AGENT_TEMPO_DEV_MODE=1) to enable.`);
134
- }
135
- // mockMode / mockScenario are only meaningful with the mock adapter —
136
- // reject silently-ignored params so users learn the right flag shape.
137
- if (mockMode != null && agent !== 'mock') {
138
- return (0, helpers_1.fail)(`mockMode is only valid when agent: "mock" (got agent: "${agent}").`);
139
- }
140
- if (mockScenario != null && agent !== 'mock') {
141
- return (0, helpers_1.fail)(`mockScenario is only valid when agent: "mock" (got agent: "${agent}").`);
142
- }
143
- if (agent === 'mock' && mockMode === 'scripted' && !mockScenario) {
144
- return (0, helpers_1.fail)(`mockMode: "scripted" requires mockScenario (a bare scenario name or path to a YAML file).`);
145
- }
146
- // PR-3: silent + chaos modes don't consult the scenario file. Reject
147
- // explicitly rather than silently ignore so users learn the right
148
- // shape and don't sit wondering why their scenario wasn't applied.
149
- if (mockScenario && (mockMode === 'silent' || mockMode === 'chaos')) {
150
- return (0, helpers_1.fail)(`mockMode: "${mockMode}" does not use a scenario. Drop mockScenario or switch to mockMode: "scripted".`);
151
- }
152
- // #131 / #449 Phase C — model knob is meaningful for claude-api AND
153
- // opencode (different shapes — bare vs `provider/model` — both flow
154
- // through the same recruit field). Reject silently-ignored params for
155
- // the other adapters so users learn the right shape.
156
- // Local-spawn pre-flight checks (env vars + SDK install + binaries)
157
- // run only when `host` is unset; cross-host recruits delegate to the
158
- // target daemon's `availableAgentTypes` advertisement (the existing
159
- // `checkHostPreflight` path), which already gates on whether the
160
- // remote daemon resolved the SDK at boot.
161
- if (model != null && agent !== 'claude-api' && agent !== 'opencode') {
162
- return (0, helpers_1.fail)(`model is only valid when agent: "claude-api" or agent: "opencode" (got agent: "${agent}").`);
163
- }
164
- // #520 — claude-code-headless permission knobs are mutually exclusive
165
- // and only meaningful for that adapter. Reject silently-ignored
166
- // params so users learn the right shape.
167
- if (permissionMode != null && agent !== 'claude-code-headless') {
168
- return (0, helpers_1.fail)(`permissionMode is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
169
- }
170
- if (dangerouslySkipPermissions && agent !== 'claude-code-headless') {
171
- return (0, helpers_1.fail)(`dangerouslySkipPermissions is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
172
- }
173
- if (permissionMode != null && dangerouslySkipPermissions) {
174
- return (0, helpers_1.fail)(`permissionMode and dangerouslySkipPermissions are mutually exclusive — pass at most one.`);
175
- }
176
- if (agent === 'claude-api' && !host && !force) {
177
- if (!process.env.ANTHROPIC_API_KEY) {
178
- return (0, helpers_1.fail)(`agent: "claude-api" requires the ANTHROPIC_API_KEY environment variable on the spawn host. Set it before recruiting (export ANTHROPIC_API_KEY=sk-...) or use \`force: true\` to bypass this check.`);
93
+ return {
94
+ name: 'recruit',
95
+ description: `Start a new named session in a directory. Rejects if the name is already active. Supports Claude Code or Copilot CLI agents. Defaults to "${ownAgentType}" (same as this session).`,
96
+ params: {
97
+ workDir: zod_1.z.string().max(validation_1.PATH_MAX).describe('The working directory for the new session'),
98
+ name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).describe('Name for the new session'),
99
+ conductor: zod_1.z.boolean().optional()
100
+ .describe('Whether this session is a conductor (default: false)'),
101
+ initialMessage: zod_1.z.string().max(validation_1.MESSAGE_MAX).optional()
102
+ .describe('Optional task or message for the new session (sent after it sets its name)'),
103
+ agent: zod_1.z.enum(types_1.AGENT_TYPES).optional()
104
+ .describe(`Which agent to use (default: "${ownAgentType}", same as this session). "mock" requires dev mode (--dev). "claude-api" runs headless via the Anthropic Messages API — requires ANTHROPIC_API_KEY env var and the @anthropic-ai/sdk optional dependency installed; has access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but NOT file-edit or shell tools (use "claude" for those). "opencode" runs headless via a local opencode serve subprocess; multi-provider (Anthropic, OpenAI, Bedrock, Ollama, …) — requires the @opencode-ai/sdk optional dep and an opencode binary on PATH. opencode players ARE file-op-capable (file edits / shell / web search via OpenCode's built-in tools). "claude-code-headless" runs the official Claude Code CLI as a headless per-turn \`claude -p\` subprocess — requires the \`claude\` binary on PATH AND a logged-in Claude Code session (\`claude auth login\`); turns bill against the host's existing subscription extra-usage credits, NOT a Console API key. claude-code-headless players have full Claude Code tool access (Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch).`),
105
+ model: zod_1.z.string().regex(/^[a-z0-9][a-z0-9-/.:_]*$/).optional()
106
+ .describe('Model id. For "claude-api": bare Anthropic id (e.g. "claude-opus-4-7"). For "opencode": combined "provider/model" (e.g. "anthropic/claude-opus-4-7", "openai/gpt-4o", "ollama/llama3"). Falls back to AGENT_TEMPO_API_MODEL (claude-api) or AGENT_TEMPO_OPENCODE_MODEL (opencode), then a constants-pinned default. Ignored for claude / copilot / mock adapters.'),
107
+ type: zod_1.z.string().optional()
108
+ .describe('Agent type name — references a Claude Code agent definition (e.g., "tempo-soloist")'),
109
+ systemPrompt: zod_1.z.string().optional()
110
+ .describe('Path to a .md file to use as custom agent system prompt (--system-prompt)'),
111
+ host: zod_1.z.string().optional()
112
+ .describe('Target hostname for cross-machine recruiting. Omit for local spawn.'),
113
+ force: zod_1.z.boolean().optional()
114
+ .describe('Force-terminate any existing session with this name before recruiting. Use when a previous session is orphaned or stuck.'),
115
+ mockMode: zod_1.z.enum(types_1.MOCK_MODES).optional()
116
+ .describe('Dev-mode only (agent: "mock"). Mock adapter mode. Default: "echo". "silent" never replies (heartbeat-stale validation); "chaos" probabilistic fail/crash injection (env-tuned).'),
117
+ mockScenario: zod_1.z.string().optional()
118
+ .describe('Dev-mode only (agent: "mock", mockMode: "scripted"). Bare scenario name (resolved against shipped scenarios/) or absolute path to a scenario YAML.'),
119
+ // #520 — claude-code-headless adapter knobs. Both ignored for other adapters.
120
+ permissionMode: zod_1.z.enum(types_2.CLAUDE_CODE_PERMISSION_MODES).optional()
121
+ .describe('claude-code-headless only. Permission mode forwarded to `claude -p --permission-mode`. Default "acceptEdits" auto-approves writes + common fs commands. "bypassPermissions" / "dangerouslySkipPermissions" trades safety for speed in trusted contexts. "plan" plans without executing — not useful for headless players. Mutually exclusive with `dangerouslySkipPermissions`.'),
122
+ dangerouslySkipPermissions: zod_1.z.boolean().optional()
123
+ .describe('claude-code-headless only. When true, passes `--dangerously-skip-permissions` to `claude -p` instead of `--permission-mode`. Use only in sandboxed/trusted contexts. Mutually exclusive with `permissionMode`.'),
124
+ // Phase 3a / MD-C — headless Pi tool-access policy. Ignored for other agents.
125
+ // NOTE: stays `.optional()` (NOT `.default('restricted')`): this tool's
126
+ // params are rendered to the Pi front-end via renderToPi → the zod→TypeBox
127
+ // converter, which is fail-loud on `.default()` (D1). A schema default would
128
+ // throw at Pi tool registration ("Pi missing tool: recruit"). The concrete
129
+ // 'restricted' default is applied once at the read site below instead.
130
+ toolAccess: zod_1.z.enum(['restricted', 'standard', 'full']).optional()
131
+ .describe('pi only. Headless Pi tool-class policy. "restricted" (default): agent-tempo tools + Read/Edit/Write; Bash/shell/exec HARD-BLOCKED. "standard": Bash enabled; no tool-level scope restriction (operator/container responsible for scoping). "full": unsandboxed — requires force: true (admin confirmation). Ignored for other agents.'),
132
+ },
133
+ handler: async (args) => {
134
+ const { workDir, name, initialMessage } = args;
135
+ const isConductor = args.conductor === true;
136
+ const agent = args.agent || ownAgentType;
137
+ const model = args.model;
138
+ const agentTypeName = args.type;
139
+ const systemPrompt = args.systemPrompt;
140
+ const host = args.host;
141
+ const force = args.force === true;
142
+ const mockMode = args.mockMode;
143
+ const mockScenario = args.mockScenario;
144
+ const permissionMode = args.permissionMode;
145
+ const dangerouslySkipPermissions = args.dangerouslySkipPermissions === true;
146
+ // N1 (adapted): normalize to a concrete value ONCE here so the guard below
147
+ // and the outbox entry both see a real value with no scattered `?? 'restricted'`.
148
+ // (A schema `.default()` would be cleaner but the Pi converter rejects it —
149
+ // see the toolAccess param note above.) Omitted → 'restricted'.
150
+ const toolAccess = (args.toolAccess ?? 'restricted');
151
+ // ADR 0014 §7 gate 3 recruit-time rejection of `agent: 'mock'`
152
+ // outside dev mode. Defense-in-depth: even if a hand-edited install
153
+ // had `dist/adapters/mock/` present (gate 1 bypassed) AND somehow
154
+ // got the registry to register the descriptor (gate 2 bypassed),
155
+ // this rejects the request with a clear, actionable error.
156
+ if (agent === 'mock' && !(0, config_1.isDevMode)()) {
157
+ return (0, descriptor_1.fail)(`agent: "mock" is only available in dev mode. Restart agent-tempo with --dev (or set AGENT_TEMPO_DEV_MODE=1) to enable.`);
179
158
  }
180
- try {
181
- require.resolve('@anthropic-ai/sdk');
159
+ // mockMode / mockScenario are only meaningful with the mock adapter —
160
+ // reject silently-ignored params so users learn the right flag shape.
161
+ if (mockMode != null && agent !== 'mock') {
162
+ return (0, descriptor_1.fail)(`mockMode is only valid when agent: "mock" (got agent: "${agent}").`);
182
163
  }
183
- catch {
184
- return (0, helpers_1.fail)(`agent: "claude-api" requires the @anthropic-ai/sdk optional dependency. Install with \`npm install @anthropic-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
164
+ if (mockScenario != null && agent !== 'mock') {
165
+ return (0, descriptor_1.fail)(`mockScenario is only valid when agent: "mock" (got agent: "${agent}").`);
185
166
  }
186
- }
187
- // #449 Phase C opencode pre-flight. Two checks: the optional SDK
188
- // (signal that opencode integration is intended on this host) AND
189
- // the `opencode` binary on PATH (the adapter spawns `opencode serve`
190
- // as a subprocess). Cross-host recruits skip both — the target
191
- // daemon's `availableAgentTypes` is the gate there.
192
- if (agent === 'opencode' && !host && !force) {
193
- if (!(0, sdk_probe_1.probeSdkInstall)('@opencode-ai/sdk')) {
194
- return (0, helpers_1.fail)(`agent: "opencode" requires the @opencode-ai/sdk optional dependency. Install with \`npm install @opencode-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
167
+ if (agent === 'mock' && mockMode === 'scripted' && !mockScenario) {
168
+ return (0, descriptor_1.fail)(`mockMode: "scripted" requires mockScenario (a bare scenario name or path to a YAML file).`);
195
169
  }
196
- if (!hasOpencodeOnPath()) {
197
- return (0, helpers_1.fail)(`agent: "opencode" requires the \`opencode\` binary on PATH. Install with \`npm install -g opencode-ai\` and retry, or use \`force: true\` to bypass this check.`);
170
+ // PR-3: silent + chaos modes don't consult the scenario file. Reject
171
+ // explicitly rather than silently ignore so users learn the right
172
+ // shape and don't sit wondering why their scenario wasn't applied.
173
+ if (mockScenario && (mockMode === 'silent' || mockMode === 'chaos')) {
174
+ return (0, descriptor_1.fail)(`mockMode: "${mockMode}" does not use a scenario. Drop mockScenario or switch to mockMode: "scripted".`);
198
175
  }
199
- }
200
- // #520claude-code-headless pre-flight. Two checks, both bounded by
201
- // short timeouts (3s + 5s). The auth probe uses the official `claude
202
- // auth status` subcommand no billed API call. Cross-host recruits
203
- // skip both the target daemon's `availableAgentTypes` is the gate
204
- // there (PR-2 wires the daemon-side probe).
205
- if (agent === 'claude-code-headless' && !host && !force) {
206
- const claudeBin = config.claudeBin ?? 'claude';
207
- const binProbe = (0, pre_flight_1.probeClaudeBinary)(claudeBin);
208
- if (!binProbe.ok) {
209
- return (0, helpers_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${binProbe.error} Use \`force: true\` to bypass.`);
176
+ // #131 / #449 Phase C — model knob is meaningful for claude-api AND
177
+ // opencode (different shapes bare vs `provider/model` both flow
178
+ // through the same recruit field). Reject silently-ignored params for
179
+ // the other adapters so users learn the right shape.
180
+ // Local-spawn pre-flight checks (env vars + SDK install + binaries)
181
+ // run only when `host` is unset; cross-host recruits delegate to the
182
+ // target daemon's `availableAgentTypes` advertisement (the existing
183
+ // `checkHostPreflight` path), which already gates on whether the
184
+ // remote daemon resolved the SDK at boot.
185
+ if (model != null && agent !== 'claude-api' && agent !== 'opencode') {
186
+ return (0, descriptor_1.fail)(`model is only valid when agent: "claude-api" or agent: "opencode" (got agent: "${agent}").`);
210
187
  }
211
- const authProbe = (0, pre_flight_1.probeClaudeAuth)(claudeBin);
212
- if (!authProbe.loggedIn) {
213
- return (0, helpers_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${authProbe.error} Use \`force: true\` to bypass.`);
188
+ // #520 claude-code-headless permission knobs are mutually exclusive
189
+ // and only meaningful for that adapter. Reject silently-ignored
190
+ // params so users learn the right shape.
191
+ if (permissionMode != null && agent !== 'claude-code-headless') {
192
+ return (0, descriptor_1.fail)(`permissionMode is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
214
193
  }
215
- }
216
- // #532 copilot pre-flight. SDK-only probe (mirrors claude-api's
217
- // pattern; copilot has no subprocess CLI on PATH). The bridge
218
- // subprocess hard-requires `@github/copilot-sdk` at module load
219
- // (`src/adapters/copilot/adapter.ts:71`), so without this gate the
220
- // user only learns of the missing dep AFTER bridge spawn —
221
- // adapter crashes with `process.exit(1)` and the player sits in
222
- // `booting` until lease timeout. We use `probeSdkInstall` (FS walk)
223
- // rather than `require.resolve` because pnpm layouts without a
224
- // top-level hoisted link otherwise false-negative; see issue #532
225
- // investigation footnote. GITHUB_TOKEN / Copilot CLI login are
226
- // intentionally NOT checked: the SDK falls through to the
227
- // logged-in user (`adapter.ts:31, :263`), so token presence is not
228
- // a hard requirement. Cross-host recruits skip — the target
229
- // daemon's `availableAgentTypes` is the gate there.
230
- if (agent === 'copilot' && !host && !force) {
231
- if (!(0, sdk_probe_1.probeSdkInstall)('@github/copilot-sdk')) {
232
- return (0, helpers_1.fail)(`agent: "copilot" requires the @github/copilot-sdk optional dependency. Install with \`npm install @github/copilot-sdk\` and retry, or use \`force: true\` to bypass this check.`);
194
+ if (dangerouslySkipPermissions && agent !== 'claude-code-headless') {
195
+ return (0, descriptor_1.fail)(`dangerouslySkipPermissions is only valid when agent: "claude-code-headless" (got agent: "${agent}").`);
233
196
  }
234
- }
235
- // Resolve agent type if provided
236
- let agentDefinition;
237
- let agentDefinitionPath;
238
- let agentDefinitionDescription;
239
- let nativeResolvable;
240
- let allowedTools;
241
- if (agentTypeName) {
242
- const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
243
- if (!info) {
244
- const available = (0, agent_types_1.listAgentTypes)().map(t => t.name);
245
- return (0, helpers_1.fail)(`Unknown agent type "${agentTypeName}". Available types: ${available.length ? available.join(', ') : '(none)'}`);
197
+ // Reject an EXPLICIT non-default toolAccess on a non-pi agent. With Zod
198
+ // `.default('restricted')` the omitted case is indistinguishable from an
199
+ // explicit `'restricted'`, so we only flag a non-default value — that is
200
+ // unambiguously a user mistake (toolAccess is ignored for non-pi agents).
201
+ if (toolAccess !== 'restricted' && agent !== 'pi') {
202
+ return (0, descriptor_1.fail)(`toolAccess is only valid when agent: "pi" (got agent: "${agent}").`);
246
203
  }
247
- agentDefinition = info.name;
248
- agentDefinitionPath = info.path;
249
- agentDefinitionDescription = info.description;
250
- nativeResolvable = info.nativeResolvable;
251
- allowedTools = info.allowedTools;
252
- }
253
- // Validate name
254
- const nameError = (0, validation_1.validatePlayerName)(name);
255
- if (nameError) {
256
- return (0, helpers_1.fail)(nameError);
257
- }
258
- if (name === 'conductor' && !isConductor) {
259
- return (0, helpers_1.fail)(`The name "conductor" is reserved for conductor sessions. Use a different name, or set conductor: true.`);
260
- }
261
- // ── #274 AC12 — cross-host recruit pre-flight ──
262
- //
263
- // When `host` is specified, verify that the target is live + has
264
- // the requested agent runtime available BEFORE submitting to the
265
- // outbox. Otherwise `recruit --host foo` could queue forever on a
266
- // per-host task queue nobody's listening on (the exact failure
267
- // mode #274 set out to close).
268
- //
269
- // `force: true` bypasses pre-flight entirely (AC12d) — covers the
270
- // scripted-recruit-on-about-to-boot-daemon case plus any other
271
- // "I know what I'm doing" override. RPC failure during pre-flight
272
- // falls through with a warning (AC12e) so today's recruit
273
- // availability characteristics are preserved.
274
- if (host && !force) {
275
- try {
276
- const hosts = await listHostsFn(client);
277
- const preflightError = checkHostPreflight(hosts, host, agent);
278
- if (preflightError)
279
- return (0, helpers_1.fail)(preflightError);
204
+ if (toolAccess === 'full' && !force) {
205
+ return (0, descriptor_1.fail)(`toolAccess: "full" (unsandboxed Pi) requires force: true (admin confirmation). "restricted" (default) and "standard" do not.`);
280
206
  }
281
- catch (err) {
282
- toolLog(`Host pre-flight RPC failed for "${host}"; proceeding without validation (use --force to silence this warning): ${err instanceof Error ? err.message : err}`);
207
+ if (permissionMode != null && dangerouslySkipPermissions) {
208
+ return (0, descriptor_1.fail)(`permissionMode and dangerouslySkipPermissions are mutually exclusive pass at most one.`);
283
209
  }
284
- }
285
- try {
286
- // Check if a conductor already exists when recruiting a conductor
287
- if (isConductor) {
210
+ if (agent === 'claude-api' && !host && !force) {
211
+ if (!process.env.ANTHROPIC_API_KEY) {
212
+ return (0, descriptor_1.fail)(`agent: "claude-api" requires the ANTHROPIC_API_KEY environment variable on the spawn host. Set it before recruiting (export ANTHROPIC_API_KEY=sk-...) or use \`force: true\` to bypass this check.`);
213
+ }
288
214
  try {
289
- const conductorWfId = (0, config_1.conductorWorkflowId)(config.ensemble);
290
- const conductorHandle = client.workflow.getHandle(conductorWfId);
291
- const desc = await conductorHandle.describe();
292
- if (desc.status.name === 'RUNNING') {
293
- if (force) {
294
- await conductorHandle.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
295
- }
296
- else {
297
- return (0, helpers_1.fail)(`A conductor is already running in ensemble "${config.ensemble}". Use \`agent-tempo conduct --replace\` from the CLI to replace it, \`stop\` it first, or use \`force: true\` to replace it.`);
298
- }
299
- }
215
+ require.resolve('@anthropic-ai/sdk');
300
216
  }
301
217
  catch {
302
- // No existing conductor proceed
218
+ return (0, descriptor_1.fail)(`agent: "claude-api" requires the @anthropic-ai/sdk optional dependency. Install with \`npm install @anthropic-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
219
+ }
220
+ }
221
+ // #449 Phase C — opencode pre-flight. Two checks: the optional SDK
222
+ // (signal that opencode integration is intended on this host) AND
223
+ // the `opencode` binary on PATH (the adapter spawns `opencode serve`
224
+ // as a subprocess). Cross-host recruits skip both — the target
225
+ // daemon's `availableAgentTypes` is the gate there.
226
+ if (agent === 'opencode' && !host && !force) {
227
+ if (!(0, sdk_probe_1.probeSdkInstall)('@opencode-ai/sdk')) {
228
+ return (0, descriptor_1.fail)(`agent: "opencode" requires the @opencode-ai/sdk optional dependency. Install with \`npm install @opencode-ai/sdk\` and retry, or use \`force: true\` to bypass this check.`);
229
+ }
230
+ if (!hasOpencodeOnPath()) {
231
+ return (0, descriptor_1.fail)(`agent: "opencode" requires the \`opencode\` binary on PATH. Install with \`npm install -g opencode-ai\` and retry, or use \`force: true\` to bypass this check.`);
232
+ }
233
+ }
234
+ // #520 — claude-code-headless pre-flight. Two checks, both bounded by
235
+ // short timeouts (3s + 5s). The auth probe uses the official `claude
236
+ // auth status` subcommand — no billed API call. Cross-host recruits
237
+ // skip both — the target daemon's `availableAgentTypes` is the gate
238
+ // there (PR-2 wires the daemon-side probe).
239
+ if (agent === 'claude-code-headless' && !host && !force) {
240
+ const claudeBin = config.claudeBin ?? 'claude';
241
+ const binProbe = (0, pre_flight_1.probeClaudeBinary)(claudeBin);
242
+ if (!binProbe.ok) {
243
+ return (0, descriptor_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${binProbe.error} Use \`force: true\` to bypass.`);
244
+ }
245
+ const authProbe = (0, pre_flight_1.probeClaudeAuth)(claudeBin);
246
+ if (!authProbe.loggedIn) {
247
+ return (0, descriptor_1.fail)(`agent: "claude-code-headless" pre-flight failed: ${authProbe.error} Use \`force: true\` to bypass.`);
248
+ }
249
+ }
250
+ // #532 — copilot pre-flight. SDK-only probe (mirrors claude-api's
251
+ // pattern; copilot has no subprocess CLI on PATH). The bridge
252
+ // subprocess hard-requires `@github/copilot-sdk` at module load
253
+ // (`src/adapters/copilot/adapter.ts:71`), so without this gate the
254
+ // user only learns of the missing dep AFTER bridge spawn —
255
+ // adapter crashes with `process.exit(1)` and the player sits in
256
+ // `booting` until lease timeout. We use `probeSdkInstall` (FS walk)
257
+ // rather than `require.resolve` because pnpm layouts without a
258
+ // top-level hoisted link otherwise false-negative; see issue #532
259
+ // investigation footnote. GITHUB_TOKEN / Copilot CLI login are
260
+ // intentionally NOT checked: the SDK falls through to the
261
+ // logged-in user (`adapter.ts:31, :263`), so token presence is not
262
+ // a hard requirement. Cross-host recruits skip — the target
263
+ // daemon's `availableAgentTypes` is the gate there.
264
+ if (agent === 'copilot' && !host && !force) {
265
+ if (!(0, sdk_probe_1.probeSdkInstall)('@github/copilot-sdk')) {
266
+ return (0, descriptor_1.fail)(`agent: "copilot" requires the @github/copilot-sdk optional dependency. Install with \`npm install @github/copilot-sdk\` and retry, or use \`force: true\` to bypass this check.`);
267
+ }
268
+ }
269
+ // Phase 3a — headless Pi pre-flight. The Pi SDK is an optional Node-22.19+
270
+ // dep required at the headless entry; gate at recruit (cross-host skips —
271
+ // the target daemon's availableAgentTypes is the gate there).
272
+ if (agent === 'pi' && !host && !force) {
273
+ // Validate the model selector format up front (provider/model).
274
+ let parsedModel;
275
+ if (model) {
276
+ const r = (0, config_1.parsePiProviderModel)(model);
277
+ if ('error' in r)
278
+ return (0, descriptor_1.fail)(`agent: "pi" — ${r.error} Or use \`force: true\` to bypass.`);
279
+ parsedModel = r;
280
+ }
281
+ if (parsedModel?.provider === 'github-copilot') {
282
+ // Copilot-via-Pi: full pre-flight (deps + version floor + Copilot auth +
283
+ // model-index Gate 4 via pi-ai's sessionless getModel, injected here).
284
+ let resolveModel;
285
+ try {
286
+ const piAi = await esmImport(probe_1.PI_AI_PACKAGE);
287
+ resolveModel = piAi.getModel;
288
+ }
289
+ catch { /* dep-present gate inside the preflight fails first if pi-ai is unimportable */ }
290
+ const pf = (0, probe_1.probeCopilotPiPreflight)({ requestedModel: parsedModel, resolveModel });
291
+ if (!pf.available)
292
+ return (0, descriptor_1.fail)(`agent: "pi" (Copilot) pre-flight failed: ${pf.reason}`);
293
+ }
294
+ else if (!(0, sdk_probe_1.probeSdkInstall)(probe_1.PI_PACKAGE)) {
295
+ // Anthropic / Pi-default path: just require the Pi SDK installed.
296
+ return (0, descriptor_1.fail)(`agent: "pi" requires the ${probe_1.PI_PACKAGE} optional dependency (Node >= 22.19). Install with \`npm install -g ${probe_1.PI_PACKAGE}\` and retry, or use \`force: true\` to bypass.`);
297
+ }
298
+ }
299
+ // Resolve agent type if provided
300
+ let agentDefinition;
301
+ let agentDefinitionPath;
302
+ let agentDefinitionDescription;
303
+ let nativeResolvable;
304
+ let allowedTools;
305
+ if (agentTypeName) {
306
+ const info = (0, agent_types_1.resolveAgentType)(agentTypeName);
307
+ if (!info) {
308
+ const available = (0, agent_types_1.listAgentTypes)().map(t => t.name);
309
+ return (0, descriptor_1.fail)(`Unknown agent type "${agentTypeName}". Available types: ${available.length ? available.join(', ') : '(none)'}`);
310
+ }
311
+ agentDefinition = info.name;
312
+ agentDefinitionPath = info.path;
313
+ agentDefinitionDescription = info.description;
314
+ nativeResolvable = info.nativeResolvable;
315
+ allowedTools = info.allowedTools;
316
+ }
317
+ // Validate name
318
+ const nameError = (0, validation_1.validatePlayerName)(name);
319
+ if (nameError) {
320
+ return (0, descriptor_1.fail)(nameError);
321
+ }
322
+ if (name === 'conductor' && !isConductor) {
323
+ return (0, descriptor_1.fail)(`The name "conductor" is reserved for conductor sessions. Use a different name, or set conductor: true.`);
324
+ }
325
+ // ── #274 AC12 — cross-host recruit pre-flight ──
326
+ //
327
+ // When `host` is specified, verify that the target is live + has
328
+ // the requested agent runtime available BEFORE submitting to the
329
+ // outbox. Otherwise `recruit --host foo` could queue forever on a
330
+ // per-host task queue nobody's listening on (the exact failure
331
+ // mode #274 set out to close).
332
+ //
333
+ // `force: true` bypasses pre-flight entirely (AC12d) — covers the
334
+ // scripted-recruit-on-about-to-boot-daemon case plus any other
335
+ // "I know what I'm doing" override. RPC failure during pre-flight
336
+ // falls through with a warning (AC12e) so today's recruit
337
+ // availability characteristics are preserved.
338
+ if (host && !force) {
339
+ try {
340
+ const hosts = await listHostsFn(client);
341
+ const preflightError = checkHostPreflight(hosts, host, agent);
342
+ if (preflightError)
343
+ return (0, descriptor_1.fail)(preflightError);
344
+ }
345
+ catch (err) {
346
+ toolLog(`Host pre-flight RPC failed for "${host}"; proceeding without validation (use --force to silence this warning): ${err instanceof Error ? err.message : err}`);
303
347
  }
304
348
  }
305
- // Check if a session with this name is already active
306
- const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, name);
307
- if (existing) {
308
- if (force) {
309
- // Force-terminate the existing session before recruiting
310
- await existing.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
311
- // Best-effort notify conductor
349
+ try {
350
+ // Check if a conductor already exists when recruiting a conductor
351
+ if (isConductor) {
312
352
  try {
313
- const condId = (0, config_1.conductorWorkflowId)(config.ensemble);
314
- const condHandle = client.workflow.getHandle(condId);
315
- await condHandle.signal('receiveMessage', {
316
- from: 'system',
317
- text: `Session "${name}" was force-terminated for re-recruit by ${getPlayerId()}.`,
318
- responseRequested: false,
319
- });
353
+ const conductorWfId = (0, config_1.conductorWorkflowId)(config.ensemble);
354
+ const conductorHandle = client.workflow.getHandle(conductorWfId);
355
+ const desc = await conductorHandle.describe();
356
+ if (desc.status.name === 'RUNNING') {
357
+ if (force) {
358
+ await conductorHandle.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
359
+ }
360
+ else {
361
+ return (0, descriptor_1.fail)(`A conductor is already running in ensemble "${config.ensemble}". Use \`agent-tempo conduct --replace\` from the CLI to replace it, \`stop\` it first, or use \`force: true\` to replace it.`);
362
+ }
363
+ }
320
364
  }
321
365
  catch {
322
- // Conductor may not exist that's fine
366
+ // No existing conductorproceed
323
367
  }
324
368
  }
325
- else {
326
- return (0, helpers_1.fail)(`Session **${name}** is already active. Use \`cue\` to send it a message, \`stop\` it first, or use \`force: true\` to replace it.`);
369
+ // Check if a session with this name is already active
370
+ const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, name);
371
+ if (existing) {
372
+ if (force) {
373
+ // Force-terminate the existing session before recruiting
374
+ await existing.terminate(`Force-terminated for re-recruit by ${getPlayerId()}`);
375
+ // Best-effort notify conductor
376
+ try {
377
+ const condId = (0, config_1.conductorWorkflowId)(config.ensemble);
378
+ const condHandle = client.workflow.getHandle(condId);
379
+ await condHandle.signal('receiveMessage', {
380
+ from: 'system',
381
+ text: `Session "${name}" was force-terminated for re-recruit by ${getPlayerId()}.`,
382
+ responseRequested: false,
383
+ });
384
+ }
385
+ catch {
386
+ // Conductor may not exist — that's fine
387
+ }
388
+ }
389
+ else {
390
+ return (0, descriptor_1.fail)(`Session **${name}** is already active. Use \`cue\` to send it a message, \`stop\` it first, or use \`force: true\` to replace it.`);
391
+ }
327
392
  }
393
+ const entry = {
394
+ type: 'recruit',
395
+ targetName: name,
396
+ workDir,
397
+ isConductor,
398
+ initialMessage,
399
+ agent,
400
+ systemPrompt: agentDefinition ? undefined : systemPrompt,
401
+ targetHostname: host,
402
+ agentDefinition,
403
+ agentDefinitionPath,
404
+ agentDefinitionDescription,
405
+ nativeResolvable,
406
+ allowedTools,
407
+ claudeBin: config.claudeBin,
408
+ ...(agent === 'mock' ? { mockMode: mockMode ?? 'echo' } : {}),
409
+ ...(agent === 'mock' && mockScenario ? { mockScenario } : {}),
410
+ ...(agent === 'claude-api' && model ? { model } : {}),
411
+ // #520 — claude-code-headless permission knobs flow through the
412
+ // outbox so PR-2's spawn helper picks them up via env. Fields
413
+ // are only present on the entry when actually set; the
414
+ // OutboxEntryInput shape will gain typed `permissionMode` /
415
+ // `dangerouslySkipPermissions` fields in PR-2.
416
+ ...(agent === 'claude-code-headless' && permissionMode ? { permissionMode } : {}),
417
+ ...(agent === 'claude-code-headless' && dangerouslySkipPermissions ? { dangerouslySkipPermissions: true } : {}),
418
+ // Phase 3a / MD-C — explicit toolAccess on the entry. Normalized at the
419
+ // read site (above) to a concrete value, so the spawned gate + audit
420
+ // always see one without a fallback here.
421
+ ...(agent === 'pi' ? { toolAccess } : {}),
422
+ };
423
+ const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
424
+ return (0, descriptor_1.ok)(`Recruit request submitted for **${name}** in ${workDir}. The session will be spawned shortly. (outbox: ${entryId})`);
328
425
  }
329
- const entry = {
330
- type: 'recruit',
331
- targetName: name,
332
- workDir,
333
- isConductor,
334
- initialMessage,
335
- agent,
336
- systemPrompt: agentDefinition ? undefined : systemPrompt,
337
- targetHostname: host,
338
- agentDefinition,
339
- agentDefinitionPath,
340
- agentDefinitionDescription,
341
- nativeResolvable,
342
- allowedTools,
343
- claudeBin: config.claudeBin,
344
- ...(agent === 'mock' ? { mockMode: mockMode ?? 'echo' } : {}),
345
- ...(agent === 'mock' && mockScenario ? { mockScenario } : {}),
346
- ...(agent === 'claude-api' && model ? { model } : {}),
347
- // #520 — claude-code-headless permission knobs flow through the
348
- // outbox so PR-2's spawn helper picks them up via env. Fields
349
- // are only present on the entry when actually set; the
350
- // OutboxEntryInput shape will gain typed `permissionMode` /
351
- // `dangerouslySkipPermissions` fields in PR-2.
352
- ...(agent === 'claude-code-headless' && permissionMode ? { permissionMode } : {}),
353
- ...(agent === 'claude-code-headless' && dangerouslySkipPermissions ? { dangerouslySkipPermissions: true } : {}),
354
- };
355
- const entryId = await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
356
- return (0, helpers_1.ok)(`Recruit request submitted for **${name}** in ${workDir}. The session will be spawned shortly. (outbox: ${entryId})`);
357
- }
358
- catch (err) {
359
- return (0, helpers_1.fail)(`Failed to recruit: ${(0, helpers_1.formatError)(err)}`);
360
- }
361
- });
426
+ catch (err) {
427
+ return (0, descriptor_1.fail)(`Failed to recruit: ${(0, descriptor_1.formatError)(err)}`);
428
+ }
429
+ },
430
+ };
362
431
  }
363
432
  // ────────────────────────────────────────────────────────────────────────
364
433
  // #274 AC12 — host pre-flight validation (exported for unit tests)