@indigoai-us/hq-cli 5.275.0 → 5.277.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 (120) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +12 -0
  3. package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
  4. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  9. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  10. package/dist/command-catalog.generated.d.ts +239 -1
  11. package/dist/command-catalog.generated.js +302 -1
  12. package/dist/commands/core-checkpoint.d.ts +4 -2
  13. package/dist/commands/core-checkpoint.js +34 -3
  14. package/dist/commands/core-command-catalog.d.ts +4 -2
  15. package/dist/commands/core-command-catalog.js +5 -2
  16. package/dist/commands/core-delegate.d.ts +6 -0
  17. package/dist/commands/core-delegate.js +987 -0
  18. package/dist/commands/core-ontology.js +3 -3
  19. package/dist/commands/core.js +11 -1
  20. package/dist/commands/daemon.d.ts +4 -0
  21. package/dist/commands/daemon.js +175 -2
  22. package/dist/commands/dm.d.ts +2 -0
  23. package/dist/commands/dm.js +11 -4
  24. package/dist/commands/feedback.d.ts +6 -0
  25. package/dist/commands/feedback.js +86 -37
  26. package/dist/commands/lanes-accounts.js +44 -2
  27. package/dist/commands/lanes-message.js +69 -12
  28. package/dist/commands/lanes-resume.js +2 -0
  29. package/dist/commands/lanes-shared.d.ts +3 -0
  30. package/dist/commands/lanes-shared.js +12 -2
  31. package/dist/commands/lanes.d.ts +5 -1
  32. package/dist/commands/lanes.js +402 -49
  33. package/dist/commands/run-feature-telemetry.d.ts +3 -0
  34. package/dist/commands/run-feature-telemetry.js +6 -0
  35. package/dist/commands/run.js +3 -2
  36. package/dist/commands/secrets.d.ts +16 -1
  37. package/dist/commands/secrets.js +80 -45
  38. package/dist/commands/sync-mode.d.ts +11 -0
  39. package/dist/commands/sync-mode.js +30 -0
  40. package/dist/commands/whoami.d.ts +4 -6
  41. package/dist/commands/whoami.js +39 -8
  42. package/dist/fast-core.d.ts +6 -7
  43. package/dist/fast-core.js +22 -8
  44. package/dist/fast-scaffold.d.ts +33 -7
  45. package/dist/fast-scaffold.js +104 -8
  46. package/dist/index.js +32 -1
  47. package/dist/lanes-drain-hook.js +154 -7
  48. package/dist/lib/agent-kit/run/inbox.js +3 -2
  49. package/dist/lib/billing/plan-lock.d.ts +3 -1
  50. package/dist/lib/billing/plan-lock.js +7 -1
  51. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  52. package/dist/lib/checkpoint-ontology/apply.js +60 -3
  53. package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
  54. package/dist/lib/checkpoint-ontology/company.js +15 -7
  55. package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
  56. package/dist/lib/checkpoint-ontology/plan.js +66 -4
  57. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
  58. package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
  59. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  60. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  61. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  62. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  63. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  64. package/dist/lib/core-utils/secret-patterns.js +33 -0
  65. package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
  66. package/dist/lib/core-utils/timeout-guard-native.js +40 -0
  67. package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
  68. package/dist/lib/core-utils/timeout-guard.js +28 -13
  69. package/dist/lib/daemon/catalog.d.ts +18 -2
  70. package/dist/lib/daemon/catalog.js +18 -2
  71. package/dist/lib/daemon/config.d.ts +6 -0
  72. package/dist/lib/daemon/config.js +15 -2
  73. package/dist/lib/daemon/requests.d.ts +4 -0
  74. package/dist/lib/daemon/requests.js +2 -0
  75. package/dist/lib/daemon/run.js +94 -8
  76. package/dist/lib/daemon/supervisor.d.ts +8 -0
  77. package/dist/lib/daemon/supervisor.js +101 -1
  78. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  79. package/dist/lib/daemon/sync-runner.js +277 -0
  80. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  81. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  82. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  83. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  84. package/dist/lib/flag-registry.d.ts +12 -7
  85. package/dist/lib/flag-registry.js +66 -75
  86. package/dist/lib/flag-snapshot-file.d.ts +20 -0
  87. package/dist/lib/flag-snapshot-file.js +91 -0
  88. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  89. package/dist/lib/lanes/claude-balance.js +46 -8
  90. package/dist/lib/lanes/config.js +15 -0
  91. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  92. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  93. package/dist/lib/lanes/index.d.ts +1 -1
  94. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  95. package/dist/lib/lanes/keep-alive.js +3 -0
  96. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  97. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  98. package/dist/lib/lanes/reconcile.js +20 -4
  99. package/dist/lib/lanes/spawn.js +17 -10
  100. package/dist/lib/lanes/store.js +45 -1
  101. package/dist/lib/lanes/types.d.ts +30 -1
  102. package/dist/lib/lanes/types.js +2 -2
  103. package/dist/lib/lanes/watcher.js +3 -2
  104. package/dist/lib/process-flag-company-context.d.ts +8 -0
  105. package/dist/lib/process-flag-company-context.js +67 -0
  106. package/dist/main.js +3 -1
  107. package/dist/native-fast-route.d.ts +3 -0
  108. package/dist/native-fast-route.js +39 -0
  109. package/dist/secrets-env-fast-path.d.ts +5 -0
  110. package/dist/secrets-env-fast-path.js +16 -0
  111. package/dist/secrets-env-fast-route.d.ts +18 -0
  112. package/dist/secrets-env-fast-route.js +54 -0
  113. package/dist/utils/cli-telemetry.d.ts +26 -2
  114. package/dist/utils/cli-telemetry.js +178 -3
  115. package/dist/utils/hq-root-resolver.d.ts +13 -0
  116. package/dist/utils/hq-root-resolver.js +103 -0
  117. package/dist/utils/scaffold-fast-import-guard.js +6 -1
  118. package/dist/utils/vault-api.d.ts +8 -0
  119. package/dist/utils/vault-api.js +13 -1
  120. package/package.json +2 -2
@@ -12,7 +12,7 @@ import * as path from "node:path";
12
12
  import { resolveLiveRoot } from "../utils/hq-roots.js";
13
13
  import { describeCoreCommand } from "./core-command-catalog.js";
14
14
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
15
- import { MULTI_WRITER_FLAG, planOntologyRun, } from "../lib/checkpoint-ontology/plan.js";
15
+ import { MULTI_WRITER_FLAG, planOntologyRun, payloadOntologyCompany, } from "../lib/checkpoint-ontology/plan.js";
16
16
  import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.js";
17
17
  import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
18
18
  import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
@@ -85,8 +85,8 @@ export async function runOntologyPlan(hqRoot, runDirOption, deps = {}) {
85
85
  }
86
86
  export function runOntologyApply(hqRoot, runDirOption, deps = {}) {
87
87
  const runDir = requireRunDir(runDirOption);
88
- const company = readPayload(runDir).company;
89
- if (typeof company !== "string" || company === "") {
88
+ const company = payloadOntologyCompany(readPayload(runDir)).slug;
89
+ if (company === null) {
90
90
  throw new OntologyUsageError("ontology: the run payload names no company");
91
91
  }
92
92
  const report = applyOntologyRun({ hqRoot, runDir, company, now: deps.now?.() });
@@ -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.
@@ -461,7 +471,7 @@ export function registerCoreCommands(program) {
461
471
  const chunks = [];
462
472
  for await (const chunk of process.stdin)
463
473
  chunks.push(chunk);
464
- const code = timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
474
+ const code = await timeoutGuardCommand(Buffer.concat(chunks).toString("utf8"));
465
475
  if (code !== 0)
466
476
  process.exit(code);
467
477
  });
@@ -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,9 +39,12 @@ 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;
47
+ systemRecordPath?: string;
44
48
  }
45
49
  /**
46
50
  * The HQ folder install records: --hq-root, else the CLI's shared resolution
@@ -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,8 @@ 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,
103
+ systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
99
104
  };
100
105
  }
101
106
  function detect(d, flag, config) {
@@ -250,7 +255,7 @@ function migrateDeps(d, paths) {
250
255
  recordPath: path.join(paths.root, "adopted.json"),
251
256
  // Box system units: root-owned, outside the login user's daemon folder (which handOver chowns to that user).
252
257
  systemAdoptedDir: path.join(SYSTEM_ADOPTED_ROOT, "adopted"),
253
- systemRecordPath: path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
258
+ systemRecordPath: d.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
254
259
  systemUnitDir: d.systemUnitDir ?? SYSTEM_UNIT_DIR,
255
260
  };
256
261
  }
@@ -372,6 +377,135 @@ function queueRequest(d, request, what) {
372
377
  const running = runningDaemonPid(paths.lockPath, d.daemonAlive) !== null;
373
378
  d.out(running ? `Asked the daemon to ${what}.` : `Queued "${what}"; the daemon is not running, so it applies when it starts.`);
374
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
+ }
375
509
  async function runToggle(d, action, services) {
376
510
  const host = currentHost(d);
377
511
  const { paths } = host;
@@ -608,6 +742,45 @@ export function registerDaemonCommand(program, over = {}) {
608
742
  .description("Show the machine type and each service's state")
609
743
  .option("--json", "Print machine-readable JSON")
610
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)));
611
784
  daemon
612
785
  .command("logs [unit]")
613
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
  }
@@ -3,6 +3,8 @@ import { type DiagnosticsBlob } from "../utils/feedback-diagnostics.js";
3
3
  import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs.js";
4
4
  import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
5
5
  export declare const BODY_MAX_BYTES: number;
6
+ /** Validate fields that must be valid before feedback attachments are uploaded. */
7
+ export declare function validateFeedbackContent(title: string, body: string): void;
6
8
  /**
7
9
  * Mirror of the server's `MAX_BODY_BYTES` in hq-pro
8
10
  * `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
@@ -36,6 +38,10 @@ export interface FeedbackSubmitOptions {
36
38
  * stays a summary and this carries the rest.
37
39
  */
38
40
  logBundle?: LogBundleRef;
41
+ /** Diagnostics collected before uploads by the command path. */
42
+ diagnostics?: DiagnosticsBlob;
43
+ /** Precomputed setting so environment lookup happens before attachments upload. */
44
+ logsEnabled?: boolean;
39
45
  }
40
46
  export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
41
47
  /**
@@ -1,12 +1,25 @@
1
1
  import * as fs from "node:fs";
2
2
  import chalk from "chalk";
3
3
  import { ensureCognitoToken } from "../utils/cognito-session.js";
4
- import { vaultApiFetch } from "../utils/vault-api.js";
4
+ import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
5
5
  import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
6
6
  import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
7
7
  import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectFeedbackLogs, feedbackLogsEnabled, } from "../utils/feedback-logs.js";
8
8
  import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
9
9
  export const BODY_MAX_BYTES = 64 * 1024;
10
+ /** Validate fields that must be valid before feedback attachments are uploaded. */
11
+ export function validateFeedbackContent(title, body) {
12
+ if (title.trim().length === 0) {
13
+ throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
14
+ }
15
+ if (body.trim().length === 0) {
16
+ throw new Error("body must not be empty. Provide at least one non-whitespace character.");
17
+ }
18
+ const bodyBytes = Buffer.byteLength(body, "utf8");
19
+ if (bodyBytes > BODY_MAX_BYTES) {
20
+ throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
21
+ }
22
+ }
10
23
  /**
11
24
  * Mirror of the server's `MAX_BODY_BYTES` in hq-pro
12
25
  * `src/vault-service/handlers/feedback.ts`, which rejects the WHOLE request
@@ -16,12 +29,36 @@ export const BODY_MAX_BYTES = 64 * 1024;
16
29
  * logs are budgeted against whatever headroom is left between them.
17
30
  */
18
31
  export const REQUEST_MAX_BYTES = 64 * 1024;
32
+ /** Reject JSON envelopes that hq-pro would refuse with HTTP 413. */
33
+ function validateFeedbackRequestSize(requestBody) {
34
+ const requestBytes = Buffer.byteLength(JSON.stringify(requestBody), "utf8");
35
+ if (requestBytes > REQUEST_MAX_BYTES) {
36
+ throw new Error(`Request exceeds 64 KiB limit (${requestBytes} bytes). Reduce the feedback content before submitting.`);
37
+ }
38
+ }
19
39
  /**
20
40
  * Headroom held back for the `"logs":{...}` JSON envelope itself — its keys,
21
41
  * braces, and the escaping of the tail text. The final size is re-measured
22
42
  * after attaching, so this reserve only needs to be roughly right.
23
43
  */
24
44
  const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
45
+ function buildFeedbackRequestBody(opts) {
46
+ const requestBody = {
47
+ type: opts.type,
48
+ title: opts.title,
49
+ body: opts.body,
50
+ diagnostics: opts.diagnostics,
51
+ };
52
+ if (opts.company) {
53
+ requestBody.company = opts.company;
54
+ }
55
+ if (opts.screenshots && opts.screenshots.length > 0) {
56
+ requestBody.screenshots = opts.screenshots;
57
+ }
58
+ if (opts.logBundle)
59
+ requestBody.logBundle = opts.logBundle;
60
+ return requestBody;
61
+ }
25
62
  export async function readBodyFile(bodyFile, stdin) {
26
63
  if (bodyFile === "-") {
27
64
  const stream = stdin ?? process.stdin;
@@ -79,48 +116,32 @@ export function attachDebugLogs(requestBody, diagnostics, enabled, deps = {}) {
79
116
  }
80
117
  }
81
118
  export async function submitFeedback(opts) {
82
- // Validate the title locally, symmetric with the body check below. Commander's
83
- // `requiredOption("--title")` only requires the flag to be PRESENT — an empty
84
- // or whitespace-only value (`--title ""`, or a title the /hq-bug skill derived
85
- // to nothing) passes the flag check, then the server rejects it with a 400
86
- // "title (non-empty string) is required" that floods Sentry as a context-free
87
- // warning (HQ-AB). Catch it here so the caller gets a clear, actionable error
88
- // and the bad request never reaches the server.
89
- if (opts.title.trim().length === 0) {
90
- throw new Error("title must not be empty. Provide a short, non-whitespace title via --title.");
91
- }
92
- if (opts.body.trim().length === 0) {
93
- throw new Error("body must not be empty. Provide at least one non-whitespace character.");
94
- }
95
- const bodyBytes = Buffer.byteLength(opts.body, "utf8");
96
- if (bodyBytes > BODY_MAX_BYTES) {
97
- throw new Error(`Body exceeds 64 KiB limit (${bodyBytes} bytes). Reduce the body size before submitting.`);
98
- }
99
- const diagnostics = collectDiagnostics();
100
- const requestBody = {
119
+ // Keep submitFeedback safe for direct callers as well as the command path,
120
+ // which invokes the same validator before uploading any attachments.
121
+ validateFeedbackContent(opts.title, opts.body);
122
+ const diagnostics = opts.diagnostics ?? collectDiagnostics();
123
+ const requestBody = buildFeedbackRequestBody({
101
124
  type: opts.type,
102
125
  title: opts.title,
103
126
  body: opts.body,
127
+ company: opts.company,
104
128
  diagnostics,
105
- };
106
- if (opts.company) {
107
- requestBody.company = opts.company;
108
- }
109
- if (opts.screenshots && opts.screenshots.length > 0) {
110
- requestBody.screenshots = opts.screenshots;
111
- }
112
- // The bundle reference is four small fields, so unlike the inline logs below
113
- // it needs no budgeting — but it is added BEFORE attachDebugLogs so those
114
- // bytes are counted against the inline budget rather than overrunning it.
115
- if (opts.logBundle) {
116
- requestBody.logBundle = opts.logBundle;
117
- }
129
+ screenshots: opts.screenshots,
130
+ logBundle: opts.logBundle,
131
+ });
132
+ // Direct callers can supply attachment references; ensure the complete base
133
+ // envelope still fits before adding optional inline logs or making a request.
134
+ validateFeedbackRequestSize(requestBody);
118
135
  // Debug logs are attached LAST and sized against the headroom the rest of
119
136
  // the request left behind. The server 413s the whole body over 64 KiB, so a
120
137
  // fixed log budget would turn a previously-working large-body submission
121
138
  // into a hard failure. Measuring the real serialized envelope — the exact
122
139
  // bytes vaultApiFetch will send — is what makes that impossible.
123
- attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false);
140
+ attachDebugLogs(requestBody, diagnostics, opts.includeLogs !== false, {
141
+ envEnabled: opts.logsEnabled === undefined
142
+ ? feedbackLogsEnabled
143
+ : () => opts.logsEnabled === true,
144
+ });
124
145
  const res = await vaultApiFetch({
125
146
  token: opts.token,
126
147
  path: "/v1/feedback",
@@ -155,8 +176,34 @@ function registerSubcommand(feedbackCmd, type) {
155
176
  try {
156
177
  const token = await ensureCognitoToken({ interactive: false });
157
178
  const body = await readBodyFile(opts.bodyFile);
158
- // Validate + upload screenshots (direct-to-S3 via presigned PUT)
159
- // before submitting, so the row references uploaded objects.
179
+ validateFeedbackContent(opts.title, body);
180
+ // Resolve every required local input before creating S3 objects. These
181
+ // diagnostics are passed through so submitFeedback does not collect
182
+ // them again after attachments have been uploaded.
183
+ const diagnostics = collectDiagnostics();
184
+ const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
185
+ // Validate the serialized server request envelope before screenshots
186
+ // or the log bundle can create remote objects. Attachment references
187
+ // are added and checked again inside submitFeedback after upload.
188
+ validateFeedbackRequestSize(buildFeedbackRequestBody({
189
+ type,
190
+ title: opts.title,
191
+ body,
192
+ company: opts.company,
193
+ diagnostics,
194
+ }));
195
+ // vaultApiFetch can reject an hqk_ token or an invalid API base URL
196
+ // locally before fetch. Check the mandatory POST, and the screenshot
197
+ // presign when requested, before any upload can create an orphan.
198
+ assertVaultApiRequestSupported({ token, path: "/v1/feedback" });
199
+ if ((opts.screenshot ?? []).length > 0) {
200
+ assertVaultApiRequestSupported({
201
+ token,
202
+ path: "/v1/feedback/screenshots/presign",
203
+ });
204
+ }
205
+ // Upload screenshots (direct-to-S3 via presigned PUT) before the
206
+ // submission, so the row references uploaded objects.
160
207
  const screenshots = await uploadScreenshots({
161
208
  paths: opts.screenshot ?? [],
162
209
  token,
@@ -167,7 +214,7 @@ function registerSubcommand(feedbackCmd, type) {
167
214
  // this resolves undefined and the report still carries inline logs.
168
215
  const logBundle = await uploadLogBundle({
169
216
  token,
170
- enabled: opts.logs !== false && feedbackLogsEnabled(),
217
+ enabled: logsEnabled,
171
218
  });
172
219
  const result = await submitFeedback({
173
220
  type,
@@ -178,6 +225,8 @@ function registerSubcommand(feedbackCmd, type) {
178
225
  screenshots,
179
226
  includeLogs: opts.logs !== false,
180
227
  logBundle,
228
+ diagnostics,
229
+ logsEnabled,
181
230
  });
182
231
  console.log(`Submitted: ${result.id}`);
183
232
  }