chainlesschain 0.45.4 → 0.45.5

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.
package/README.md CHANGED
@@ -1037,6 +1037,60 @@ chainlesschain ui --host 0.0.0.0 # Bind to all interfaces (remote access)
1037
1037
 
1038
1038
  ---
1039
1039
 
1040
+ ## AI Orchestration Layer (v0.45.4)
1041
+
1042
+ ### `chainlesschain orchestrate`
1043
+
1044
+ Use ChainlessChain as an orchestration layer — automatically decompose tasks, dispatch to parallel AI coding agents (Claude Code / Codex / Gemini / OpenAI / Ollama), verify with CI/CD, and notify via Telegram / WeCom / DingTalk / Feishu.
1045
+
1046
+ ```bash
1047
+ chainlesschain orchestrate "Fix the auth bug" # Auto-detect AI tool and run
1048
+ chainlesschain orchestrate "Refactor payments" \
1049
+ --backends claude,gemini --strategy parallel-all # Multi-backend, parallel
1050
+ chainlesschain orchestrate "Add tests" \
1051
+ --ci "npm run test:unit" --retries 5 # Custom CI + retries
1052
+ chainlesschain orchestrate "task" --no-ci # Skip CI verification
1053
+ chainlesschain orchestrate "task" --json # JSON output (for scripts)
1054
+ chainlesschain orchestrate detect # Detect installed AI CLIs
1055
+ chainlesschain orchestrate --status # Show orchestrator status
1056
+ chainlesschain orchestrate --status --json # JSON status with backends list
1057
+ chainlesschain orchestrate --webhook # Start IM webhook server (port 18820)
1058
+ chainlesschain orchestrate --webhook --webhook-port 9090 # Custom port
1059
+ ```
1060
+
1061
+ **Routing strategies** (`--strategy`):
1062
+
1063
+ | Strategy | Description |
1064
+ | -------------- | -------------------------------------------------------------- |
1065
+ | `round-robin` | Weighted round-robin across all backends (default) |
1066
+ | `primary` | Use first backend, auto-fallback on failure |
1067
+ | `parallel-all` | Run all backends simultaneously, pick best result |
1068
+ | `by-type` | Route by task type (`code-generation` / `analysis` / `review`) |
1069
+
1070
+ **Auto-detected backends**: `claude` (CLI), `codex` (CLI), `gemini` / `openai` / `anthropic` (API key env vars), `ollama` (always included as local fallback).
1071
+
1072
+ **Notification channels** (configured via env vars):
1073
+
1074
+ ```bash
1075
+ TELEGRAM_BOT_TOKEN=... TELEGRAM_CHAT_ID=... # Telegram
1076
+ WECOM_WEBHOOK_URL=... # WeCom (企业微信)
1077
+ DINGTALK_WEBHOOK_URL=... DINGTALK_SECRET=... # DingTalk (钉钉)
1078
+ FEISHU_WEBHOOK_URL=... FEISHU_SECRET=... # Feishu (飞书)
1079
+ ```
1080
+
1081
+ **Incoming webhooks** — receive task commands from IM platforms:
1082
+
1083
+ ```bash
1084
+ chainlesschain orchestrate --webhook --webhook-port 18820
1085
+ # POST /wecom (WeCom XML)
1086
+ # POST /dingtalk (DingTalk JSON)
1087
+ # POST /feishu (Feishu JSON + challenge verification)
1088
+ ```
1089
+
1090
+ **WebSocket integration** — trigger via `{ "type": "orchestrate", "task": "...", "cwd": "..." }`, receive real-time `orchestrate:event` progress events and final `orchestrate:done`.
1091
+
1092
+ ---
1093
+
1040
1094
  ## Global Options
1041
1095
 
1042
1096
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.45.4",
3
+ "version": "0.45.5",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Orchestrate command — ChainlessChain as orchestration layer,
3
+ * Claude Code / Codex as parallel execution agents.
4
+ *
5
+ * Usage:
6
+ * cc orchestrate "Fix the auth bug in login.ts"
7
+ * cc orchestrate "Refactor payment service" --agents 5 --ci "npm run test:unit"
8
+ * cc orchestrate --status
9
+ * cc orchestrate --watch --interval 10
10
+ * cc orchestrate detect # Check which AI CLI is installed
11
+ */
12
+
13
+ import chalk from "chalk";
14
+ import ora from "ora";
15
+ import fs from "fs";
16
+ import path from "path";
17
+ import { logger } from "../lib/logger.js";
18
+
19
+ export function registerOrchestrateCommand(program) {
20
+ const cmd = program
21
+ .command("orchestrate [task]")
22
+ .description(
23
+ "Orchestrate AI coding tasks: ChainlessChain → Claude Code/Codex agents → CI/CD → Notify",
24
+ )
25
+ .option("-a, --agents <n>", "Max parallel agents", "3")
26
+ .option(
27
+ "--ci <command>",
28
+ "CI command to run after agents complete",
29
+ "npm test",
30
+ )
31
+ .option("--no-ci", "Skip CI/CD verification step")
32
+ .option(
33
+ "--source <type>",
34
+ "Input source: cli|sentry|github|file|wecom|dingtalk|feishu",
35
+ "cli",
36
+ )
37
+ .option("--file <path>", "Read task from file (use with --source file)")
38
+ .option("--context <text>", "Extra context for the task (e.g. stack trace)")
39
+ .option("--cwd <path>", "Project root directory (default: current dir)")
40
+ .option("--provider <name>", "LLM provider for decomposition")
41
+ .option("--model <name>", "Model for decomposition LLM calls")
42
+ .option("--cli-tool <name>", "Execution CLI: claude|codex (auto-detected)")
43
+ .option(
44
+ "--backends <list>",
45
+ "Agent backends: claude,codex,gemini,openai,ollama (comma-separated)",
46
+ )
47
+ .option(
48
+ "--strategy <name>",
49
+ "Agent routing: round-robin|by-type|parallel-all|primary",
50
+ "round-robin",
51
+ )
52
+ .option("--retries <n>", "Max CI retry cycles", "3")
53
+ .option("--timeout <sec>", "Per-agent timeout in seconds", "300")
54
+ .option("--no-notify", "Disable notifications")
55
+ .option("--status", "Show orchestrator and agent pool status")
56
+ .option("--watch", "Start cron watch mode")
57
+ .option("--interval <min>", "Cron interval in minutes (watch mode)", "10")
58
+ .option("--webhook", "Start HTTP webhook server for IM platform commands")
59
+ .option("--webhook-port <port>", "Webhook server port", "18820")
60
+ .option("--json", "Output as JSON")
61
+ .option("--verbose", "Verbose output");
62
+
63
+ cmd.action(async (task, options) => {
64
+ // Special sub-keyword: cc orchestrate detect
65
+ if (task === "detect") {
66
+ const { detectClaudeCode, detectCodex } =
67
+ await import("../lib/claude-code-bridge.js");
68
+ const claude = detectClaudeCode();
69
+ const codex = detectCodex();
70
+ console.log(chalk.bold("\n\uD83D\uDD0D AI CLI Detection\n"));
71
+ console.log(
72
+ claude.found
73
+ ? chalk.green(` \u2713 claude ${claude.version}`)
74
+ : chalk.red(
75
+ " \u2717 claude not found (install: npm i -g @anthropic-ai/claude-code)",
76
+ ),
77
+ );
78
+ console.log(
79
+ codex.found
80
+ ? chalk.green(` \u2713 codex ${codex.version}`)
81
+ : chalk.gray(" \u2717 codex not found"),
82
+ );
83
+ if (!claude.found && !codex.found) {
84
+ console.log(
85
+ chalk.yellow(
86
+ "\n \u26A0 No AI CLI found. Install Claude Code:\n npm install -g @anthropic-ai/claude-code\n",
87
+ ),
88
+ );
89
+ }
90
+ return;
91
+ }
92
+
93
+ const { Orchestrator, TASK_SOURCE, TASK_STATUS } =
94
+ await import("../lib/orchestrator.js");
95
+
96
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
97
+
98
+ // --status mode
99
+ if (options.status) {
100
+ await _showStatus(cwd, options);
101
+ return;
102
+ }
103
+
104
+ // --watch mode
105
+ if (options.watch) {
106
+ await _watchMode(cwd, options);
107
+ return;
108
+ }
109
+
110
+ // --webhook mode: start HTTP server to receive commands from IM platforms
111
+ if (options.webhook) {
112
+ await _webhookMode(cwd, options);
113
+ return;
114
+ }
115
+
116
+ // Resolve task text
117
+ let taskText = task || "";
118
+ if (options.source === "file" && options.file) {
119
+ try {
120
+ taskText = fs.readFileSync(path.resolve(options.file), "utf-8").trim();
121
+ } catch (err) {
122
+ logger.error(`Cannot read file: ${err.message}`);
123
+ process.exit(1);
124
+ }
125
+ }
126
+
127
+ if (!taskText) {
128
+ console.log(
129
+ chalk.yellow('Usage: cc orchestrate "<task description>"') +
130
+ chalk.gray(
131
+ "\n cc orchestrate --status\n cc orchestrate --watch\n cc orchestrate --webhook # receive commands from WeCom/DingTalk/Feishu\n",
132
+ ),
133
+ );
134
+ process.exit(1);
135
+ }
136
+
137
+ // Build agent backends from --backends option
138
+ let agentsConfig;
139
+ if (options.backends) {
140
+ const { BACKEND_TYPE } = await import("../lib/agent-router.js");
141
+ const backendNames = options.backends.split(",").map((s) => s.trim());
142
+ agentsConfig = {
143
+ backends: backendNames.map((type) => ({ type, weight: 1 })),
144
+ strategy: options.strategy || "round-robin",
145
+ };
146
+ } else if (options.strategy && options.strategy !== "round-robin") {
147
+ agentsConfig = { strategy: options.strategy };
148
+ }
149
+
150
+ // Build orchestrator
151
+ const orch = new Orchestrator({
152
+ cwd,
153
+ maxParallel: parseInt(options.agents, 10) || 3,
154
+ maxRetries: parseInt(options.retries, 10) || 3,
155
+ ciCommand: options.ci || "npm test",
156
+ agents: agentsConfig,
157
+ model: options.model || undefined,
158
+ llm: options.provider
159
+ ? { provider: options.provider, model: options.model }
160
+ : {},
161
+ verbose: options.verbose,
162
+ });
163
+
164
+ if (options.json) {
165
+ _runJson(orch, taskText, options);
166
+ return;
167
+ }
168
+
169
+ // Pretty output
170
+ _runPretty(orch, taskText, options, cwd);
171
+ });
172
+ }
173
+
174
+ // ─── Pretty (interactive) run ────────────────────────────────────
175
+
176
+ async function _runPretty(orch, taskText, options, cwd) {
177
+ const { TASK_STATUS } = await import("../lib/orchestrator.js");
178
+
179
+ console.log(chalk.bold.cyan("\n⚡ ChainlessChain Orchestrator\n"));
180
+ console.log(chalk.gray(` Task: `) + chalk.white(taskText.slice(0, 120)));
181
+ console.log(chalk.gray(` CWD: `) + chalk.white(cwd));
182
+ console.log(
183
+ chalk.gray(` Agents: `) +
184
+ chalk.white(`max ${options.agents} × ${orch.cliCommand}`),
185
+ );
186
+ if (options.ci !== false) {
187
+ console.log(chalk.gray(` CI: `) + chalk.white(orch.ciCommand));
188
+ }
189
+ console.log();
190
+
191
+ const spinner = ora("Decomposing task...").start();
192
+
193
+ orch.on("task:decomposed", ({ subtasks }) => {
194
+ spinner.text = `Decomposed into ${subtasks.length} subtask(s)`;
195
+ });
196
+
197
+ orch.on("agents:dispatched", ({ count }) => {
198
+ spinner.text = `Dispatching ${count} subtask(s) to ${orch.cliCommand} agents...`;
199
+ });
200
+
201
+ orch.on("batch:start", ({ count }) => {
202
+ spinner.text = `Running batch of ${count} agent(s)...`;
203
+ });
204
+
205
+ orch.on("agent:complete", ({ taskId, success, duration }) => {
206
+ const icon = success ? chalk.green("✓") : chalk.red("✗");
207
+ spinner.text = `Agent done: ${icon} ${taskId} (${Math.round(duration / 1000)}s)`;
208
+ });
209
+
210
+ orch.on("ci:checking", ({ attempt }) => {
211
+ spinner.text = `Running CI check (attempt ${attempt + 1})...`;
212
+ });
213
+
214
+ orch.on("ci:fail", ({ errors, attempt }) => {
215
+ spinner.text = `CI failed (attempt ${attempt + 1}) — retrying with agents...`;
216
+ });
217
+
218
+ orch.on("task:complete", (task) => {
219
+ spinner.succeed(
220
+ chalk.green(`✅ Task completed`) +
221
+ chalk.gray(` [${task.id}] status: ${task.status}`),
222
+ );
223
+ _printSummary(task);
224
+ });
225
+
226
+ orch.on("task:failed", ({ task, error }) => {
227
+ spinner.fail(chalk.red(`❌ Task failed: ${error}`));
228
+ _printSummary(task);
229
+ });
230
+
231
+ orch.on("log", (msg) => {
232
+ if (options.verbose) spinner.text = msg;
233
+ });
234
+
235
+ try {
236
+ const { TASK_SOURCE } = await import("../lib/orchestrator.js");
237
+ await orch.addTask(taskText, {
238
+ source:
239
+ options.source === "sentry"
240
+ ? TASK_SOURCE.SENTRY
241
+ : options.source === "github"
242
+ ? TASK_SOURCE.GITHUB
243
+ : options.source === "file"
244
+ ? TASK_SOURCE.FILE
245
+ : TASK_SOURCE.CLI,
246
+ context: options.context || "",
247
+ cwd,
248
+ runCI: options.ci !== false,
249
+ notify: options.notify !== false,
250
+ });
251
+ } catch (err) {
252
+ spinner.fail(chalk.red(`Orchestration error: ${err.message}`));
253
+ if (options.verbose) console.error(err);
254
+ process.exit(1);
255
+ }
256
+ }
257
+
258
+ // ─── JSON run ────────────────────────────────────────────────────
259
+
260
+ async function _runJson(orch, taskText, options) {
261
+ const { TASK_SOURCE } = await import("../lib/orchestrator.js");
262
+ try {
263
+ const task = await orch.addTask(taskText, {
264
+ source: TASK_SOURCE.CLI,
265
+ context: options.context || "",
266
+ runCI: options.ci !== false,
267
+ notify: false,
268
+ });
269
+ console.log(JSON.stringify(task, null, 2));
270
+ } catch (err) {
271
+ console.log(JSON.stringify({ error: err.message }, null, 2));
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ // ─── Status ──────────────────────────────────────────────────────
277
+
278
+ async function _showStatus(cwd, options) {
279
+ const { detectClaudeCode, detectCodex } =
280
+ await import("../lib/claude-code-bridge.js");
281
+ const claude = detectClaudeCode();
282
+ const codex = detectCodex();
283
+
284
+ const status = {
285
+ cliTools: {
286
+ claude: claude.found ? claude.version : "not found",
287
+ codex: codex.found ? codex.version : "not found",
288
+ },
289
+ activeCliTool: claude.found ? "claude" : codex.found ? "codex" : "none",
290
+ };
291
+
292
+ const { AgentRouter } = await import("../lib/agent-router.js");
293
+ const router = AgentRouter.autoDetect();
294
+ const backends = router.summary();
295
+
296
+ if (options.json) {
297
+ console.log(JSON.stringify({ ...status, backends }, null, 2));
298
+ return;
299
+ }
300
+
301
+ console.log(chalk.bold.cyan("\n⚡ Orchestrator Status\n"));
302
+ console.log(chalk.bold(" CLI Tools"));
303
+ console.log(
304
+ ` ${chalk.gray("claude:")} ` +
305
+ (claude.found ? chalk.green(claude.version) : chalk.red("not installed")),
306
+ );
307
+ console.log(
308
+ ` ${chalk.gray("codex:")} ` +
309
+ (codex.found ? chalk.green(codex.version) : chalk.gray("not installed")),
310
+ );
311
+ console.log();
312
+ console.log(chalk.bold(" Auto-detected Backends"));
313
+ for (const b of backends) {
314
+ const icon = b.kind === "cli" ? "🖥" : "🌐";
315
+ console.log(
316
+ ` ${icon} ${chalk.cyan(b.type.padEnd(12))} ${chalk.gray(b.kind)} weight:${b.weight}`,
317
+ );
318
+ }
319
+
320
+ console.log();
321
+ console.log(chalk.bold(" Notification Channels"));
322
+ const { NotificationManager } = await import("../lib/notifiers/index.js");
323
+ const nm = NotificationManager.fromEnv();
324
+ const channels = nm.activeChannels;
325
+ if (channels.length === 0) {
326
+ console.log(
327
+ chalk.gray(
328
+ " (none configured — set TELEGRAM_BOT_TOKEN, WECOM_WEBHOOK_URL, etc.)",
329
+ ),
330
+ );
331
+ } else {
332
+ for (const ch of channels) {
333
+ console.log(` ${chalk.green("✓")} ${ch}`);
334
+ }
335
+ }
336
+ console.log();
337
+ }
338
+
339
+ // ─── Webhook mode (receive commands from IM platforms) ─────────────
340
+
341
+ async function _webhookMode(cwd, options) {
342
+ const { createServer } = await import("http");
343
+ const { parseDingTalkIncoming, parseFeishuIncoming, parseWeComIncoming } =
344
+ await import("../lib/notifiers/index.js");
345
+ const { Orchestrator, TASK_SOURCE } = await import("../lib/orchestrator.js");
346
+
347
+ const port = parseInt(options.webhookPort, 10) || 18820;
348
+
349
+ const orch = new Orchestrator({ cwd, verbose: options.verbose });
350
+
351
+ const server = createServer(async (req, res) => {
352
+ if (req.method !== "POST") {
353
+ res.writeHead(405);
354
+ res.end("Method Not Allowed");
355
+ return;
356
+ }
357
+
358
+ let body = "";
359
+ req.on("data", (chunk) => (body += chunk.toString("utf8")));
360
+ req.on("end", async () => {
361
+ let taskText = null;
362
+ let source = TASK_SOURCE.CLI;
363
+
364
+ const url = req.url?.split("?")[0] || "/";
365
+
366
+ try {
367
+ if (url === "/wecom") {
368
+ taskText = parseWeComIncoming(body);
369
+ source = TASK_SOURCE.CLI;
370
+ } else if (url === "/dingtalk") {
371
+ const parsed = JSON.parse(body);
372
+ taskText = parseDingTalkIncoming(parsed);
373
+ source = TASK_SOURCE.CLI;
374
+ } else if (url === "/feishu") {
375
+ const parsed = JSON.parse(body);
376
+ // Feishu challenge verification
377
+ if (parsed.challenge) {
378
+ res.writeHead(200, { "Content-Type": "application/json" });
379
+ res.end(JSON.stringify({ challenge: parsed.challenge }));
380
+ return;
381
+ }
382
+ taskText = parseFeishuIncoming(parsed);
383
+ source = TASK_SOURCE.CLI;
384
+ } else {
385
+ res.writeHead(404);
386
+ res.end("Not Found");
387
+ return;
388
+ }
389
+ } catch (_err) {
390
+ res.writeHead(400);
391
+ res.end("Bad Request");
392
+ return;
393
+ }
394
+
395
+ if (!taskText) {
396
+ res.writeHead(200, { "Content-Type": "application/json" });
397
+ res.end(JSON.stringify({ ok: true, message: "no task detected" }));
398
+ return;
399
+ }
400
+
401
+ // Acknowledge immediately (IM platforms require fast response)
402
+ res.writeHead(200, { "Content-Type": "application/json" });
403
+ res.end(
404
+ JSON.stringify({
405
+ ok: true,
406
+ message: `task queued: ${taskText.slice(0, 60)}`,
407
+ }),
408
+ );
409
+
410
+ // Run orchestration async
411
+ if (!options.json) {
412
+ console.log(
413
+ chalk.cyan(`[webhook] Received task: `) + taskText.slice(0, 80),
414
+ );
415
+ }
416
+ orch
417
+ .addTask(taskText, {
418
+ source,
419
+ cwd,
420
+ runCI: options.ci !== false,
421
+ notify: true,
422
+ })
423
+ .catch((err) =>
424
+ console.error(chalk.red(`[webhook] Error: ${err.message}`)),
425
+ );
426
+ });
427
+ });
428
+
429
+ server.listen(port, "127.0.0.1", () => {
430
+ console.log(chalk.bold.cyan("\n⚡ Orchestrator Webhook Server\n"));
431
+ console.log(
432
+ ` ${chalk.gray("WeCom:")} POST http://localhost:${port}/wecom`,
433
+ );
434
+ console.log(
435
+ ` ${chalk.gray("DingTalk:")} POST http://localhost:${port}/dingtalk`,
436
+ );
437
+ console.log(
438
+ ` ${chalk.gray("Feishu:")} POST http://localhost:${port}/feishu`,
439
+ );
440
+ console.log(chalk.gray("\n Press Ctrl+C to stop\n"));
441
+ });
442
+
443
+ process.on("SIGINT", () => {
444
+ server.close();
445
+ orch.stopCronWatch();
446
+ console.log(chalk.gray("\nWebhook server stopped."));
447
+ process.exit(0);
448
+ });
449
+ }
450
+
451
+ // ─── Watch mode ──────────────────────────────────────────────────
452
+
453
+ async function _watchMode(cwd, options) {
454
+ const { Orchestrator } = await import("../lib/orchestrator.js");
455
+ const intervalMs = parseInt(options.interval, 10) * 60_000 || 600_000;
456
+
457
+ const orch = new Orchestrator({ cwd, verbose: options.verbose });
458
+ orch.startCronWatch(intervalMs);
459
+
460
+ orch.on("cron:tick", ({ at }) => {
461
+ if (!options.json) console.log(chalk.gray(`[cron] tick at ${at}`));
462
+ });
463
+
464
+ console.log(
465
+ chalk.cyan(`\n⚡ Orchestrator watch mode started`) +
466
+ chalk.gray(` (interval: ${options.interval}m)`),
467
+ );
468
+ console.log(chalk.gray(" Press Ctrl+C to stop\n"));
469
+
470
+ // Keep alive
471
+ process.on("SIGINT", () => {
472
+ orch.stopCronWatch();
473
+ console.log(chalk.gray("\nOrchestrator stopped."));
474
+ process.exit(0);
475
+ });
476
+ }
477
+
478
+ // ─── Summary printer ─────────────────────────────────────────────
479
+
480
+ function _printSummary(task) {
481
+ console.log();
482
+ console.log(chalk.bold(" Summary"));
483
+ console.log(chalk.gray(" ─────────────────────────────────"));
484
+ console.log(` ID: ${chalk.cyan(task.id)}`);
485
+ console.log(` Source: ${task.source}`);
486
+ console.log(` Retries: ${task.retries}`);
487
+ console.log(` Status: ${_statusColor(task.status)}`);
488
+
489
+ if (task.subtasks?.length) {
490
+ console.log(` Subtasks: ${task.subtasks.length}`);
491
+ }
492
+
493
+ if (task.agentResults?.length) {
494
+ const passed = task.agentResults.filter((r) => r.success).length;
495
+ console.log(
496
+ ` Agents: ${chalk.green(passed)} passed / ${chalk.red(task.agentResults.length - passed)} failed`,
497
+ );
498
+ }
499
+ console.log();
500
+ }
501
+
502
+ function _statusColor(status) {
503
+ const colors = {
504
+ completed: chalk.green,
505
+ "ci-passed": chalk.green,
506
+ failed: chalk.red,
507
+ "ci-failed": chalk.red,
508
+ retrying: chalk.yellow,
509
+ dispatched: chalk.cyan,
510
+ "ci-checking": chalk.cyan,
511
+ };
512
+ return (colors[status] || chalk.white)(status);
513
+ }
package/src/index.js CHANGED
@@ -89,6 +89,9 @@ import { registerServeCommand } from "./commands/serve.js";
89
89
  // Web UI
90
90
  import { registerUiCommand } from "./commands/ui.js";
91
91
 
92
+ // Orchestration Layer: ChainlessChain → Claude Code/Codex agents → CI → Notify
93
+ import { registerOrchestrateCommand } from "./commands/orchestrate.js";
94
+
92
95
  export function createProgram() {
93
96
  const program = new Command();
94
97
 
@@ -207,5 +210,8 @@ export function createProgram() {
207
210
  // Web UI
208
211
  registerUiCommand(program);
209
212
 
213
+ // Orchestration Layer
214
+ registerOrchestrateCommand(program);
215
+
210
216
  return program;
211
217
  }