@staix/agent-hub 0.12.31 → 0.12.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/README.md +13 -7
  3. package/docs/agent-notes/adapters.md +7 -4
  4. package/docs/agent-notes/bus.md +1 -0
  5. package/docs/agent-notes/daemon.md +10 -7
  6. package/docs/agent-notes/local-worker.md +1 -1
  7. package/docs/agent-notes/tasks.md +3 -2
  8. package/docs/agent-notes/tests.md +1 -0
  9. package/docs/cooperbench.md +1 -1
  10. package/docs/gsd-collaboration-analysis.md +1 -1
  11. package/docs/memory.md +1 -1
  12. package/docs/operations.md +114 -22
  13. package/docs/quickstart.md +10 -6
  14. package/docs/security.md +12 -8
  15. package/docs/smoke.md +34 -0
  16. package/docs/specs/2026-09-19-agent-hub-design.md +204 -0
  17. package/docs/specs/2026-09-20-upgrade-recovery-design.md +191 -0
  18. package/docs/verification/2026-10-18-0.12.32.md +54 -0
  19. package/docs/verified.json +20 -15
  20. package/package.json +2 -2
  21. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  22. package/plugins/agent-hub/server.js +1296 -26
  23. package/src/adapters/acp.ts +5 -6
  24. package/src/adapters/definitions/builtin-metadata.ts +121 -0
  25. package/src/adapters/definitions/kimi-native.ts +4 -0
  26. package/src/adapters/definitions/qwen-native.ts +24 -0
  27. package/src/adapters/definitions/qwen.ts +71 -0
  28. package/src/adapters/pi.ts +6 -2
  29. package/src/adapters/registry-types.ts +174 -0
  30. package/src/adapters/registry.ts +36 -0
  31. package/src/cli/console-state.ts +74 -5
  32. package/src/cli/console.ts +35 -2
  33. package/src/cli/help.ts +3 -1
  34. package/src/cli/identity.ts +13 -3
  35. package/src/cli/launch.ts +1 -6
  36. package/src/cli/main.ts +200 -39
  37. package/src/cli/output.ts +2 -0
  38. package/src/cli/preview.ts +8 -5
  39. package/src/cli/recovery-guidance.ts +137 -0
  40. package/src/cli/recovery-presentation-options.ts +8 -0
  41. package/src/cli/recovery-session-file.ts +18 -0
  42. package/src/cli/terminal-recovery.ts +48 -0
  43. package/src/cli/upgrade-interactive.ts +129 -75
  44. package/src/cli/upgrade-labels.ts +30 -0
  45. package/src/cli/upgrade-presentation-data.ts +41 -0
  46. package/src/cli/upgrade-presentation-flags.ts +31 -0
  47. package/src/cli/upgrade-presentation.ts +217 -0
  48. package/src/cli/upgrade-restore-state.ts +6 -2
  49. package/src/cli/upgrade-runtime.ts +345 -194
  50. package/src/cli/upgrade.ts +276 -25
  51. package/src/hub/bus.ts +11 -0
  52. package/src/hub/cohorts.ts +2 -2
  53. package/src/hub/config-apply.ts +242 -0
  54. package/src/hub/config-reload.ts +120 -0
  55. package/src/hub/config-trust.ts +11 -2
  56. package/src/hub/crash.ts +8 -6
  57. package/src/hub/daemon.ts +559 -170
  58. package/src/hub/events.ts +2 -2
  59. package/src/hub/manager.ts +1 -1
  60. package/src/hub/peer-catalog.ts +10 -13
  61. package/src/hub/peers.ts +12 -2
  62. package/src/hub/permission-mode.ts +5 -3
  63. package/src/hub/recovery-continuity.ts +152 -0
  64. package/src/hub/recovery-store.ts +10 -4
  65. package/src/hub/sdlc.ts +5 -1
  66. package/src/hub/settings.ts +50 -36
  67. package/src/hub/settled-roles.ts +35 -0
  68. package/src/hub/supervision.ts +3 -2
  69. package/src/hub/tasks.ts +28 -8
  70. package/src/hub/ui.ts +1 -1
  71. package/src/memory/brief.ts +6 -5
  72. package/src/pi/session.ts +23 -4
  73. 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 { recordTerminalLaunch } from "./terminal-recovery.ts";
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("live-test cannot write person-attributed content; use peer tools or a person's terminal"); }
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 spawnRecovery(operation: RecoveryOperation, quiet = false): void {
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) console.log(`Recovery operation ${operation.id} scheduled.\n${recoveryCommand(operation, "status")}`);
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>): 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 child = spawn(process.execPath, [argv[0]!, ...liveTestArgs, ...argv.slice(1)], { cwd, stdio: "inherit" });
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 { one, rest } = takeFlags(args, ["--to"], []);
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 dry = rest.includes("--dry-run"), yes = rest.includes("--yes");
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 && !!process.stdin.isTTY && !!process.stdout.isTTY;
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
- console.log(JSON.stringify(plan, null, 2));
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 [action, given, ...more] = args;
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 && process.stdin.isTTY && process.stdout.isTTY) {
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 operation = readOperation<RecoveryOperation>(id);
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") { await abortRecovery(id, makeRecoveryDriver(undefined, delegatedPeer)); console.log("preflight cancelled; no committed transition was rolled back"); }
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 (including explicit --live-test delegation); the reason is kept in the operation's audit.
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) { console.log(`${one["--fresh-session"]}: a fresh session is accepted and the lost one is recorded; resuming`); return spawnRecovery(result); }
566
- console.log(JSON.stringify(publicOperation(result), null, 2));
567
- console.log(`${result.plan.kind} abandoned, not completed; the recovery lock is released. A project whose target ran starts again with that version's CLI.`);
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") console.log(`whether a runner holds this operation could not be read; nothing was started; ${recoveryCommand(operation, "status")}`);
573
- else if (runner) console.log(`runner ${runner} is still working on this operation; ${recoveryCommand(operation, "status")}`);
574
- else spawnRecovery(operation);
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
- if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Qwen session identity");
808
- if (rest.length || (one["--mode"] !== undefined && one["--mode"] !== "headless")) fail("Qwen TUI is not offered before verification; usage: ahub qwen [--mode headless] [--model <alias>] [--session-id <id>]");
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 !== "kimi" && peer !== "qwen") fail("usage: ahub watch kimi|qwen [--no-thoughts] [--color=auto|always|never]");
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 === "peer start" ? "applies at the peer's next start" : "read at hub start", ...(row.pending !== undefined ? [`${settingText(row.pending)} at the next ${row.applies === "peer start" ? "peer" : "hub"} start`] : []), ...(row.note ? [row.note] : [])];
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) {
@@ -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, buildQwenLaunch, buildKimiTuiLaunch, nativeLaunchEnv, claudeObservationHooks, type Launch } from "./launch.ts";
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
- if (mode !== "headless") throw new Error("Qwen TUI is not offered before verification");
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 && !qwenSessionId(sessionId)) throw new Error("invalid Qwen session identity");
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 = buildQwenLaunch(nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model ? REDACTED : undefined);
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");