@staix/agent-hub 0.12.31 → 0.12.33
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/CHANGELOG.md +20 -1
- package/README.md +14 -7
- package/docs/agent-notes/adapters.md +7 -4
- package/docs/agent-notes/bus.md +3 -0
- package/docs/agent-notes/daemon.md +13 -8
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +4 -3
- package/docs/agent-notes/tests.md +1 -0
- package/docs/cooperbench.md +1 -1
- package/docs/gsd-collaboration-analysis.md +1 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +141 -25
- package/docs/quickstart.md +10 -6
- package/docs/security.md +18 -9
- package/docs/smoke.md +34 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +204 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +211 -0
- package/docs/verification/2026-10-18-0.12.32.md +54 -0
- package/docs/verification/2026-10-18-0.12.33.md +42 -0
- package/docs/verified.json +24 -16
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1296 -26
- package/src/adapters/acp.ts +5 -6
- package/src/adapters/definitions/builtin-metadata.ts +121 -0
- package/src/adapters/definitions/kimi-native.ts +4 -0
- package/src/adapters/definitions/qwen-native.ts +24 -0
- package/src/adapters/definitions/qwen.ts +71 -0
- package/src/adapters/pi.ts +6 -2
- package/src/adapters/registry-types.ts +174 -0
- package/src/adapters/registry.ts +36 -0
- package/src/cli/console-state.ts +74 -5
- package/src/cli/console.ts +35 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +13 -3
- package/src/cli/launch.ts +1 -6
- package/src/cli/main.ts +200 -39
- package/src/cli/output.ts +2 -0
- package/src/cli/preview.ts +8 -5
- package/src/cli/recovery-guidance.ts +137 -0
- package/src/cli/recovery-presentation-options.ts +8 -0
- package/src/cli/recovery-session-file.ts +18 -0
- package/src/cli/reset.ts +13 -1
- package/src/cli/terminal-recovery.ts +48 -0
- package/src/cli/upgrade-interactive.ts +129 -75
- package/src/cli/upgrade-labels.ts +30 -0
- package/src/cli/upgrade-presentation-data.ts +41 -0
- package/src/cli/upgrade-presentation-flags.ts +31 -0
- package/src/cli/upgrade-presentation.ts +227 -0
- package/src/cli/upgrade-restore-state.ts +6 -2
- package/src/cli/upgrade-runtime.ts +360 -199
- package/src/cli/upgrade.ts +278 -25
- package/src/hub/bus.ts +144 -14
- package/src/hub/cohorts.ts +2 -2
- package/src/hub/config-apply.ts +242 -0
- package/src/hub/config-reload.ts +120 -0
- package/src/hub/config-trust.ts +11 -2
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +577 -179
- package/src/hub/deferred-hub.ts +60 -0
- package/src/hub/delivery-journal.ts +6 -0
- package/src/hub/events.ts +2 -2
- package/src/hub/manager.ts +1 -1
- package/src/hub/peer-catalog.ts +10 -13
- package/src/hub/peers.ts +12 -2
- package/src/hub/permission-mode.ts +5 -3
- package/src/hub/recovery-continuity.ts +152 -0
- package/src/hub/recovery-store.ts +10 -4
- package/src/hub/restart.ts +12 -1
- package/src/hub/sdlc.ts +5 -1
- package/src/hub/settings.ts +50 -36
- package/src/hub/settled-roles.ts +35 -0
- package/src/hub/supervision.ts +3 -2
- package/src/hub/tasks.ts +29 -9
- package/src/hub/ui.ts +1 -1
- package/src/memory/brief.ts +6 -5
- package/src/pi/session.ts +23 -4
- package/src/ui/index.html +23 -7
package/src/cli/main.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { definitionOf } from "../adapters/registry.ts";
|
|
2
3
|
import { supportsBoardReleaseFilter, supportsBoardColumnMetadata } from "./board-read.ts";
|
|
3
4
|
import { matchesPolicy } from "../hub/path-policy.ts";
|
|
4
5
|
import { projectedDiff } from "../hub/projected-diff.ts";
|
|
@@ -43,13 +44,16 @@ import { VERSION } from "../version.ts";
|
|
|
43
44
|
import { freeText } from "./free-text.ts";
|
|
44
45
|
import { createInterface } from "node:readline/promises";
|
|
45
46
|
import { createInterface as lineReader } from "node:readline";
|
|
46
|
-
import { activeOperation, assertLifecycleAvailable, operationIdPattern, readOperation, recoveryLock, recoveryRunner, signedRunner, stopSignedRunner } from "../hub/recovery-store.ts";
|
|
47
|
+
import { activeOperation, assertLifecycleAvailable, operationIdPattern, readOperation, recoveryLock, recoveryRunner, signedRunner, stopSignedRunner, supportsAction } from "../hub/recovery-store.ts";
|
|
47
48
|
import { childEnv } from "../hub/child-process.ts";
|
|
48
|
-
import { abortRecovery, createOperation, disposeRecovery, liveProjects, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation } from "./upgrade.ts";
|
|
49
|
+
import { abortRecovery, createOperation, disposeRecovery, repairedFreshSession, reattachRecovery, liveProjects, nextChoices, nextActionsText, publicOperation, recoveryCommand, registeredProjects, runRecovery, type RecoveryOperation, type Inspection } from "./upgrade.ts";
|
|
49
50
|
import { endPlannedPeer, makeRecoveryDriver, makeUpgradePlan, preserveSource } from "./upgrade-runtime.ts";
|
|
50
51
|
import { exactVersion, latestRelease, newerVersion } from "./recovery-package.ts";
|
|
51
52
|
import { operationScreen, planScreen, type ScreenIO, type UpgradeHost } from "./upgrade-interactive.ts";
|
|
52
|
-
import {
|
|
53
|
+
import { readRecoveryPresentationFlags } from "./upgrade-presentation-flags.ts";
|
|
54
|
+
import { renderOperation, renderBlocked, renderSummary, type RecoveryPresentationOptions } from "./upgrade-presentation.ts";
|
|
55
|
+
import { recordTerminalLaunch, type TerminalPeer } from "./terminal-recovery.ts";
|
|
56
|
+
import { stopAndArchiveSummary } from "./recovery-guidance.ts";
|
|
53
57
|
import { ensureMlx, inspectMlx, stopMlx } from "../models/mlx.ts";
|
|
54
58
|
import { setupOllamaModel } from "./models-setup.ts";
|
|
55
59
|
|
|
@@ -65,7 +69,7 @@ import { addonReport, appendRecords, formatAddons, formatResearch, LABELS, label
|
|
|
65
69
|
import { summarize, summarizeByTask } from "../hub/report.ts";
|
|
66
70
|
import { hasTree, planUndo, repoOf, restore, Turns } from "../hub/snapshots.ts";
|
|
67
71
|
import { pathWarnings } from "../hub/conflicts.ts";
|
|
68
|
-
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, liveTestRefusal, peerCommandRefusal } from "./identity.ts";
|
|
72
|
+
import { classifyPeerCommand, cliCommandLabel, detectCliIdentity, LIVE_TEST_PERSON_REFUSAL, liveTestRefusal, peerCommandRefusal } from "./identity.ts";
|
|
69
73
|
import { recordCliAudit } from "./identity-audit.ts";
|
|
70
74
|
import { runConsole } from "./console.ts";
|
|
71
75
|
import { paint, permissionBoundary, resolveColor, wrap, type Span } from "./console-state.ts";
|
|
@@ -138,7 +142,7 @@ function audit(outcome: "run" | "refused" | "invalid"): void {
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
if (identity.role === "invalid") { audit("invalid"); fail(`${identity.reason}; use ahub console or a terminal with no agent markers`); }
|
|
141
|
-
if (delegationRefused) { audit("refused"); fail(
|
|
145
|
+
if (delegationRefused) { audit("refused"); fail(LIVE_TEST_PERSON_REFUSAL); }
|
|
142
146
|
if (identity.role === "tools" && commandAccess === "console") { audit("refused"); fail(peerCommandRefusal(identity.peer, commandLabel)); }
|
|
143
147
|
if (identity.role === "tools" || delegatedPeer) audit("run");
|
|
144
148
|
// Forward only to ahub's own continuation commands, never to native agent argv or an environment flag.
|
|
@@ -399,21 +403,50 @@ async function hold(t: "pause" | "resume"): Promise<void> {
|
|
|
399
403
|
console.log(`${args[0]} is ${res.state}`);
|
|
400
404
|
}
|
|
401
405
|
|
|
402
|
-
function
|
|
406
|
+
function recoveryDisplay() {
|
|
407
|
+
return readRecoveryPresentationFlags(args, { isTTY: !!process.stdout.isTTY, columns: process.stdout.columns ?? 80, TERM: process.env.TERM, NO_COLOR: process.env.NO_COLOR });
|
|
408
|
+
}
|
|
409
|
+
const humanRecovery = (json: boolean) => !json && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
410
|
+
const printRecovery = (lines: readonly string[]) => { for (const line of lines) console.log(line); };
|
|
411
|
+
function recoveryFormatting(entry: string, presentation: RecoveryPresentationOptions): string[] {
|
|
412
|
+
// ponytail: old retained coordinators have no presentation parser; capability metadata can replace this display-only module-presence check.
|
|
413
|
+
return existsSync(join(dirname(entry), "upgrade-presentation-flags.ts"))
|
|
414
|
+
? [`--color=${presentation.color ? "always" : "never"}`, ...(presentation.verbose ? ["--verbose"] : [])] : [];
|
|
415
|
+
}
|
|
416
|
+
function printRecoveryStatus(op: RecoveryOperation, message: string, presentation: RecoveryPresentationOptions): void {
|
|
417
|
+
printRecovery(wrap(message, presentation.columns));
|
|
418
|
+
printRecovery(wrap(`ahub recovery status ${op.id}`, presentation.columns));
|
|
419
|
+
if (presentation.verbose) {
|
|
420
|
+
printRecovery(wrap("Retained bun coordinator (main.js), wrapped command:", presentation.columns));
|
|
421
|
+
printRecovery(wrap(recoveryCommand(op, "status"), presentation.columns));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function recoveryEndInspection(op: RecoveryOperation): Promise<Record<string, Inspection | undefined>> {
|
|
425
|
+
const driver = makeRecoveryDriver(undefined, delegatedPeer);
|
|
426
|
+
return Object.fromEntries(await Promise.all(op.plan.projects.map(async planned => [planned.project.id, await driver.inspect(planned.project).catch(() => undefined)] as const)));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function spawnRecovery(operation: RecoveryOperation, quiet = false, presentation?: RecoveryPresentationOptions): void {
|
|
403
430
|
const child = spawn(process.execPath, [join(operation.sourceRoot, "src/cli/main.js"), ...liveTestArgs, "recovery-run", operation.id], {
|
|
404
431
|
cwd, detached: true, stdio: "ignore", env: { ...process.env, AGENTHUB_RECOVERY_OPERATION: operation.id },
|
|
405
432
|
});
|
|
406
433
|
child.on("error", () => console.error(`runner launch failed; use ${recoveryCommand(operation, "resume")}`));
|
|
407
434
|
child.unref();
|
|
408
435
|
// #215: the operation's own coordinator, which has every recovery command; the global ahub may be older mid-upgrade.
|
|
409
|
-
if (!quiet)
|
|
436
|
+
if (!quiet) {
|
|
437
|
+
if (presentation) {
|
|
438
|
+
printRecovery(wrap(`Recovery operation ${operation.id} scheduled.`, presentation.columns));
|
|
439
|
+
printRecovery(wrap(`ahub recovery status ${operation.id}`, presentation.columns));
|
|
440
|
+
if (presentation.verbose) printRecovery(wrap(recoveryCommand(operation, "status"), presentation.columns));
|
|
441
|
+
} else console.log(`Recovery operation ${operation.id} scheduled.\n${recoveryCommand(operation, "status")}`);
|
|
442
|
+
}
|
|
410
443
|
}
|
|
411
444
|
|
|
412
445
|
/** #272: a terminal session for the interactive screens. One line reader for the whole session, in the terminal's own
|
|
413
446
|
* line mode, so Ctrl+C stays a signal: from then on every question answers null and the screens leave. The terminal
|
|
414
447
|
* is not read again after it (a parent that died on the same Ctrl+C may have handed it back to the shell), and no
|
|
415
448
|
* runner is ever stopped by it. */
|
|
416
|
-
async function onTerminal(screens: (io: ScreenIO) => Promise<void
|
|
449
|
+
async function onTerminal(screens: (io: ScreenIO) => Promise<void>, presentation?: RecoveryPresentationOptions): Promise<void> {
|
|
417
450
|
const rl = lineReader({ input: process.stdin, terminal: false });
|
|
418
451
|
const lines: string[] = [], waiters: ((line: string | null) => void)[] = [];
|
|
419
452
|
let ended = false, interrupted = false;
|
|
@@ -423,6 +456,7 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>): Promise<voi
|
|
|
423
456
|
process.on("SIGINT", onInterrupt);
|
|
424
457
|
try {
|
|
425
458
|
await screens({
|
|
459
|
+
...(presentation ? { presentation: { ...presentation, get columns() { return process.stdout.columns ?? presentation.columns; } } } : {}),
|
|
426
460
|
ask: (question) => {
|
|
427
461
|
if (interrupted) return Promise.resolve(null);
|
|
428
462
|
process.stdout.write(question);
|
|
@@ -434,7 +468,13 @@ async function onTerminal(screens: (io: ScreenIO) => Promise<void>): Promise<voi
|
|
|
434
468
|
// The command owns the terminal while it runs; a reader whose input already ended has nothing to pause.
|
|
435
469
|
if (!ended) rl.pause();
|
|
436
470
|
const done = (code: number) => { if (!ended) rl.resume(); resolve(code); };
|
|
437
|
-
const
|
|
471
|
+
const formatting = presentation && argv[1] === "recovery" ? recoveryFormatting(argv[0]!, presentation) : [];
|
|
472
|
+
// A capable screen child updates the receipt; its parent owns the human view after the action.
|
|
473
|
+
// Older coordinators keep their existing terminal contract and receive no unsupported JSON flag.
|
|
474
|
+
const quiet = formatting.length > 0;
|
|
475
|
+
const child = spawn(process.execPath, [argv[0]!, ...liveTestArgs, ...argv.slice(1), ...formatting, ...(quiet ? ["--json"] : [])], {
|
|
476
|
+
cwd, stdio: quiet ? ["inherit", "ignore", "inherit"] : "inherit",
|
|
477
|
+
});
|
|
438
478
|
child.on("error", () => done(1));
|
|
439
479
|
child.on("close", (code) => done(code ?? 1));
|
|
440
480
|
}),
|
|
@@ -464,6 +504,7 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
464
504
|
read: (id) => readOperation<RecoveryOperation>(id),
|
|
465
505
|
runner: recoveryRunner,
|
|
466
506
|
live: (op) => liveProjects(op, makeRecoveryDriver(undefined, delegatedPeer).inspect),
|
|
507
|
+
endInspection: recoveryEndInspection,
|
|
467
508
|
endPeer: (planned, peer) => endPlannedPeer(planned, peer),
|
|
468
509
|
runnerStoppable: (id) => signedRunner(id) !== undefined,
|
|
469
510
|
stopRunner: (id) => stopSignedRunner(id),
|
|
@@ -473,12 +514,13 @@ function upgradeHost(kind: "restart" | "upgrade", version: string): UpgradeHost
|
|
|
473
514
|
}
|
|
474
515
|
|
|
475
516
|
async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
476
|
-
const
|
|
517
|
+
const display = recoveryDisplay();
|
|
518
|
+
const { one, rest } = takeFlags(display.args, ["--to"], []);
|
|
477
519
|
if (rest.some((a) => !["--dry-run", "--yes"].includes(a)) || (kind === "restart" && one["--to"])) fail("usage: ahub upgrade [--to <version>] [--dry-run] [--yes] | ahub restart [--dry-run] [--yes]");
|
|
478
520
|
if (kind === "upgrade" && selector) fail("upgrade changes the shared package/plugin; omit --project to review all affected running projects");
|
|
479
|
-
const
|
|
521
|
+
const yes = rest.includes("--yes"), dry = rest.includes("--dry-run") || (display.json && !yes);
|
|
480
522
|
// #272: on a terminal, with neither flag, the plan is reviewed, applied and followed in one session.
|
|
481
|
-
const interactive = !dry && !yes &&
|
|
523
|
+
const interactive = !dry && !yes && humanRecovery(display.json);
|
|
482
524
|
let to = one["--to"];
|
|
483
525
|
if (kind === "upgrade" && !to) {
|
|
484
526
|
if (yes || (!dry && !interactive)) fail("upgrade requires --to <exact-version>");
|
|
@@ -489,7 +531,7 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
489
531
|
if (to !== undefined) { try { exactVersion(to); } catch (error) { fail((error as Error).message); } }
|
|
490
532
|
// An older CLI must not coordinate a newer target (docs/operations.md): the target's own coordinator takes over.
|
|
491
533
|
if (kind === "upgrade" && newerVersion(to!, VERSION) && process.env.AGENTHUB_UPGRADE_HANDOVER !== "1") {
|
|
492
|
-
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest];
|
|
534
|
+
const argv = ["x", "--package", `@staix/agent-hub@${to}`, "ahub", ...liveTestArgs, "upgrade", "--to", to!, ...rest, ...display.flags, ...(dry && !rest.includes("--dry-run") ? ["--dry-run"] : [])];
|
|
493
535
|
console.error(`ahub: ${to} is newer than this CLI (${VERSION}), so its own coordinator runs the upgrade: bun ${argv.join(" ")}`);
|
|
494
536
|
if (interactive) {
|
|
495
537
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -506,16 +548,17 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
506
548
|
process.exit(code);
|
|
507
549
|
}
|
|
508
550
|
if (kind === "upgrade") console.error("ahub: Kimi now defaults to its own TUI (pull-only) in projects without peers.kimi.start_mode. To keep ACP: ahub settings set peers.kimi.start_mode headless");
|
|
509
|
-
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io));
|
|
551
|
+
if (interactive) return onTerminal((io) => planScreen(upgradeHost(kind, to ?? VERSION), io), display.options);
|
|
510
552
|
const plan = await makeUpgradePlan(kind, to ?? VERSION, kind === "restart" ? cwd : undefined);
|
|
511
|
-
|
|
553
|
+
const jsonApply = display.json && yes && !dry;
|
|
554
|
+
if (!jsonApply) console.log(JSON.stringify(plan, null, 2));
|
|
512
555
|
// #206: name every blocker and reconnect-only session on stderr, not only inside the JSON above.
|
|
513
556
|
for (const p of plan.projects) for (const peer of p.reconnectOnly ?? []) console.error(`ahub: ${p.project.id}: ${peer} is reconnect-only (unmanaged session): its plugin reattaches to the new hub; no terminal is closed or relaunched`);
|
|
514
557
|
for (const p of plan.projects) for (const peer of p.freshStart ?? []) console.error(`ahub: ${p.project.id}: ${peer} restarts as a new session: its thread has no rollout and the hub recorded no turn on it, so nothing is lost`);
|
|
515
558
|
const blockers = [...plan.blockers, ...plan.projects.flatMap((p) => p.blockers.map((b) => `${p.project.id}: ${b}`))];
|
|
516
559
|
for (const blocker of blockers) console.error(`ahub: blocker: ${blocker}`);
|
|
517
560
|
if (dry) return;
|
|
518
|
-
if (blockers.length) fail("plan has blockers; no runtime was changed");
|
|
561
|
+
if (blockers.length) { if (jsonApply) console.log(JSON.stringify(plan, null, 2)); fail("plan has blockers; no runtime was changed"); }
|
|
519
562
|
assertLifecycleAvailable();
|
|
520
563
|
if (!yes) {
|
|
521
564
|
if (!process.stdin.isTTY) fail("review --dry-run and use --yes in non-interactive sessions");
|
|
@@ -528,50 +571,128 @@ async function upgrade(kind: "restart" | "upgrade"): Promise<void> {
|
|
|
528
571
|
if (current.fingerprint !== plan.fingerprint) fail("plan changed during review; run the command again");
|
|
529
572
|
const sourceRoot = preserveSource(plan);
|
|
530
573
|
const operation = createOperation(plan, sourceRoot);
|
|
531
|
-
spawnRecovery(operation);
|
|
574
|
+
if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
575
|
+
else spawnRecovery(operation, false, humanRecovery(false) ? display.options : undefined);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Strict person command: recorded identity only, no session or terminal overrides. */
|
|
579
|
+
function parseRecoveryReattach(argv: readonly string[]): { id: string; peer: TerminalPeer } | undefined {
|
|
580
|
+
if (argv.length !== 3 || argv[0] !== "reattach" || !operationIdPattern.test(argv[1]!) || !["codex", "claude", "pi"].includes(argv[2]!)) return undefined;
|
|
581
|
+
return { id: argv[1]!, peer: argv[2] as TerminalPeer };
|
|
532
582
|
}
|
|
533
583
|
|
|
534
584
|
const commands: Record<string, () => Promise<void> | void> = {
|
|
535
585
|
upgrade: () => upgrade("upgrade"),
|
|
536
586
|
restart: () => upgrade("restart"),
|
|
537
587
|
recovery: async () => {
|
|
538
|
-
const
|
|
588
|
+
const display = recoveryDisplay();
|
|
589
|
+
const [action, given, ...more] = display.args;
|
|
539
590
|
const usage = "usage: ahub recovery [status|resume|abort] [<operation-id>] | ahub recovery dispose [<operation-id>] --fresh-session <peer>|--stop-and-archive --reason <text>";
|
|
591
|
+
if (action === "reattach") {
|
|
592
|
+
const selected = parseRecoveryReattach(display.args);
|
|
593
|
+
if (!selected) fail("usage: ahub recovery reattach <operation-id> codex|claude|pi");
|
|
594
|
+
let operation: RecoveryOperation;
|
|
595
|
+
try { operation = readOperation<RecoveryOperation>(selected.id); }
|
|
596
|
+
catch { fail(`recovery receipt ${selected.id} cannot be read; inspect the retained operation with ahub recovery status ${selected.id}; no session was started`); }
|
|
597
|
+
if (typeof operation.sourceRoot !== "string" || !operation.sourceRoot.trim()) fail(`recovery receipt ${selected.id} has no retained coordinator path; inspect it with ahub recovery status ${selected.id}; no session was started`);
|
|
598
|
+
const coordinator = join(operation.sourceRoot, "src/cli/main.js");
|
|
599
|
+
if (!existsSync(coordinator)) fail(`the retained coordinator for ${selected.id} is unavailable; restore its retained package and inspect ahub recovery status ${selected.id} before retrying; no session was started`);
|
|
600
|
+
let coordinatorIsCurrent: boolean;
|
|
601
|
+
try { coordinatorIsCurrent = realPath(coordinator) === realPath(join(import.meta.dir, "main.js")); }
|
|
602
|
+
catch { fail(`the retained coordinator for ${selected.id} cannot be resolved; inspect ahub recovery status ${selected.id} before retrying; no session was started`); }
|
|
603
|
+
if (supportsAction(operation.sourceRoot, "reattach") && !coordinatorIsCurrent) {
|
|
604
|
+
if (humanRecovery(display.json)) {
|
|
605
|
+
for (const line of wrap("ahub: using the operation's retained coordinator for reattachment", display.options.columns)) console.error(line);
|
|
606
|
+
if (display.options.verbose) for (const line of wrap(recoveryCommand(operation, "reattach", selected.peer), display.options.columns)) console.error(line);
|
|
607
|
+
} else console.error(`ahub: using the operation's coordinator: ${recoveryCommand(operation, "reattach", selected.peer)}`);
|
|
608
|
+
const formatting = recoveryFormatting(coordinator, display.options);
|
|
609
|
+
const code = await new Promise<number>(resolve => {
|
|
610
|
+
const child = spawn(process.execPath, [coordinator, "recovery", "reattach", selected.id, selected.peer, ...formatting], { cwd, stdio: display.json ? ["inherit", "ignore", "inherit"] : "inherit", env: process.env });
|
|
611
|
+
child.on("error", () => { console.error(`ahub: coordinator could not start; ${recoveryCommand(operation, "status")}`); resolve(1); });
|
|
612
|
+
child.on("close", status => resolve(status ?? 1));
|
|
613
|
+
});
|
|
614
|
+
process.exitCode = code;
|
|
615
|
+
if (display.json && code === 0) console.log(JSON.stringify(publicOperation(readOperation<RecoveryOperation>(selected.id)), null, 2));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const result = await reattachRecovery(selected.id, selected.peer, makeRecoveryDriver());
|
|
619
|
+
if (humanRecovery(display.json)) {
|
|
620
|
+
printRecovery(renderOperation(result, undefined, display.options));
|
|
621
|
+
const live = await liveProjects(result, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
622
|
+
printRecovery(renderBlocked(result, nextChoices(result, undefined, live), display.options));
|
|
623
|
+
} else {
|
|
624
|
+
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
625
|
+
if (!display.json) console.log(nextActionsText(result));
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
540
629
|
// #272: without an id, the operation that holds the machine's lock; bare on a terminal, its screen.
|
|
541
630
|
const named = !!given && operationIdPattern.test(given);
|
|
542
631
|
const id = named ? given : recoveryLock();
|
|
543
632
|
const rest = named || given === undefined ? more : [given, ...more];
|
|
544
|
-
if (action === undefined &&
|
|
633
|
+
if (action === undefined && humanRecovery(display.json)) {
|
|
545
634
|
if (!id) return console.log("no recovery operation is open");
|
|
546
|
-
return onTerminal(async (io) => { await operationScreen(upgradeHost("restart", VERSION), io, id); });
|
|
635
|
+
return onTerminal(async (io) => { await operationScreen(upgradeHost("restart", VERSION), io, id); }, display.options);
|
|
547
636
|
}
|
|
548
637
|
if (!["status", "resume", "abort", "dispose"].includes(action ?? "") || (action !== "dispose" && rest.length)) fail(usage);
|
|
549
638
|
if (!id) fail("no recovery operation holds the lock; name one by its id");
|
|
550
|
-
const
|
|
639
|
+
const delegatedFresh = delegatedPeer && action === "dispose" ? takeFlags(rest, ["--fresh-session", "--reason"], []).one["--fresh-session"] : undefined;
|
|
640
|
+
let operation: RecoveryOperation;
|
|
641
|
+
try { operation = readOperation<RecoveryOperation>(id); }
|
|
642
|
+
catch (error) {
|
|
643
|
+
if (delegatedFresh !== undefined) { audit("refused"); fail(LIVE_TEST_PERSON_REFUSAL); }
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
646
|
+
if (delegatedFresh !== undefined) {
|
|
647
|
+
const repairedFresh = repairedFreshSession(operation, delegatedFresh);
|
|
648
|
+
if (repairedFresh === undefined || liveTestRefusal("recovery", display.args, { repairedFresh })) {
|
|
649
|
+
audit("refused"); fail(LIVE_TEST_PERSON_REFUSAL);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
551
652
|
const runner = recoveryRunner(id);
|
|
552
653
|
if (action === "status") {
|
|
553
654
|
// #215: abort and resume are offered by what the open sources show, read as the runner's errors read them.
|
|
554
655
|
const live = runner ? {} : await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect);
|
|
555
656
|
console.log(JSON.stringify(publicOperation(operation, runner, live), null, 2));
|
|
556
657
|
}
|
|
557
|
-
else if (action === "abort") {
|
|
658
|
+
else if (action === "abort") {
|
|
659
|
+
await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer));
|
|
660
|
+
if (display.json) console.log(JSON.stringify(publicOperation(readOperation<RecoveryOperation>(id)), null, 2));
|
|
661
|
+
else console.log("preflight cancelled; no committed transition was rolled back");
|
|
662
|
+
}
|
|
558
663
|
else if (action === "dispose") {
|
|
559
|
-
// Console authority
|
|
664
|
+
// Console authority; #408 live-test delegation remains for ordinary fresh choices, never repaired loss.
|
|
560
665
|
const { one, rest: flags } = takeFlags(rest, ["--fresh-session", "--reason"], []);
|
|
561
666
|
const stop = flags.length === 1 && flags[0] === "--stop-and-archive";
|
|
562
667
|
const reason = one["--reason"]?.trim() ?? "";
|
|
563
668
|
if (!reason || reason.length > 500 || (flags.length && !stop) || stop === !!one["--fresh-session"]) fail(usage);
|
|
564
669
|
const result = await disposeRecovery(id, stop ? { stop: true } : { fresh: one["--fresh-session"]! }, reason, makeRecoveryDriver(undefined, delegatedPeer));
|
|
565
|
-
if (!stop) {
|
|
566
|
-
|
|
567
|
-
|
|
670
|
+
if (!stop) {
|
|
671
|
+
if (display.json) { console.log(JSON.stringify(publicOperation(result), null, 2)); return spawnRecovery(result, true); }
|
|
672
|
+
console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`);
|
|
673
|
+
return spawnRecovery(result, false, humanRecovery(false) ? display.options : undefined);
|
|
674
|
+
}
|
|
675
|
+
if (humanRecovery(display.json)) printRecovery(renderSummary(result, await recoveryEndInspection(result), display.options));
|
|
676
|
+
else {
|
|
677
|
+
console.log(JSON.stringify(publicOperation(result), null, 2));
|
|
678
|
+
if (!display.json) for (const line of stopAndArchiveSummary(result)) console.log(line);
|
|
679
|
+
}
|
|
568
680
|
}
|
|
569
|
-
else if (["completed", "cancelled"].includes(operation.phase)) console.log(`recovery is already ${operation.phase}`);
|
|
681
|
+
else if (["completed", "cancelled"].includes(operation.phase)) console.log(display.json ? JSON.stringify(publicOperation(operation, runner), null, 2) : `recovery is already ${operation.phase}`);
|
|
570
682
|
// #215: an operation being abandoned is never resumed, whichever coordinator started it (an older one would).
|
|
571
683
|
else if (operation.disposition) fail(`a stop-and-archive of this operation is partway; nothing was resumed; ${nextActionsText(operation, await liveProjects(operation, makeRecoveryDriver(undefined, delegatedPeer).inspect))}`);
|
|
572
|
-
else if (runner === "unknown")
|
|
573
|
-
|
|
574
|
-
|
|
684
|
+
else if (runner === "unknown") {
|
|
685
|
+
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner), null, 2));
|
|
686
|
+
else if (humanRecovery(false)) printRecoveryStatus(operation, "Runner ownership is unknown; nothing was started.", display.options);
|
|
687
|
+
else console.log(`whether a runner holds this operation could not be read; nothing was started; ${recoveryCommand(operation, "status")}`);
|
|
688
|
+
}
|
|
689
|
+
else if (runner) {
|
|
690
|
+
if (display.json) console.log(JSON.stringify(publicOperation(operation, runner), null, 2));
|
|
691
|
+
else if (humanRecovery(false)) printRecoveryStatus(operation, `runner ${runner} is still working on this operation`, display.options);
|
|
692
|
+
else console.log(`runner ${runner} is still working on this operation; ${recoveryCommand(operation, "status")}`);
|
|
693
|
+
}
|
|
694
|
+
else if (display.json) { spawnRecovery(operation, true); console.log(JSON.stringify(publicOperation(operation), null, 2)); }
|
|
695
|
+
else spawnRecovery(operation, false, humanRecovery(false) ? display.options : undefined);
|
|
575
696
|
},
|
|
576
697
|
"recovery-run": async () => {
|
|
577
698
|
if (args.length !== 1 || process.env.AGENTHUB_RECOVERY_OPERATION !== args[0]) fail("recovery-run is an internal command");
|
|
@@ -804,8 +925,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
804
925
|
}
|
|
805
926
|
assertLifecycleAvailable();
|
|
806
927
|
const { one, rest } = takeFlags(args, ["--mode", "--model", "--session-id"], []);
|
|
807
|
-
|
|
808
|
-
if (
|
|
928
|
+
const adapter = definitionOf("qwen")!;
|
|
929
|
+
if (one["--session-id"] !== undefined && !adapter.session.crashIdentity?.({ sessionId: one["--session-id"] }).sessionId) fail("invalid Qwen session identity");
|
|
930
|
+
if (rest.length || (one["--mode"] !== undefined && !(adapter.start.forms as readonly string[]).includes(one["--mode"]))) fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
|
|
809
931
|
const hub = await connect();
|
|
810
932
|
try {
|
|
811
933
|
const reply = await hub.request({ t: "start", peer: "qwen", args: { mode: "headless", ...(one["--model"] ? { model: one["--model"] } : {}), ...(one["--session-id"] ? { sessionId: one["--session-id"] } : {}) }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 35_000);
|
|
@@ -1003,7 +1125,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1003
1125
|
|
|
1004
1126
|
watch: async () => {
|
|
1005
1127
|
const peer = args[0];
|
|
1006
|
-
if (peer !== "
|
|
1128
|
+
if (typeof peer !== "string" || !definitionOf(peer)?.capabilities?.watch) fail("usage: ahub watch kimi|qwen [--no-thoughts] [--color=auto|always|never]");
|
|
1007
1129
|
if (args.slice(1).some(arg => arg !== "--no-thoughts" && !/^--color=(auto|always|never)$/.test(arg))) fail("unsupported watch option");
|
|
1008
1130
|
const color = resolveColor(args.find(arg => arg.startsWith("--color="))?.slice(8), { isTTY: !!process.stdout.isTTY, ...process.env });
|
|
1009
1131
|
if (color instanceof Error) fail(color.message);
|
|
@@ -1331,11 +1453,11 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1331
1453
|
} finally { hub.close(); }
|
|
1332
1454
|
},
|
|
1333
1455
|
settings: async () => {
|
|
1334
|
-
const usage = "usage: ahub settings [list] [--json] | get <key> [--json] | set <key> <value|inherit> [--yes] [--preview] | undo";
|
|
1456
|
+
const usage = "usage: ahub settings [list] [--json] | get <key> [--json] | set <key> <value|inherit> [--yes] [--preview] | undo | reload";
|
|
1335
1457
|
const flags = args.filter((arg) => arg.startsWith("--")), [sub = "list", key, text, ...extra] = args.filter((arg) => !arg.startsWith("--"));
|
|
1336
|
-
const allowed: Record<string, string[]> = { list: ["--json"], get: ["--json"], set: ["--yes", "--preview", "--now"], undo: [] };
|
|
1458
|
+
const allowed: Record<string, string[]> = { list: ["--json"], get: ["--json"], set: ["--yes", "--preview", "--now"], undo: [], reload: [] };
|
|
1337
1459
|
if (!Object.hasOwn(allowed, sub) || flags.some((flag) => !allowed[sub]!.includes(flag)) || extra.length) fail(usage);
|
|
1338
|
-
if ((sub === "get" && (!key || text !== undefined)) || (sub === "set" && (!key || text === undefined)) || ((sub === "list" || sub === "undo") && key !== undefined)) fail(usage);
|
|
1460
|
+
if ((sub === "get" && (!key || text !== undefined)) || (sub === "set" && (!key || text === undefined)) || ((sub === "list" || sub === "undo" || sub === "reload") && key !== undefined)) fail(usage);
|
|
1339
1461
|
const def = key === undefined ? undefined : settingDef(key);
|
|
1340
1462
|
if (typeof def === "string") fail(`${key}: ${def}`);
|
|
1341
1463
|
if (flags.includes("--now") && (typeof def !== "object" || def.group !== "Roster")) fail(usage);
|
|
@@ -1355,6 +1477,43 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1355
1477
|
}
|
|
1356
1478
|
return reply;
|
|
1357
1479
|
};
|
|
1480
|
+
if (sub === "reload") {
|
|
1481
|
+
let result = await request({ t: "settings_reload" });
|
|
1482
|
+
const confirmation = result.confirmation;
|
|
1483
|
+
if (Array.isArray(result.rejected)) for (const row of result.rejected.slice(0, 100)) console.log(terminalText(`${String(row.key).slice(0, 160)}: ${String(row.reason).slice(0, 1000)}`));
|
|
1484
|
+
if (confirmation !== undefined) {
|
|
1485
|
+
if (!confirmation || typeof confirmation.token !== "string" || !confirmation.token || confirmation.token.length > 256 || !Array.isArray(confirmation.keys) || !confirmation.keys.length || confirmation.keys.length > 200 || confirmation.keys.some((key: unknown) => typeof key !== "string" || key.length > 160)) fail("invalid reload confirmation; guarded settings were not applied");
|
|
1486
|
+
const fullValue = (value: unknown): boolean => {
|
|
1487
|
+
if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 16_384) return false;
|
|
1488
|
+
if (value === "(unset)") return true;
|
|
1489
|
+
try { JSON.parse(value); return true; } catch { return false; }
|
|
1490
|
+
};
|
|
1491
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") > 131_072 || new Set(confirmation.keys).size !== confirmation.keys.length || !Array.isArray(confirmation.changes) || confirmation.changes.length !== confirmation.keys.length || confirmation.changes.some((change: unknown, index: number) => {
|
|
1492
|
+
if (!change || typeof change !== "object" || Array.isArray(change)) return true;
|
|
1493
|
+
const row = change as Record<string, unknown>;
|
|
1494
|
+
return row.key !== confirmation.keys[index] || !fullValue(row.before) || !fullValue(row.after) || (row.effect !== undefined && (typeof row.effect !== "string" || Buffer.byteLength(row.effect, "utf8") > 2048));
|
|
1495
|
+
})) fail("reload preview is incomplete or unavailable; upgrade the running hub and run ahub settings reload again to review every full value; guarded settings were not applied");
|
|
1496
|
+
const token = confirmation.token;
|
|
1497
|
+
console.log("Guarded settings awaiting confirmation:");
|
|
1498
|
+
for (const change of confirmation.changes) {
|
|
1499
|
+
console.log(` ${terminalText(change.key)}: ${terminalText(change.before)} -> ${terminalText(change.after)}`);
|
|
1500
|
+
if (change.effect !== undefined) console.log(` ${terminalText(change.effect)}`);
|
|
1501
|
+
}
|
|
1502
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) fail("guarded settings need an interactive person's terminal; run ahub settings reload there; guarded settings were not applied");
|
|
1503
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
1504
|
+
const ended = new AbortController();
|
|
1505
|
+
rl.once("close", () => ended.abort());
|
|
1506
|
+
let accepted = false;
|
|
1507
|
+
try { accepted = (await rl.question("Apply guarded settings? [y/N] ", { signal: AbortSignal.any([AbortSignal.timeout(30_000), ended.signal]) })).trim().toLowerCase() === "y"; }
|
|
1508
|
+
catch { /* EOF, cancellation and a missed confirmation deadline grant nothing. */ }
|
|
1509
|
+
finally { rl.close(); }
|
|
1510
|
+
if (!accepted) return console.log("guarded settings were not applied");
|
|
1511
|
+
result = await request({ t: "settings_reload", confirm: token });
|
|
1512
|
+
if (result.confirmation !== undefined) fail("reload candidate changed; run ahub settings reload again to review it; guarded settings were not applied");
|
|
1513
|
+
if (Array.isArray(result.rejected)) for (const row of result.rejected.slice(0, 100)) console.log(terminalText(`${String(row.key).slice(0, 160)}: ${String(row.reason).slice(0, 1000)}`));
|
|
1514
|
+
}
|
|
1515
|
+
return console.log(paint([{ text: result.text }], false));
|
|
1516
|
+
}
|
|
1358
1517
|
if (sub === "undo") {
|
|
1359
1518
|
const view = await request({ t: "settings_get" }), undoDef = settingDef(view.undo);
|
|
1360
1519
|
const rosterPeer = typeof undoDef !== "string" && undoDef.group === "Roster" ? undoDef.path[1] : undefined;
|
|
@@ -1382,15 +1541,16 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1382
1541
|
}
|
|
1383
1542
|
const view = await request({ t: "settings_get" });
|
|
1384
1543
|
const rows = (view.rows as SettingRow[]).filter((row) => sub === "list" || row.key === key);
|
|
1385
|
-
if (flags.includes("--json")) return console.log(JSON.stringify(sub === "get" ? rows[0] : { rows, ...(view.undo ? { undo: view.undo } : {}) }, null, 2));
|
|
1544
|
+
if (flags.includes("--json")) return console.log(JSON.stringify(sub === "get" ? rows[0] : { rows, pendingRestart: view.pendingRestart ?? [], ...(view.undo ? { undo: view.undo } : {}) }, null, 2));
|
|
1386
1545
|
const width = Math.max(...rows.map((row) => row.key.length)), valueWidth = Math.max(...rows.map((row) => settingText(row.value).length));
|
|
1387
1546
|
let group = "";
|
|
1388
1547
|
for (const row of rows) {
|
|
1389
1548
|
if (sub === "list" && row.group !== group) console.log(`${group ? "\n" : ""}${group = row.group}`);
|
|
1390
|
-
const facts = [`from ${row.source}`, row.applies === "live" ? "applies at once" : row.applies === "
|
|
1549
|
+
const facts = [`from ${row.source}`, `file: ${settingText(row.fileValue === undefined ? row.pending ?? row.value : row.fileValue)}`, row.applies === "live" ? "applies at once" : row.applies === "next use" ? "applies at next use" : "pending restart", ...(row.restartReason ? [row.restartReason] : []), ...(row.note ? [row.note] : [])];
|
|
1391
1550
|
console.log(` ${row.key.padEnd(width)} ${settingText(row.value).padEnd(valueWidth)} ${facts.join("; ")}`);
|
|
1392
|
-
if (sub === "get") console.log(` ${row.label}; written to ${row.file}; takes ${row.type === "boolean" ? "true, false" : row.type === "peers" ? "a comma list of peer ids" : row.type === "roles" ? "up to two comma-separated catalog ids" : (row.values ?? []).join(", ")}${row.file === "this hub run" ? "" : ", or inherit to remove the machine-local value"}${row.risk === "raises" ? "; a dashboard needs a settings session (ahub ui --settings) to raise it" : ""}`);
|
|
1551
|
+
if (sub === "get") console.log(` ${row.label}; written to ${row.file}; takes ${row.type === "boolean" ? "true, false" : row.type === "integer" || row.type === "number" ? `a number from ${row.min ?? 0} to ${row.max ?? "the configured bound"}` : row.type === "peers" ? "a comma list of peer ids" : row.type === "roles" ? "up to two comma-separated catalog ids" : (row.values ?? []).join(", ")}${row.file === "this hub run" ? "" : ", or inherit to remove the machine-local value"}${row.risk === "raises" ? "; a dashboard needs a settings session (ahub ui --settings) to raise it" : ""}`);
|
|
1393
1552
|
}
|
|
1553
|
+
if (sub === "list" && view.pendingRestart?.length) console.log(`\npending restart: ${view.pendingRestart.map((entry: { key: string }) => entry.key).join(", ")}`);
|
|
1394
1554
|
if (sub === "list" && view.undo) console.log(`\nahub settings undo puts back the last write (${view.undo})`);
|
|
1395
1555
|
} finally { hub.close(); }
|
|
1396
1556
|
},
|
|
@@ -1842,6 +2002,7 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
1842
2002
|
row(verifiedJcodeCommand(config.jcode_cmd, cwd), "jcode", "CLI/source identity only; provider, project MCP and native teardown require separate receipts; native policy unmanaged");
|
|
1843
2003
|
}
|
|
1844
2004
|
if (config.roster.qwen === true) {
|
|
2005
|
+
// ponytail: keep this bounded CLI-version probe and 0.25.0 pin in the A1 guard baseline; A2 moves every version probe together when VersionContext gains its observed input.
|
|
1845
2006
|
const v = version(config.qwen_cmd[0]!);
|
|
1846
2007
|
row(v === "0.25.0", "qwen", v === "0.25.0" ? "CLI reports 0.25.0; native ACP identity/modes and provider access require a separate live check" : "Qwen Code 0.25.0 executable not confirmed; provider and ACP readiness unknown");
|
|
1847
2008
|
} // issue #83
|
package/src/cli/output.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { formatReport, formatTaskReport, type Report, type TaskReport } from "..
|
|
|
7
7
|
|
|
8
8
|
export interface DoctorCheck { section: "Tools" | "Hub" | "Config" | "Models" | "Memory"; level: "ok" | "warn" | "fail" | "unknown"; name: string; detail: string }
|
|
9
9
|
export interface StatusOutput {
|
|
10
|
+
pendingRestart?: readonly { key: string; reason: string }[];
|
|
10
11
|
taskStalls?: readonly { task: number; cause: { kind: string; peer?: string; dependency?: number; code: string } }[];
|
|
11
12
|
pendingTaskOffers?: readonly { task: number; peer: string }[];
|
|
12
13
|
isolatedChecks?: { pending?: number; orphans?: string[]; unknownReceipts?: readonly string[]; maintenanceUnavailable?: boolean };
|
|
@@ -188,6 +189,7 @@ export function renderStatus(data: StatusOutput, columns?: number, now = Date.no
|
|
|
188
189
|
if (data.roleInfo) data = { ...data, peers: Object.fromEntries(Object.entries(data.peers).map(([peer, value]) => [peer, { ...value, ...data.roleInfo?.[peer] }])) };
|
|
189
190
|
const out = lines(`agent-hub${data.version ? ` ${data.version}` : ""} pid ${data.pid ?? "unknown"} control 127.0.0.1:${data.controlPort ?? "unknown"}`, columns, "info");
|
|
190
191
|
if (data.cwd) out.push(...lines(data.cwd, columns));
|
|
192
|
+
for (const pending of data.pendingRestart ?? []) out.push(...detail("pending restart", `${pending.key}: ${pending.reason}`, columns, "attention"));
|
|
191
193
|
for (const stall of data.taskStalls ?? []) out.push(...detail(`task #${stall.task} stalled`, `${stall.cause.kind}${stall.cause.peer ? ` ${stall.cause.peer}` : ""}${stall.cause.dependency ? ` #${stall.cause.dependency}` : ""}: ${stall.cause.code}; configure the peer, ahub task assign ${stall.task} <peer>, or ahub task cancel ${stall.task} --reason <text>`, columns, "attention"));
|
|
192
194
|
for (const offer of data.pendingTaskOffers ?? []) out.push(...detail("task offer", `open ${offer.peer}: task #${offer.task} is waiting for it`, columns, "attention"));
|
|
193
195
|
if (data.memoryProfile) {
|
package/src/cli/preview.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { definitionOf, kindOf } from "../adapters/registry.ts";
|
|
1
2
|
import { jcodeCommand } from "../adapters/jcode.ts";
|
|
2
3
|
import { qwenSessionId } from "../hub/crash.ts";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
@@ -7,7 +8,7 @@ import { childEnv } from "../hub/child-process.ts";
|
|
|
7
8
|
import { relayModelIds } from "../models/relay.ts";
|
|
8
9
|
import { buildPiLaunch } from "../pi/launch.ts";
|
|
9
10
|
import { startModeOf } from "../hub/start-mode.ts";
|
|
10
|
-
import { buildLaunch, buildKimiLaunch,
|
|
11
|
+
import { buildLaunch, buildKimiLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks, type Launch } from "./launch.ts";
|
|
11
12
|
import { kimiMcpPlan } from "./kimi-mcp.ts";
|
|
12
13
|
|
|
13
14
|
const REDACTED = "[redacted]";
|
|
@@ -71,14 +72,16 @@ export function launcherPreview(tool: "claude" | "codex" | "kimi" | "pi" | "qwen
|
|
|
71
72
|
launch = { cmd: cmd[0]!, args: cmd.slice(1) };
|
|
72
73
|
unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
|
|
73
74
|
unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
|
|
74
|
-
} else if (tool === "qwen") {
|
|
75
|
+
} else if (kindOf(tool) === "qwen") {
|
|
75
76
|
const mode = value("--mode") ?? "headless", model = value("--model");
|
|
76
|
-
|
|
77
|
+
const adapter = definitionOf(tool)!;
|
|
78
|
+
if (!(adapter.start.forms as readonly string[]).includes(mode)) throw new Error("Qwen TUI is not offered before verification");
|
|
77
79
|
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Qwen option");
|
|
78
80
|
const sessionId = value("--session-id");
|
|
79
|
-
if (sessionId !== undefined && !
|
|
81
|
+
if (sessionId !== undefined && !adapter.session.crashIdentity?.({ sessionId }).sessionId) throw new Error("invalid Qwen session identity");
|
|
80
82
|
if (sessionId) unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
|
|
81
|
-
launch =
|
|
83
|
+
launch = adapter.start.launch({ instance: tool, form: "headless", projectRoot: cwd, stateDir, entrypoint: join(import.meta.dir, "main.js"),
|
|
84
|
+
userArgs: [], unattended: false, env: {}, preview: true, nativeCommand: nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model: model ? REDACTED : undefined });
|
|
82
85
|
unresolved.push({ field: "nativeIdentity", reason: "Qwen Code 0.25.0 identity and permission modes are checked at ACP initialization; preview starts nothing" });
|
|
83
86
|
} else if (tool === "kimi") {
|
|
84
87
|
const model = value("--model");
|