@indigoai-us/hq-cli 5.345.170 → 5.345.172

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 (142) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +206 -7
  4. package/dist/command-catalog.generated.js +252 -8
  5. package/dist/command-registration-plan.d.ts +7 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/access.d.ts +32 -1
  8. package/dist/commands/access.js +112 -3
  9. package/dist/commands/agent-kit.d.ts +1 -1
  10. package/dist/commands/agent-kit.js +3 -1
  11. package/dist/commands/agents.js +2 -1
  12. package/dist/commands/bot.d.ts +3 -0
  13. package/dist/commands/bot.js +179 -18
  14. package/dist/commands/cloud.d.ts +58 -1
  15. package/dist/commands/cloud.js +228 -10
  16. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  17. package/dist/commands/core-company-index-scope-flag.js +9 -2
  18. package/dist/commands/core-delegate-w1b.js +5 -2
  19. package/dist/commands/core-dm-bind-native.js +19 -4
  20. package/dist/commands/core-rebuild-index-fast.js +24 -6
  21. package/dist/commands/core.js +18 -6
  22. package/dist/commands/daemon.js +16 -1
  23. package/dist/commands/dm.d.ts +17 -3
  24. package/dist/commands/dm.js +121 -26
  25. package/dist/commands/files.d.ts +1 -0
  26. package/dist/commands/files.js +22 -4
  27. package/dist/commands/integrations-api.d.ts +4 -0
  28. package/dist/commands/integrations-api.js +18 -2
  29. package/dist/commands/integrations-connect.d.ts +10 -3
  30. package/dist/commands/integrations-connect.js +70 -6
  31. package/dist/commands/integrations-core.d.ts +23 -0
  32. package/dist/commands/integrations-core.js +100 -0
  33. package/dist/commands/integrations-manage.js +8 -1
  34. package/dist/commands/integrations.js +49 -5
  35. package/dist/commands/lanes-loop.d.ts +20 -0
  36. package/dist/commands/lanes-loop.js +69 -11
  37. package/dist/commands/lanes.js +9 -1
  38. package/dist/commands/rescue.d.ts +9 -0
  39. package/dist/commands/rescue.js +31 -0
  40. package/dist/commands/run.js +115 -40
  41. package/dist/commands/scan.d.ts +5 -1
  42. package/dist/commands/scan.js +16 -4
  43. package/dist/commands/secrets.js +50 -9
  44. package/dist/commands/sync-core.d.ts +22 -0
  45. package/dist/commands/sync-core.js +124 -0
  46. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
  47. package/dist/hq-cloud-sync-runner/package.json +1 -1
  48. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  49. package/dist/lib/agent-kit/inbox-state.js +41 -20
  50. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  51. package/dist/lib/agent-kit/kit-config.js +6 -0
  52. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  53. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  54. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  55. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  56. package/dist/lib/agent-kit/run/inbox.js +42 -10
  57. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  58. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  59. package/dist/lib/bot/api.d.ts +1 -1
  60. package/dist/lib/bot/config.d.ts +10 -0
  61. package/dist/lib/bot/config.js +17 -1
  62. package/dist/lib/bot/fleet-local.d.ts +142 -0
  63. package/dist/lib/bot/fleet-local.js +689 -0
  64. package/dist/lib/bot/fleet-python.d.ts +47 -0
  65. package/dist/lib/bot/fleet-python.js +218 -0
  66. package/dist/lib/bot/index.d.ts +2 -0
  67. package/dist/lib/bot/index.js +2 -0
  68. package/dist/lib/bot/local-config.d.ts +3 -2
  69. package/dist/lib/bot/local-config.js +5 -2
  70. package/dist/lib/bot/promote.d.ts +10 -0
  71. package/dist/lib/bot/promote.js +3 -2
  72. package/dist/lib/bot/prompt.d.ts +6 -1
  73. package/dist/lib/bot/prompt.js +8 -3
  74. package/dist/lib/bot/runtime/index.js +8 -4
  75. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  76. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  77. package/dist/lib/caller-principal.d.ts +4 -0
  78. package/dist/lib/caller-principal.js +40 -0
  79. package/dist/lib/capacity/collectors/grok.js +8 -2
  80. package/dist/lib/cli-flag-keys.d.ts +2 -0
  81. package/dist/lib/cli-flag-keys.js +2 -0
  82. package/dist/lib/daemon/catalog.d.ts +12 -4
  83. package/dist/lib/daemon/catalog.js +70 -24
  84. package/dist/lib/daemon/config.d.ts +0 -7
  85. package/dist/lib/daemon/config.js +0 -32
  86. package/dist/lib/daemon/run.d.ts +5 -1
  87. package/dist/lib/daemon/run.js +48 -11
  88. package/dist/lib/daemon/supervisor.js +3 -1
  89. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  90. package/dist/lib/doctor/checks/release-links.js +188 -2
  91. package/dist/lib/doctor/fix/apply.js +54 -0
  92. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  93. package/dist/lib/doctor/fix/remediation.js +22 -0
  94. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  95. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  96. package/dist/lib/index-render/index.d.ts +9 -0
  97. package/dist/lib/index-render/index.js +65 -5
  98. package/dist/lib/index-render/session-scope.d.ts +69 -0
  99. package/dist/lib/index-render/session-scope.js +279 -0
  100. package/dist/lib/lanes/admission.js +7 -20
  101. package/dist/lib/lanes/reconcile.js +13 -15
  102. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  103. package/dist/lib/lanes/stale-lock.js +105 -0
  104. package/dist/lib/lanes/store.js +11 -76
  105. package/dist/lib/notify-recipient.d.ts +10 -4
  106. package/dist/lib/notify-recipient.js +16 -4
  107. package/dist/lib/secret-input-link.d.ts +42 -0
  108. package/dist/lib/secret-input-link.js +93 -0
  109. package/dist/lib/secret-scan/range.d.ts +57 -0
  110. package/dist/lib/secret-scan/range.js +266 -0
  111. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  112. package/dist/lib/secret-scan/repo-config.js +135 -2
  113. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  114. package/dist/lib/sync-core/engine-home.js +193 -0
  115. package/dist/lib/sync-core/flag.d.ts +17 -0
  116. package/dist/lib/sync-core/flag.js +47 -0
  117. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  118. package/dist/lib/sync-core/reconcile.js +196 -0
  119. package/dist/lib/sync-core/release.d.ts +32 -0
  120. package/dist/lib/sync-core/release.js +140 -0
  121. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  122. package/dist/lib/sync-core/rollout-state.js +94 -0
  123. package/dist/lib/vault-sync-switches.d.ts +5 -0
  124. package/dist/lib/vault-sync-switches.js +43 -0
  125. package/dist/run/script-bound-schema.d.ts +27 -0
  126. package/dist/run/script-bound-schema.js +97 -0
  127. package/dist/secrets-env-action.d.ts +2 -0
  128. package/dist/secrets-env-action.js +7 -2
  129. package/dist/utils/cognito-session.d.ts +3 -30
  130. package/dist/utils/cognito-session.js +2 -100
  131. package/dist/utils/machine-principal-state.d.ts +38 -0
  132. package/dist/utils/machine-principal-state.js +110 -0
  133. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  134. package/dist/utils/remote-list-not-converging.js +40 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
  141. package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
  142. package/dist/lib/daemon/sync-core-canary.js +0 -56
@@ -11,11 +11,11 @@
11
11
  */
12
12
  import type { MachineType } from "./machine-type.js";
13
13
  import type { SyncOwner } from "./config.js";
14
- import type { SyncCoreCanaryPlan } from "./sync-core-canary.js";
14
+ import { type SyncCoreVaultPlan } from "../sync-core/engine-home.js";
15
15
  import type { RemoteControlPoolsConfig } from "./remote-control-pools.js";
16
16
  import { type SyncRunnerProcess } from "./sync-runner.js";
17
17
  import { type VaultSyncSwitches } from "../vault-sync-switches.js";
18
- export declare const SERVICE_NAMES: readonly ["sync", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
18
+ export declare const SERVICE_NAMES: readonly ["sync", "sync-core", "mesh", "heartbeat", "inbox", "bots", "remote-control", "codex-remote-control", "session-host", "lanes-reconcile", "search-index", "jobs-reconcile", "updater", "housekeeping"];
19
19
  export type ServiceName = (typeof SERVICE_NAMES)[number];
20
20
  export declare function isServiceName(value: unknown): value is ServiceName;
21
21
  export declare const SERVICE_DESCRIPTIONS: Record<ServiceName, string>;
@@ -86,6 +86,13 @@ export interface UnitPlan {
86
86
  idleIntervalSec: number;
87
87
  };
88
88
  }
89
+ export interface SyncCoreRunning {
90
+ personal: boolean;
91
+ companies: ReadonlySet<string>;
92
+ }
93
+ export declare const SYNC_CORE_RECONCILE_UNIT = "sync-core";
94
+ export declare const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
95
+ export declare const SYNC_CORE_LEGACY_SETTLE_MS: number;
89
96
  export interface PlanContext {
90
97
  machineType: MachineType;
91
98
  platform: NodeJS.Platform;
@@ -113,9 +120,10 @@ export interface PlanContext {
113
120
  vaultSync?: VaultSyncSwitches;
114
121
  /** Persisted/effective owner selected for this machine. */
115
122
  syncOwner: SyncOwner;
116
- syncCoreCanary?: SyncCoreCanaryPlan;
123
+ syncCoreVaults?: readonly SyncCoreVaultPlan[];
124
+ syncCoreRunning?: SyncCoreRunning;
117
125
  legacySyncReadyForCore?: boolean;
118
- syncCoreCanaryRunning?: boolean;
126
+ now?: number;
119
127
  /** A legacy desktop app or sync unit is still managing sync. */
120
128
  desktopAppManagesSync: boolean;
121
129
  /** Full process list, or null when the daemon could not read it. */
@@ -9,10 +9,13 @@
9
9
  * with no entrypoint yet stay in the catalog as `unavailable` so status shows
10
10
  * them instead of silently dropping them.
11
11
  */
12
+ import * as path from "node:path";
13
+ import { syncCoreUnitId } from "../sync-core/engine-home.js";
12
14
  import { SYNC_RUNNER_OWNER, syncRunnerUnleasedBlockReason } from "./sync-runner.js";
13
15
  import { vaultSkipEnv } from "../vault-sync-switches.js";
14
16
  export const SERVICE_NAMES = [
15
17
  "sync",
18
+ "sync-core",
16
19
  "mesh",
17
20
  "heartbeat",
18
21
  "inbox",
@@ -31,6 +34,7 @@ export function isServiceName(value) {
31
34
  }
32
35
  export const SERVICE_DESCRIPTIONS = {
33
36
  sync: "keeps company vaults in sync with the cloud",
37
+ "sync-core": "moves vaults to the new sync engine when their company's switch is on",
34
38
  mesh: "Work Mesh Live presence",
35
39
  heartbeat: "reports this machine's status to HQ",
36
40
  inbox: "picks up the agent's incoming messages and tasks",
@@ -46,9 +50,10 @@ export const SERVICE_DESCRIPTIONS = {
46
50
  };
47
51
  /** The services each machine type runs without being asked. */
48
52
  export const DEFAULT_SERVICES = {
49
- desktop: ["sync", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "updater", "housekeeping"],
53
+ desktop: ["sync", "sync-core", "mesh", "heartbeat", "bots", "lanes-reconcile", "search-index", "updater", "housekeeping"],
50
54
  "self-hosted": [
51
55
  "sync",
56
+ "sync-core",
52
57
  "mesh",
53
58
  "heartbeat",
54
59
  "bots",
@@ -79,6 +84,9 @@ export function enabledServices(machineType, overrides) {
79
84
  const defaults = DEFAULT_SERVICES[machineType];
80
85
  return SERVICE_NAMES.filter((s) => (defaults.includes(s) || overrides.enable.includes(s)) && !overrides.disable.includes(s));
81
86
  }
87
+ export const SYNC_CORE_RECONCILE_UNIT = "sync-core";
88
+ export const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
89
+ export const SYNC_CORE_LEGACY_SETTLE_MS = 10 * 60 * 1000;
82
90
  function base(service, over = {}) {
83
91
  return {
84
92
  id: service,
@@ -123,11 +131,12 @@ function planService(service, ctx) {
123
131
  const agentMachine = ctx.machineType === "agent-kit" || ctx.machineType === "agent-box";
124
132
  switch (service) {
125
133
  case "sync": {
126
- const vaultSync = ctx.syncCoreCanaryRunning
134
+ const running = ctx.syncCoreRunning;
135
+ const vaultSync = running && (running.personal || running.companies.size > 0)
127
136
  ? {
128
137
  allOff: ctx.vaultSync?.allOff === true,
129
- personalOff: true,
130
- offCompanies: ctx.vaultSync?.offCompanies ?? new Set(),
138
+ personalOff: ctx.vaultSync?.personalOff === true || running.personal,
139
+ offCompanies: new Set([...(ctx.vaultSync?.offCompanies ?? []), ...running.companies]),
131
140
  }
132
141
  : ctx.vaultSync;
133
142
  let ownerBlock;
@@ -181,30 +190,67 @@ function planService(service, ctx) {
181
190
  blocked: commonBlock,
182
191
  })
183
192
  : unavailable(service, "the hq-cloud sync runner is missing from this CLI install");
184
- if (!ctx.syncCoreCanary)
185
- return [unit];
186
- const core = base(service, {
187
- id: "sync-core-canary",
188
- description: "keeps one personal-vault canary in sync with sync-core",
189
- command: ctx.syncCoreCanary.binaryPath
190
- ? { type: "exec", file: ctx.syncCoreCanary.binaryPath, args: ["start", "--loop"] }
191
- : null,
192
- env: {
193
- HQ_SYNC_ENGINE: "rust-test",
194
- HQ_SYNC_MODE: "full",
195
- HQ_SYNC_PROTOCOL: "1",
196
- ...(ctx.syncCoreCanary.vaultUid ? { HQ_SYNC_CANARY_VAULT: ctx.syncCoreCanary.vaultUid } : {}),
197
- ...(ctx.syncCoreCanary.canaryPath ? { HQ_SYNC_CANARY_PATH: ctx.syncCoreCanary.canaryPath } : {}),
193
+ return [unit];
194
+ }
195
+ case "sync-core": {
196
+ if (ctx.platform !== "darwin" && ctx.platform !== "linux") {
197
+ return [unavailable(service, "the new sync engine runs on macOS and Linux only")];
198
+ }
199
+ const reconcile = base(service, {
200
+ id: SYNC_CORE_RECONCILE_UNIT,
201
+ kind: "scheduled",
202
+ intervalSec: SYNC_CORE_RECONCILE_INTERVAL_SEC,
203
+ initialDelaySec: 30,
204
+ command: {
205
+ type: "hq",
206
+ args: ["daemon", "service", "sync-core-reconcile", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])],
198
207
  },
199
- heartbeatTimeoutMs: 180_000,
208
+ env: { HQ_NO_UPDATE_CHECK: "1" },
200
209
  blocked: firstBlock([
201
- [!!commonBlock, commonBlock?.reason ?? ""],
202
- [!!ctx.syncCoreCanary.blockedReason, ctx.syncCoreCanary.blockedReason ?? ""],
203
- [ctx.vaultSync?.personalOff !== true, "turn the personal vault off in the current sync before starting the sync-core canary"],
204
- [ctx.legacySyncReadyForCore !== true, "waiting for legacy sync to run its company-only plan"],
210
+ [!ctx.hqRoot, NO_HQ_ROOT],
211
+ [!ctx.signedIn, NOT_SIGNED_IN],
205
212
  ]),
206
213
  });
207
- return [unit, core];
214
+ const now = ctx.now ?? Date.now();
215
+ const engines = (ctx.syncCoreVaults ?? []).map((vault) => {
216
+ const full = vault.phase === "full";
217
+ const legacyOff = vault.kind === "personal"
218
+ ? ctx.vaultSync?.personalOff === true
219
+ : ctx.vaultSync?.offCompanies.has(vault.slug ?? "") === true;
220
+ const offSince = vault.legacyOffSince ? Date.parse(vault.legacyOffSince) : Number.NaN;
221
+ const settled = Number.isFinite(offSince) && now - offSince >= SYNC_CORE_LEGACY_SETTLE_MS;
222
+ const legacyReady = ctx.legacySyncReadyForCore === true && (!ctx.desktopAppManagesSync || settled);
223
+ const what = vault.kind === "personal" ? "the personal vault" : `the ${vault.slug} vault`;
224
+ return base(service, {
225
+ id: syncCoreUnitId(vault.uid),
226
+ description: full
227
+ ? `keeps ${what} in sync with the new sync engine (${vault.release})`
228
+ : `warms the new sync engine's index for ${what} (${vault.release}, read-only)`,
229
+ command: { type: "exec", file: vault.binaryPath, args: ["start", "--loop"] },
230
+ env: {
231
+ HOME: vault.engineHome,
232
+ HQ_STATE_DIR: path.join(ctx.home, ".hq"),
233
+ HQ_SYNC_ENGINE: "rust-test",
234
+ HQ_SYNC_MODE: full ? "full" : "readonly",
235
+ ...(full ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
236
+ HQ_SYNC_PROTOCOL: "1",
237
+ HQ_SYNC_CORE_RELEASE: vault.release,
238
+ },
239
+ heartbeatTimeoutMs: 180_000,
240
+ blocked: firstBlock([
241
+ [ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
242
+ [ctx.vaultSync?.allOff === true, "cloud sync is off for every vault on this computer (turn it back on in HQ Settings > Sync)"],
243
+ [!ctx.hqRoot, NO_HQ_ROOT],
244
+ [!ctx.signedIn, NOT_SIGNED_IN],
245
+ [!!vault.blockedReason, vault.blockedReason ?? ""],
246
+ [full && !legacyOff, `waiting for the current sync to be turned off for ${what}`],
247
+ [full && !legacyReady, ctx.legacySyncReadyForCore !== true
248
+ ? "waiting for the current sync to run its plan without this vault"
249
+ : "waiting for the desktop app's sync to pick up the off-switch"],
250
+ ]),
251
+ });
252
+ });
253
+ return [reconcile, ...engines];
208
254
  }
209
255
  case "mesh": {
210
256
  const args = ["mesh", "daemon", "run", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])];
@@ -18,19 +18,12 @@ export interface DaemonConfig {
18
18
  instantSyncEnabled?: boolean;
19
19
  /** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
20
20
  syncOwner?: SyncOwner;
21
- syncCoreCanary?: SyncCoreCanaryConfig;
22
- syncCoreCanaryError?: string;
23
- syncCoreCanaryInvalid?: unknown;
24
21
  /** Services turned on here beyond the machine type's defaults. */
25
22
  enable: string[];
26
23
  /** Default services turned off here. */
27
24
  disable: string[];
28
25
  }
29
26
  export type SyncOwner = "daemon" | "desktop-app";
30
- export interface SyncCoreCanaryConfig {
31
- binaryPath: string;
32
- vaultUid: string;
33
- }
34
27
  export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
35
28
  export declare function readDaemonConfig(file: string): DaemonConfig;
36
29
  export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
@@ -19,27 +19,6 @@ function emptyDaemonConfig() {
19
19
  function stringList(value) {
20
20
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
21
21
  }
22
- function syncCoreCanary(value, file) {
23
- if (value === undefined)
24
- return {};
25
- if (!value || typeof value !== "object" || Array.isArray(value)) {
26
- return { error: `hq daemon config at ${file} has invalid syncCoreCanary`, invalid: value };
27
- }
28
- const candidate = value;
29
- if (typeof candidate.binaryPath !== "string" || !path.isAbsolute(candidate.binaryPath)) {
30
- return {
31
- error: `hq daemon config at ${file} has invalid syncCoreCanary: binaryPath must be absolute`,
32
- invalid: value,
33
- };
34
- }
35
- if (typeof candidate.vaultUid !== "string" || !/^prs_[A-Za-z0-9][A-Za-z0-9_-]*$/.test(candidate.vaultUid)) {
36
- return {
37
- error: `hq daemon config at ${file} has invalid syncCoreCanary: vaultUid must name one personal vault`,
38
- invalid: value,
39
- };
40
- }
41
- return { config: { binaryPath: candidate.binaryPath, vaultUid: candidate.vaultUid } };
42
- }
43
22
  export function readDaemonConfig(file) {
44
23
  let text;
45
24
  try {
@@ -72,13 +51,6 @@ export function readDaemonConfig(file) {
72
51
  config.machineType = obj.machineType;
73
52
  if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
74
53
  config.syncOwner = obj.syncOwner;
75
- const parsedCanary = syncCoreCanary(obj.syncCoreCanary, file);
76
- if (parsedCanary.config)
77
- config.syncCoreCanary = parsedCanary.config;
78
- if (parsedCanary.error)
79
- config.syncCoreCanaryError = parsedCanary.error;
80
- if (parsedCanary.error)
81
- config.syncCoreCanaryInvalid = parsedCanary.invalid;
82
54
  if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
83
55
  config.hqRoot = obj.hqRoot;
84
56
  if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
@@ -101,10 +73,6 @@ export function writeDaemonConfig(file, config) {
101
73
  ordered.instantSyncEnabled = config.instantSyncEnabled;
102
74
  if (config.syncOwner)
103
75
  ordered.syncOwner = config.syncOwner;
104
- if (config.syncCoreCanary)
105
- ordered.syncCoreCanary = config.syncCoreCanary;
106
- else if (config.syncCoreCanaryError)
107
- ordered.syncCoreCanary = config.syncCoreCanaryInvalid;
108
76
  ordered.enable = config.enable;
109
77
  ordered.disable = config.disable;
110
78
  fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
@@ -14,7 +14,7 @@ import { type HostApp, type Supervisor } from "./supervisor.js";
14
14
  export declare const LOG_ROTATE_BYTES: number;
15
15
  /** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
16
16
  export declare const RESTART_EXIT_CODE = 75;
17
- export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey"> | null | undefined): UnitPlan[];
17
+ export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey" | "state"> | null | undefined): UnitPlan[];
18
18
  /** A bounded shutdown deadline that can account for sync push persistence. */
19
19
  export declare function createShutdownCap(): {
20
20
  promise: Promise<"timeout">;
@@ -94,6 +94,10 @@ export declare function daemonSyncUnitIsPresent(units: readonly {
94
94
  export declare function resolveSyncRunnerPath(): string | null;
95
95
  /** The hosted desktop app uses this env value when planning daemon services. */
96
96
  export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
97
+ /**
98
+ * The bots the daemon runs itself. A bot on the HQ Fleet Runtime has its own
99
+ * background service, so it is not one of them.
100
+ */
97
101
  export declare function listLocalBots(root?: string): string[];
98
102
  export declare function windowsTaskExists(taskName: string): boolean;
99
103
  export declare function probeLegacyUnits(platform: NodeJS.Platform, home: string): LegacyUnit[];
@@ -18,11 +18,12 @@ import { anywhereRuntimeAllowed, readHqAnywherePersonSetting, stickyPersonSettin
18
18
  import { currentPackageUseLeaseInstall, currentPackageUseUpdateRequested } from "../../utils/package-use-lease.js";
19
19
  import { isLocalDependencyInstall, resolveRunningInstall } from "../../utils/version-gate.js";
20
20
  import { agentKitPaths } from "../agent-kit/paths.js";
21
- import { readBotConfig } from "../bot/config.js";
21
+ import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../bot/config.js";
22
22
  import { botsRoot } from "../bot/paths.js";
23
23
  import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
24
24
  import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
25
- import { inspectSyncCoreCanary } from "./sync-core-canary.js";
25
+ import { inspectEngineHome } from "../sync-core/engine-home.js";
26
+ import { readSyncCoreRolloutState } from "../sync-core/rollout-state.js";
26
27
  import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
27
28
  import { loadLanesConfig } from "../lanes/config.js";
28
29
  import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
@@ -64,17 +65,48 @@ export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
64
65
  export const RESTART_EXIT_CODE = 75;
65
66
  const NON_SYNC_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + 5_000;
66
67
  const SYNC_RUNNER_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + SYNC_RUNNER_PERSIST_GRACE_MS + 5_000;
68
+ function syncCoreVaultPlans(home, hqRoot, platform) {
69
+ const state = readSyncCoreRolloutState(home);
70
+ if (!state)
71
+ return [];
72
+ return state.vaults.map((vault) => inspectEngineHome(home, hqRoot, {
73
+ uid: vault.uid,
74
+ kind: vault.kind,
75
+ ...(vault.slug ? { slug: vault.slug } : {}),
76
+ phase: vault.phase,
77
+ engineHome: vault.engineHome,
78
+ binaryPath: state.binaryPath,
79
+ release: state.release,
80
+ ...(vault.legacyOffSince ? { legacyOffSince: vault.legacyOffSince } : {}),
81
+ }, platform));
82
+ }
83
+ const rememberedSyncCoreVaults = new Map();
67
84
  export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
68
- const syncCoreCanaryRunning = supervisor?.runningPlanKey("sync-core-canary") !== undefined;
85
+ const companies = new Set();
86
+ let personal = false;
87
+ for (const vault of context.syncCoreVaults ?? []) {
88
+ rememberedSyncCoreVaults.set(vault.uid, { kind: vault.kind, slug: vault.slug, full: vault.phase === "full" });
89
+ }
90
+ for (const unit of supervisor?.state().units ?? []) {
91
+ if (!unit.id.startsWith("sync-core:") || supervisor?.runningPlanKey(unit.id) === undefined)
92
+ continue;
93
+ const vault = rememberedSyncCoreVaults.get(unit.id.slice("sync-core:".length));
94
+ if (!vault?.full)
95
+ continue;
96
+ if (vault.kind === "personal")
97
+ personal = true;
98
+ else if (vault.slug)
99
+ companies.add(vault.slug);
100
+ }
69
101
  const transitionContext = {
70
102
  ...context,
71
- syncCoreCanaryRunning,
103
+ syncCoreRunning: { personal, companies },
72
104
  legacySyncReadyForCore: false,
73
105
  };
74
106
  const waiting = planUnits(services, transitionContext);
75
107
  const legacy = waiting.find((unit) => unit.id === "sync");
76
108
  const legacySyncReadyForCore = legacy !== undefined
77
- && supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION);
109
+ && (legacy.blocked !== undefined || supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION));
78
110
  return planUnits(services, { ...transitionContext, legacySyncReadyForCore });
79
111
  }
80
112
  /** A bounded shutdown deadline that can account for sync push persistence. */
@@ -260,12 +292,21 @@ export function resolveSyncRunnerPath() {
260
292
  export function daemonInstantSyncEnabled(env, configured) {
261
293
  return configured ?? env.HQ_DAEMON_INSTANT_SYNC !== "0";
262
294
  }
295
+ /**
296
+ * The bots the daemon runs itself. A bot on the HQ Fleet Runtime has its own
297
+ * background service, so it is not one of them.
298
+ */
263
299
  export function listLocalBots(root = botsRoot()) {
264
300
  if (!fs.existsSync(root))
265
301
  return [];
266
302
  return fs
267
303
  .readdirSync(root, { withFileTypes: true })
268
- .filter((d) => d.isDirectory() && readBotConfig(path.join(root, d.name)) !== null)
304
+ .filter((d) => {
305
+ if (!d.isDirectory())
306
+ return false;
307
+ const config = readBotConfig(path.join(root, d.name));
308
+ return config !== null && config.engine?.id !== HQ_FLEET_RUNTIME_ENGINE_ID;
309
+ })
269
310
  .map((d) => d.name)
270
311
  .sort();
271
312
  }
@@ -978,11 +1019,7 @@ export async function runDaemon(opts) {
978
1019
  return switches;
979
1020
  })(),
980
1021
  syncOwner,
981
- syncCoreCanary: config.syncCoreCanaryError
982
- ? { blockedReason: config.syncCoreCanaryError }
983
- : config.syncCoreCanary
984
- ? inspectSyncCoreCanary(home, hqRoot, config.syncCoreCanary, platform)
985
- : undefined,
1022
+ syncCoreVaults: syncCoreVaultPlans(home, hqRoot, platform),
986
1023
  desktopAppManagesSync: appManagesSync,
987
1024
  syncRunnerProcesses,
988
1025
  syncRunnerOwnerPid,
@@ -87,7 +87,7 @@ function keyHash(key) {
87
87
  return createHash("sha256").update(key).digest("hex");
88
88
  }
89
89
  function recoverableHold(plan, hold) {
90
- return plan.kind === "long-running" && (plan.service === "sync" || plan.service === "mesh")
90
+ return plan.kind === "long-running" && (plan.service === "sync" || plan.service === "sync-core" || plan.service === "mesh")
91
91
  && hold?.kind === "exhausted" && hold.cause === "transient";
92
92
  }
93
93
  function heldReason(unitId, hold) {
@@ -388,6 +388,8 @@ export function createSupervisor(deps) {
388
388
  const cause = classifyExit(evidence);
389
389
  if (cause.kind !== "permanent")
390
390
  return false;
391
+ if (slot.plan.service === "sync-core" && cause.cause === "bad-config")
392
+ return false;
391
393
  slot.failures += 1;
392
394
  holdUnit(slot, "permanent", cause.cause, cause.detail);
393
395
  return true;
@@ -9,7 +9,9 @@ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
9
9
  export declare const RELEASE_LINKS_FAMILY_ID = "release-links";
10
10
  export declare const RELEASE_LINKS_FAMILY_TITLE = "Release links";
11
11
  export declare const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
12
- export declare const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y` again. On Windows without Developer Mode, a directory link is written as a junction and a file link as a copy.";
12
+ export declare const RELEASE_LINK_REMEDIATION = "Run `hq doctor --fix` (or `hq rescue -y`) to restore the shipped link. A dangling link that points outside the HQ root and the temp folder is treated as your own and left alone. On Windows, a directory link is written as a junction and a file link as a copy when symlinks are not allowed.";
13
+ /** Message prefix for a link whose target is missing; `--fix` keys off it. */
14
+ export declare const DANGLING_RELEASE_LINK_PROBLEM = "does not resolve.";
13
15
  interface ReleaseLink {
14
16
  rel: string;
15
17
  linkText: string;
@@ -19,5 +21,64 @@ interface ReleaseLink {
19
21
  export declare const RELEASE_LINKS: readonly ReleaseLink[];
20
22
  export declare const releaseLinksFamily: CheckFamily;
21
23
  export declare function checkReleaseLinks(context: CheckContext): CheckResult[];
24
+ /** Where a rescue may stage its extraction folder. */
25
+ export interface RescueStagingDeps {
26
+ /** The system temp folder. Default `os.tmpdir()`. */
27
+ tmpDir?: string;
28
+ /** The rescue backup root. Default `$HQ_BACKUP_DIR` or `~/.hq/backups`. */
29
+ backupRoot?: string;
30
+ }
31
+ /** What {@link repairDanglingReleaseLinks} did to one release link. */
32
+ export interface ReleaseLinkRepair {
33
+ rel: string;
34
+ /**
35
+ * `repointed`: the link was rewritten (see `linkText`). `removed`: the
36
+ * shipped target is missing too, so the dangling link was deleted.
37
+ * `skipped`: the link points outside the HQ root and the temp folder, so it
38
+ * is treated as the user's own link and left alone (see `reason`).
39
+ * `failed`: the repair threw for this link only (see `reason`).
40
+ */
41
+ action: "repointed" | "removed" | "skipped" | "failed";
42
+ /** The absolute target the link now resolves to (repointed only). */
43
+ target?: string;
44
+ /** The text written into the link (repointed only). */
45
+ linkText?: string;
46
+ reason?: string;
47
+ }
48
+ export interface RepairReleaseLinksDeps {
49
+ platform?: NodeJS.Platform;
50
+ /** Repair only this release link (a `RELEASE_LINKS` rel). */
51
+ only?: string;
52
+ /** The temp folder a rescue extracts into. Default `os.tmpdir()`. */
53
+ tmpDir?: string;
54
+ /** The rescue backup root. Default `$HQ_BACKUP_DIR` or `~/.hq/backups`. */
55
+ backupRoot?: string;
56
+ }
57
+ /**
58
+ * Repair HQ-managed release links whose target no longer exists, such as a
59
+ * `.agents/skills` or `.codex/claude` link an earlier rescue pointed at its
60
+ * deleted temp extraction folder (feedback_2ab63e50).
61
+ *
62
+ * Only the fixed {@link RELEASE_LINKS} paths are considered, and only when the
63
+ * entry itself is a symlink or junction that does not resolve. A real file or
64
+ * directory is never touched, a link that resolves anywhere is left alone, and
65
+ * the link's parent folder must sit inside the HQ root, so a `.agents` folder
66
+ * that is itself a link out of the tree is skipped.
67
+ *
68
+ * Ownership rule: a dangling link is repaired only when its current target is
69
+ * under the HQ root or under the system temp folder (where a rescue extracts
70
+ * its staging copy). A dangling link anywhere else, such as `/Volumes/X`, is
71
+ * the user's own link to a drive that is not mounted; it is left alone and
72
+ * reported as `skipped`.
73
+ *
74
+ * The repaired link carries the text HQ ships (`../.claude/skills`), so the
75
+ * git-tracked link is unchanged. On Windows a directory link is an absolute
76
+ * junction and a file link an absolute symlink, falling back to a copy. When
77
+ * the shipped target is missing too, the dangling link is removed and nothing
78
+ * is created. The old entry is removed with {@link removeLinkOnly}, which never
79
+ * follows it. A failure on one link is reported as `failed` and the remaining
80
+ * links are still repaired.
81
+ */
82
+ export declare function repairDanglingReleaseLinks(hqRoot: string, deps?: RepairReleaseLinksDeps): ReleaseLinkRepair[];
22
83
  export {};
23
84
  //# sourceMappingURL=release-links.d.ts.map