metheus-governance-mcp-cli 0.2.291 → 0.2.293
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 +2 -0
- package/cli.mjs +887 -125
- package/lib/local-ai-adapters.mjs +1 -0
- package/lib/runner-orchestration-intent-contracts.mjs +68 -8
- package/lib/selftest-runner-scenarios.mjs +73 -3
- package/package.json +1 -1
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
|
|
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>]`,
|
|
@@ -7500,26 +7501,31 @@ async function syncRunnerRequestLedgerForProjectToServer({ normalizedRoute, runt
|
|
|
7500
7501
|
}
|
|
7501
7502
|
}
|
|
7502
7503
|
|
|
7503
|
-
function normalizeRunnerProcessLaunchEntry(rawEntry) {
|
|
7504
|
-
const entry = safeObject(rawEntry);
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7504
|
+
function normalizeRunnerProcessLaunchEntry(rawEntry) {
|
|
7505
|
+
const entry = safeObject(rawEntry);
|
|
7506
|
+
const routeKeys = ensureArray(entry.route_keys || entry.routeKeys).map((item) => String(item || "").trim()).filter(Boolean);
|
|
7507
|
+
return cleanupRunnerStateRecord({
|
|
7508
|
+
launch_id: String(entry.launch_id || entry.launchID || "").trim(),
|
|
7509
|
+
pid: intFromRawAllowZero(entry.pid, 0) || undefined,
|
|
7510
|
+
started_at: String(entry.started_at || entry.startedAt || "").trim(),
|
|
7511
|
+
command: String(entry.command || "").trim(),
|
|
7512
|
+
cli_path: String(entry.cli_path || entry.cliPath || "").trim(),
|
|
7513
|
+
working_directory: sanitizeWorkspaceCandidate(entry.working_directory || entry.workingDirectory) || String(entry.working_directory || entry.workingDirectory || "").trim(),
|
|
7514
|
+
route_set_signature: String(entry.route_set_signature || entry.routeSetSignature || "").trim(),
|
|
7515
|
+
route_keys: routeKeys,
|
|
7516
|
+
route_names: ensureArray(entry.route_names || entry.routeNames).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7517
|
+
project_ids: ensureArray(entry.project_ids || entry.projectIds).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7518
|
+
provider: normalizeBotProvider(entry.provider),
|
|
7519
|
+
destination_labels: ensureArray(entry.destination_labels || entry.destinationLabels).map((item) => String(item || "").trim()).filter(Boolean),
|
|
7520
|
+
scheduling_target_keys: uniqueOrderedStrings([
|
|
7521
|
+
...ensureArray(entry.scheduling_target_keys || entry.schedulingTargetKeys).map((item) => String(item || "").trim()),
|
|
7522
|
+
...routeKeys.map((routeKey) => runnerSchedulingTargetKeyFromRouteKey(routeKey)).filter(Boolean),
|
|
7523
|
+
], (value) => String(value || "").trim()),
|
|
7524
|
+
log_file: String(entry.log_file || entry.logFile || "").trim(),
|
|
7525
|
+
created_by_pid: intFromRawAllowZero(entry.created_by_pid || entry.createdByPid, 0) || undefined,
|
|
7526
|
+
source_command: String(entry.source_command || entry.sourceCommand || "").trim(),
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7523
7529
|
|
|
7524
7530
|
function normalizeBotRunnerProcessRegistryContents(rawValue) {
|
|
7525
7531
|
const launchesInput = safeObject(rawValue).launches;
|
|
@@ -12203,8 +12209,8 @@ function resolveRunnerProjectUpRoutes({
|
|
|
12203
12209
|
});
|
|
12204
12210
|
}
|
|
12205
12211
|
|
|
12206
|
-
async function runRunnerProjectUp(flags) {
|
|
12207
|
-
const result = await buildRunnerProjectUpResult(flags);
|
|
12212
|
+
async function runRunnerProjectUp(flags) {
|
|
12213
|
+
const result = await buildRunnerProjectUpResult(flags);
|
|
12208
12214
|
const {
|
|
12209
12215
|
summaryPayload,
|
|
12210
12216
|
applyRequested,
|
|
@@ -12267,16 +12273,440 @@ async function runRunnerProjectUp(flags) {
|
|
|
12267
12273
|
await runRunnerStartDetachedResolvedRoutes(matchingRoutes, startFlags, "runner project up");
|
|
12268
12274
|
return;
|
|
12269
12275
|
}
|
|
12270
|
-
await runRunnerStartResolvedRoutes(matchingRoutes, startFlags, {
|
|
12271
|
-
sourceLabel: "runner project up",
|
|
12272
|
-
bootstrapEvent: {
|
|
12273
|
-
route_name: "runner project up",
|
|
12276
|
+
await runRunnerStartResolvedRoutes(matchingRoutes, startFlags, {
|
|
12277
|
+
sourceLabel: "runner project up",
|
|
12278
|
+
bootstrapEvent: {
|
|
12279
|
+
route_name: "runner project up",
|
|
12274
12280
|
outcome: "prepared",
|
|
12275
12281
|
detail: `prepared ${matchingRoutes.length} enabled route(s) for ${summaryPayload.destination_label || summaryPayload.destination_id || summaryPayload.project_id || "project"}`,
|
|
12276
|
-
},
|
|
12277
|
-
});
|
|
12278
|
-
}
|
|
12279
|
-
|
|
12282
|
+
},
|
|
12283
|
+
});
|
|
12284
|
+
}
|
|
12285
|
+
|
|
12286
|
+
function resolveRunnerProjectTUIRuntimePolicy(flags = {}) {
|
|
12287
|
+
const explicitStartRequested = Object.prototype.hasOwnProperty.call(flags, "start");
|
|
12288
|
+
return {
|
|
12289
|
+
autoStart: explicitStartRequested
|
|
12290
|
+
? boolFromRaw(flags.start, true)
|
|
12291
|
+
: true,
|
|
12292
|
+
auditFlags: {
|
|
12293
|
+
...safeObject(flags),
|
|
12294
|
+
provider: String(flags.provider || "").trim()
|
|
12295
|
+
? normalizeBotProvider(flags.provider)
|
|
12296
|
+
: "telegram",
|
|
12297
|
+
start: false,
|
|
12298
|
+
"start-detached": false,
|
|
12299
|
+
tui: false,
|
|
12300
|
+
json: true,
|
|
12301
|
+
},
|
|
12302
|
+
};
|
|
12303
|
+
}
|
|
12304
|
+
|
|
12305
|
+
function readTextFileTailLines(filePath = "", maxLines = 12) {
|
|
12306
|
+
const normalizedPath = String(filePath || "").trim();
|
|
12307
|
+
if (!normalizedPath) return [];
|
|
12308
|
+
try {
|
|
12309
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
12310
|
+
return ["(log file not created yet)"];
|
|
12311
|
+
}
|
|
12312
|
+
const lines = String(fs.readFileSync(normalizedPath, "utf8") || "")
|
|
12313
|
+
.split(/\r?\n/)
|
|
12314
|
+
.map((line) => String(line || "").trimEnd());
|
|
12315
|
+
return lines.filter((line) => line.length > 0).slice(-Math.max(1, intFromRaw(maxLines, 12)));
|
|
12316
|
+
} catch (err) {
|
|
12317
|
+
return [`(failed to read log: ${String(err?.message || err).trim()})`];
|
|
12318
|
+
}
|
|
12319
|
+
}
|
|
12320
|
+
|
|
12321
|
+
function buildRunnerProjectTUISnapshot({
|
|
12322
|
+
auditResult,
|
|
12323
|
+
autoStart = true,
|
|
12324
|
+
statusMessage = "",
|
|
12325
|
+
errorMessage = "",
|
|
12326
|
+
lastAuditAt = "",
|
|
12327
|
+
}) {
|
|
12328
|
+
const normalizedAuditResult = safeObject(auditResult);
|
|
12329
|
+
const summaryPayload = safeObject(normalizedAuditResult.summaryPayload);
|
|
12330
|
+
const matchingRoutes = ensureArray(normalizedAuditResult.matchingRoutes)
|
|
12331
|
+
.map((route) => normalizeRunnerRoute(route));
|
|
12332
|
+
const matchingRouteNames = matchingRoutes
|
|
12333
|
+
.map((route) => String(route.name || runnerRouteKey(route)).trim())
|
|
12334
|
+
.filter(Boolean);
|
|
12335
|
+
const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
|
|
12336
|
+
const existingLaunch = matchingRoutes.length
|
|
12337
|
+
? findExistingDetachedRunnerLaunch(registry, matchingRoutes)
|
|
12338
|
+
: null;
|
|
12339
|
+
const launch = existingLaunch
|
|
12340
|
+
? {
|
|
12341
|
+
...normalizeRunnerProcessLaunchEntry(existingLaunch),
|
|
12342
|
+
alive: true,
|
|
12343
|
+
}
|
|
12344
|
+
: null;
|
|
12345
|
+
const destinationID = String(summaryPayload.destination_id || "").trim();
|
|
12346
|
+
const destinationLabel = String(summaryPayload.destination_label || "").trim();
|
|
12347
|
+
const projectID = String(summaryPayload.project_id || "").trim();
|
|
12348
|
+
const relatedLaunches = Object.values(safeObject(registry).launches || {})
|
|
12349
|
+
.map((entry) => normalizeRunnerProcessLaunchEntry(entry))
|
|
12350
|
+
.filter((entry) => isProcessAlive(entry.pid))
|
|
12351
|
+
.filter((entry) => {
|
|
12352
|
+
const sameProject = ensureArray(entry.project_ids)
|
|
12353
|
+
.map((item) => String(item || "").trim())
|
|
12354
|
+
.includes(projectID);
|
|
12355
|
+
if (!sameProject) return false;
|
|
12356
|
+
const sameDestinationLabel = destinationLabel
|
|
12357
|
+
? ensureArray(entry.destination_labels)
|
|
12358
|
+
.map((item) => String(item || "").trim().toLowerCase())
|
|
12359
|
+
.includes(destinationLabel.toLowerCase())
|
|
12360
|
+
: false;
|
|
12361
|
+
const sameDestinationID = destinationID
|
|
12362
|
+
? ensureArray(entry.route_keys)
|
|
12363
|
+
.map((item) => String(item || "").trim())
|
|
12364
|
+
.some((item) => item.includes(`::${destinationID}`))
|
|
12365
|
+
: false;
|
|
12366
|
+
return sameDestinationLabel || sameDestinationID;
|
|
12367
|
+
})
|
|
12368
|
+
.map((entry) => {
|
|
12369
|
+
const routeNames = ensureArray(entry.route_names)
|
|
12370
|
+
.map((item) => String(item || "").trim())
|
|
12371
|
+
.filter(Boolean);
|
|
12372
|
+
const sharedRouteNames = routeNames.filter((routeName) => matchingRouteNames.includes(routeName));
|
|
12373
|
+
return {
|
|
12374
|
+
launch_id: String(entry.launch_id || "").trim(),
|
|
12375
|
+
pid: intFromRawAllowZero(entry.pid, 0),
|
|
12376
|
+
route_names: routeNames,
|
|
12377
|
+
shared_route_names: sharedRouteNames,
|
|
12378
|
+
source_command: String(entry.source_command || "").trim(),
|
|
12379
|
+
same_selection: Boolean(launch && String(launch.launch_id || "").trim() === String(entry.launch_id || "").trim()),
|
|
12380
|
+
};
|
|
12381
|
+
});
|
|
12382
|
+
const logFilePath = String(launch?.log_file || "").trim();
|
|
12383
|
+
const logLines = readTextFileTailLines(logFilePath, 12);
|
|
12384
|
+
return {
|
|
12385
|
+
project_id: projectID,
|
|
12386
|
+
destination_label: destinationLabel,
|
|
12387
|
+
destination_id: destinationID,
|
|
12388
|
+
room_probe_ok: Boolean(summaryPayload.room_probe_ok),
|
|
12389
|
+
route_apply_requested: Boolean(summaryPayload.route_apply_requested),
|
|
12390
|
+
route_apply_changed: Boolean(summaryPayload.route_apply_changed),
|
|
12391
|
+
route_config_file: String(summaryPayload.route_config_file || "").trim(),
|
|
12392
|
+
enabled_routes: ensureArray(summaryPayload.enabled_routes_for_selection).map((item) => String(item || "").trim()).filter(Boolean),
|
|
12393
|
+
matching_routes: matchingRouteNames,
|
|
12394
|
+
next_steps: ensureArray(summaryPayload.next_steps).map((item) => String(item || "").trim()).filter(Boolean),
|
|
12395
|
+
warning: String(summaryPayload.warning || "").trim(),
|
|
12396
|
+
error: String(summaryPayload.error || errorMessage || "").trim(),
|
|
12397
|
+
launch,
|
|
12398
|
+
related_launches: relatedLaunches,
|
|
12399
|
+
auto_start: autoStart,
|
|
12400
|
+
status_message: String(statusMessage || "").trim(),
|
|
12401
|
+
last_audit_at: String(lastAuditAt || "").trim() || new Date().toISOString(),
|
|
12402
|
+
log_file: logFilePath,
|
|
12403
|
+
log_lines: logLines.length
|
|
12404
|
+
? logLines
|
|
12405
|
+
: [launch ? "(waiting for runner log output)" : "(no active detached runner)"],
|
|
12406
|
+
};
|
|
12407
|
+
}
|
|
12408
|
+
|
|
12409
|
+
function buildRunnerProjectTUIKeyLine(label, value, labelWidth = 14) {
|
|
12410
|
+
return `${bootstrapPadRight(`${label}:`, labelWidth)} ${String(value || "-").trim() || "-"}`;
|
|
12411
|
+
}
|
|
12412
|
+
|
|
12413
|
+
function buildRunnerProjectTUIList(items = [], {
|
|
12414
|
+
emptyText = "-",
|
|
12415
|
+
bullet = "- ",
|
|
12416
|
+
indent = " ",
|
|
12417
|
+
width = 140,
|
|
12418
|
+
} = {}) {
|
|
12419
|
+
const normalizedItems = ensureArray(items)
|
|
12420
|
+
.map((item) => String(item || "").trim())
|
|
12421
|
+
.filter(Boolean);
|
|
12422
|
+
if (!normalizedItems.length) {
|
|
12423
|
+
return [`${indent}${emptyText}`];
|
|
12424
|
+
}
|
|
12425
|
+
return normalizedItems.map((item) => `${indent}${bullet}${truncateRunnerTUIText(item, width)}`);
|
|
12426
|
+
}
|
|
12427
|
+
|
|
12428
|
+
function buildRunnerProjectTUIFrame({
|
|
12429
|
+
snapshot,
|
|
12430
|
+
now = Date.now(),
|
|
12431
|
+
stopping = false,
|
|
12432
|
+
useColor = false,
|
|
12433
|
+
}) {
|
|
12434
|
+
const normalizedSnapshot = safeObject(snapshot);
|
|
12435
|
+
const alive = Boolean(safeObject(normalizedSnapshot.launch).alive);
|
|
12436
|
+
const statusLabel = alive ? "[ RUNNING ]" : "[ STOPPED ]";
|
|
12437
|
+
const statusDetail = alive
|
|
12438
|
+
? `launch=${String(safeObject(normalizedSnapshot.launch).launch_id || "").trim() || "-"} pid=${String(safeObject(normalizedSnapshot.launch).pid || "").trim() || "-"}`
|
|
12439
|
+
: "no detached runner is active for this selection";
|
|
12440
|
+
const enabledRoutes = ensureArray(normalizedSnapshot.enabled_routes);
|
|
12441
|
+
const matchingRoutes = ensureArray(normalizedSnapshot.matching_routes);
|
|
12442
|
+
const relatedLaunches = ensureArray(normalizedSnapshot.related_launches);
|
|
12443
|
+
const selectedLaunch = safeObject(normalizedSnapshot.launch);
|
|
12444
|
+
const otherLaunches = relatedLaunches.filter((launch) => !safeObject(launch).same_selection);
|
|
12445
|
+
const overlappingLaunches = otherLaunches.filter((launch) => ensureArray(safeObject(launch).shared_route_names).length > 0);
|
|
12446
|
+
const alertLines = [];
|
|
12447
|
+
if (String(normalizedSnapshot.warning || "").trim()) {
|
|
12448
|
+
alertLines.push(`warning: ${String(normalizedSnapshot.warning || "").trim()}`);
|
|
12449
|
+
}
|
|
12450
|
+
if (String(normalizedSnapshot.error || "").trim()) {
|
|
12451
|
+
alertLines.push(`error: ${String(normalizedSnapshot.error || "").trim()}`);
|
|
12452
|
+
}
|
|
12453
|
+
if (overlappingLaunches.length) {
|
|
12454
|
+
alertLines.push(`overlap: ${overlappingLaunches.length} other detached launch(es) share at least one route in this room`);
|
|
12455
|
+
} else if (otherLaunches.length) {
|
|
12456
|
+
alertLines.push(`notice: ${otherLaunches.length} other detached launch(es) are active for this project/destination`);
|
|
12457
|
+
}
|
|
12458
|
+
if (!alive) {
|
|
12459
|
+
alertLines.push("runner is not active; press s to start or reuse a detached runner");
|
|
12460
|
+
}
|
|
12461
|
+
const stateHint = alive
|
|
12462
|
+
? "This screen can be closed with q. The detached runner keeps running."
|
|
12463
|
+
: "No detached runner is attached to this selection yet.";
|
|
12464
|
+
const lines = [
|
|
12465
|
+
"+======================================================================================+",
|
|
12466
|
+
"| METHEUS PROJECT RUNNER |",
|
|
12467
|
+
"| Detached runner dashboard |",
|
|
12468
|
+
"+======================================================================================+",
|
|
12469
|
+
"",
|
|
12470
|
+
"[ STATUS ]",
|
|
12471
|
+
` ${statusLabel}${stopping ? " (closing TUI)" : ""}`,
|
|
12472
|
+
` ${statusDetail}`,
|
|
12473
|
+
` ${stateHint}`,
|
|
12474
|
+
"",
|
|
12475
|
+
"[ PROJECT ]",
|
|
12476
|
+
` ${buildRunnerProjectTUIKeyLine("Project", normalizedSnapshot.project_id)}`,
|
|
12477
|
+
` ${buildRunnerProjectTUIKeyLine("Destination", String(normalizedSnapshot.destination_label || normalizedSnapshot.destination_id || "").trim() || "-")}`,
|
|
12478
|
+
` ${buildRunnerProjectTUIKeyLine("Updated", new Date(now).toLocaleString("sv-SE", { hour12: false }))}`,
|
|
12479
|
+
` ${buildRunnerProjectTUIKeyLine("Last Audit", String(normalizedSnapshot.last_audit_at || "").trim() || "-")}`,
|
|
12480
|
+
"",
|
|
12481
|
+
"[ RUNNER ]",
|
|
12482
|
+
` ${buildRunnerProjectTUIKeyLine("Auto Start", normalizedSnapshot.auto_start ? "enabled" : "prepare-only")}`,
|
|
12483
|
+
` ${buildRunnerProjectTUIKeyLine("Room Probe", normalizedSnapshot.room_probe_ok ? "ok" : "failed")}`,
|
|
12484
|
+
` ${buildRunnerProjectTUIKeyLine("Apply Result", normalizedSnapshot.route_apply_changed ? "route file changed" : "no route file change")}`,
|
|
12485
|
+
` ${buildRunnerProjectTUIKeyLine("Active Launches", `${relatedLaunches.length}`)}`,
|
|
12486
|
+
` ${buildRunnerProjectTUIKeyLine("This Launch", alive ? String(selectedLaunch.launch_id || "-").trim() || "-" : "-")}`,
|
|
12487
|
+
` ${buildRunnerProjectTUIKeyLine("PID", alive ? String(selectedLaunch.pid || "-").trim() || "-" : "-")}`,
|
|
12488
|
+
` ${buildRunnerProjectTUIKeyLine("Log File", String(normalizedSnapshot.log_file || "").trim() || "-")}`,
|
|
12489
|
+
"",
|
|
12490
|
+
"[ ROUTES ]",
|
|
12491
|
+
` ${buildRunnerProjectTUIKeyLine("Enabled", `${enabledRoutes.length}`)}`,
|
|
12492
|
+
` ${buildRunnerProjectTUIKeyLine("Matched", `${matchingRoutes.length}`)}`,
|
|
12493
|
+
...buildRunnerProjectTUIList(matchingRoutes.length ? matchingRoutes : enabledRoutes, {
|
|
12494
|
+
emptyText: "no routes selected",
|
|
12495
|
+
bullet: "- ",
|
|
12496
|
+
indent: " ",
|
|
12497
|
+
width: 96,
|
|
12498
|
+
}),
|
|
12499
|
+
"",
|
|
12500
|
+
"[ ALERTS ]",
|
|
12501
|
+
...buildRunnerProjectTUIList(alertLines, {
|
|
12502
|
+
emptyText: "none",
|
|
12503
|
+
bullet: "- ",
|
|
12504
|
+
indent: " ",
|
|
12505
|
+
width: 96,
|
|
12506
|
+
}),
|
|
12507
|
+
"",
|
|
12508
|
+
"[ CONTROLS ]",
|
|
12509
|
+
" s = start/reuse detached runner",
|
|
12510
|
+
" x = stop detached runner",
|
|
12511
|
+
" r = refresh status",
|
|
12512
|
+
" a = re-audit route selection",
|
|
12513
|
+
" q = quit this dashboard",
|
|
12514
|
+
"",
|
|
12515
|
+
"[ RECENT LOG TAIL ]",
|
|
12516
|
+
...ensureArray(normalizedSnapshot.log_lines).map((line) => ` ${truncateRunnerTUIText(line, 108)}`),
|
|
12517
|
+
];
|
|
12518
|
+
if (otherLaunches.length) {
|
|
12519
|
+
lines.push("");
|
|
12520
|
+
lines.push("[ OTHER ACTIVE LAUNCHES ]");
|
|
12521
|
+
for (const launch of otherLaunches) {
|
|
12522
|
+
const currentLaunch = safeObject(launch);
|
|
12523
|
+
const sharedRoutes = ensureArray(currentLaunch.shared_route_names).join(", ") || "-";
|
|
12524
|
+
lines.push(` - ${String(currentLaunch.launch_id || "-").trim() || "-"} pid=${String(currentLaunch.pid || "-").trim() || "-"} source=${String(currentLaunch.source_command || "-").trim() || "-"}`);
|
|
12525
|
+
lines.push(` shared routes: ${truncateRunnerTUIText(sharedRoutes, 96)}`);
|
|
12526
|
+
lines.push(` all routes: ${truncateRunnerTUIText(ensureArray(currentLaunch.route_names).join(", ") || "-", 96)}`);
|
|
12527
|
+
}
|
|
12528
|
+
}
|
|
12529
|
+
const frame = lines.join("\n");
|
|
12530
|
+
if (!useColor) {
|
|
12531
|
+
return frame;
|
|
12532
|
+
}
|
|
12533
|
+
const statusColor = alive ? "\u001b[32m" : "\u001b[33m";
|
|
12534
|
+
return frame.replace(statusLabel, `${statusColor}${statusLabel}\u001b[0m`);
|
|
12535
|
+
}
|
|
12536
|
+
|
|
12537
|
+
async function runRunnerProjectTUI(flags) {
|
|
12538
|
+
if (!process.stdout?.isTTY || !process.stdin?.isTTY) {
|
|
12539
|
+
throw new Error("runner project tui requires an interactive terminal (TTY)");
|
|
12540
|
+
}
|
|
12541
|
+
const { autoStart, auditFlags } = resolveRunnerProjectTUIRuntimePolicy(flags);
|
|
12542
|
+
const useColor = bootstrapSupportsANSIColors();
|
|
12543
|
+
const state = {
|
|
12544
|
+
auditResult: null,
|
|
12545
|
+
snapshot: null,
|
|
12546
|
+
busy: false,
|
|
12547
|
+
disposed: false,
|
|
12548
|
+
};
|
|
12549
|
+
let intervalHandle = null;
|
|
12550
|
+
let stdinHandler = null;
|
|
12551
|
+
|
|
12552
|
+
const render = (stopping = false) => {
|
|
12553
|
+
if (state.disposed) return;
|
|
12554
|
+
const frame = buildRunnerProjectTUIFrame({
|
|
12555
|
+
snapshot: state.snapshot,
|
|
12556
|
+
now: Date.now(),
|
|
12557
|
+
stopping,
|
|
12558
|
+
useColor,
|
|
12559
|
+
});
|
|
12560
|
+
process.stdout.write(`\u001b[?25l\u001b[2J\u001b[H${frame}`);
|
|
12561
|
+
};
|
|
12562
|
+
|
|
12563
|
+
const refreshSnapshot = async ({
|
|
12564
|
+
reAudit = false,
|
|
12565
|
+
ensureStarted = false,
|
|
12566
|
+
stopRunner = false,
|
|
12567
|
+
statusMessage = "",
|
|
12568
|
+
} = {}) => {
|
|
12569
|
+
if (state.busy) return;
|
|
12570
|
+
state.busy = true;
|
|
12571
|
+
try {
|
|
12572
|
+
if (reAudit || !state.auditResult) {
|
|
12573
|
+
state.auditResult = await buildRunnerProjectUpResult(auditFlags);
|
|
12574
|
+
}
|
|
12575
|
+
let nextStatusMessage = String(statusMessage || "").trim();
|
|
12576
|
+
let nextErrorMessage = "";
|
|
12577
|
+
if (stopRunner) {
|
|
12578
|
+
const currentSnapshot = buildRunnerProjectTUISnapshot({
|
|
12579
|
+
auditResult: state.auditResult,
|
|
12580
|
+
autoStart,
|
|
12581
|
+
statusMessage: nextStatusMessage || "stopping detached runner",
|
|
12582
|
+
});
|
|
12583
|
+
if (safeObject(currentSnapshot.launch).launch_id) {
|
|
12584
|
+
const stopPayload = stopDetachedRunnerWithFlags({
|
|
12585
|
+
"launch-id": safeObject(currentSnapshot.launch).launch_id,
|
|
12586
|
+
});
|
|
12587
|
+
nextStatusMessage = `stopped ${ensureArray(stopPayload.stopped).map((item) => String(item.launch_id || "").trim()).filter(Boolean).join(", ") || "runner launch"}`;
|
|
12588
|
+
} else {
|
|
12589
|
+
nextStatusMessage = "no detached runner was active for this selection";
|
|
12590
|
+
}
|
|
12591
|
+
}
|
|
12592
|
+
if (ensureStarted) {
|
|
12593
|
+
if (state.auditResult.applyFailureBlocksStart) {
|
|
12594
|
+
throw new Error(String(safeObject(state.auditResult.applyResult).error || "runner project tui could not apply runner routes").trim());
|
|
12595
|
+
}
|
|
12596
|
+
if (!ensureArray(state.auditResult.matchingRoutes).length) {
|
|
12597
|
+
throw new Error("runner project tui did not find any enabled routes for the selected project destination");
|
|
12598
|
+
}
|
|
12599
|
+
const launchPayload = await runRunnerStartDetachedResolvedRoutes(
|
|
12600
|
+
state.auditResult.matchingRoutes,
|
|
12601
|
+
state.auditResult.startFlags,
|
|
12602
|
+
"runner project tui",
|
|
12603
|
+
{ silent: true },
|
|
12604
|
+
);
|
|
12605
|
+
nextStatusMessage = launchPayload.already_running
|
|
12606
|
+
? `detached runner already running: ${String(safeObject(launchPayload.launch).launch_id || "").trim() || "-"}`
|
|
12607
|
+
: `detached runner started: ${String(safeObject(launchPayload.launch).launch_id || "").trim() || "-"}`;
|
|
12608
|
+
}
|
|
12609
|
+
state.snapshot = buildRunnerProjectTUISnapshot({
|
|
12610
|
+
auditResult: state.auditResult,
|
|
12611
|
+
autoStart,
|
|
12612
|
+
statusMessage: nextStatusMessage,
|
|
12613
|
+
errorMessage: nextErrorMessage,
|
|
12614
|
+
lastAuditAt: new Date().toISOString(),
|
|
12615
|
+
});
|
|
12616
|
+
} catch (err) {
|
|
12617
|
+
state.snapshot = buildRunnerProjectTUISnapshot({
|
|
12618
|
+
auditResult: state.auditResult || {
|
|
12619
|
+
summaryPayload: {
|
|
12620
|
+
project_id: String(auditFlags["project-id"] || auditFlags.project_id || "").trim(),
|
|
12621
|
+
destination_label: String(auditFlags["destination-label"] || auditFlags.destination_label || "").trim(),
|
|
12622
|
+
destination_id: String(auditFlags["destination-id"] || auditFlags.destination_id || "").trim(),
|
|
12623
|
+
},
|
|
12624
|
+
matchingRoutes: [],
|
|
12625
|
+
},
|
|
12626
|
+
autoStart,
|
|
12627
|
+
statusMessage,
|
|
12628
|
+
errorMessage: String(err?.message || err).trim(),
|
|
12629
|
+
lastAuditAt: new Date().toISOString(),
|
|
12630
|
+
});
|
|
12631
|
+
} finally {
|
|
12632
|
+
state.busy = false;
|
|
12633
|
+
render(false);
|
|
12634
|
+
}
|
|
12635
|
+
};
|
|
12636
|
+
|
|
12637
|
+
const queueRefresh = (options = {}) => {
|
|
12638
|
+
void refreshSnapshot(options);
|
|
12639
|
+
};
|
|
12640
|
+
|
|
12641
|
+
try {
|
|
12642
|
+
await refreshSnapshot({
|
|
12643
|
+
reAudit: true,
|
|
12644
|
+
ensureStarted: autoStart,
|
|
12645
|
+
statusMessage: autoStart
|
|
12646
|
+
? "audited routes and ensured detached runner is running"
|
|
12647
|
+
: "audited routes in prepare-only mode",
|
|
12648
|
+
});
|
|
12649
|
+
|
|
12650
|
+
intervalHandle = setInterval(() => {
|
|
12651
|
+
queueRefresh({ reAudit: false, ensureStarted: false, statusMessage: "status refreshed from detached registry" });
|
|
12652
|
+
}, 2000);
|
|
12653
|
+
|
|
12654
|
+
process.stdin.setRawMode(true);
|
|
12655
|
+
process.stdin.resume();
|
|
12656
|
+
stdinHandler = (chunk) => {
|
|
12657
|
+
const key = String(chunk || "");
|
|
12658
|
+
if (key === "\u0003" || key.toLowerCase() === "q") {
|
|
12659
|
+
state.disposed = true;
|
|
12660
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
12661
|
+
render(true);
|
|
12662
|
+
process.stdin.setRawMode(false);
|
|
12663
|
+
process.stdin.pause();
|
|
12664
|
+
process.stdin.off("data", stdinHandler);
|
|
12665
|
+
process.stdout.write("\u001b[?25h\n");
|
|
12666
|
+
return;
|
|
12667
|
+
}
|
|
12668
|
+
if (key.toLowerCase() === "r") {
|
|
12669
|
+
queueRefresh({ reAudit: false, ensureStarted: false, statusMessage: "status refreshed from detached registry" });
|
|
12670
|
+
return;
|
|
12671
|
+
}
|
|
12672
|
+
if (key.toLowerCase() === "a") {
|
|
12673
|
+
queueRefresh({ reAudit: true, ensureStarted: false, statusMessage: "route audit refreshed" });
|
|
12674
|
+
return;
|
|
12675
|
+
}
|
|
12676
|
+
if (key.toLowerCase() === "s") {
|
|
12677
|
+
queueRefresh({ reAudit: false, ensureStarted: true, statusMessage: "ensuring detached runner is active" });
|
|
12678
|
+
return;
|
|
12679
|
+
}
|
|
12680
|
+
if (key.toLowerCase() === "x") {
|
|
12681
|
+
queueRefresh({ reAudit: false, ensureStarted: false, stopRunner: true, statusMessage: "stopping detached runner" });
|
|
12682
|
+
}
|
|
12683
|
+
};
|
|
12684
|
+
process.stdin.on("data", stdinHandler);
|
|
12685
|
+
await new Promise((resolve) => {
|
|
12686
|
+
const waitForDispose = setInterval(() => {
|
|
12687
|
+
if (!state.disposed) return;
|
|
12688
|
+
clearInterval(waitForDispose);
|
|
12689
|
+
resolve();
|
|
12690
|
+
}, 100);
|
|
12691
|
+
});
|
|
12692
|
+
} finally {
|
|
12693
|
+
if (intervalHandle) clearInterval(intervalHandle);
|
|
12694
|
+
if (stdinHandler) {
|
|
12695
|
+
try {
|
|
12696
|
+
process.stdin.off("data", stdinHandler);
|
|
12697
|
+
} catch {}
|
|
12698
|
+
}
|
|
12699
|
+
if (process.stdin?.isTTY) {
|
|
12700
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
12701
|
+
try { process.stdin.pause(); } catch {}
|
|
12702
|
+
}
|
|
12703
|
+
if (!state.disposed) {
|
|
12704
|
+
render(true);
|
|
12705
|
+
process.stdout.write("\u001b[?25h\n");
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
}
|
|
12709
|
+
|
|
12280
12710
|
function canStartRunnerDespiteProjectUpApplyFailure({ applyRequested, applyResult, matchingRoutes }) {
|
|
12281
12711
|
if (!applyRequested) return false;
|
|
12282
12712
|
const normalizedApplyResult = safeObject(applyResult);
|
|
@@ -12782,13 +13212,121 @@ function serializeCLIFlags(flags, options = {}) {
|
|
|
12782
13212
|
return args;
|
|
12783
13213
|
}
|
|
12784
13214
|
|
|
12785
|
-
function runnerDetachedRouteSetSignature(routes) {
|
|
12786
|
-
return ensureArray(routes)
|
|
12787
|
-
.map((route) => runnerRouteKey(normalizeRunnerRoute(route)))
|
|
12788
|
-
.filter(Boolean)
|
|
12789
|
-
.sort()
|
|
12790
|
-
.join("||");
|
|
12791
|
-
}
|
|
13215
|
+
function runnerDetachedRouteSetSignature(routes) {
|
|
13216
|
+
return ensureArray(routes)
|
|
13217
|
+
.map((route) => runnerRouteKey(normalizeRunnerRoute(route)))
|
|
13218
|
+
.filter(Boolean)
|
|
13219
|
+
.sort()
|
|
13220
|
+
.join("||");
|
|
13221
|
+
}
|
|
13222
|
+
|
|
13223
|
+
function runnerSchedulingTargetKeyFromRouteKey(routeKey) {
|
|
13224
|
+
const parts = String(routeKey || "").trim().split("::").filter(Boolean);
|
|
13225
|
+
if (parts.length < 6) {
|
|
13226
|
+
return "";
|
|
13227
|
+
}
|
|
13228
|
+
return [
|
|
13229
|
+
String(parts[parts.length - 5] || "").trim(),
|
|
13230
|
+
String(parts[parts.length - 4] || "").trim(),
|
|
13231
|
+
String(parts[parts.length - 1] || "").trim(),
|
|
13232
|
+
].join("::");
|
|
13233
|
+
}
|
|
13234
|
+
|
|
13235
|
+
function collectRunnerDetachedLaunchSchedulingTargetKeys(entryRaw) {
|
|
13236
|
+
const entry = normalizeRunnerProcessLaunchEntry(entryRaw);
|
|
13237
|
+
const explicitKeys = uniqueOrderedStrings(
|
|
13238
|
+
ensureArray(entry.scheduling_target_keys || entry.schedulingTargetKeys),
|
|
13239
|
+
(value) => String(value || "").trim(),
|
|
13240
|
+
).filter(Boolean);
|
|
13241
|
+
if (explicitKeys.length > 0) {
|
|
13242
|
+
return explicitKeys;
|
|
13243
|
+
}
|
|
13244
|
+
const routeKeyDerived = uniqueOrderedStrings(
|
|
13245
|
+
ensureArray(entry.route_keys).map((value) => runnerSchedulingTargetKeyFromRouteKey(value)).filter(Boolean),
|
|
13246
|
+
(value) => String(value || "").trim(),
|
|
13247
|
+
).filter(Boolean);
|
|
13248
|
+
if (routeKeyDerived.length > 0) {
|
|
13249
|
+
return routeKeyDerived;
|
|
13250
|
+
}
|
|
13251
|
+
const provider = normalizeBotProvider(entry.provider);
|
|
13252
|
+
const projectIDs = ensureArray(entry.project_ids).map((value) => String(value || "").trim()).filter(Boolean);
|
|
13253
|
+
const destinations = ensureArray(entry.destination_labels).map((value) => String(value || "").trim()).filter(Boolean);
|
|
13254
|
+
return uniqueOrderedStrings(
|
|
13255
|
+
projectIDs.flatMap((projectID) => destinations.map((destination) => [projectID, provider, destination].join("::"))),
|
|
13256
|
+
(value) => String(value || "").trim(),
|
|
13257
|
+
).filter(Boolean);
|
|
13258
|
+
}
|
|
13259
|
+
|
|
13260
|
+
function describeDetachedRunnerLaunch(entryRaw) {
|
|
13261
|
+
const entry = normalizeRunnerProcessLaunchEntry(entryRaw);
|
|
13262
|
+
return `launch_id=${entry.launch_id || "-"} pid=${entry.pid || "-"} routes=${ensureArray(entry.route_names).join(", ") || "-"}`;
|
|
13263
|
+
}
|
|
13264
|
+
|
|
13265
|
+
function classifyDetachedRunnerLaunchReuse(registry, routes) {
|
|
13266
|
+
const normalizedRoutes = ensureArray(routes).map((route) => normalizeRunnerRoute(route));
|
|
13267
|
+
const targetSignature = runnerDetachedRouteSetSignature(normalizedRoutes);
|
|
13268
|
+
const targetRouteKeys = normalizedRoutes.map((route) => runnerRouteKey(route)).filter(Boolean);
|
|
13269
|
+
const targetRouteKeySet = new Set(targetRouteKeys);
|
|
13270
|
+
const targetSchedulingTargetKeys = uniqueOrderedStrings(
|
|
13271
|
+
normalizedRoutes.map((route) => runnerRouteSchedulingGroupKey(route)).filter(Boolean),
|
|
13272
|
+
(value) => String(value || "").trim(),
|
|
13273
|
+
).filter(Boolean);
|
|
13274
|
+
const aliveLaunches = Object.values(safeObject(registry).launches || {})
|
|
13275
|
+
.map((entryRaw) => normalizeRunnerProcessLaunchEntry(entryRaw))
|
|
13276
|
+
.filter((entry) => entry.launch_id && entry.pid && isProcessAlive(entry.pid));
|
|
13277
|
+
const exact = aliveLaunches.find((entry) => entry.route_set_signature === targetSignature) || null;
|
|
13278
|
+
if (exact) {
|
|
13279
|
+
return {
|
|
13280
|
+
kind: "reuse",
|
|
13281
|
+
reason: "exact",
|
|
13282
|
+
launch: exact,
|
|
13283
|
+
};
|
|
13284
|
+
}
|
|
13285
|
+
if (!targetSchedulingTargetKeys.length) {
|
|
13286
|
+
return {
|
|
13287
|
+
kind: "none",
|
|
13288
|
+
};
|
|
13289
|
+
}
|
|
13290
|
+
const sameTargetLaunches = aliveLaunches.filter((entry) => {
|
|
13291
|
+
const entryTargetKeys = collectRunnerDetachedLaunchSchedulingTargetKeys(entry);
|
|
13292
|
+
return entryTargetKeys.some((key) => targetSchedulingTargetKeys.includes(key));
|
|
13293
|
+
});
|
|
13294
|
+
if (!sameTargetLaunches.length) {
|
|
13295
|
+
return {
|
|
13296
|
+
kind: "none",
|
|
13297
|
+
};
|
|
13298
|
+
}
|
|
13299
|
+
const supersets = sameTargetLaunches.filter((entry) => targetRouteKeys.every((routeKey) => ensureArray(entry.route_keys).includes(routeKey)));
|
|
13300
|
+
if (supersets.length === 1) {
|
|
13301
|
+
return {
|
|
13302
|
+
kind: "reuse",
|
|
13303
|
+
reason: "superset",
|
|
13304
|
+
launch: supersets[0],
|
|
13305
|
+
};
|
|
13306
|
+
}
|
|
13307
|
+
const conflictingLaunches = supersets.length > 1 ? supersets : sameTargetLaunches;
|
|
13308
|
+
const overlapRouteNames = Array.from(
|
|
13309
|
+
new Set(
|
|
13310
|
+
conflictingLaunches.flatMap((entry) => ensureArray(entry.route_keys))
|
|
13311
|
+
.filter((routeKey) => targetRouteKeySet.has(routeKey))
|
|
13312
|
+
.map((routeKey) => {
|
|
13313
|
+
const matchedRoute = normalizedRoutes.find((route) => runnerRouteKey(route) === routeKey);
|
|
13314
|
+
return matchedRoute?.name || routeKey;
|
|
13315
|
+
})
|
|
13316
|
+
.filter(Boolean),
|
|
13317
|
+
),
|
|
13318
|
+
);
|
|
13319
|
+
const detail = supersets.length > 1
|
|
13320
|
+
? `multiple detached runners already cover the requested route set for the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")})`
|
|
13321
|
+
: `another detached runner already owns the same project/provider/destination target (${conflictingLaunches.map((entry) => describeDetachedRunnerLaunch(entry)).join(" | ")}). Stop it first or use runner project tui so one detached runner owns the full route set for that destination`;
|
|
13322
|
+
return {
|
|
13323
|
+
kind: "conflict",
|
|
13324
|
+
detail,
|
|
13325
|
+
launch: conflictingLaunches.length === 1 ? conflictingLaunches[0] : null,
|
|
13326
|
+
conflicting_launches: conflictingLaunches,
|
|
13327
|
+
overlapping_route_names: overlapRouteNames,
|
|
13328
|
+
};
|
|
13329
|
+
}
|
|
12792
13330
|
|
|
12793
13331
|
function buildRunnerDetachedPosixLaunchScript({
|
|
12794
13332
|
scriptPath,
|
|
@@ -12848,25 +13386,28 @@ function buildRunnerDetachedLaunchRecord(childPID, routes, flags, sourceCommand,
|
|
|
12848
13386
|
cli_path: cliPath,
|
|
12849
13387
|
working_directory: path.dirname(cliPath),
|
|
12850
13388
|
route_set_signature: runnerDetachedRouteSetSignature(normalizedRoutes),
|
|
12851
|
-
route_keys: normalizedRoutes.map((route) => runnerRouteKey(route)),
|
|
12852
|
-
route_names: normalizedRoutes.map((route) => route.name || runnerRouteKey(route)),
|
|
12853
|
-
project_ids: Array.from(new Set(normalizedRoutes.map((route) => String(route.projectID || "").trim()).filter(Boolean))),
|
|
12854
|
-
provider: firstNonEmptyString(normalizedRoutes.map((route) => route.provider)),
|
|
12855
|
-
destination_labels: Array.from(new Set(normalizedRoutes.map((route) => String(route.destinationLabel || route.destinationID || "").trim()).filter(Boolean))),
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
13389
|
+
route_keys: normalizedRoutes.map((route) => runnerRouteKey(route)),
|
|
13390
|
+
route_names: normalizedRoutes.map((route) => route.name || runnerRouteKey(route)),
|
|
13391
|
+
project_ids: Array.from(new Set(normalizedRoutes.map((route) => String(route.projectID || "").trim()).filter(Boolean))),
|
|
13392
|
+
provider: firstNonEmptyString(normalizedRoutes.map((route) => route.provider)),
|
|
13393
|
+
destination_labels: Array.from(new Set(normalizedRoutes.map((route) => String(route.destinationLabel || route.destinationID || "").trim()).filter(Boolean))),
|
|
13394
|
+
scheduling_target_keys: uniqueOrderedStrings(
|
|
13395
|
+
normalizedRoutes.map((route) => runnerRouteSchedulingGroupKey(route)).filter(Boolean),
|
|
13396
|
+
(value) => String(value || "").trim(),
|
|
13397
|
+
),
|
|
13398
|
+
log_file: String(logFilePath || "").trim(),
|
|
13399
|
+
created_by_pid: process.pid,
|
|
13400
|
+
source_command: sourceCommand,
|
|
13401
|
+
});
|
|
13402
|
+
}
|
|
12861
13403
|
|
|
12862
|
-
function findExistingDetachedRunnerLaunch(registry, routes) {
|
|
12863
|
-
const
|
|
12864
|
-
if (
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
}
|
|
13404
|
+
function findExistingDetachedRunnerLaunch(registry, routes) {
|
|
13405
|
+
const decision = classifyDetachedRunnerLaunchReuse(registry, routes);
|
|
13406
|
+
if (decision.kind === "reuse" && decision.launch) {
|
|
13407
|
+
return decision.launch;
|
|
13408
|
+
}
|
|
13409
|
+
return null;
|
|
13410
|
+
}
|
|
12870
13411
|
|
|
12871
13412
|
async function launchDetachedRunnerProcess(flags, routes, sourceCommand) {
|
|
12872
13413
|
const cliPath = fileURLToPath(import.meta.url);
|
|
@@ -13055,60 +13596,76 @@ async function runRunnerStop(flags) {
|
|
|
13055
13596
|
}
|
|
13056
13597
|
}
|
|
13057
13598
|
|
|
13058
|
-
async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand = "runner start-detached") {
|
|
13059
|
-
const
|
|
13060
|
-
const
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
|
|
13080
|
-
|
|
13081
|
-
|
|
13082
|
-
}
|
|
13599
|
+
async function runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand = "runner start-detached", options = {}) {
|
|
13600
|
+
const silent = boolFromRaw(safeObject(options).silent, false);
|
|
13601
|
+
const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
|
|
13602
|
+
const reuseDecision = classifyDetachedRunnerLaunchReuse(registry, routes);
|
|
13603
|
+
if (reuseDecision.kind === "reuse" && reuseDecision.launch) {
|
|
13604
|
+
const existing = reuseDecision.launch;
|
|
13605
|
+
const payload = {
|
|
13606
|
+
ok: true,
|
|
13607
|
+
already_running: true,
|
|
13608
|
+
reuse_reason: reuseDecision.reason,
|
|
13609
|
+
registry_file: registry.filePath,
|
|
13610
|
+
launch: {
|
|
13611
|
+
...existing,
|
|
13612
|
+
alive: true,
|
|
13613
|
+
},
|
|
13614
|
+
};
|
|
13615
|
+
if (!silent) {
|
|
13616
|
+
if (boolFromRaw(flags.json, false)) {
|
|
13617
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
13618
|
+
return payload;
|
|
13619
|
+
}
|
|
13620
|
+
process.stdout.write(`Detached runner already running: launch_id=${existing.launch_id} pid=${existing.pid}${existing.log_file ? ` log_file=${existing.log_file}` : ""}${reuseDecision.reason ? ` reuse=${reuseDecision.reason}` : ""}\n`);
|
|
13621
|
+
}
|
|
13622
|
+
return payload;
|
|
13623
|
+
}
|
|
13624
|
+
if (reuseDecision.kind === "conflict") {
|
|
13625
|
+
const overlappingRouteNames = ensureArray(reuseDecision.overlapping_route_names)
|
|
13626
|
+
.map((item) => String(item || "").trim())
|
|
13627
|
+
.filter(Boolean);
|
|
13628
|
+
const overlapSuffix = overlappingRouteNames.length
|
|
13629
|
+
? ` overlapping_routes=${overlappingRouteNames.join(", ")}`
|
|
13630
|
+
: "";
|
|
13631
|
+
throw new Error(`${String(reuseDecision.detail || "detached runner conflict").trim()}${overlapSuffix}`);
|
|
13632
|
+
}
|
|
13633
|
+
const launch = await launchDetachedRunnerProcess(flags, routes, sourceCommand);
|
|
13634
|
+
const nextLaunches = {
|
|
13635
|
+
...safeObject(registry).launches,
|
|
13636
|
+
[launch.launch_id]: launch,
|
|
13637
|
+
};
|
|
13083
13638
|
saveBotRunnerProcessRegistry({ launches: nextLaunches });
|
|
13084
13639
|
const payload = {
|
|
13085
13640
|
ok: true,
|
|
13086
13641
|
already_running: false,
|
|
13087
13642
|
registry_file: registry.filePath,
|
|
13088
13643
|
launch: {
|
|
13089
|
-
...launch,
|
|
13090
|
-
alive: true,
|
|
13091
|
-
},
|
|
13092
|
-
};
|
|
13093
|
-
if (
|
|
13094
|
-
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
}
|
|
13644
|
+
...launch,
|
|
13645
|
+
alive: true,
|
|
13646
|
+
},
|
|
13647
|
+
};
|
|
13648
|
+
if (!silent) {
|
|
13649
|
+
if (boolFromRaw(flags.json, false)) {
|
|
13650
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
13651
|
+
return payload;
|
|
13652
|
+
}
|
|
13653
|
+
process.stdout.write(
|
|
13654
|
+
[
|
|
13655
|
+
"Detached runner start: OK",
|
|
13656
|
+
`launch_id: ${launch.launch_id}`,
|
|
13657
|
+
`pid: ${launch.pid}`,
|
|
13658
|
+
`registry_file: ${registry.filePath}`,
|
|
13659
|
+
`project_ids: ${launch.project_ids.join(", ") || "-"}`,
|
|
13660
|
+
`route_names: ${launch.route_names.join(", ") || "-"}`,
|
|
13661
|
+
`destination_labels: ${launch.destination_labels.join(", ") || "-"}`,
|
|
13662
|
+
`log_file: ${launch.log_file || "-"}`,
|
|
13663
|
+
`command: ${launch.command}`,
|
|
13664
|
+
].join("\n") + "\n",
|
|
13665
|
+
);
|
|
13666
|
+
}
|
|
13667
|
+
return payload;
|
|
13668
|
+
}
|
|
13112
13669
|
|
|
13113
13670
|
function stopDetachedRunnerWithFlags(flags) {
|
|
13114
13671
|
const registry = loadBotRunnerProcessRegistry({ persistIfNeeded: true });
|
|
@@ -13141,10 +13698,10 @@ function stopDetachedRunnerWithFlags(flags) {
|
|
|
13141
13698
|
};
|
|
13142
13699
|
}
|
|
13143
13700
|
|
|
13144
|
-
function startDetachedRunnerWithFlags(flags, sourceCommand = "runner.start_detached") {
|
|
13145
|
-
const routes = resolveRunnerRoutes(flags, "start");
|
|
13146
|
-
return runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand);
|
|
13147
|
-
}
|
|
13701
|
+
function startDetachedRunnerWithFlags(flags, sourceCommand = "runner.start_detached", options = {}) {
|
|
13702
|
+
const routes = resolveRunnerRoutes(flags, "start");
|
|
13703
|
+
return runRunnerStartDetachedResolvedRoutes(routes, flags, sourceCommand, options);
|
|
13704
|
+
}
|
|
13148
13705
|
|
|
13149
13706
|
async function runRunnerStartDetached(flags) {
|
|
13150
13707
|
await startDetachedRunnerWithFlags(flags, "runner start-detached");
|
|
@@ -14284,19 +14841,23 @@ async function runRunner(argv) {
|
|
|
14284
14841
|
}
|
|
14285
14842
|
throw new Error("runner route requires a subcommand: list | add | edit | remove");
|
|
14286
14843
|
}
|
|
14287
|
-
if (subcommand === "project" || subcommand === "projects") {
|
|
14844
|
+
if (subcommand === "project" || subcommand === "projects") {
|
|
14288
14845
|
const [projectSubcommandRaw = "", ...projectRest] = rest;
|
|
14289
14846
|
const projectSubcommand = String(projectSubcommandRaw || "").trim().toLowerCase();
|
|
14290
14847
|
const projectArgv = !projectSubcommand || projectSubcommand.startsWith("-")
|
|
14291
14848
|
? [projectSubcommandRaw, ...projectRest].filter((value) => String(value || "").trim())
|
|
14292
14849
|
: projectRest;
|
|
14293
|
-
const projectFlags = parseArgs(projectArgv);
|
|
14294
|
-
if (projectSubcommand === "up") {
|
|
14295
|
-
await runRunnerProjectUp(projectFlags);
|
|
14296
|
-
return;
|
|
14297
|
-
}
|
|
14298
|
-
|
|
14299
|
-
|
|
14850
|
+
const projectFlags = parseArgs(projectArgv);
|
|
14851
|
+
if (projectSubcommand === "up") {
|
|
14852
|
+
await runRunnerProjectUp(projectFlags);
|
|
14853
|
+
return;
|
|
14854
|
+
}
|
|
14855
|
+
if (projectSubcommand === "tui") {
|
|
14856
|
+
await runRunnerProjectTUI(projectFlags);
|
|
14857
|
+
return;
|
|
14858
|
+
}
|
|
14859
|
+
throw new Error("runner project requires a subcommand: up | tui");
|
|
14860
|
+
}
|
|
14300
14861
|
if (subcommand === "artifact" || subcommand === "artifacts") {
|
|
14301
14862
|
const [artifactSubcommandRaw = "", ...artifactRest] = rest;
|
|
14302
14863
|
const artifactSubcommand = String(artifactSubcommandRaw || "").trim().toLowerCase();
|
|
@@ -14339,8 +14900,8 @@ async function runRunner(argv) {
|
|
|
14339
14900
|
await runRunnerStop(flags);
|
|
14340
14901
|
return;
|
|
14341
14902
|
}
|
|
14342
|
-
throw new Error("runner requires a subcommand: list | show | status | once | start | start-detached | stop | route | project | artifact");
|
|
14343
|
-
}
|
|
14903
|
+
throw new Error("runner requires a subcommand: list | show | status | once | start | start-detached | stop | route | project | artifact");
|
|
14904
|
+
}
|
|
14344
14905
|
|
|
14345
14906
|
async function runLocalBotBridge(argv) {
|
|
14346
14907
|
const helperPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "scripts", "local-bot-ai-bridge.mjs");
|
|
@@ -19348,10 +19909,176 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
19348
19909
|
} catch (err) {
|
|
19349
19910
|
push("detached_runner_launch_record_persists_log_file", false, String(err?.message || err));
|
|
19350
19911
|
}
|
|
19351
|
-
|
|
19352
|
-
|
|
19353
|
-
|
|
19354
|
-
|
|
19912
|
+
|
|
19913
|
+
try {
|
|
19914
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19915
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19916
|
+
projectID: selftestProjectID,
|
|
19917
|
+
provider: "telegram",
|
|
19918
|
+
role: "monitor",
|
|
19919
|
+
botID: "bot-1",
|
|
19920
|
+
botName: "SelftestBot1",
|
|
19921
|
+
destinationID: "dest-1",
|
|
19922
|
+
destinationLabel: "Selftest Room",
|
|
19923
|
+
});
|
|
19924
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
19925
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
19926
|
+
projectID: selftestProjectID,
|
|
19927
|
+
provider: "telegram",
|
|
19928
|
+
role: "monitor",
|
|
19929
|
+
botID: "bot-2",
|
|
19930
|
+
botName: "SelftestBot2",
|
|
19931
|
+
destinationID: "dest-1",
|
|
19932
|
+
destinationLabel: "Selftest Room",
|
|
19933
|
+
});
|
|
19934
|
+
const supersetLaunch = buildRunnerDetachedLaunchRecord(
|
|
19935
|
+
process.pid,
|
|
19936
|
+
[routeBot1, routeBot2],
|
|
19937
|
+
{ tui: false },
|
|
19938
|
+
"runner start-detached",
|
|
19939
|
+
);
|
|
19940
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
19941
|
+
{ launches: { [supersetLaunch.launch_id]: supersetLaunch } },
|
|
19942
|
+
[routeBot2],
|
|
19943
|
+
);
|
|
19944
|
+
push(
|
|
19945
|
+
"detached_runner_reuses_existing_superset_launch_for_same_target",
|
|
19946
|
+
decision.kind === "reuse"
|
|
19947
|
+
&& decision.reason === "superset"
|
|
19948
|
+
&& String(safeObject(decision.launch).launch_id || "").trim() === supersetLaunch.launch_id,
|
|
19949
|
+
`kind=${String(decision.kind || "")} reason=${String(decision.reason || "")} launch=${String(safeObject(decision.launch).launch_id || "")}`,
|
|
19950
|
+
);
|
|
19951
|
+
} catch (err) {
|
|
19952
|
+
push("detached_runner_reuses_existing_superset_launch_for_same_target", false, String(err?.message || err));
|
|
19953
|
+
}
|
|
19954
|
+
|
|
19955
|
+
try {
|
|
19956
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19957
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19958
|
+
projectID: selftestProjectID,
|
|
19959
|
+
provider: "telegram",
|
|
19960
|
+
role: "monitor",
|
|
19961
|
+
botID: "bot-1",
|
|
19962
|
+
botName: "SelftestBot1",
|
|
19963
|
+
destinationID: "dest-1",
|
|
19964
|
+
destinationLabel: "Selftest Room",
|
|
19965
|
+
});
|
|
19966
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
19967
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
19968
|
+
projectID: selftestProjectID,
|
|
19969
|
+
provider: "telegram",
|
|
19970
|
+
role: "monitor",
|
|
19971
|
+
botID: "bot-2",
|
|
19972
|
+
botName: "SelftestBot2",
|
|
19973
|
+
destinationID: "dest-1",
|
|
19974
|
+
destinationLabel: "Selftest Room",
|
|
19975
|
+
});
|
|
19976
|
+
const supersetLaunch = buildRunnerDetachedLaunchRecord(
|
|
19977
|
+
process.pid,
|
|
19978
|
+
[routeBot1, routeBot2],
|
|
19979
|
+
{ tui: false },
|
|
19980
|
+
"runner project tui",
|
|
19981
|
+
);
|
|
19982
|
+
const existing = findExistingDetachedRunnerLaunch(
|
|
19983
|
+
{ launches: { [supersetLaunch.launch_id]: supersetLaunch } },
|
|
19984
|
+
[routeBot2],
|
|
19985
|
+
);
|
|
19986
|
+
push(
|
|
19987
|
+
"detached_runner_existing_launch_lookup_reuses_superset_for_tui_snapshot",
|
|
19988
|
+
String(safeObject(existing).launch_id || "").trim() === supersetLaunch.launch_id,
|
|
19989
|
+
`launch=${String(safeObject(existing).launch_id || "").trim() || "(none)"}`,
|
|
19990
|
+
);
|
|
19991
|
+
} catch (err) {
|
|
19992
|
+
push("detached_runner_existing_launch_lookup_reuses_superset_for_tui_snapshot", false, String(err?.message || err));
|
|
19993
|
+
}
|
|
19994
|
+
|
|
19995
|
+
try {
|
|
19996
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
19997
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
19998
|
+
projectID: selftestProjectID,
|
|
19999
|
+
provider: "telegram",
|
|
20000
|
+
role: "monitor",
|
|
20001
|
+
botID: "bot-1",
|
|
20002
|
+
botName: "SelftestBot1",
|
|
20003
|
+
destinationID: "dest-1",
|
|
20004
|
+
destinationLabel: "Selftest Room",
|
|
20005
|
+
});
|
|
20006
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
20007
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
20008
|
+
projectID: selftestProjectID,
|
|
20009
|
+
provider: "telegram",
|
|
20010
|
+
role: "monitor",
|
|
20011
|
+
botID: "bot-2",
|
|
20012
|
+
botName: "SelftestBot2",
|
|
20013
|
+
destinationID: "dest-1",
|
|
20014
|
+
destinationLabel: "Selftest Room",
|
|
20015
|
+
});
|
|
20016
|
+
const subsetLaunch = buildRunnerDetachedLaunchRecord(
|
|
20017
|
+
process.pid,
|
|
20018
|
+
[routeBot2],
|
|
20019
|
+
{ tui: false },
|
|
20020
|
+
"runner start-detached",
|
|
20021
|
+
);
|
|
20022
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
20023
|
+
{ launches: { [subsetLaunch.launch_id]: subsetLaunch } },
|
|
20024
|
+
[routeBot1, routeBot2],
|
|
20025
|
+
);
|
|
20026
|
+
push(
|
|
20027
|
+
"detached_runner_blocks_partial_overlap_for_same_target",
|
|
20028
|
+
decision.kind === "conflict"
|
|
20029
|
+
&& ensureArray(decision.overlapping_route_names).includes("telegram-monitor-selftest-bot-2")
|
|
20030
|
+
&& String(decision.detail || "").includes("runner project tui"),
|
|
20031
|
+
`kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
|
|
20032
|
+
);
|
|
20033
|
+
} catch (err) {
|
|
20034
|
+
push("detached_runner_blocks_partial_overlap_for_same_target", false, String(err?.message || err));
|
|
20035
|
+
}
|
|
20036
|
+
|
|
20037
|
+
try {
|
|
20038
|
+
const routeBot1 = normalizeRunnerRoute({
|
|
20039
|
+
name: "telegram-monitor-selftest-bot-1",
|
|
20040
|
+
projectID: selftestProjectID,
|
|
20041
|
+
provider: "telegram",
|
|
20042
|
+
role: "monitor",
|
|
20043
|
+
botID: "bot-1",
|
|
20044
|
+
botName: "SelftestBot1",
|
|
20045
|
+
destinationID: "dest-1",
|
|
20046
|
+
destinationLabel: "Selftest Room",
|
|
20047
|
+
});
|
|
20048
|
+
const routeBot2 = normalizeRunnerRoute({
|
|
20049
|
+
name: "telegram-monitor-selftest-bot-2",
|
|
20050
|
+
projectID: selftestProjectID,
|
|
20051
|
+
provider: "telegram",
|
|
20052
|
+
role: "monitor",
|
|
20053
|
+
botID: "bot-2",
|
|
20054
|
+
botName: "SelftestBot2",
|
|
20055
|
+
destinationID: "dest-1",
|
|
20056
|
+
destinationLabel: "Selftest Room",
|
|
20057
|
+
});
|
|
20058
|
+
const singleLaunch = buildRunnerDetachedLaunchRecord(
|
|
20059
|
+
process.pid,
|
|
20060
|
+
[routeBot1],
|
|
20061
|
+
{ tui: false },
|
|
20062
|
+
"runner start-detached",
|
|
20063
|
+
);
|
|
20064
|
+
const decision = classifyDetachedRunnerLaunchReuse(
|
|
20065
|
+
{ launches: { [singleLaunch.launch_id]: singleLaunch } },
|
|
20066
|
+
[routeBot2],
|
|
20067
|
+
);
|
|
20068
|
+
push(
|
|
20069
|
+
"detached_runner_blocks_disjoint_same_target_parallel_launches",
|
|
20070
|
+
decision.kind === "conflict"
|
|
20071
|
+
&& ensureArray(decision.overlapping_route_names).length === 0
|
|
20072
|
+
&& String(decision.detail || "").includes("same project/provider/destination target"),
|
|
20073
|
+
`kind=${String(decision.kind || "")} overlap=${ensureArray(decision.overlapping_route_names).join(", ")} detail=${String(decision.detail || "")}`,
|
|
20074
|
+
);
|
|
20075
|
+
} catch (err) {
|
|
20076
|
+
push("detached_runner_blocks_disjoint_same_target_parallel_launches", false, String(err?.message || err));
|
|
20077
|
+
}
|
|
20078
|
+
|
|
20079
|
+
let detachedRunnerPosixTempDir = "";
|
|
20080
|
+
try {
|
|
20081
|
+
detachedRunnerPosixTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-detached-script-selftest-"));
|
|
19355
20082
|
const scriptPath = path.join(detachedRunnerPosixTempDir, "start-runner.sh");
|
|
19356
20083
|
const pidFilePath = path.join(detachedRunnerPosixTempDir, "runner.pid");
|
|
19357
20084
|
buildRunnerDetachedPosixLaunchScript({
|
|
@@ -19485,6 +20212,41 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
|
|
|
19485
20212
|
push("runner_project_up_tool_flag_accepts_start_detached_snake_case", false, String(err?.message || err));
|
|
19486
20213
|
}
|
|
19487
20214
|
|
|
20215
|
+
try {
|
|
20216
|
+
const projectTUIRuntime = resolveRunnerProjectTUIRuntimePolicy({
|
|
20217
|
+
"project-id": selftestProjectID,
|
|
20218
|
+
"destination-label": "Selftest Room",
|
|
20219
|
+
});
|
|
20220
|
+
push(
|
|
20221
|
+
"runner_project_tui_defaults_to_detached_bootstrap_mode",
|
|
20222
|
+
projectTUIRuntime.autoStart === true
|
|
20223
|
+
&& projectTUIRuntime.auditFlags.start === false
|
|
20224
|
+
&& projectTUIRuntime.auditFlags["start-detached"] === false
|
|
20225
|
+
&& projectTUIRuntime.auditFlags.tui === false
|
|
20226
|
+
&& projectTUIRuntime.auditFlags.json === true,
|
|
20227
|
+
`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)}`,
|
|
20228
|
+
);
|
|
20229
|
+
} catch (err) {
|
|
20230
|
+
push("runner_project_tui_defaults_to_detached_bootstrap_mode", false, String(err?.message || err));
|
|
20231
|
+
}
|
|
20232
|
+
|
|
20233
|
+
try {
|
|
20234
|
+
const projectTUIRuntime = resolveRunnerProjectTUIRuntimePolicy({
|
|
20235
|
+
"project-id": selftestProjectID,
|
|
20236
|
+
"destination-label": "Selftest Room",
|
|
20237
|
+
start: false,
|
|
20238
|
+
});
|
|
20239
|
+
push(
|
|
20240
|
+
"runner_project_tui_respects_prepare_only_flag",
|
|
20241
|
+
projectTUIRuntime.autoStart === false
|
|
20242
|
+
&& projectTUIRuntime.auditFlags.start === false
|
|
20243
|
+
&& projectTUIRuntime.auditFlags["start-detached"] === false,
|
|
20244
|
+
`autoStart=${String(projectTUIRuntime.autoStart)} start=${String(projectTUIRuntime.auditFlags.start)} detached=${String(projectTUIRuntime.auditFlags["start-detached"])}`,
|
|
20245
|
+
);
|
|
20246
|
+
} catch (err) {
|
|
20247
|
+
push("runner_project_tui_respects_prepare_only_flag", false, String(err?.message || err));
|
|
20248
|
+
}
|
|
20249
|
+
|
|
19488
20250
|
try {
|
|
19489
20251
|
const projectUpResponse = await handleLocalProjectToolDispatchImpl(
|
|
19490
20252
|
{
|
|
@@ -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
|
-
|
|
227
|
+
humanIntent = normalizeDirectedManagedLeadHumanIntent({
|
|
164
228
|
text: parsed.body,
|
|
165
|
-
|
|
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
|
-
&&
|
|
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) {
|