@indigoai-us/hq-cli 5.276.0 → 5.278.0

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 (83) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +111 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +23 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.d.ts +2 -1
  61. package/dist/lib/lanes/config.js +31 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  63. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  64. package/dist/lib/lanes/index.d.ts +1 -1
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  66. package/dist/lib/lanes/keep-alive.js +3 -0
  67. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  68. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  69. package/dist/lib/lanes/reconcile.d.ts +9 -0
  70. package/dist/lib/lanes/reconcile.js +127 -2
  71. package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
  72. package/dist/lib/lanes/runaway-tool-guard.js +284 -0
  73. package/dist/lib/lanes/spawn.js +17 -10
  74. package/dist/lib/lanes/store.js +45 -1
  75. package/dist/lib/lanes/types.d.ts +37 -1
  76. package/dist/lib/lanes/types.js +2 -2
  77. package/dist/lib/lanes/watcher.d.ts +2 -0
  78. package/dist/lib/lanes/watcher.js +13 -2
  79. package/dist/secrets-env-fast-path.d.ts +5 -0
  80. package/dist/secrets-env-fast-path.js +16 -0
  81. package/dist/secrets-env-fast-route.d.ts +18 -0
  82. package/dist/secrets-env-fast-route.js +54 -0
  83. package/package.json +2 -2
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { Option } from "commander";
32
32
  import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
33
+ import { registerCoreDelegateCommand } from "./core-delegate.js";
33
34
  import { registerCoreOntologyCommand } from "./core-ontology.js";
34
35
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
35
36
  import { resolveLiveRoot } from "../utils/hq-roots.js";
@@ -213,6 +214,14 @@ export const SCAFFOLD_ONLY_ASSETS = [
213
214
  { asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
214
215
  { asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
215
216
  { asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
217
+ // Wave 1a delegate shells remain bundled as the default-off rollback path.
218
+ // Their roots mirror the original scripts; pickup does not read an HQ root.
219
+ { asset: "core/scripts/hq-delegate-resolve.sh", root: "live", summary: "Rollback shell for native delegate resolve" },
220
+ { asset: "core/scripts/hq-delegate-pickup.sh", root: "cwd", summary: "Rollback shell for native delegate pickup (root independent)" },
221
+ { asset: "core/scripts/hq-delegate-repo.sh", root: "live", summary: "Rollback shell for native delegate repo" },
222
+ { asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
223
+ { asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
224
+ { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
216
225
  ...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
217
226
  asset,
218
227
  root,
@@ -288,6 +297,7 @@ export function registerCoreCommands(program) {
288
297
  registerCoreCheckpointCommand(core);
289
298
  registerCoreOntologyCommand(core);
290
299
  registerCoreCheckpointGateSettingCommand(core);
300
+ registerCoreDelegateCommand(core);
291
301
  // These command names intentionally own native implementations. Their
292
302
  // similarly named scaffold assets are forwarders, so sending them through
293
303
  // runEntry would recurse back into this command forever.
@@ -16,6 +16,7 @@
16
16
  import type { Command } from "commander";
17
17
  import { type LegacyUnit } from "../lib/daemon/legacy.js";
18
18
  import type { HostApp } from "../lib/daemon/supervisor.js";
19
+ import { type SyncModeVaultClient } from "./sync-mode.js";
19
20
  import type { RunCommand } from "../lib/service-manager/launchd.js";
20
21
  export interface DaemonCommandDeps {
21
22
  env: NodeJS.ProcessEnv;
@@ -38,6 +39,8 @@ export interface DaemonCommandDeps {
38
39
  userHome: (user: string) => string;
39
40
  /** The pid is a live `hq daemon run`. */
40
41
  daemonAlive: (pid: number) => boolean;
42
+ /** Test seam for the company sync-mode commands. */
43
+ syncModeClient?: () => Promise<SyncModeVaultClient>;
41
44
  windowsUser?: string;
42
45
  windowsHeadlessLauncher?: string;
43
46
  systemUnitDir?: string;
@@ -18,7 +18,7 @@ import * as os from "node:os";
18
18
  import * as path from "node:path";
19
19
  import { major as semverMajor, valid as validSemver } from "semver";
20
20
  import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits } from "../lib/daemon/catalog.js";
21
- import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
21
+ import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
22
22
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
23
23
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
24
24
  import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
@@ -27,7 +27,10 @@ import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
27
27
  import { bundledHqCloud } from "../lib/daemon/hq-cloud.js";
28
28
  import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
29
29
  import { daemonPaths } from "../lib/daemon/paths.js";
30
+ import { processCommandLine } from "../lib/daemon/lock.js";
30
31
  import { writeControlRequest } from "../lib/daemon/requests.js";
32
+ import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
33
+ import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
31
34
  import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
32
35
  class DaemonCommandError extends Error {
33
36
  }
@@ -96,6 +99,7 @@ async function loadDefaults(over) {
96
99
  windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
97
100
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
98
101
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
102
+ syncModeClient: over.syncModeClient,
99
103
  systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
100
104
  };
101
105
  }
@@ -373,6 +377,135 @@ function queueRequest(d, request, what) {
373
377
  const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
374
378
  d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
375
379
  }
380
+ function daemonSyncModeClient(d) {
381
+ if (d.syncModeClient)
382
+ return d.syncModeClient();
383
+ return (async () => {
384
+ const [{ VaultClient }, { buildVaultConfig, ensureCognitoToken }] = await Promise.all([
385
+ import("@indigoai-us/hq-cloud"),
386
+ import("../utils/cognito-session.js"),
387
+ ]);
388
+ const token = await ensureCognitoToken();
389
+ return new VaultClient(buildVaultConfig(token));
390
+ })();
391
+ }
392
+ function daemonSyncStatus(d) {
393
+ const host = currentHost(d);
394
+ const { paths } = host;
395
+ const config = readDaemonConfig(paths.configPath);
396
+ const daemonPid = runningDaemonPid(paths.lockPath, d.daemonAlive);
397
+ const daemonState = daemonPid === null ? null : readState(paths);
398
+ const syncUnit = daemonState?.units.find((unit) => unit.id === "sync");
399
+ const hqRoot = config.hqRoot ?? d.env.HQ_ROOT?.trim();
400
+ let ownerStatusCandidate = null;
401
+ if (hqRoot) {
402
+ try {
403
+ ownerStatusCandidate = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, d.home, d.env));
404
+ }
405
+ catch (error) {
406
+ const detail = error instanceof Error ? error.message : String(error);
407
+ d.err(`Could not read sync runner owner status; treating it as unavailable: ${detail}`);
408
+ }
409
+ }
410
+ const ownerCommandLine = ownerStatusCandidate
411
+ ? processCommandLine(ownerStatusCandidate.pid, { platform: d.platform, run: d.run })
412
+ : null;
413
+ const ownerStatus = ownerCommandLine && isSyncRunnerCommandLine(ownerCommandLine)
414
+ ? ownerStatusCandidate
415
+ : null;
416
+ return {
417
+ running: syncUnit?.status === "running",
418
+ paused: config.syncPaused === true,
419
+ syncOwner: effectiveSyncOwner(config, host.detected.type),
420
+ owner: ownerStatus?.owner ?? (syncUnit?.status === "running" ? "hq-daemon" : null),
421
+ runnerPid: ownerStatus?.pid ?? syncUnit?.pid ?? null,
422
+ hqCloudVersion: ownerStatus?.version ?? null,
423
+ lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
424
+ lastPassResult: syncUnit?.lastPassResult ?? null,
425
+ unitStatus: syncUnit?.status ?? "not-running",
426
+ reason: syncUnit?.reason ?? null,
427
+ logPath: paths.unitLogPath("sync"),
428
+ };
429
+ }
430
+ async function runDaemonSyncStatus(d, opts) {
431
+ const status = daemonSyncStatus(d);
432
+ if (opts.json) {
433
+ d.out(JSON.stringify(status, null, 2));
434
+ return;
435
+ }
436
+ d.out(`Sync ${status.running ? "running" : status.unitStatus}${status.paused ? " (paused)" : ""}.`);
437
+ d.out(`Configured sync owner: ${status.syncOwner}.`);
438
+ d.out(`Owner: ${status.owner ?? "none"}; runner pid: ${status.runnerPid ?? "none"}; hq-cloud: ${status.hqCloudVersion ?? "unknown"}.`);
439
+ if (status.reason)
440
+ d.out(`Reason: ${status.reason}`);
441
+ d.out(`Log: ${status.logPath}`);
442
+ }
443
+ function queueDaemonSyncControl(d, request) {
444
+ const host = currentHost(d);
445
+ writeControlRequest(host.paths.requestsDir, request);
446
+ handOver(d, host);
447
+ return runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
448
+ }
449
+ async function runDaemonSyncNow(d, opts) {
450
+ const host = currentHost(d);
451
+ const config = readDaemonConfig(host.paths.configPath);
452
+ if (config.syncPaused)
453
+ throw new DaemonCommandError("sync is paused; run `hq daemon sync resume` first");
454
+ if (!enabledServices(host.detected.type, config).includes("sync")) {
455
+ throw new DaemonCommandError("sync is disabled for this machine; run `hq daemon enable sync` first");
456
+ }
457
+ const running = queueDaemonSyncControl(d, { action: "restart", unit: "sync" });
458
+ const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
459
+ d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
460
+ }
461
+ async function runDaemonSyncPause(d, paused, opts) {
462
+ const host = currentHost(d);
463
+ const config = readDaemonConfig(host.paths.configPath);
464
+ if (config.syncPaused !== paused)
465
+ writeDaemonConfig(host.paths.configPath, { ...config, syncPaused: paused });
466
+ const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
467
+ const result = { paused, persisted: true, daemonRunning };
468
+ d.out(opts.json ? JSON.stringify(result, null, 2) : `Sync ${paused ? "paused" : "resumed"}${daemonRunning ? "" : "; the setting will apply when the daemon starts"}.`);
469
+ }
470
+ async function runDaemonSyncTakeOwnership(d, opts) {
471
+ const host = currentHost(d);
472
+ const config = readDaemonConfig(host.paths.configPath);
473
+ if (config.syncOwner !== "daemon")
474
+ writeDaemonConfig(host.paths.configPath, { ...config, syncOwner: "daemon" });
475
+ const daemonRunning = queueDaemonSyncControl(d, { action: "reconcile" });
476
+ const result = { syncOwner: "daemon", persisted: true, daemonRunning };
477
+ d.out(opts.json ? JSON.stringify(result, null, 2) : `Daemon sync ownership persisted${daemonRunning ? "; asked the daemon to reconcile" : "; it applies when the daemon starts"}.`);
478
+ }
479
+ async function runDaemonSyncModeGet(d, companySlug, opts) {
480
+ const result = await getSyncModeForCompany({ companySlug, vaultClient: await daemonSyncModeClient(d) });
481
+ const value = {
482
+ companySlug: result.companySlug,
483
+ membershipId: result.membershipId,
484
+ mode: result.config.syncMode,
485
+ isDefault: result.config.isDefault,
486
+ ...(result.config.updatedAt ? { updatedAt: result.config.updatedAt } : {}),
487
+ };
488
+ d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.mode}${value.isDefault ? " (default)" : ""}`);
489
+ }
490
+ async function runDaemonSyncModeSet(d, companySlug, mode, opts) {
491
+ if (mode !== "all" && mode !== "shared") {
492
+ throw new DaemonCommandError(`Invalid sync mode '${mode}'. Use all or shared.`);
493
+ }
494
+ const result = await setSyncMode({
495
+ mode,
496
+ companySlug,
497
+ vaultClient: await daemonSyncModeClient(d),
498
+ });
499
+ const value = {
500
+ companySlug: result.companySlug,
501
+ membershipId: result.membershipId,
502
+ previousMode: result.previousMode,
503
+ mode: result.newMode,
504
+ isDefault: result.newConfig.isDefault,
505
+ ...(result.newConfig.updatedAt ? { updatedAt: result.newConfig.updatedAt } : {}),
506
+ };
507
+ d.out(opts.json ? JSON.stringify(value, null, 2) : `${value.companySlug}: ${value.previousMode} → ${value.mode}`);
508
+ }
376
509
  async function runToggle(d, action, services) {
377
510
  const host = currentHost(d);
378
511
  const { paths } = host;
@@ -609,6 +742,45 @@ export function registerDaemonCommand(program, over = {}) {
609
742
  .description("Show the machine type and each service's state")
610
743
  .option("--json", "Print machine-readable JSON")
611
744
  .action(wrap((d, opts) => runStatus(d, opts)));
745
+ const sync = daemon
746
+ .command("sync")
747
+ .description("Inspect and control the daemon's company sync runner");
748
+ sync
749
+ .command("status")
750
+ .description("Show sync ownership, heartbeat, last pass and log path")
751
+ .option("--json", "Print machine-readable JSON")
752
+ .action(wrap((d, opts) => runDaemonSyncStatus(d, opts)));
753
+ sync
754
+ .command("now")
755
+ .description("Start a fresh sync pass immediately")
756
+ .option("--json", "Print machine-readable JSON")
757
+ .action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
758
+ sync
759
+ .command("pause")
760
+ .description("Pause daemon sync until resumed")
761
+ .option("--json", "Print machine-readable JSON")
762
+ .action(wrap((d, opts) => runDaemonSyncPause(d, true, opts)));
763
+ sync
764
+ .command("resume")
765
+ .description("Resume daemon sync after a pause")
766
+ .option("--json", "Print machine-readable JSON")
767
+ .action(wrap((d, opts) => runDaemonSyncPause(d, false, opts)));
768
+ sync
769
+ .command("take-ownership")
770
+ .description("Persist daemon ownership of desktop sync and reconcile")
771
+ .option("--json", "Print machine-readable JSON")
772
+ .action(wrap((d, opts) => runDaemonSyncTakeOwnership(d, opts)));
773
+ const mode = sync.command("mode").description("Read or change a company's sync footprint");
774
+ mode
775
+ .command("get <company>")
776
+ .description("Read a company's current all/shared sync mode")
777
+ .option("--json", "Print machine-readable JSON")
778
+ .action(wrap((d, company, opts) => runDaemonSyncModeGet(d, company, opts)));
779
+ mode
780
+ .command("set <company> <mode>")
781
+ .description("Set a company's sync mode to all or shared")
782
+ .option("--json", "Print machine-readable JSON")
783
+ .action(wrap((d, company, mode, opts) => runDaemonSyncModeSet(d, company, mode, opts)));
612
784
  daemon
613
785
  .command("logs [unit]")
614
786
  .description("Print the supervisor log, or one service's log")
@@ -368,5 +368,7 @@ export declare function agentCallerUid(token: string, machine?: () => boolean):
368
368
  export declare const AGENT_THREAD_UNAVAILABLE: string;
369
369
  /** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
370
370
  export declare function formatAgentInboxItem(item: Record<string, unknown>): string;
371
+ /** Remove delegated execution context before agent inbox items can be emitted. */
372
+ export declare function redactAgentInboxItem<T extends object>(item: T): Omit<T, "actingContext">;
371
373
  export declare function registerDmCommand(program: Command): void;
372
374
  //# sourceMappingURL=dm.d.ts.map
@@ -1064,16 +1064,22 @@ export function formatAgentInboxItem(item) {
1064
1064
  const text = str("text") || str("body") || str("subject");
1065
1065
  return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
1066
1066
  }
1067
+ /** Remove delegated execution context before agent inbox items can be emitted. */
1068
+ export function redactAgentInboxItem(item) {
1069
+ const { actingContext: _actingContext, ...safe } = item;
1070
+ return safe;
1071
+ }
1067
1072
  async function runAgentInbox(token, agentUid, opts) {
1068
1073
  const route = `/v1/agents/${encodeURIComponent(agentUid)}/inbox`;
1069
1074
  const res = await vaultApiFetch({ token, path: route });
1070
1075
  if (!res.ok) {
1071
- const text = await res.text().catch(() => "");
1072
- console.error(chalk.red(`GET ${route} → ${res.status}${text ? `: ${text.slice(0, 300)}` : ""}`));
1076
+ // Error bodies are not constrained to the success response shape. Do not
1077
+ // echo them: a proxy or partial failure could include inbox items.
1078
+ console.error(chalk.red(`GET ${route} → ${res.status}`));
1073
1079
  process.exit(1);
1074
1080
  }
1075
1081
  const data = (await res.json().catch(() => ({})));
1076
- let items = Array.isArray(data.messages) ? data.messages : [];
1082
+ let items = (Array.isArray(data.messages) ? data.messages : []).map(redactAgentInboxItem);
1077
1083
  if (opts.limit && Number(opts.limit) > 0)
1078
1084
  items = items.slice(0, Number(opts.limit));
1079
1085
  if (opts.json) {
@@ -1251,7 +1257,8 @@ async function runDmChannel(target, opts) {
1251
1257
  }
1252
1258
  const data = (await res.json());
1253
1259
  const messages = data.messages ?? [];
1254
- const ordered = [...messages].reverse(); // oldest-first for reading
1260
+ const agentCaller = agentCallerUid(token) !== null;
1261
+ const ordered = [...messages].reverse().map((message) => agentCaller ? redactAgentInboxItem(message) : message); // oldest-first for reading; agent JSON must not expose delegated context
1255
1262
  if (opts.json) {
1256
1263
  console.log(JSON.stringify(ordered, null, 2));
1257
1264
  }
@@ -130,6 +130,10 @@ export function createLaneAccountsHandlers(deps) {
130
130
  return {
131
131
  ...status,
132
132
  effective_home: status.home,
133
+ ...(status.auth_state === "home_missing" ? {
134
+ error: "home_missing",
135
+ fix_command: `hq lanes accounts set-home ${provider} ${status.id} <host-home>`,
136
+ } : {}),
133
137
  model: accountCfg?.model ?? null,
134
138
  effort: accountCfg?.effort ?? null,
135
139
  effective_model: model ?? null,
@@ -165,6 +169,7 @@ export function createLaneAccountsHandlers(deps) {
165
169
  // Retain the established response field for clients; balancing is now
166
170
  // unconditional, so this compatibility value is always true.
167
171
  const balanceGateEnabled = true;
172
+ const recentUsageEnabled = await deps.readClaudeAccountBalanceSignalsEnabled(ctx);
168
173
  const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
169
174
  const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
170
175
  const selections = loaded.snapshots.map((host) => {
@@ -173,6 +178,8 @@ export function createLaneAccountsHandlers(deps) {
173
178
  hosts: loaded.snapshots,
174
179
  targetHost: host.capacity.host,
175
180
  config,
181
+ recentUsageEnabled,
182
+ now: ctx.now(),
176
183
  unavailableHosts: loaded.unavailableHosts,
177
184
  });
178
185
  return {
@@ -192,11 +199,19 @@ export function createLaneAccountsHandlers(deps) {
192
199
  });
193
200
  const accounts = Object.keys(config.providers.claude.accounts).sort().map((account) => {
194
201
  const load = loads.find((row) => row.account === account);
202
+ const currentStatus = loaded.snapshots[0]?.capacity.providers.claude.accounts[account];
203
+ const currentHomeMissing = config.providers.claude.accounts[account]?.enabled === true &&
204
+ currentStatus?.auth_state === "home_missing";
195
205
  return {
196
206
  account,
197
207
  enabled: config.providers.claude.accounts[account]?.enabled === true,
198
208
  live_lanes: load?.live_lanes ?? 0,
199
209
  counts_complete: load?.counts_complete ?? false,
210
+ ...(load ? { recent_usage_at: load.recent_usage_at } : {}),
211
+ ...(currentHomeMissing ? {
212
+ error: "home_missing",
213
+ fix_command: `hq lanes accounts set-home claude ${account} <host-home>`,
214
+ } : {}),
200
215
  hosts: loaded.snapshots.map((host) => {
201
216
  const accountStatus = host.capacity.providers.claude.accounts[account];
202
217
  const accountLoad = load?.hosts.find((row) => row.host.kind === host.capacity.host.kind && row.host.id === host.capacity.host.id);
@@ -204,6 +219,11 @@ export function createLaneAccountsHandlers(deps) {
204
219
  host: host.capacity.host,
205
220
  enabled: accountStatus?.enabled ?? false,
206
221
  logged_in: accountStatus?.logged_in ?? null,
222
+ ...(accountStatus?.auth_state !== undefined ? { auth_state: accountStatus.auth_state } : {}),
223
+ ...(accountStatus?.auth_state === "home_missing" ? {
224
+ error: "home_missing",
225
+ fix_command: `hq lanes accounts set-home claude ${account} <host-home>`,
226
+ } : {}),
207
227
  live_lanes: accountLoad?.live_lanes ?? null,
208
228
  lane_counts: accountLoad?.lane_counts ?? { running: null, queued: null, awaiting_input: null },
209
229
  count_source: accountLoad?.count_source ?? "unavailable",
@@ -251,6 +271,10 @@ export function createLaneAccountsHandlers(deps) {
251
271
  suggestions,
252
272
  ...(opts.apply ? { moved, skipped } : {}),
253
273
  });
274
+ const currentHostMissing = Object.entries(config.providers.claude.accounts).some(([account, accountConfig]) => accountConfig.enabled &&
275
+ loaded.snapshots[0]?.capacity.providers.claude.accounts[account]?.auth_state === "home_missing");
276
+ if (currentHostMissing)
277
+ process.exitCode = 1;
254
278
  }
255
279
  async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
256
280
  const callerCwd = ctx.callerCwd();
@@ -276,6 +300,12 @@ export function createLaneAccountsHandlers(deps) {
276
300
  if (opts.company !== undefined) {
277
301
  await resolveSelectedAccountHomeCompany(ctx, opts.company);
278
302
  }
303
+ if (process.platform === "linux" &&
304
+ requestedHome === undefined &&
305
+ registeredAccount !== undefined &&
306
+ effectiveAccountHome(registeredAccount, hostname).source !== "host") {
307
+ throw new LanesError("account_host_home_required", `Account ${id} for ${provider} has no home for this Linux host; pass --home <host-home> to store a host-specific home`, { provider, account: id, hostname });
308
+ }
279
309
  const homeRaw = requestedHome ??
280
310
  (registeredAccount !== undefined
281
311
  ? effectiveAccountHome(registeredAccount, hostname).home
@@ -291,10 +321,22 @@ export function createLaneAccountsHandlers(deps) {
291
321
  existsSync: (p) => fs.existsSync(p),
292
322
  });
293
323
  const nextAccount = registeredAccount === undefined
294
- ? { home, enabled: true }
324
+ ? process.platform === "linux"
325
+ ? {
326
+ home: id === "a" ? DEFAULT_ACCOUNT_HOMES[provider] : `~/.${provider}-${id}`,
327
+ hosts: { [hostname]: { home } },
328
+ enabled: true,
329
+ }
330
+ : { home, enabled: true }
295
331
  : { ...registeredAccount, enabled: true };
296
332
  if (registeredAccount !== undefined && requestedHome !== undefined) {
297
- if (home === registeredAccount.home) {
333
+ if (process.platform === "linux") {
334
+ nextAccount.hosts = {
335
+ ...(registeredAccount.hosts ?? {}),
336
+ [hostname]: { home },
337
+ };
338
+ }
339
+ else if (home === registeredAccount.home) {
298
340
  if (nextAccount.hosts !== undefined) {
299
341
  delete nextAccount.hosts[hostname];
300
342
  if (Object.keys(nextAccount.hosts).length === 0)
@@ -9,7 +9,7 @@ import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMon
9
9
  import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision } from "../lib/lanes/watcher.js";
10
10
  import { laneStories, laneStory, storyWarning } from "../lib/lanes/stories.js";
11
11
  import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
12
- import { keepAliveExclusion, keepAliveSupersededBy, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
12
+ import { keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
13
13
  import { managerStatusGateEnabledForLane } from "../lib/lanes/manager-status-gate.js";
14
14
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
15
15
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -90,20 +90,20 @@ export function createLaneMessageHandlers(deps) {
90
90
  company: companyForLane(lane),
91
91
  env: ctx.env,
92
92
  });
93
- const currentLane = readLane(ctx.hqRoot(), lane.lane_id);
93
+ let currentLane = readLane(ctx.hqRoot(), lane.lane_id);
94
94
  if (currentLane.state === "killed" || currentLane.state === "cancelled") {
95
95
  return { lane: currentLane, exclusion: currentLane.state };
96
96
  }
97
97
  const localLanes = loadAllLanes(ctx.hqRoot());
98
98
  let lanes = localLanes;
99
- const retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
99
+ let retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
100
100
  const dispatchedCallerId = grantContext?.superseded_message_caller_id;
101
101
  const dispatchedSessionId = grantContext?.superseded_message_session_id;
102
- const dispatchedContextBound = grantContext !== undefined &&
102
+ let dispatchedContextBound = grantContext !== undefined &&
103
103
  ctx.isSsmOwnerDispatch() &&
104
104
  sameHireGrantSenior(grantContext.expected_senior, currentLane.senior);
105
- const dispatchedSessionContext = dispatchedContextBound && currentLane.senior.kind === "session";
106
- const dispatchedContextAuthorized = dispatchedContextBound && (dispatchedCallerId !== undefined ||
105
+ let dispatchedSessionContext = dispatchedContextBound && currentLane.senior.kind === "session";
106
+ let dispatchedContextAuthorized = dispatchedContextBound && (dispatchedCallerId !== undefined ||
107
107
  (dispatchedSessionId !== undefined && dispatchedSessionId === currentLane.senior.id));
108
108
  if (dispatchedContextBound && grantContext &&
109
109
  (dispatchedCallerId !== undefined || dispatchedSessionContext)) {
@@ -116,7 +116,61 @@ export function createLaneMessageHandlers(deps) {
116
116
  merged.set(currentLane.lane_id, currentLane);
117
117
  lanes = [...merged.values()];
118
118
  }
119
- const exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
119
+ let exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
120
+ if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
121
+ const runningSupersededMessageDeliveryEnabled = await readLanesRegistryFlag(LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, { company: companyForLane(currentLane), env: ctx.env });
122
+ if (runningSupersededMessageDeliveryEnabled) {
123
+ const latestLane = readLane(ctx.hqRoot(), currentLane.lane_id);
124
+ if (latestLane.state === "killed" || latestLane.state === "cancelled") {
125
+ return { lane: latestLane, exclusion: latestLane.state };
126
+ }
127
+ const latestLocalLanes = loadAllLanes(ctx.hqRoot());
128
+ currentLane = latestLane;
129
+ let latestLanes = latestLocalLanes;
130
+ dispatchedContextBound =
131
+ grantContext !== undefined &&
132
+ ctx.isSsmOwnerDispatch() &&
133
+ sameHireGrantSenior(grantContext.expected_senior, currentLane.senior);
134
+ dispatchedSessionContext = dispatchedContextBound && currentLane.senior.kind === "session";
135
+ dispatchedContextAuthorized = dispatchedContextBound && (dispatchedCallerId !== undefined ||
136
+ (dispatchedSessionId !== undefined && dispatchedSessionId === currentLane.senior.id));
137
+ if (dispatchedContextBound && grantContext &&
138
+ (dispatchedCallerId !== undefined || dispatchedSessionContext)) {
139
+ const merged = new Map(latestLocalLanes.map((record) => [record.lane_id, record]));
140
+ for (const snapshot of grantContext.hierarchy) {
141
+ const contextLane = keepAliveLaneFromSnapshot(snapshot);
142
+ if (contextLane)
143
+ merged.set(contextLane.lane_id, contextLane);
144
+ }
145
+ merged.set(currentLane.lane_id, currentLane);
146
+ latestLanes = [...merged.values()];
147
+ }
148
+ lanes = latestLanes;
149
+ retiredLaneIds = loadRetiredLaneIds(ctx.hqRoot());
150
+ exclusion = keepAliveExclusion(currentLane, lanes, retiredLaneIds, lineageAware, companyForLane);
151
+ if (exclusion === "superseded" && currentLane.state !== "done" && currentLane.state !== "interrupted") {
152
+ const callerLaneId = dispatchedContextBound ? dispatchedCallerId : undefined;
153
+ const callerSessionId = dispatchedContextBound
154
+ ? (dispatchedSessionId === undefined ? null : dispatchedSessionId)
155
+ : undefined;
156
+ const callerMayMessage = callerMayMessageSupersededLane(ctx, currentLane, lanes, lineageAware, companyForLane, callerLaneId, callerSessionId);
157
+ return {
158
+ lane: currentLane,
159
+ exclusion: undefined,
160
+ ...(!dispatchedContextAuthorized && callerMayMessage && ctx.env.HQ_LANE_ID?.trim()
161
+ ? { supersededMessageCallerId: ctx.env.HQ_LANE_ID.trim() }
162
+ : {}),
163
+ ...(callerMayMessage && !dispatchedContextBound && currentLane.senior.kind === "session" &&
164
+ typeof currentLane.senior.id === "string" &&
165
+ currentLane.senior.id.length > 0 &&
166
+ callerMatchesSessionSenior(ctx, currentLane.senior.id)
167
+ ? { supersededMessageSessionId: currentLane.senior.id }
168
+ : {}),
169
+ supersededMessageWarning: `A newer lead supersedes this ${currentLane.state} lane; message delivery is allowed while the lane remains active.`,
170
+ };
171
+ }
172
+ }
173
+ }
120
174
  const callerLaneId = dispatchedContextBound ? dispatchedCallerId : undefined;
121
175
  const callerSessionId = dispatchedContextBound
122
176
  ? (dispatchedSessionId === undefined ? null : dispatchedSessionId)
@@ -552,11 +606,14 @@ export function createLaneMessageHandlers(deps) {
552
606
  : mechanism === "dropbox"
553
607
  ? "awaiting_next_tool_call"
554
608
  : "worker_spawned";
555
- const warning = mechanism === "dropbox" && drainHook !== null && !drainHook.registered
556
- ? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
557
- : mechanism === "dropbox" && next.stalled === true
558
- ? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
559
- : null;
609
+ const warning = [
610
+ eligibility.supersededMessageWarning,
611
+ mechanism === "dropbox" && drainHook !== null && !drainHook.registered
612
+ ? `The drain hook is not registered in ${drainHook.path}; the running worker will not receive this message. It stays in the inbox and is delivered on the lane's next start.`
613
+ : mechanism === "dropbox" && next.stalled === true
614
+ ? `The worker has made no tool call since ${next.stalled_at ?? "an unknown time"} (${next.stalled_reason ?? "stalled"}); delivery waits on its next tool call.`
615
+ : null,
616
+ ].filter((message) => message !== undefined && message !== null).join(" ") || null;
560
617
  if (!opts.silent)
561
618
  printJson({
562
619
  ok: true,
@@ -223,6 +223,7 @@ export function createLaneResumeHandlers(deps) {
223
223
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
224
224
  ...(profileRepin !== undefined ? { profileRepin } : {}),
225
225
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
226
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
226
227
  pending: {
227
228
  trigger: "resume",
228
229
  resumed_from: resumedFrom,
@@ -252,6 +253,7 @@ export function createLaneResumeHandlers(deps) {
252
253
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
253
254
  ...(profileRepin !== undefined ? { profileRepin } : {}),
254
255
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
256
+ ...(hireGrantContext !== undefined ? { grantContext: hireGrantContext } : {}),
255
257
  pending: {
256
258
  trigger: "resume",
257
259
  resumed_from: resumedFrom,
@@ -245,9 +245,11 @@ export interface Ctx {
245
245
  mergeReviewEnabled?: (company: LaneCompanyRef) => boolean | Promise<boolean>;
246
246
  }
247
247
  export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
248
+ export declare const CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG = "lanes.claude-account-balance-signals";
248
249
  export declare function defaultLanePlacementEnabled(company: LaneCompanyRef | undefined): Promise<boolean>;
249
250
  export declare function readLanePlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined): Promise<boolean>;
250
251
  export declare function processLanePlacementCompany(ctx: Ctx): LaneCompanyRef | undefined;
252
+ export declare function readClaudeAccountBalanceSignalsEnabled(ctx: Ctx, company?: LaneCompanyRef): Promise<boolean>;
251
253
  export declare function parsePlacementPolicy(raw: string): LanePlacementPolicy;
252
254
  export declare function assertPlacementEnabled(ctx: Ctx, company: LaneCompanyRef | undefined, readPlacementEnabled?: () => Promise<boolean>): Promise<void>;
253
255
  /** Match a direct session-senior caller while refusing lane-worker identities. */
@@ -349,6 +351,7 @@ export interface LaneAccountsHandlerDeps {
349
351
  to: string;
350
352
  }>;
351
353
  printJson(value: unknown): void;
354
+ readClaudeAccountBalanceSignalsEnabled(ctx: Ctx): Promise<boolean>;
352
355
  reconcileHostFromCtx(ctx: Ctx): ReconcileHost;
353
356
  resolveCallerPath(callerCwd: string, filePath: string): string;
354
357
  }
@@ -72,9 +72,9 @@ export function resolveResumeEffort(ctx, lane, input) {
72
72
  ? undefined
73
73
  : providerConfig.accounts[accountId];
74
74
  const profileDefaultsApply = input.profile?.profile.meta.provider === lane.provider;
75
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
75
+ const pinnedModel = lane.pending_settings?.model ?? (lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
76
76
  ? lane.model
77
- : undefined;
77
+ : undefined);
78
78
  const model = resolveLaneModel({
79
79
  cliModel: input.model?.trim() || pinnedModel,
80
80
  profileModel: profileDefaultsApply ? input.profile?.profile.meta.model : undefined,
@@ -90,12 +90,16 @@ export function resolveResumeEffort(ctx, lane, input) {
90
90
  });
91
91
  }
92
92
  export function recordedResumeEffort(lane) {
93
+ if (lane.pending_settings?.effort !== undefined) {
94
+ return { effort: lane.pending_settings.effort, source: "cli" };
95
+ }
93
96
  return {
94
97
  ...(lane.effort !== undefined ? { effort: lane.effort } : {}),
95
98
  source: lane.effort_source ?? "none",
96
99
  };
97
100
  }
98
101
  export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
102
+ export const CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG = "lanes.claude-account-balance-signals";
99
103
  function lanePlacementFlagErrorCode(error) {
100
104
  if (error && typeof error === "object") {
101
105
  for (const key of ["code", "error", "name"]) {
@@ -135,6 +139,12 @@ export function processLanePlacementCompany(ctx) {
135
139
  return undefined;
136
140
  return { slug: context.companySlug, uid: context.companyUid ?? "" };
137
141
  }
142
+ export function readClaudeAccountBalanceSignalsEnabled(ctx, company) {
143
+ return readLanesRegistryFlag(CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, {
144
+ company: company ?? processLanePlacementCompany(ctx),
145
+ env: ctx.env,
146
+ });
147
+ }
138
148
  export function parsePlacementPolicy(raw) {
139
149
  const value = raw.trim();
140
150
  if (value === "local")
@@ -8,7 +8,7 @@
8
8
  import { Command } from "commander";
9
9
  import { type LanesMesh, type SpawnLoginResult } from "./lanes-shared.js";
10
10
  export type { LanesMesh, SpawnLoginResult } from "./lanes-shared.js";
11
- export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
11
+ export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
12
12
  export { resolveAnswerChoice } from "./lanes-questions.js";
13
13
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
14
14
  export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
@@ -139,5 +139,9 @@ export declare function pickupFailureUpdater(reasonRaw: string, details: Record<
139
139
  export declare function applyOutpostAccountPickup(hqRoot: string, laneId: string, modelDeps?: KnownModelsDeps): LaneRecord;
140
140
  export declare function cancelPendingQuestionsForLane(hqRoot: string, laneId: string, nowIso: string, operatorReason?: string): QuestionRecord[];
141
141
  export declare function stopCommittedState(state: LaneRecord["state"]): "cancelled" | "killed";
142
+ /** Stderr notice that `stop` ends a lane for good, naming the resumable path. */
143
+ export declare function stopTerminalWarning(laneId: string): string;
144
+ /** Stderr notice when an interrupted lane has no session to resume from. */
145
+ export declare function interruptNotResumableWarning(laneId: string): string;
142
146
  export declare function registerLanesCommand(parent: Command, deps?: LanesCommandDeps): Command;
143
147
  //# sourceMappingURL=lanes.d.ts.map