metheus-governance-mcp-cli 0.2.291 → 0.2.292

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
@@ -380,6 +380,7 @@ Behavior:
380
380
  - `bot room-audit` defaults to `monitor` only unless you pass `--role` or `--roles`.
381
381
  - `bot room-audit --apply true` writes missing suggested routes into `~/.metheus/bot-runner.json` and disables overlapping enabled routes in the same project/provider/destination/bot scope that are outside the selected role set.
382
382
  - `runner project up` is the direct CLI preparation path for Telegram project operations: it runs the same room audit and applies the selected role routes. Use `runner start-detached` for persistent polling, or let the TUI call `runner.project_up` to bootstrap detached polling in one step.
383
+ - `runner project tui` is the direct CLI project-scoped dashboard: it audits one project destination, ensures one detached runner is reused or started for the selected route set, and then shows status, route selection, and recent log tail in one interactive screen.
383
384
  - `runner project up` can be narrowed with `--bot-name`, `--bot-id`, `--role`, or `--roles <csv>` when you do not want every suggested role route for that room.
384
385
  - `bot remove` without flags starts a guided numbered flow: provider -> bot entry -> confirm removal.
385
386
  - Telegram stores one bot file per entry under `~/.metheus/telegram-bots/<ServerBotName>.env` with generic fields:
@@ -423,6 +424,7 @@ metheus-governance-mcp-cli bot room-audit --provider telegram --project-id <proj
423
424
  metheus-governance-mcp-cli bot room-audit --provider telegram --project-id <project_uuid> --destination-label <room_label> --apply true --json true
424
425
  metheus-governance-mcp-cli runner project up --project-id <project_uuid> --provider telegram --destination-label <room_label> --start false
425
426
  metheus-governance-mcp-cli runner start-detached --project-id <project_uuid> --provider telegram --destination-label <room_label>
427
+ metheus-governance-mcp-cli runner project tui --project-id <project_uuid> --provider telegram --destination-label <room_label>
426
428
  metheus-governance-mcp-cli runner project up --project-id <project_uuid> --provider telegram --destination-label <room_label> --bot-name <server_bot_name> --roles monitor,review --start false
427
429
  ```
428
430
 
package/cli.mjs CHANGED
@@ -402,10 +402,11 @@ function printUsage() {
402
402
  ` ${cmd} selftest [--json <true|false>]`,
403
403
  ` ${cmd} local-bot-bridge [--client <gpt|claude|gemini|sample>] [--cwd <path>] [--model <name>] [--permission-mode <read_only|workspace_write|danger_full_access>] [--reasoning-effort <low|medium|high>]`,
404
404
  ` ${cmd} runner list [--json <true|false>]`,
405
- ` ${cmd} runner route list [--json <true|false>]`,
406
- ` ${cmd} runner route add [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--bot-name <server_name> | --bot-id <uuid>] [--destination-label <label> | --destination-id <uuid>] [--poll-interval-ms <n>] [--enabled <true|false>]`,
407
- ` ${cmd} runner project up [--project-id <uuid>] [--provider <telegram>] [--destination-label <label> | --destination-id <uuid>] [--bot-name <server_name> | --bot-id <uuid>] [--role <monitor|review|worker|approval> | --roles <csv>] [--apply <true|false>] [--start <true|false>] [--start-detached <true|false>] [--tui <true|false>] [--log-file <path>] [--dry-run-delivery <true|false>] [--concurrency <n>] [--json <true|false>]`,
408
- ` ${cmd} runner artifact scan [--workspace-dir <path>] [--file-limit <n>] [--json <true|false>]`,
405
+ ` ${cmd} runner route list [--json <true|false>]`,
406
+ ` ${cmd} runner route add [--project-id <uuid>] [--provider <telegram|slack|kakaotalk>] [--role <monitor|review|worker|approval>] [--bot-name <server_name> | --bot-id <uuid>] [--destination-label <label> | --destination-id <uuid>] [--poll-interval-ms <n>] [--enabled <true|false>]`,
407
+ ` ${cmd} runner project up [--project-id <uuid>] [--provider <telegram>] [--destination-label <label> | --destination-id <uuid>] [--bot-name <server_name> | --bot-id <uuid>] [--role <monitor|review|worker|approval> | --roles <csv>] [--apply <true|false>] [--start <true|false>] [--start-detached <true|false>] [--tui <true|false>] [--log-file <path>] [--dry-run-delivery <true|false>] [--concurrency <n>] [--json <true|false>]`,
408
+ ` ${cmd} runner project tui [--project-id <uuid>] [--provider <telegram>] [--destination-label <label> | --destination-id <uuid>] [--bot-name <server_name> | --bot-id <uuid>] [--role <monitor|review|worker|approval> | --roles <csv>] [--start <true|false>]`,
409
+ ` ${cmd} runner artifact scan [--workspace-dir <path>] [--file-limit <n>] [--json <true|false>]`,
409
410
  ` ${cmd} runner route edit [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
410
411
  ` ${cmd} runner route remove [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>]`,
411
412
  ` ${cmd} runner show [--route-name <name> | --bot-name <server_name> | --bot-id <uuid>] [--json <true|false>]`,
@@ -12203,8 +12204,8 @@ function resolveRunnerProjectUpRoutes({
12203
12204
  });
12204
12205
  }
12205
12206
 
12206
- async function runRunnerProjectUp(flags) {
12207
- const result = await buildRunnerProjectUpResult(flags);
12207
+ async function runRunnerProjectUp(flags) {
12208
+ const result = await buildRunnerProjectUpResult(flags);
12208
12209
  const {
12209
12210
  summaryPayload,
12210
12211
  applyRequested,
@@ -12267,16 +12268,316 @@ async function runRunnerProjectUp(flags) {
12267
12268
  await runRunnerStartDetachedResolvedRoutes(matchingRoutes, startFlags, "runner project up");
12268
12269
  return;
12269
12270
  }
12270
- await runRunnerStartResolvedRoutes(matchingRoutes, startFlags, {
12271
- sourceLabel: "runner project up",
12272
- bootstrapEvent: {
12273
- route_name: "runner project up",
12271
+ await runRunnerStartResolvedRoutes(matchingRoutes, startFlags, {
12272
+ sourceLabel: "runner project up",
12273
+ bootstrapEvent: {
12274
+ route_name: "runner project up",
12274
12275
  outcome: "prepared",
12275
12276
  detail: `prepared ${matchingRoutes.length} enabled route(s) for ${summaryPayload.destination_label || summaryPayload.destination_id || summaryPayload.project_id || "project"}`,
12276
- },
12277
- });
12278
- }
12279
-
12277
+ },
12278
+ });
12279
+ }
12280
+
12281
+ function resolveRunnerProjectTUIRuntimePolicy(flags = {}) {
12282
+ const explicitStartRequested = Object.prototype.hasOwnProperty.call(flags, "start");
12283
+ return {
12284
+ autoStart: explicitStartRequested
12285
+ ? boolFromRaw(flags.start, true)
12286
+ : true,
12287
+ auditFlags: {
12288
+ ...safeObject(flags),
12289
+ provider: String(flags.provider || "").trim()
12290
+ ? normalizeBotProvider(flags.provider)
12291
+ : "telegram",
12292
+ start: false,
12293
+ "start-detached": false,
12294
+ tui: false,
12295
+ json: true,
12296
+ },
12297
+ };
12298
+ }
12299
+
12300
+ function readTextFileTailLines(filePath = "", maxLines = 12) {
12301
+ const normalizedPath = String(filePath || "").trim();
12302
+ if (!normalizedPath) return [];
12303
+ try {
12304
+ if (!fs.existsSync(normalizedPath)) {
12305
+ return ["(log file not created yet)"];
12306
+ }
12307
+ const lines = String(fs.readFileSync(normalizedPath, "utf8") || "")
12308
+ .split(/\r?\n/)
12309
+ .map((line) => String(line || "").trimEnd());
12310
+ return lines.filter((line) => line.length > 0).slice(-Math.max(1, intFromRaw(maxLines, 12)));
12311
+ } catch (err) {
12312
+ return [`(failed to read log: ${String(err?.message || err).trim()})`];
12313
+ }
12314
+ }
12315
+
12316
+ function buildRunnerProjectTUISnapshot({
12317
+ auditResult,
12318
+ autoStart = true,
12319
+ statusMessage = "",
12320
+ errorMessage = "",
12321
+ lastAuditAt = "",
12322
+ }) {
12323
+ const normalizedAuditResult = safeObject(auditResult);
12324
+ const summaryPayload = safeObject(normalizedAuditResult.summaryPayload);
12325
+ const matchingRoutes = ensureArray(normalizedAuditResult.matchingRoutes)
12326
+ .map((route) => normalizeRunnerRoute(route));
12327
+ const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
12328
+ const existingLaunch = matchingRoutes.length
12329
+ ? findExistingDetachedRunnerLaunch(registry, matchingRoutes)
12330
+ : null;
12331
+ const launch = existingLaunch
12332
+ ? {
12333
+ ...normalizeRunnerProcessLaunchEntry(existingLaunch),
12334
+ alive: true,
12335
+ }
12336
+ : null;
12337
+ const logFilePath = String(launch?.log_file || "").trim();
12338
+ const logLines = readTextFileTailLines(logFilePath, 12);
12339
+ return {
12340
+ project_id: String(summaryPayload.project_id || "").trim(),
12341
+ destination_label: String(summaryPayload.destination_label || "").trim(),
12342
+ destination_id: String(summaryPayload.destination_id || "").trim(),
12343
+ room_probe_ok: Boolean(summaryPayload.room_probe_ok),
12344
+ route_apply_requested: Boolean(summaryPayload.route_apply_requested),
12345
+ route_apply_changed: Boolean(summaryPayload.route_apply_changed),
12346
+ route_config_file: String(summaryPayload.route_config_file || "").trim(),
12347
+ enabled_routes: ensureArray(summaryPayload.enabled_routes_for_selection).map((item) => String(item || "").trim()).filter(Boolean),
12348
+ matching_routes: matchingRoutes.map((route) => String(route.name || runnerRouteKey(route)).trim()).filter(Boolean),
12349
+ next_steps: ensureArray(summaryPayload.next_steps).map((item) => String(item || "").trim()).filter(Boolean),
12350
+ warning: String(summaryPayload.warning || "").trim(),
12351
+ error: String(summaryPayload.error || errorMessage || "").trim(),
12352
+ launch,
12353
+ auto_start: autoStart,
12354
+ status_message: String(statusMessage || "").trim(),
12355
+ last_audit_at: String(lastAuditAt || "").trim() || new Date().toISOString(),
12356
+ log_file: logFilePath,
12357
+ log_lines: logLines.length
12358
+ ? logLines
12359
+ : [launch ? "(waiting for runner log output)" : "(no active detached runner)"],
12360
+ };
12361
+ }
12362
+
12363
+ function buildRunnerProjectTUIFrame({
12364
+ snapshot,
12365
+ now = Date.now(),
12366
+ stopping = false,
12367
+ useColor = false,
12368
+ }) {
12369
+ const normalizedSnapshot = safeObject(snapshot);
12370
+ const alive = Boolean(safeObject(normalizedSnapshot.launch).alive);
12371
+ const statusLabel = alive ? "RUNNING" : "STOPPED";
12372
+ const statusDetail = alive
12373
+ ? `launch=${String(safeObject(normalizedSnapshot.launch).launch_id || "").trim() || "-"} pid=${String(safeObject(normalizedSnapshot.launch).pid || "").trim() || "-"}`
12374
+ : "no detached runner is active for this selection";
12375
+ const lines = [
12376
+ "+----------------------------------------------------------------+",
12377
+ "| RUNNER PROJECT TUI |",
12378
+ "| Project-scoped detached runner dashboard |",
12379
+ "+----------------------------------------------------------------+",
12380
+ `Status: ${statusLabel}${stopping ? " (closing TUI)" : ""}`,
12381
+ `Detail: ${statusDetail}`,
12382
+ `Project: ${String(normalizedSnapshot.project_id || "").trim() || "-"}`,
12383
+ `Destination: ${String(normalizedSnapshot.destination_label || normalizedSnapshot.destination_id || "").trim() || "-"}`,
12384
+ `Auto Start: ${normalizedSnapshot.auto_start ? "true" : "false"}`,
12385
+ `Routes: ${ensureArray(normalizedSnapshot.enabled_routes).join(", ") || "-"}`,
12386
+ `Room Probe OK: ${normalizedSnapshot.room_probe_ok ? "true" : "false"}`,
12387
+ `Route Apply Changed: ${normalizedSnapshot.route_apply_changed ? "true" : "false"}`,
12388
+ `Log File: ${String(normalizedSnapshot.log_file || "").trim() || "-"}`,
12389
+ `Last Audit: ${String(normalizedSnapshot.last_audit_at || "").trim() || "-"}`,
12390
+ `Updated: ${new Date(now).toLocaleString("sv-SE", { hour12: false })}`,
12391
+ `Message: ${String(normalizedSnapshot.status_message || "").trim() || "-"}`,
12392
+ `Warning: ${String(normalizedSnapshot.warning || "").trim() || "-"}`,
12393
+ `Error: ${String(normalizedSnapshot.error || "").trim() || "-"}`,
12394
+ "",
12395
+ "Controls: [s] start/reuse [x] stop [r] refresh [a] re-audit [q] quit",
12396
+ "",
12397
+ "Recent Log Tail:",
12398
+ ...ensureArray(normalizedSnapshot.log_lines).map((line) => ` ${truncateRunnerTUIText(line, 140)}`),
12399
+ ];
12400
+ const frame = lines.join("\n");
12401
+ if (!useColor) {
12402
+ return frame;
12403
+ }
12404
+ const statusColor = alive ? "\u001b[32m" : "\u001b[33m";
12405
+ return frame.replace(`Status: ${statusLabel}`, `Status: ${statusColor}${statusLabel}\u001b[0m`);
12406
+ }
12407
+
12408
+ async function runRunnerProjectTUI(flags) {
12409
+ if (!process.stdout?.isTTY || !process.stdin?.isTTY) {
12410
+ throw new Error("runner project tui requires an interactive terminal (TTY)");
12411
+ }
12412
+ const { autoStart, auditFlags } = resolveRunnerProjectTUIRuntimePolicy(flags);
12413
+ const useColor = bootstrapSupportsANSIColors();
12414
+ const state = {
12415
+ auditResult: null,
12416
+ snapshot: null,
12417
+ busy: false,
12418
+ disposed: false,
12419
+ };
12420
+ let intervalHandle = null;
12421
+ let stdinHandler = null;
12422
+
12423
+ const render = (stopping = false) => {
12424
+ if (state.disposed) return;
12425
+ const frame = buildRunnerProjectTUIFrame({
12426
+ snapshot: state.snapshot,
12427
+ now: Date.now(),
12428
+ stopping,
12429
+ useColor,
12430
+ });
12431
+ process.stdout.write(`\u001b[?25l\u001b[2J\u001b[H${frame}`);
12432
+ };
12433
+
12434
+ const refreshSnapshot = async ({
12435
+ reAudit = false,
12436
+ ensureStarted = false,
12437
+ stopRunner = false,
12438
+ statusMessage = "",
12439
+ } = {}) => {
12440
+ if (state.busy) return;
12441
+ state.busy = true;
12442
+ try {
12443
+ if (reAudit || !state.auditResult) {
12444
+ state.auditResult = await buildRunnerProjectUpResult(auditFlags);
12445
+ }
12446
+ let nextStatusMessage = String(statusMessage || "").trim();
12447
+ let nextErrorMessage = "";
12448
+ if (stopRunner) {
12449
+ const currentSnapshot = buildRunnerProjectTUISnapshot({
12450
+ auditResult: state.auditResult,
12451
+ autoStart,
12452
+ statusMessage: nextStatusMessage || "stopping detached runner",
12453
+ });
12454
+ if (safeObject(currentSnapshot.launch).launch_id) {
12455
+ const stopPayload = stopDetachedRunnerWithFlags({
12456
+ "launch-id": safeObject(currentSnapshot.launch).launch_id,
12457
+ });
12458
+ nextStatusMessage = `stopped ${ensureArray(stopPayload.stopped).map((item) => String(item.launch_id || "").trim()).filter(Boolean).join(", ") || "runner launch"}`;
12459
+ } else {
12460
+ nextStatusMessage = "no detached runner was active for this selection";
12461
+ }
12462
+ }
12463
+ if (ensureStarted) {
12464
+ if (state.auditResult.applyFailureBlocksStart) {
12465
+ throw new Error(String(safeObject(state.auditResult.applyResult).error || "runner project tui could not apply runner routes").trim());
12466
+ }
12467
+ if (!ensureArray(state.auditResult.matchingRoutes).length) {
12468
+ throw new Error("runner project tui did not find any enabled routes for the selected project destination");
12469
+ }
12470
+ const launchPayload = await runRunnerStartDetachedResolvedRoutes(
12471
+ state.auditResult.matchingRoutes,
12472
+ state.auditResult.startFlags,
12473
+ "runner project tui",
12474
+ { silent: true },
12475
+ );
12476
+ nextStatusMessage = launchPayload.already_running
12477
+ ? `detached runner already running: ${String(safeObject(launchPayload.launch).launch_id || "").trim() || "-"}`
12478
+ : `detached runner started: ${String(safeObject(launchPayload.launch).launch_id || "").trim() || "-"}`;
12479
+ }
12480
+ state.snapshot = buildRunnerProjectTUISnapshot({
12481
+ auditResult: state.auditResult,
12482
+ autoStart,
12483
+ statusMessage: nextStatusMessage,
12484
+ errorMessage: nextErrorMessage,
12485
+ lastAuditAt: new Date().toISOString(),
12486
+ });
12487
+ } catch (err) {
12488
+ state.snapshot = buildRunnerProjectTUISnapshot({
12489
+ auditResult: state.auditResult || {
12490
+ summaryPayload: {
12491
+ project_id: String(auditFlags["project-id"] || auditFlags.project_id || "").trim(),
12492
+ destination_label: String(auditFlags["destination-label"] || auditFlags.destination_label || "").trim(),
12493
+ destination_id: String(auditFlags["destination-id"] || auditFlags.destination_id || "").trim(),
12494
+ },
12495
+ matchingRoutes: [],
12496
+ },
12497
+ autoStart,
12498
+ statusMessage,
12499
+ errorMessage: String(err?.message || err).trim(),
12500
+ lastAuditAt: new Date().toISOString(),
12501
+ });
12502
+ } finally {
12503
+ state.busy = false;
12504
+ render(false);
12505
+ }
12506
+ };
12507
+
12508
+ const queueRefresh = (options = {}) => {
12509
+ void refreshSnapshot(options);
12510
+ };
12511
+
12512
+ try {
12513
+ await refreshSnapshot({
12514
+ reAudit: true,
12515
+ ensureStarted: autoStart,
12516
+ statusMessage: autoStart
12517
+ ? "audited routes and ensured detached runner is running"
12518
+ : "audited routes in prepare-only mode",
12519
+ });
12520
+
12521
+ intervalHandle = setInterval(() => {
12522
+ queueRefresh({ reAudit: false, ensureStarted: false, statusMessage: "status refreshed from detached registry" });
12523
+ }, 2000);
12524
+
12525
+ process.stdin.setRawMode(true);
12526
+ process.stdin.resume();
12527
+ stdinHandler = (chunk) => {
12528
+ const key = String(chunk || "");
12529
+ if (key === "\u0003" || key.toLowerCase() === "q") {
12530
+ state.disposed = true;
12531
+ if (intervalHandle) clearInterval(intervalHandle);
12532
+ render(true);
12533
+ process.stdin.setRawMode(false);
12534
+ process.stdin.pause();
12535
+ process.stdin.off("data", stdinHandler);
12536
+ process.stdout.write("\u001b[?25h\n");
12537
+ return;
12538
+ }
12539
+ if (key.toLowerCase() === "r") {
12540
+ queueRefresh({ reAudit: false, ensureStarted: false, statusMessage: "status refreshed from detached registry" });
12541
+ return;
12542
+ }
12543
+ if (key.toLowerCase() === "a") {
12544
+ queueRefresh({ reAudit: true, ensureStarted: false, statusMessage: "route audit refreshed" });
12545
+ return;
12546
+ }
12547
+ if (key.toLowerCase() === "s") {
12548
+ queueRefresh({ reAudit: false, ensureStarted: true, statusMessage: "ensuring detached runner is active" });
12549
+ return;
12550
+ }
12551
+ if (key.toLowerCase() === "x") {
12552
+ queueRefresh({ reAudit: false, ensureStarted: false, stopRunner: true, statusMessage: "stopping detached runner" });
12553
+ }
12554
+ };
12555
+ process.stdin.on("data", stdinHandler);
12556
+ await new Promise((resolve) => {
12557
+ const waitForDispose = setInterval(() => {
12558
+ if (!state.disposed) return;
12559
+ clearInterval(waitForDispose);
12560
+ resolve();
12561
+ }, 100);
12562
+ });
12563
+ } finally {
12564
+ if (intervalHandle) clearInterval(intervalHandle);
12565
+ if (stdinHandler) {
12566
+ try {
12567
+ process.stdin.off("data", stdinHandler);
12568
+ } catch {}
12569
+ }
12570
+ if (process.stdin?.isTTY) {
12571
+ try { process.stdin.setRawMode(false); } catch {}
12572
+ try { process.stdin.pause(); } catch {}
12573
+ }
12574
+ if (!state.disposed) {
12575
+ render(true);
12576
+ process.stdout.write("\u001b[?25h\n");
12577
+ }
12578
+ }
12579
+ }
12580
+
12280
12581
  function canStartRunnerDespiteProjectUpApplyFailure({ applyRequested, applyResult, matchingRoutes }) {
12281
12582
  if (!applyRequested) return false;
12282
12583
  const normalizedApplyResult = safeObject(applyResult);
@@ -13055,30 +13356,33 @@ async function runRunnerStop(flags) {
13055
13356
  }
13056
13357
  }
13057
13358
 
13058
- async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand = "runner start-detached") {
13059
- const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
13060
- const existing = findExistingDetachedRunnerLaunch(registry, routes);
13061
- if (existing) {
13062
- const payload = {
13063
- ok: true,
13359
+ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand = "runner start-detached", options = {}) {
13360
+ const silent = boolFromRaw(safeObject(options).silent, false);
13361
+ const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
13362
+ const existing = findExistingDetachedRunnerLaunch(registry, routes);
13363
+ if (existing) {
13364
+ const payload = {
13365
+ ok: true,
13064
13366
  already_running: true,
13065
13367
  registry_file: registry.filePath,
13066
13368
  launch: {
13067
- ...existing,
13068
- alive: true,
13069
- },
13070
- };
13071
- if (boolFromRaw(flags.json, false)) {
13072
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
13073
- return payload;
13074
- }
13075
- process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}\n`);
13076
- return payload;
13077
- }
13078
- const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
13079
- const nextLaunches = {
13080
- ...safeObject(registry).launches,
13081
- [launch.launch_id]: launch,
13369
+ ...existing,
13370
+ alive: true,
13371
+ },
13372
+ };
13373
+ if (!silent) {
13374
+ if (boolFromRaw(flags.json, false)) {
13375
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
13376
+ return payload;
13377
+ }
13378
+ process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}\n`);
13379
+ }
13380
+ return payload;
13381
+ }
13382
+ const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
13383
+ const nextLaunches = {
13384
+ ...safeObject(registry).launches,
13385
+ [launch.launch_id]: launch,
13082
13386
  };
13083
13387
  saveBotRunnerProcessRegistry({ launches: nextLaunches });
13084
13388
  const payload = {
@@ -13086,29 +13390,31 @@ async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand
13086
13390
  already_running: false,
13087
13391
  registry_file: registry.filePath,
13088
13392
  launch: {
13089
- ...launch,
13090
- alive: true,
13091
- },
13092
- };
13093
- if (boolFromRaw(flags.json, false)) {
13094
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
13095
- return payload;
13096
- }
13097
- process.stdout.write(
13098
- [
13099
- "Detached runner start: OK",
13100
- `launch_id: ${launch.launch_id}`,
13101
- `pid: ${launch.pid}`,
13102
- `registry_file: ${registry.filePath}`,
13103
- `project_ids: ${launch.project_ids.join(", ") || "-"}`,
13104
- `route_names: ${launch.route_names.join(", ") || "-"}`,
13105
- `destination_labels: ${launch.destination_labels.join(", ") || "-"}`,
13106
- `log_file: ${launch.log_file || "-"}`,
13107
- `command: ${launch.command}`,
13108
- ].join("\n") + "\n",
13109
- );
13110
- return payload;
13111
- }
13393
+ ...launch,
13394
+ alive: true,
13395
+ },
13396
+ };
13397
+ if (!silent) {
13398
+ if (boolFromRaw(flags.json, false)) {
13399
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
13400
+ return payload;
13401
+ }
13402
+ process.stdout.write(
13403
+ [
13404
+ "Detached runner start: OK",
13405
+ `launch_id: ${launch.launch_id}`,
13406
+ `pid: ${launch.pid}`,
13407
+ `registry_file: ${registry.filePath}`,
13408
+ `project_ids: ${launch.project_ids.join(", ") || "-"}`,
13409
+ `route_names: ${launch.route_names.join(", ") || "-"}`,
13410
+ `destination_labels: ${launch.destination_labels.join(", ") || "-"}`,
13411
+ `log_file: ${launch.log_file || "-"}`,
13412
+ `command: ${launch.command}`,
13413
+ ].join("\n") + "\n",
13414
+ );
13415
+ }
13416
+ return payload;
13417
+ }
13112
13418
 
13113
13419
  function stopDetachedRunnerWithFlags(flags) {
13114
13420
  const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
@@ -13141,10 +13447,10 @@ function stopDetachedRunnerWithFlags(flags) {
13141
13447
  };
13142
13448
  }
13143
13449
 
13144
- function startDetachedRunnerWithFlags(flags, sourceCommand = "runner.start_detached") {
13145
- const routes = resolveRunnerRoutes(flags, "start");
13146
- return runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand);
13147
- }
13450
+ function startDetachedRunnerWithFlags(flags, sourceCommand = "runner.start_detached", options = {}) {
13451
+ const routes = resolveRunnerRoutes(flags, "start");
13452
+ return runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand, options);
13453
+ }
13148
13454
 
13149
13455
  async function runRunnerStartDetached(flags) {
13150
13456
  await startDetachedRunnerWithFlags(flags, "runner start-detached");
@@ -14284,19 +14590,23 @@ async function runRunner(argv) {
14284
14590
  }
14285
14591
  throw new Error("runner route requires a subcommand: list | add | edit | remove");
14286
14592
  }
14287
- if (subcommand === "project" || subcommand === "projects") {
14593
+ if (subcommand === "project" || subcommand === "projects") {
14288
14594
  const [projectSubcommandRaw = "", ...projectRest] = rest;
14289
14595
  const projectSubcommand = String(projectSubcommandRaw || "").trim().toLowerCase();
14290
14596
  const projectArgv = !projectSubcommand || projectSubcommand.startsWith("-")
14291
14597
  ? [projectSubcommandRaw, ...projectRest].filter((value) => String(value || "").trim())
14292
14598
  : projectRest;
14293
- const projectFlags = parseArgs(projectArgv);
14294
- if (projectSubcommand === "up") {
14295
- await runRunnerProjectUp(projectFlags);
14296
- return;
14297
- }
14298
- throw new Error("runner project requires a subcommand: up");
14299
- }
14599
+ const projectFlags = parseArgs(projectArgv);
14600
+ if (projectSubcommand === "up") {
14601
+ await runRunnerProjectUp(projectFlags);
14602
+ return;
14603
+ }
14604
+ if (projectSubcommand === "tui") {
14605
+ await runRunnerProjectTUI(projectFlags);
14606
+ return;
14607
+ }
14608
+ throw new Error("runner project requires a subcommand: up | tui");
14609
+ }
14300
14610
  if (subcommand === "artifact" || subcommand === "artifacts") {
14301
14611
  const [artifactSubcommandRaw = "", ...artifactRest] = rest;
14302
14612
  const artifactSubcommand = String(artifactSubcommandRaw || "").trim().toLowerCase();
@@ -14339,8 +14649,8 @@ async function runRunner(argv) {
14339
14649
  await runRunnerStop(flags);
14340
14650
  return;
14341
14651
  }
14342
- throw new Error("runner requires a subcommand: list | show | status | once | start | start-detached | stop | route | project | artifact");
14343
- }
14652
+ throw new Error("runner requires a subcommand: list | show | status | once | start | start-detached | stop | route | project | artifact");
14653
+ }
14344
14654
 
14345
14655
  async function runLocalBotBridge(argv) {
14346
14656
  const helperPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "scripts", "local-bot-ai-bridge.mjs");
@@ -19485,6 +19795,41 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
19485
19795
  push("runner_project_up_tool_flag_accepts_start_detached_snake_case", false, String(err?.message || err));
19486
19796
  }
19487
19797
 
19798
+ try {
19799
+ const projectTUIRuntime = resolveRunnerProjectTUIRuntimePolicy({
19800
+ "project-id": selftestProjectID,
19801
+ "destination-label": "Selftest Room",
19802
+ });
19803
+ push(
19804
+ "runner_project_tui_defaults_to_detached_bootstrap_mode",
19805
+ projectTUIRuntime.autoStart === true
19806
+ && projectTUIRuntime.auditFlags.start === false
19807
+ && projectTUIRuntime.auditFlags["start-detached"] === false
19808
+ && projectTUIRuntime.auditFlags.tui === false
19809
+ && projectTUIRuntime.auditFlags.json === true,
19810
+ `autoStart=${String(projectTUIRuntime.autoStart)} start=${String(projectTUIRuntime.auditFlags.start)} detached=${String(projectTUIRuntime.auditFlags["start-detached"])} tui=${String(projectTUIRuntime.auditFlags.tui)} json=${String(projectTUIRuntime.auditFlags.json)}`,
19811
+ );
19812
+ } catch (err) {
19813
+ push("runner_project_tui_defaults_to_detached_bootstrap_mode", false, String(err?.message || err));
19814
+ }
19815
+
19816
+ try {
19817
+ const projectTUIRuntime = resolveRunnerProjectTUIRuntimePolicy({
19818
+ "project-id": selftestProjectID,
19819
+ "destination-label": "Selftest Room",
19820
+ start: false,
19821
+ });
19822
+ push(
19823
+ "runner_project_tui_respects_prepare_only_flag",
19824
+ projectTUIRuntime.autoStart === false
19825
+ && projectTUIRuntime.auditFlags.start === false
19826
+ && projectTUIRuntime.auditFlags["start-detached"] === false,
19827
+ `autoStart=${String(projectTUIRuntime.autoStart)} start=${String(projectTUIRuntime.auditFlags.start)} detached=${String(projectTUIRuntime.auditFlags["start-detached"])}`,
19828
+ );
19829
+ } catch (err) {
19830
+ push("runner_project_tui_respects_prepare_only_flag", false, String(err?.message || err));
19831
+ }
19832
+
19488
19833
  try {
19489
19834
  const projectUpResponse = await handleLocalProjectToolDispatchImpl(
19490
19835
  {
@@ -2408,6 +2408,7 @@ function buildConversationIntentAnalysisPrompt({
2408
2408
  "- allowed_responders must be the full set of bots allowed to speak in this conversation.",
2409
2409
  "- If mode is single_bot, initial_responders and allowed_responders should contain only that bot.",
2410
2410
  "- If mode is delegated_single_lead, lead_bot must be set and initial_responders should contain only the lead bot.",
2411
+ "- If the human tells one managed bot to say, greet, mention, introduce, or relay something to another managed bot, use delegated_single_lead. The instructed bot is lead_bot, initial_responders must contain only that bot, and the target bot belongs in allowed_responders rather than initial_responders.",
2411
2412
  "- If the human explicitly asks one bot to summarize or finalize, set summary_bot to that bot.",
2412
2413
  "- intent_type is optional secondary metadata, not the main control surface. Omit the key entirely unless a more specific lookup or execution subtype is clearly explicit.",
2413
2414
  "- If the human is only greeting or chatting socially, intent_type may be small_talk if you choose to include it.",
@@ -93,6 +93,70 @@ function detectDirectedManagedReplyTargetV2({
93
93
  return "";
94
94
  }
95
95
 
96
+ function normalizeDirectedManagedLeadHumanIntent({
97
+ text,
98
+ humanIntent,
99
+ managedMentions = [],
100
+ normalizeMentionSelector,
101
+ uniqueOrdered,
102
+ }) {
103
+ const base = safeObject(humanIntent);
104
+ const participants = uniqueOrdered([
105
+ ...ensureArray(base.participantSelectors || base.participants),
106
+ ...ensureArray(managedMentions),
107
+ ].map((item) => normalizeMentionSelector(item)).filter(Boolean));
108
+ if (participants.length < 2) {
109
+ return base;
110
+ }
111
+ const normalizedText = String(text || "").trim();
112
+ if (!normalizedText) {
113
+ return base;
114
+ }
115
+ const instructionPattern = new RegExp(DIRECTED_MANAGED_REPLY_ACTION_PATTERN, "iu");
116
+ if (!instructionPattern.test(normalizedText)) {
117
+ return base;
118
+ }
119
+ const explicitLeadBotSelector = normalizeMentionSelector(
120
+ base.leadBotSelector || base.lead_bot,
121
+ );
122
+ const leadBotSelector = explicitLeadBotSelector && participants.includes(explicitLeadBotSelector)
123
+ ? explicitLeadBotSelector
124
+ : participants[0] || "";
125
+ if (!leadBotSelector) {
126
+ return base;
127
+ }
128
+ const replyTargetBotSelector = detectDirectedManagedReplyTargetV2({
129
+ text: normalizedText,
130
+ currentBotSelector: leadBotSelector,
131
+ managedMentions: participants,
132
+ });
133
+ if (!replyTargetBotSelector || replyTargetBotSelector === leadBotSelector) {
134
+ return base;
135
+ }
136
+ const allowedResponderSelectors = uniqueOrdered([
137
+ ...ensureArray(base.allowedResponderSelectors || base.allowed_responders),
138
+ ...participants,
139
+ leadBotSelector,
140
+ replyTargetBotSelector,
141
+ ].map((item) => normalizeMentionSelector(item)).filter(Boolean));
142
+ const summaryBotSelector = normalizeMentionSelector(
143
+ base.summaryBotSelector || base.summary_bot,
144
+ );
145
+ return {
146
+ ...base,
147
+ intentMode: "delegated_single_lead",
148
+ allowBotToBot: true,
149
+ participantSelectors: participants,
150
+ initialResponderSelectors: [leadBotSelector],
151
+ allowedResponderSelectors,
152
+ leadBotSelector,
153
+ summaryBotSelector: summaryBotSelector && allowedResponderSelectors.includes(summaryBotSelector)
154
+ ? summaryBotSelector
155
+ : "",
156
+ replyTargetBotSelector,
157
+ };
158
+ }
159
+
96
160
  export async function resolveHumanIntentContext({
97
161
  selectedRecord,
98
162
  normalizedRoute,
@@ -160,17 +224,13 @@ export async function resolveHumanIntentContext({
160
224
  runnerHumanIntentPromises.set(cacheKey, promise);
161
225
  }
162
226
  let humanIntent = await runnerHumanIntentPromises.get(cacheKey);
163
- const directedReplyTargetSelector = detectDirectedManagedReplyTargetV2({
227
+ humanIntent = normalizeDirectedManagedLeadHumanIntent({
164
228
  text: parsed.body,
165
- currentBotSelector,
229
+ humanIntent,
166
230
  managedMentions,
231
+ normalizeMentionSelector,
232
+ uniqueOrdered,
167
233
  });
168
- if (directedReplyTargetSelector) {
169
- humanIntent = {
170
- ...safeObject(humanIntent),
171
- replyTargetBotSelector: directedReplyTargetSelector,
172
- };
173
- }
174
234
  if (!isCompleteHumanIntentContract(humanIntent)) {
175
235
  return {
176
236
  currentBotSelector,
@@ -4171,9 +4171,76 @@ export async function runSelftestRunnerScenarios(push, deps) {
4171
4171
  );
4172
4172
  }
4173
4173
 
4174
+ try {
4175
+ const promotedDirectedLeadIntentContext = await resolveHumanIntentContext({
4176
+ selectedRecord: {
4177
+ id: "comment-directed-lead-korean",
4178
+ parsedArchive: {
4179
+ kind: "telegram_message",
4180
+ body: "@RyoAI_bot 너가 @SangHoon01_bot 인사 시켜봐",
4181
+ senderIsBot: false,
4182
+ },
4183
+ },
4184
+ normalizedRoute: {
4185
+ name: "telegram-monitor-ryoai-bot-2",
4186
+ },
4187
+ bot: {
4188
+ username: "ryoai_bot",
4189
+ name: "RyoAI_bot",
4190
+ },
4191
+ executionPlan: {},
4192
+ deps: {
4193
+ resolveConversationPeerBots: () => [
4194
+ { id: "bot-self-1", name: "RyoAI_bot" },
4195
+ { id: "bot-peer-1", name: "SangHoon01_bot" },
4196
+ ],
4197
+ },
4198
+ intentDeps: {
4199
+ normalizeMentionSelector: normalizeSelftestMentionSelector,
4200
+ buildConversationPeerMap: (_bot, _route, runtimeDeps) => new Map(
4201
+ ensureArray(runtimeDeps?.resolveConversationPeerBots?.() || []).map((item) => {
4202
+ const selector = normalizeSelftestMentionSelector(item?.name || item?.username);
4203
+ return [selector, item];
4204
+ }).filter(([selector]) => selector),
4205
+ ),
4206
+ extractOrderedMentionSelectors: (text) => Array.from(String(text || "").matchAll(/@([A-Za-z0-9_]+)/g)).map((match) => normalizeSelftestMentionSelector(match[1] || "")),
4207
+ uniqueOrdered: normalizeSelftestConversationSelectorList,
4208
+ buildHumanIntentFromPersistedRunnerRequest: () => null,
4209
+ buildRunnerHumanIntentCacheKey: () => "directed-lead-korean",
4210
+ runnerHumanIntentPromises: new Map(),
4211
+ analyzeHumanConversationIntentWithContractResolver: async () => ({
4212
+ mode: "multi_bot_direct",
4213
+ participants: ["ryoai_bot", "sanghoon01_bot"],
4214
+ initial_responders: ["ryoai_bot", "sanghoon01_bot"],
4215
+ allowed_responders: ["ryoai_bot", "sanghoon01_bot"],
4216
+ reply_expectation: "actionable",
4217
+ intent_type: "actionable_request",
4218
+ }),
4219
+ scheduleRunnerHumanIntentCacheCleanup: () => {},
4220
+ isCompleteHumanIntentContract: () => true,
4221
+ normalizeHumanIntentType: (value, fallback = "") => String(value || "").trim() || fallback,
4222
+ },
4223
+ });
4224
+ push(
4225
+ "runner_human_intent_context_promotes_directed_multi_bot_direct_to_delegated_single_lead",
4226
+ String(promotedDirectedLeadIntentContext?.humanIntent?.intentMode || promotedDirectedLeadIntentContext?.humanIntent?.mode || "") === "delegated_single_lead"
4227
+ && String(promotedDirectedLeadIntentContext?.humanIntent?.leadBotSelector || "") === "ryoai_bot"
4228
+ && JSON.stringify(ensureArray(promotedDirectedLeadIntentContext?.humanIntent?.initialResponderSelectors || [])) === JSON.stringify(["ryoai_bot"])
4229
+ && JSON.stringify(ensureArray(promotedDirectedLeadIntentContext?.humanIntent?.allowedResponderSelectors || [])) === JSON.stringify(["ryoai_bot", "sanghoon01_bot"])
4230
+ && String(promotedDirectedLeadIntentContext?.humanIntent?.replyTargetBotSelector || "") === "sanghoon01_bot",
4231
+ `mode=${String(promotedDirectedLeadIntentContext?.humanIntent?.intentMode || promotedDirectedLeadIntentContext?.humanIntent?.mode || "(none)")} lead=${String(promotedDirectedLeadIntentContext?.humanIntent?.leadBotSelector || "(none)")} initial=${JSON.stringify(ensureArray(promotedDirectedLeadIntentContext?.humanIntent?.initialResponderSelectors || []))} allowed=${JSON.stringify(ensureArray(promotedDirectedLeadIntentContext?.humanIntent?.allowedResponderSelectors || []))} target=${String(promotedDirectedLeadIntentContext?.humanIntent?.replyTargetBotSelector || "(none)")}`,
4232
+ );
4233
+ } catch (err) {
4234
+ push(
4235
+ "runner_human_intent_context_promotes_directed_multi_bot_direct_to_delegated_single_lead",
4236
+ false,
4237
+ String(err?.message || err),
4238
+ );
4239
+ }
4240
+
4174
4241
  const mentionOverridesReplyForUnmentionedBot = evaluateTelegramRunnerTrigger(
4175
- {
4176
- id: "comment-2b",
4242
+ {
4243
+ id: "comment-2b",
4177
4244
  parsedArchive: {
4178
4245
  kind: "telegram_message",
4179
4246
  chatID: "-100123",
@@ -7704,7 +7771,10 @@ export async function runSelftestRunnerScenarios(push, deps) {
7704
7771
  && plannerCalls === 0
7705
7772
  && deliveryCalls === 0
7706
7773
  && String(processed.result?.outcome || "") === "invalid_ai_output"
7707
- && String(processed.result?.response_contract_validation_status || "") === "reply_target_not_visible_in_reply",
7774
+ && [
7775
+ "reply_target_not_visible_in_reply",
7776
+ "missing_required_contract",
7777
+ ].includes(String(processed.result?.response_contract_validation_status || "")),
7708
7778
  `kind=${String(processed.kind || "(none)")} ai_calls=${aiCalls} planner_calls=${plannerCalls} delivery_calls=${deliveryCalls} delivered=${String(deliveredText || "(none)")} validation=${String(processed.result?.response_contract_validation_status || "(none)")}`,
7709
7779
  );
7710
7780
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.291",
3
+ "version": "0.2.292",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [