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
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerLoadLineupTool = registerLoadLineupTool;
3
+ exports.buildLoadLineupTool = buildLoadLineupTool;
4
4
  const zod_1 = require("zod");
5
5
  const croner_1 = require("croner");
6
6
  const client_1 = require("@temporalio/client");
@@ -12,370 +12,375 @@ const resolve_2 = require("../activities/resolve");
12
12
  const signals_1 = require("../workflows/signals");
13
13
  const duration_1 = require("../utils/duration");
14
14
  const safe_path_1 = require("../utils/safe-path");
15
- const helpers_1 = require("./helpers");
15
+ const descriptor_1 = require("./descriptor");
16
16
  const validation_1 = require("../utils/validation");
17
17
  const constants_1 = require("../constants");
18
18
  const log = (...args) => console.error('[agent-tempo:load-lineup]', ...args);
19
- function registerLoadLineupTool(server, client, config, getPlayerId, ownAgentType = 'claude', handle, setPlayerId, isConductor) {
20
- (0, helpers_1.defineTool)(server, 'load_lineup', 'Load an ensemble lineup — recruits players and creates schedules.', {
21
- name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Name of a lineup — resolves saved lineups, then shipped examples (e.g. "tempo-dev-team")'),
22
- path: zod_1.z.string().max(validation_1.PATH_MAX).optional().describe('Explicit file path to a lineup YAML file'),
23
- hold: zod_1.z.boolean().optional().describe('When true, spawn players in "warm hold": processes start and attach but their outbox is locked, so they receive a standby message and defer their initial task until `release` is called.'),
24
- initialStartup: zod_1.z.boolean().optional().describe('Issue #172: when true, the lineup was loaded as part of initial ensemble startup (`up --lineup` / `conduct --lineup`). Conductor instructions are stored as pending context and combined with the user\'s first message instead of firing immediately. Also recruits players with `hold: true`. Defaults to false — conductor-invoked mid-work `load_lineup` keeps the legacy behavior.'),
25
- }, async (args) => {
26
- const lineupName = args.name;
27
- const lineupPath = args.path;
28
- const initialStartup = args.initialStartup === true;
29
- // Initial-startup always implies warm-hold — players should wait for
30
- // the conductor to decompose the user's first message before starting.
31
- const hold = args.hold === true || initialStartup;
32
- if (!lineupName && !lineupPath) {
33
- return (0, helpers_1.fail)('Provide either `name` (saved lineup) or `path` (file path). Exactly one is required.');
34
- }
35
- if (lineupName && lineupPath) {
36
- return (0, helpers_1.fail)('Provide either `name` or `path`, not both.');
37
- }
38
- try {
39
- // Resolve the file path: saved → shipped examples → direct file path
40
- let filePath;
41
- if (lineupPath) {
42
- // User-provided path — validate against allowed roots
43
- filePath = (0, safe_path_1.safeLineupPath)(lineupPath, process.cwd());
19
+ function buildLoadLineupTool(client, config, getPlayerId, ownAgentType = 'claude', handle, setPlayerId, isConductor) {
20
+ return {
21
+ name: 'load_lineup',
22
+ description: 'Load an ensemble lineup recruits players and creates schedules.',
23
+ params: {
24
+ name: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('Name of a lineup resolves saved lineups, then shipped examples (e.g. "tempo-dev-team")'),
25
+ path: zod_1.z.string().max(validation_1.PATH_MAX).optional().describe('Explicit file path to a lineup YAML file'),
26
+ hold: zod_1.z.boolean().optional().describe('When true, spawn players in "warm hold": processes start and attach but their outbox is locked, so they receive a standby message and defer their initial task until `release` is called.'),
27
+ initialStartup: zod_1.z.boolean().optional().describe('Issue #172: when true, the lineup was loaded as part of initial ensemble startup (`up --lineup` / `conduct --lineup`). Conductor instructions are stored as pending context and combined with the user\'s first message instead of firing immediately. Also recruits players with `hold: true`. Defaults to false — conductor-invoked mid-work `load_lineup` keeps the legacy behavior.'),
28
+ },
29
+ handler: async (args) => {
30
+ const lineupName = args.name;
31
+ const lineupPath = args.path;
32
+ const initialStartup = args.initialStartup === true;
33
+ // Initial-startup always implies warm-hold players should wait for
34
+ // the conductor to decompose the user's first message before starting.
35
+ const hold = args.hold === true || initialStartup;
36
+ if (!lineupName && !lineupPath) {
37
+ return (0, descriptor_1.fail)('Provide either `name` (saved lineup) or `path` (file path). Exactly one is required.');
44
38
  }
45
- else {
46
- const resolution = (0, loader_1.resolveLineupPath)(lineupName);
47
- filePath = resolution.path;
48
- // Only validate user-facing paths (saved lineups, file paths).
49
- // Shipped examples are package-controlled and may live outside
50
- // allowed roots when globally installed.
51
- if (resolution.source !== 'shipped') {
52
- filePath = (0, safe_path_1.safeLineupPath)(filePath, process.cwd());
53
- }
39
+ if (lineupName && lineupPath) {
40
+ return (0, descriptor_1.fail)('Provide either `name` or `path`, not both.');
54
41
  }
55
- const lineup = (0, agent_types_1.loadAndResolveLineup)(filePath);
56
- const recruited = [];
57
- const failed = [];
58
- const conductorActions = [];
59
- // Issue #172 follow-up: on `initialStartup`, fire the `from: 'system'`
60
- // directive BEFORE any lineup instructions. Earlier messages weigh
61
- // more heavily with the LLM — putting the "call resume_ensemble +
62
- // release FIRST" framing at the top of the conductor's inbox reduces
63
- // the chance the model skims past it and goes straight to broadcast.
64
- // Runs independently of whether the lineup has a `conductor:` section
65
- // (e.g. players-only lineups still need the banner + directive).
66
- if (initialStartup && isConductor && handle) {
67
- try {
68
- const playerCount = lineup.players.length;
69
- await handle.signal('receiveMessage', {
70
- from: 'system',
71
- text: (0, constants_1.ensembleReadyDirective)(lineup.name, playerCount),
72
- responseRequested: false,
73
- });
74
- conductorActions.push('startup banner + directive delivered');
42
+ try {
43
+ // Resolve the file path: saved → shipped examples → direct file path
44
+ let filePath;
45
+ if (lineupPath) {
46
+ // User-provided path validate against allowed roots
47
+ filePath = (0, safe_path_1.safeLineupPath)(lineupPath, process.cwd());
75
48
  }
76
- catch (err) {
77
- failed.push(`conductor startup banner: ${err}`);
49
+ else {
50
+ const resolution = (0, loader_1.resolveLineupPath)(lineupName);
51
+ filePath = resolution.path;
52
+ // Only validate user-facing paths (saved lineups, file paths).
53
+ // Shipped examples are package-controlled and may live outside
54
+ // allowed roots when globally installed.
55
+ if (resolution.source !== 'shipped') {
56
+ filePath = (0, safe_path_1.safeLineupPath)(filePath, process.cwd());
57
+ }
78
58
  }
79
- }
80
- // Apply conductor section if present and this session is the conductor
81
- if (lineup.conductor && isConductor && handle) {
82
- // Apply conductor name
83
- if (lineup.conductor.name && lineup.conductor.name !== getPlayerId()) {
59
+ const lineup = (0, agent_types_1.loadAndResolveLineup)(filePath);
60
+ const recruited = [];
61
+ const failed = [];
62
+ const conductorActions = [];
63
+ // Issue #172 follow-up: on `initialStartup`, fire the `from: 'system'`
64
+ // directive BEFORE any lineup instructions. Earlier messages weigh
65
+ // more heavily with the LLM — putting the "call resume_ensemble +
66
+ // release FIRST" framing at the top of the conductor's inbox reduces
67
+ // the chance the model skims past it and goes straight to broadcast.
68
+ // Runs independently of whether the lineup has a `conductor:` section
69
+ // (e.g. players-only lineups still need the banner + directive).
70
+ if (initialStartup && isConductor && handle) {
84
71
  try {
85
- // Check if the name is already taken
86
- const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, lineup.conductor.name);
87
- if (existing && existing.workflowId !== handle.workflowId) {
88
- failed.push(`conductor name "${lineup.conductor.name}": already taken by another session`);
89
- }
90
- else {
91
- await handle.signal('setName', lineup.conductor.name);
92
- if (setPlayerId)
93
- setPlayerId(lineup.conductor.name);
94
- conductorActions.push(`name → ${lineup.conductor.name}`);
95
- log(`Conductor name set to "${lineup.conductor.name}"`);
96
- }
72
+ const playerCount = lineup.players.length;
73
+ await handle.signal('receiveMessage', {
74
+ from: 'system',
75
+ text: (0, constants_1.ensembleReadyDirective)(lineup.name, playerCount),
76
+ responseRequested: false,
77
+ });
78
+ conductorActions.push('startup banner + directive delivered');
97
79
  }
98
80
  catch (err) {
99
- failed.push(`conductor name: ${err}`);
81
+ failed.push(`conductor startup banner: ${err}`);
100
82
  }
101
83
  }
102
- // Apply conductor type (update metadata)
103
- if (lineup.conductor.type) {
104
- try {
105
- const typeInfo = (0, agent_types_1.resolveAgentType)(lineup.conductor.type);
106
- if (typeInfo) {
107
- await handle.signal('updateMetadata', {
108
- playerType: typeInfo.name,
109
- playerTypeDescription: typeInfo.description || '',
110
- });
111
- conductorActions.push(`type → ${typeInfo.name}`);
112
- log(`Conductor type set to "${typeInfo.name}"`);
84
+ // Apply conductor section if present and this session is the conductor
85
+ if (lineup.conductor && isConductor && handle) {
86
+ // Apply conductor name
87
+ if (lineup.conductor.name && lineup.conductor.name !== getPlayerId()) {
88
+ try {
89
+ // Check if the name is already taken
90
+ const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, lineup.conductor.name);
91
+ if (existing && existing.workflowId !== handle.workflowId) {
92
+ failed.push(`conductor name "${lineup.conductor.name}": already taken by another session`);
93
+ }
94
+ else {
95
+ await handle.signal('setName', lineup.conductor.name);
96
+ if (setPlayerId)
97
+ setPlayerId(lineup.conductor.name);
98
+ conductorActions.push(`name → ${lineup.conductor.name}`);
99
+ log(`Conductor name set to "${lineup.conductor.name}"`);
100
+ }
113
101
  }
114
- else {
115
- failed.push(`conductor type "${lineup.conductor.type}": agent type not found`);
102
+ catch (err) {
103
+ failed.push(`conductor name: ${err}`);
116
104
  }
117
105
  }
118
- catch (err) {
119
- failed.push(`conductor type: ${err}`);
106
+ // Apply conductor type (update metadata)
107
+ if (lineup.conductor.type) {
108
+ try {
109
+ const typeInfo = (0, agent_types_1.resolveAgentType)(lineup.conductor.type);
110
+ if (typeInfo) {
111
+ await handle.signal('updateMetadata', {
112
+ playerType: typeInfo.name,
113
+ playerTypeDescription: typeInfo.description || '',
114
+ });
115
+ conductorActions.push(`type → ${typeInfo.name}`);
116
+ log(`Conductor type set to "${typeInfo.name}"`);
117
+ }
118
+ else {
119
+ failed.push(`conductor type "${lineup.conductor.type}": agent type not found`);
120
+ }
121
+ }
122
+ catch (err) {
123
+ failed.push(`conductor type: ${err}`);
124
+ }
125
+ }
126
+ // Send conductor instructions.
127
+ // Issue #172 (v0.26 simplification): on the initial-startup path,
128
+ // signal the lineup instructions as a `receiveMessage` immediately
129
+ // — the ensemble-wide pause (below) stops any downstream dispatch,
130
+ // and the banner+directive signal (also below) tells the LLM to
131
+ // wait silently until the user speaks and then call
132
+ // `resume_ensemble` first. Legacy mid-work path also signals
133
+ // immediately — no branching required.
134
+ if (lineup.conductor.instructions) {
135
+ try {
136
+ await handle.signal('receiveMessage', {
137
+ from: 'lineup',
138
+ text: lineup.conductor.instructions,
139
+ responseRequested: false,
140
+ });
141
+ conductorActions.push(initialStartup ? 'instructions seeded (initial startup)' : 'instructions delivered');
142
+ log('Conductor instructions delivered');
143
+ }
144
+ catch (err) {
145
+ failed.push(`conductor instructions: ${err}`);
146
+ }
120
147
  }
121
148
  }
122
- // Send conductor instructions.
123
- // Issue #172 (v0.26 simplification): on the initial-startup path,
124
- // signal the lineup instructions as a `receiveMessage` immediately
125
- // the ensemble-wide pause (below) stops any downstream dispatch,
126
- // and the banner+directive signal (also below) tells the LLM to
127
- // wait silently until the user speaks and then call
128
- // `resume_ensemble` first. Legacy mid-work path also signals
129
- // immediately — no branching required.
130
- if (lineup.conductor.instructions) {
149
+ // Legacy hold-mode standby (conductor-invoked mid-work with hold: true).
150
+ // Runs ONLY on the non-initial-startup path — initialStartup handling
151
+ // happens earlier above, ordered before the lineup instructions.
152
+ if (!initialStartup && hold && isConductor && handle) {
131
153
  try {
132
154
  await handle.signal('receiveMessage', {
133
- from: 'lineup',
134
- text: lineup.conductor.instructions,
155
+ from: 'system',
156
+ text: 'Ensemble is loading in hold mode — players are connecting but on standby. Wait for instructions from the user or maestro before directing the ensemble. When ready, use the `release` tool to deliver task assignments to all held players.',
135
157
  responseRequested: false,
136
158
  });
137
- conductorActions.push(initialStartup ? 'instructions seeded (initial startup)' : 'instructions delivered');
138
- log('Conductor instructions delivered');
159
+ conductorActions.push('hold mode standby');
139
160
  }
140
161
  catch (err) {
141
- failed.push(`conductor instructions: ${err}`);
162
+ failed.push(`conductor hold message: ${err}`);
142
163
  }
143
164
  }
144
- }
145
- // Legacy hold-mode standby (conductor-invoked mid-work with hold: true).
146
- // Runs ONLY on the non-initial-startup path — initialStartup handling
147
- // happens earlier above, ordered before the lineup instructions.
148
- if (!initialStartup && hold && isConductor && handle) {
149
- try {
150
- await handle.signal('receiveMessage', {
151
- from: 'system',
152
- text: 'Ensemble is loading in hold mode — players are connecting but on standby. Wait for instructions from the user or maestro before directing the ensemble. When ready, use the `release` tool to deliver task assignments to all held players.',
153
- responseRequested: false,
154
- });
155
- conductorActions.push('hold mode standby');
156
- }
157
- catch (err) {
158
- failed.push(`conductor hold message: ${err}`);
159
- }
160
- }
161
- // Recruit players via outbox — no polling needed
162
- for (const player of lineup.players) {
163
- const playerName = player.name;
164
- const workDir = player.workDir || process.cwd();
165
- const agentType = player.agent === 'copilot' ? 'copilot' : 'claude';
166
- const isCustomAgent = player.agent && player.agent !== 'default' && player.agent !== 'copilot';
167
- const systemPrompt = player._agentDefinition ? undefined : (isCustomAgent ? player.agent : undefined);
168
- const agentDefinition = player._agentDefinition;
169
- const agentDefinitionPath = player._agentDefinitionPath;
170
- // Skip if already active — send instructions via cue instead
171
- const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, playerName);
172
- if (existing) {
173
- log(`Player "${playerName}" already active — skipping recruit`);
174
- recruited.push(`${playerName} (already active)`);
175
- if (player.instructions && handle) {
176
- try {
177
- const cueEntry = {
178
- type: 'cue',
179
- targetPlayerId: playerName,
180
- message: player.instructions,
181
- };
182
- await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [cueEntry] });
183
- }
184
- catch (err) {
185
- log(`Failed to send instructions to already-active player "${playerName}":`, err);
165
+ // Recruit players via outbox — no polling needed
166
+ for (const player of lineup.players) {
167
+ const playerName = player.name;
168
+ const workDir = player.workDir || process.cwd();
169
+ const agentType = player.agent === 'copilot' ? 'copilot' : 'claude';
170
+ const isCustomAgent = player.agent && player.agent !== 'default' && player.agent !== 'copilot';
171
+ const systemPrompt = player._agentDefinition ? undefined : (isCustomAgent ? player.agent : undefined);
172
+ const agentDefinition = player._agentDefinition;
173
+ const agentDefinitionPath = player._agentDefinitionPath;
174
+ // Skip if already active — send instructions via cue instead
175
+ const existing = await (0, resolve_1.resolveSession)(client, config.ensemble, playerName);
176
+ if (existing) {
177
+ log(`Player "${playerName}" already active — skipping recruit`);
178
+ recruited.push(`${playerName} (already active)`);
179
+ if (player.instructions && handle) {
180
+ try {
181
+ const cueEntry = {
182
+ type: 'cue',
183
+ targetPlayerId: playerName,
184
+ message: player.instructions,
185
+ };
186
+ await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [cueEntry] });
187
+ }
188
+ catch (err) {
189
+ log(`Failed to send instructions to already-active player "${playerName}":`, err);
190
+ }
186
191
  }
192
+ continue;
193
+ }
194
+ // Submit recruit via outbox — pre-creates workflow with pending status
195
+ if (!handle) {
196
+ failed.push(`${playerName}: load_lineup requires a workflow handle to recruit`);
197
+ continue;
187
198
  }
188
- continue;
189
- }
190
- // Submit recruit via outbox — pre-creates workflow with pending status
191
- if (!handle) {
192
- failed.push(`${playerName}: load_lineup requires a workflow handle to recruit`);
193
- continue;
194
- }
195
- try {
196
- // Resolve full agent type info (description, nativeResolvable) if available
197
- const resolvedType = agentDefinition ? (0, agent_types_1.resolveAgentType)(agentDefinition) : null;
198
- const entry = {
199
- type: 'recruit',
200
- targetName: playerName,
201
- workDir,
202
- isConductor: false,
203
- initialMessage: player.instructions,
204
- agent: agentType,
205
- systemPrompt: agentDefinition ? undefined : systemPrompt,
206
- agentDefinition: resolvedType?.name || agentDefinition,
207
- agentDefinitionPath: resolvedType?.path || agentDefinitionPath,
208
- agentDefinitionDescription: resolvedType?.description,
209
- nativeResolvable: resolvedType?.nativeResolvable,
210
- allowedTools: player.allowedTools,
211
- claudeBin: config.claudeBin,
212
- ...(hold ? { held: true } : {}),
213
- };
214
- await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
215
- recruited.push(playerName);
216
- log(`Recruit request submitted for "${playerName}" in ${workDir}`);
217
- }
218
- catch (err) {
219
- failed.push(`${playerName}: recruit failed — ${err}`);
220
- }
221
- }
222
- // Create schedules
223
- const schedulesCreated = [];
224
- const scheduleWarnings = [];
225
- if (lineup.schedules && lineup.schedules.length > 0) {
226
- // Build valid target set from lineup player names + special values
227
- const validTargets = new Set(lineup.players.map((p) => p.name));
228
- validTargets.add('conductor');
229
- validTargets.add('all');
230
- for (const sched of lineup.schedules) {
231
199
  try {
232
- // Validate schedule target against known player names
233
- if (!validTargets.has(sched.target)) {
234
- scheduleWarnings.push(`schedule "${sched.name}": target "${sched.target}" does not match any player in this lineup (known: ${[...validTargets].join(', ')})`);
235
- }
236
- const now = Date.now();
237
- let nextFireAt;
238
- let interval;
239
- let cronExpression;
240
- let timezone;
241
- if (sched.at) {
242
- nextFireAt = Date.parse(sched.at);
243
- // Support at + every: use `at` as the initial fire time, `every` as the interval
244
- if (sched.every) {
200
+ // Resolve full agent type info (description, nativeResolvable) if available
201
+ const resolvedType = agentDefinition ? (0, agent_types_1.resolveAgentType)(agentDefinition) : null;
202
+ const entry = {
203
+ type: 'recruit',
204
+ targetName: playerName,
205
+ workDir,
206
+ isConductor: false,
207
+ initialMessage: player.instructions,
208
+ agent: agentType,
209
+ systemPrompt: agentDefinition ? undefined : systemPrompt,
210
+ agentDefinition: resolvedType?.name || agentDefinition,
211
+ agentDefinitionPath: resolvedType?.path || agentDefinitionPath,
212
+ agentDefinitionDescription: resolvedType?.description,
213
+ nativeResolvable: resolvedType?.nativeResolvable,
214
+ allowedTools: player.allowedTools,
215
+ claudeBin: config.claudeBin,
216
+ ...(hold ? { held: true } : {}),
217
+ };
218
+ await handle.executeUpdate(signals_1.submitOutboxUpdate, { args: [entry] });
219
+ recruited.push(playerName);
220
+ log(`Recruit request submitted for "${playerName}" in ${workDir}`);
221
+ }
222
+ catch (err) {
223
+ failed.push(`${playerName}: recruit failed — ${err}`);
224
+ }
225
+ }
226
+ // Create schedules
227
+ const schedulesCreated = [];
228
+ const scheduleWarnings = [];
229
+ if (lineup.schedules && lineup.schedules.length > 0) {
230
+ // Build valid target set from lineup player names + special values
231
+ const validTargets = new Set(lineup.players.map((p) => p.name));
232
+ validTargets.add('conductor');
233
+ validTargets.add('all');
234
+ for (const sched of lineup.schedules) {
235
+ try {
236
+ // Validate schedule target against known player names
237
+ if (!validTargets.has(sched.target)) {
238
+ scheduleWarnings.push(`schedule "${sched.name}": target "${sched.target}" does not match any player in this lineup (known: ${[...validTargets].join(', ')})`);
239
+ }
240
+ const now = Date.now();
241
+ let nextFireAt;
242
+ let interval;
243
+ let cronExpression;
244
+ let timezone;
245
+ if (sched.at) {
246
+ nextFireAt = Date.parse(sched.at);
247
+ // Support at + every: use `at` as the initial fire time, `every` as the interval
248
+ if (sched.every) {
249
+ const ms = (0, duration_1.parseDuration)(sched.every);
250
+ if (!ms)
251
+ throw new Error(`Invalid interval: ${sched.every}`);
252
+ interval = ms;
253
+ }
254
+ }
255
+ else if (sched.delay) {
256
+ const ms = (0, duration_1.parseDuration)(sched.delay);
257
+ if (!ms)
258
+ throw new Error(`Invalid delay: ${sched.delay}`);
259
+ nextFireAt = now + ms;
260
+ }
261
+ else if (sched.every) {
245
262
  const ms = (0, duration_1.parseDuration)(sched.every);
246
263
  if (!ms)
247
264
  throw new Error(`Invalid interval: ${sched.every}`);
265
+ nextFireAt = now + ms;
248
266
  interval = ms;
249
267
  }
268
+ else if (sched.cron) {
269
+ cronExpression = sched.cron;
270
+ timezone = sched.timezone || 'UTC';
271
+ const job = new croner_1.Cron(cronExpression, { timezone });
272
+ const next = job.nextRun();
273
+ if (!next)
274
+ throw new Error(`Cron expression "${sched.cron}" has no upcoming fire time`);
275
+ nextFireAt = next.getTime();
276
+ }
277
+ else {
278
+ throw new Error('No timing specified');
279
+ }
280
+ const type = sched.cron ? 'cron' : (sched.every || interval) ? 'interval' : 'once';
281
+ const scheduleEntry = {
282
+ name: sched.name,
283
+ message: sched.message,
284
+ target: sched.target,
285
+ type,
286
+ nextFireAt: new Date(nextFireAt).toISOString(),
287
+ interval,
288
+ cronExpression,
289
+ timezone,
290
+ until: sched.until,
291
+ remainingCount: sched.count,
292
+ firedCount: 0,
293
+ createdBy: getPlayerId(),
294
+ };
295
+ const wfId = (0, config_1.schedulerWorkflowId)(config.ensemble);
296
+ try {
297
+ const handle = client.workflow.getHandle(wfId);
298
+ await handle.describe();
299
+ await handle.signal('addSchedule', scheduleEntry);
300
+ }
301
+ catch {
302
+ await client.workflow.start('agentSchedulerWorkflow', {
303
+ workflowId: wfId,
304
+ taskQueue: config.taskQueue,
305
+ args: [{ ensemble: config.ensemble, entries: [scheduleEntry] }],
306
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
307
+ searchAttributes: {
308
+ AgentTempoEnsemble: [config.ensemble],
309
+ },
310
+ });
311
+ }
312
+ schedulesCreated.push(sched.name);
250
313
  }
251
- else if (sched.delay) {
252
- const ms = (0, duration_1.parseDuration)(sched.delay);
253
- if (!ms)
254
- throw new Error(`Invalid delay: ${sched.delay}`);
255
- nextFireAt = now + ms;
256
- }
257
- else if (sched.every) {
258
- const ms = (0, duration_1.parseDuration)(sched.every);
259
- if (!ms)
260
- throw new Error(`Invalid interval: ${sched.every}`);
261
- nextFireAt = now + ms;
262
- interval = ms;
263
- }
264
- else if (sched.cron) {
265
- cronExpression = sched.cron;
266
- timezone = sched.timezone || 'UTC';
267
- const job = new croner_1.Cron(cronExpression, { timezone });
268
- const next = job.nextRun();
269
- if (!next)
270
- throw new Error(`Cron expression "${sched.cron}" has no upcoming fire time`);
271
- nextFireAt = next.getTime();
272
- }
273
- else {
274
- throw new Error('No timing specified');
275
- }
276
- const type = sched.cron ? 'cron' : (sched.every || interval) ? 'interval' : 'once';
277
- const scheduleEntry = {
278
- name: sched.name,
279
- message: sched.message,
280
- target: sched.target,
281
- type,
282
- nextFireAt: new Date(nextFireAt).toISOString(),
283
- interval,
284
- cronExpression,
285
- timezone,
286
- until: sched.until,
287
- remainingCount: sched.count,
288
- firedCount: 0,
289
- createdBy: getPlayerId(),
290
- };
291
- const wfId = (0, config_1.schedulerWorkflowId)(config.ensemble);
292
- try {
293
- const handle = client.workflow.getHandle(wfId);
294
- await handle.describe();
295
- await handle.signal('addSchedule', scheduleEntry);
314
+ catch (err) {
315
+ failed.push(`schedule "${sched.name}": ${err}`);
296
316
  }
297
- catch {
298
- await client.workflow.start('agentSchedulerWorkflow', {
299
- workflowId: wfId,
300
- taskQueue: config.taskQueue,
301
- args: [{ ensemble: config.ensemble, entries: [scheduleEntry] }],
302
- workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
303
- searchAttributes: {
304
- AgentTempoEnsemble: [config.ensemble],
305
- },
306
- });
317
+ }
318
+ }
319
+ // Issue #172: on the initial-startup path, pause the whole ensemble
320
+ // so scheduler fires, maestro nudges, and per-session outbox dispatch
321
+ // are all halted while we wait for the user's first message. The
322
+ // conductor's `receiveMessageSignal` handler combines lineup context +
323
+ // user text and the combined prompt instructs Claude Code to call
324
+ // `resume_ensemble` BEFORE any other action. Inlines the three signals
325
+ // that `pause_ensemble` fires so we don't depend on the tool impl.
326
+ if (initialStartup && isConductor && handle) {
327
+ try {
328
+ const maestroId = (0, config_1.maestroWorkflowId)(config.ensemble);
329
+ await client.workflow.getHandle(maestroId).signal('maestroSetPaused', true);
330
+ }
331
+ catch {
332
+ // Maestro may not be running yet — fine, it will start paused once spawned.
333
+ }
334
+ try {
335
+ const schedulerId = (0, config_1.schedulerWorkflowId)(config.ensemble);
336
+ await client.workflow.getHandle(schedulerId).signal('setSchedulerPaused', true);
337
+ }
338
+ catch {
339
+ // Scheduler may not exist yet if no schedules are defined — ignore.
340
+ }
341
+ try {
342
+ const sessions = await (0, resolve_2.scanEnsembleSessions)(client, config.ensemble);
343
+ for (const session of sessions) {
344
+ try {
345
+ await client.workflow.getHandle(session.workflowId).signal('setPaused', true);
346
+ }
347
+ catch {
348
+ // Individual session may have just terminated — skip.
349
+ }
307
350
  }
308
- schedulesCreated.push(sched.name);
351
+ conductorActions.push('ensemble paused (awaiting first user message)');
309
352
  }
310
353
  catch (err) {
311
- failed.push(`schedule "${sched.name}": ${err}`);
354
+ failed.push(`pause ensemble: ${(0, descriptor_1.formatError)(err)}`);
312
355
  }
313
356
  }
314
- }
315
- // Issue #172: on the initial-startup path, pause the whole ensemble
316
- // so scheduler fires, maestro nudges, and per-session outbox dispatch
317
- // are all halted while we wait for the user's first message. The
318
- // conductor's `receiveMessageSignal` handler combines lineup context +
319
- // user text and the combined prompt instructs Claude Code to call
320
- // `resume_ensemble` BEFORE any other action. Inlines the three signals
321
- // that `pause_ensemble` fires so we don't depend on the tool impl.
322
- if (initialStartup && isConductor && handle) {
323
- try {
324
- const maestroId = (0, config_1.maestroWorkflowId)(config.ensemble);
325
- await client.workflow.getHandle(maestroId).signal('maestroSetPaused', true);
326
- }
327
- catch {
328
- // Maestro may not be running yet — fine, it will start paused once spawned.
357
+ // Build summary
358
+ const lines = [`Loaded lineup **${lineup.name}**.`];
359
+ if (conductorActions.length > 0) {
360
+ lines.push(`Conductor: ${conductorActions.join(', ')}`);
329
361
  }
330
- try {
331
- const schedulerId = (0, config_1.schedulerWorkflowId)(config.ensemble);
332
- await client.workflow.getHandle(schedulerId).signal('setSchedulerPaused', true);
333
- }
334
- catch {
335
- // Scheduler may not exist yet if no schedules are defined — ignore.
336
- }
337
- try {
338
- const sessions = await (0, resolve_2.scanEnsembleSessions)(client, config.ensemble);
339
- for (const session of sessions) {
340
- try {
341
- await client.workflow.getHandle(session.workflowId).signal('setPaused', true);
342
- }
343
- catch {
344
- // Individual session may have just terminated — skip.
345
- }
362
+ if (recruited.length > 0) {
363
+ if (hold) {
364
+ lines.push(`Held: ${recruited.join(', ')} — ${recruited.length} player(s) held. Use \`release\` to start them.`);
365
+ }
366
+ else {
367
+ lines.push(`Recruited: ${recruited.join(', ')}`);
346
368
  }
347
- conductorActions.push('ensemble paused (awaiting first user message)');
348
369
  }
349
- catch (err) {
350
- failed.push(`pause ensemble: ${(0, helpers_1.formatError)(err)}`);
370
+ if (schedulesCreated.length > 0) {
371
+ lines.push(`Schedules created: ${schedulesCreated.join(', ')}`);
351
372
  }
352
- }
353
- // Build summary
354
- const lines = [`Loaded lineup **${lineup.name}**.`];
355
- if (conductorActions.length > 0) {
356
- lines.push(`Conductor: ${conductorActions.join(', ')}`);
357
- }
358
- if (recruited.length > 0) {
359
- if (hold) {
360
- lines.push(`Held: ${recruited.join(', ')} — ${recruited.length} player(s) held. Use \`release\` to start them.`);
373
+ if (scheduleWarnings.length > 0) {
374
+ lines.push(`⚠ Schedule target warnings:\n${scheduleWarnings.map(w => ` - ${w}`).join('\n')}`);
361
375
  }
362
- else {
363
- lines.push(`Recruited: ${recruited.join(', ')}`);
376
+ if (failed.length > 0) {
377
+ lines.push(`Failures:\n${failed.map(f => ` - ${f}`).join('\n')}`);
364
378
  }
379
+ return (0, descriptor_1.ok)(lines.join('\n'));
365
380
  }
366
- if (schedulesCreated.length > 0) {
367
- lines.push(`Schedules created: ${schedulesCreated.join(', ')}`);
368
- }
369
- if (scheduleWarnings.length > 0) {
370
- lines.push(`⚠ Schedule target warnings:\n${scheduleWarnings.map(w => ` - ${w}`).join('\n')}`);
371
- }
372
- if (failed.length > 0) {
373
- lines.push(`Failures:\n${failed.map(f => ` - ${f}`).join('\n')}`);
381
+ catch (err) {
382
+ return (0, descriptor_1.fail)(`Failed to load lineup: ${(0, descriptor_1.formatError)(err)}`);
374
383
  }
375
- return (0, helpers_1.ok)(lines.join('\n'));
376
- }
377
- catch (err) {
378
- return (0, helpers_1.fail)(`Failed to load lineup: ${(0, helpers_1.formatError)(err)}`);
379
- }
380
- });
384
+ },
385
+ };
381
386
  }