@indigoai-us/hq-cli 5.162.0 → 5.164.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 (71) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/command-catalog.generated.d.ts +188 -0
  3. package/dist/command-catalog.generated.js +235 -0
  4. package/dist/command-registration-plan.d.ts +7 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +6 -1
  7. package/dist/commands/agent-enroll.js +30 -3
  8. package/dist/commands/agents.js +15 -22
  9. package/dist/commands/bot.js +56 -6
  10. package/dist/commands/daemon.d.ts +53 -0
  11. package/dist/commands/daemon.js +554 -0
  12. package/dist/commands/lanes.js +7 -0
  13. package/dist/commands/meetings.js +18 -16
  14. package/dist/commands/mesh.js +9 -0
  15. package/dist/lib/daemon/catalog.d.ts +85 -0
  16. package/dist/lib/daemon/catalog.js +274 -0
  17. package/dist/lib/daemon/config.d.ts +26 -0
  18. package/dist/lib/daemon/config.js +85 -0
  19. package/dist/lib/daemon/handoff.d.ts +53 -0
  20. package/dist/lib/daemon/handoff.js +136 -0
  21. package/dist/lib/daemon/install.d.ts +76 -0
  22. package/dist/lib/daemon/install.js +299 -0
  23. package/dist/lib/daemon/legacy.d.ts +35 -0
  24. package/dist/lib/daemon/legacy.js +120 -0
  25. package/dist/lib/daemon/lock.d.ts +47 -0
  26. package/dist/lib/daemon/lock.js +138 -0
  27. package/dist/lib/daemon/machine-type.d.ts +48 -0
  28. package/dist/lib/daemon/machine-type.js +139 -0
  29. package/dist/lib/daemon/migrate.d.ts +61 -0
  30. package/dist/lib/daemon/migrate.js +238 -0
  31. package/dist/lib/daemon/paths.d.ts +22 -0
  32. package/dist/lib/daemon/paths.js +42 -0
  33. package/dist/lib/daemon/remote-control.d.ts +35 -0
  34. package/dist/lib/daemon/remote-control.js +47 -0
  35. package/dist/lib/daemon/requests.d.ts +24 -0
  36. package/dist/lib/daemon/requests.js +63 -0
  37. package/dist/lib/daemon/run.d.ts +41 -0
  38. package/dist/lib/daemon/run.js +319 -0
  39. package/dist/lib/daemon/supervisor.d.ts +87 -0
  40. package/dist/lib/daemon/supervisor.js +523 -0
  41. package/dist/lib/daemon/updater.d.ts +39 -0
  42. package/dist/lib/daemon/updater.js +103 -0
  43. package/dist/lib/flag-registry.d.ts +7 -1
  44. package/dist/lib/flag-registry.js +8 -0
  45. package/dist/lib/mesh/live/backoff.d.ts +2 -2
  46. package/dist/lib/mesh/live/backoff.js +16 -3
  47. package/dist/lib/mesh/live/daemon/run.d.ts +2 -2
  48. package/dist/lib/mesh/live/daemon/run.js +5 -2
  49. package/dist/lib/mesh/live/flush.d.ts +3 -2
  50. package/dist/lib/mesh/live/flush.js +34 -7
  51. package/dist/lib/mesh/live/session-events-client.d.ts +2 -0
  52. package/dist/lib/mesh/live/session-events-client.js +25 -0
  53. package/dist/lib/service-manager/systemd.d.ts +2 -2
  54. package/dist/lib/service-manager/systemd.js +1 -1
  55. package/dist/lib/service-manager/types.d.ts +2 -0
  56. package/dist/main.d.ts +6 -0
  57. package/dist/main.js +53 -13
  58. package/dist/sentry.js +5 -1
  59. package/dist/startup-registration.d.ts +3 -1
  60. package/dist/startup-registration.js +25 -4
  61. package/dist/utils/cognito-session.d.ts +4 -0
  62. package/dist/utils/cognito-session.js +11 -10
  63. package/dist/utils/install-tree-torn.d.ts +29 -10
  64. package/dist/utils/install-tree-torn.js +262 -29
  65. package/dist/utils/plan-gate-error.d.ts +27 -13
  66. package/dist/utils/plan-gate-error.js +133 -58
  67. package/dist/utils/self-update.d.ts +9 -0
  68. package/dist/utils/self-update.js +15 -2
  69. package/dist/utils/sentry-fingerprint.d.ts +7 -1
  70. package/dist/utils/sentry-fingerprint.js +95 -15
  71. package/package.json +1 -1
@@ -27,6 +27,8 @@ import { classifyEvalThrow, classifyModuleNotFound, installRewriteInProgress, In
27
27
  * shadow each other.
28
28
  */
29
29
  export const INSTALL_TREE_RECOVERY_GUARD_ENV = "HQ_INSTALL_TREE_RECOVERED";
30
+ /** Carries the writer-state observed by the parent into the guarded re-exec. */
31
+ export const INSTALL_TREE_RECOVERY_TREE_STATE_ENV = "HQ_INSTALL_TREE_RECOVERY_TREE_STATE";
30
32
  /** Operator override for the settle wait's deadline. */
31
33
  export const INSTALL_SETTLE_TIMEOUT_ENV = "HQ_INSTALL_SETTLE_TIMEOUT_MS";
32
34
  /** Default settle deadline (ms). Overridable by {@link INSTALL_SETTLE_TIMEOUT_ENV}. */
@@ -36,7 +38,7 @@ export const DEFAULT_INSTALL_SETTLE_TIMEOUT_MS = 90_000;
36
38
  * actually happens. It tells a human why the command paused and is the e2e's
37
39
  * synchronization point; it is never a remedy and never sets an error exit code.
38
40
  */
39
- export const INSTALL_TREE_WAIT_NOTICE = "hq: the hq-cli install is being updated underneath this command; waiting for it to finish…";
41
+ export const INSTALL_TREE_WAIT_NOTICE = "hq: checking whether the hq-cli install tree is still changing before retrying…";
40
42
  /** Resolve the settle deadline from the environment (0 = evaluate once; default on invalid). */
41
43
  export function resolveSettleTimeoutMs(env) {
42
44
  const raw = env[INSTALL_SETTLE_TIMEOUT_ENV];
@@ -121,10 +123,12 @@ export async function registerCommandsWithRecovery(args) {
121
123
  classified,
122
124
  packageRoot,
123
125
  outcome: "guarded",
126
+ treeState: env[INSTALL_TREE_RECOVERY_TREE_STATE_ENV] === "stale" ? "stale" : "in_flight",
124
127
  attempt: 2,
125
128
  waitedMs: 0,
126
129
  sawLock: false,
127
130
  sawRetired: false,
131
+ sawTreeChange: false,
128
132
  });
129
133
  }
130
134
  stderr.write(`${INSTALL_TREE_WAIT_NOTICE}\n`);
@@ -137,6 +141,7 @@ export async function registerCommandsWithRecovery(args) {
137
141
  onLockExtended: () => stderr.write(`${INSTALL_TREE_LOCK_WAIT_NOTICE}\n`),
138
142
  });
139
143
  if (!result.settled) {
144
+ const treeState = result.staleTree === true ? "stale" : "in_flight";
140
145
  throw new InstallTreeTornError({
141
146
  cause: err,
142
147
  classified,
@@ -145,12 +150,15 @@ export async function registerCommandsWithRecovery(args) {
145
150
  // writer held the shared lock past its own ceiling — report 'lock-held'
146
151
  // (its remedy tells the user to let the update finish, never to reinstall
147
152
  // over it). Otherwise the tree genuinely never settled with no writer
148
- // visible, which keeps meaning 'unsettled'.
149
- outcome: result.lockHeldAtEnd ? "lock-held" : "unsettled",
153
+ // visible. The settle probe only uses `stale_tree` when it could observe
154
+ // an unchanged tree with no active writer or retired sibling.
155
+ outcome: result.lockHeldAtEnd ? "lock-held" : result.staleTree ? "stale_tree" : "unsettled",
156
+ treeState,
150
157
  attempt: 1,
151
158
  waitedMs: result.waitedMs,
152
159
  sawLock: result.sawLock,
153
160
  sawRetired: result.sawRetired,
161
+ sawTreeChange: result.sawTreeChange,
154
162
  lockHeldAtEnd: result.lockHeldAtEnd,
155
163
  lockedMs: result.lockedMs,
156
164
  lockTool: result.lockTool,
@@ -158,17 +166,30 @@ export async function registerCommandsWithRecovery(args) {
158
166
  }
159
167
  const spawn = deps.spawn ?? spawnSync;
160
168
  const execArgv = deps.execArgv ?? process.execArgv;
161
- const child = spawn(process.execPath, [...execArgv, entryPath, ...argv.slice(2)], { stdio: "inherit", env: { ...env, [INSTALL_TREE_RECOVERY_GUARD_ENV]: "1" } });
169
+ const treeState = result.sawLock || result.sawRetired || result.sawTreeChange
170
+ ? "in_flight"
171
+ : "stale";
172
+ const child = spawn(process.execPath, [...execArgv, entryPath, ...argv.slice(2)], {
173
+ stdio: "inherit",
174
+ env: {
175
+ ...env,
176
+ [INSTALL_TREE_RECOVERY_GUARD_ENV]: "1",
177
+ [INSTALL_TREE_RECOVERY_TREE_STATE_ENV]: treeState,
178
+ },
179
+ });
162
180
  if (child.error) {
163
181
  throw new InstallTreeTornError({
164
182
  cause: err,
165
183
  classified,
166
184
  packageRoot,
167
185
  outcome: "reexec-failed",
186
+ treeState,
168
187
  attempt: 1,
169
188
  waitedMs: result.waitedMs,
170
189
  sawLock: result.sawLock,
171
190
  sawRetired: result.sawRetired,
191
+ sawTreeChange: result.sawTreeChange,
192
+ lockTool: result.lockTool,
172
193
  });
173
194
  }
174
195
  // A signal-killed child has status null; surface it as a failure exit rather
@@ -81,6 +81,10 @@ export declare const DEFAULT_VAULT_API_URL: string;
81
81
  */
82
82
  export declare function resolveDefaultHqRoot(opts?: {
83
83
  onMissing?: "throw" | "fallback";
84
+ /** Defaults to process.env, process.cwd() and os.homedir(). */
85
+ env?: NodeJS.ProcessEnv;
86
+ cwd?: string;
87
+ home?: string;
84
88
  }): string;
85
89
  export declare const DEFAULT_HQ_ROOT: string;
86
90
  /**
@@ -102,22 +102,24 @@ export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ?? "https://hq
102
102
  * Re-importable as a function for command-time resolution and tests.
103
103
  */
104
104
  export function resolveDefaultHqRoot(opts = {}) {
105
+ const env = opts.env ?? process.env;
106
+ const home = opts.home ?? os.homedir();
105
107
  // Tier 1: explicit override.
106
- if (process.env.HQ_ROOT)
107
- return path.resolve(process.env.HQ_ROOT);
108
+ if (env.HQ_ROOT)
109
+ return path.resolve(env.HQ_ROOT);
108
110
  // Tier 2: working-tree walk-up — operate on the HQ tree you're inside.
109
- let cur = path.resolve(process.cwd());
111
+ let cur = path.resolve(opts.cwd ?? process.cwd());
110
112
  while (cur !== path.dirname(cur)) {
111
113
  if (isHqRoot(cur))
112
114
  return cur;
113
115
  cur = path.dirname(cur);
114
116
  }
115
117
  // Tiers 3–4: the installer/menubar's canonical "where is HQ" records.
116
- const fromConfig = readHqRootFromHqConfig(path.join(os.homedir(), ".hq"));
118
+ const fromConfig = readHqRootFromHqConfig(path.join(home, ".hq"));
117
119
  if (fromConfig)
118
120
  return path.resolve(fromConfig);
119
121
  // Tier 5: discover a valid HQ root in the well-known locations.
120
- const discovered = discoverHqRootViaCoreYaml();
122
+ const discovered = discoverHqRootViaCoreYaml(home);
121
123
  if (discovered)
122
124
  return discovered;
123
125
  // Tier 6: nothing resolved.
@@ -127,7 +129,7 @@ export function resolveDefaultHqRoot(opts = {}) {
127
129
  "or install HQ via the installer/menubar so ~/.hq/menubar.json records " +
128
130
  "its location.");
129
131
  }
130
- return path.join(os.homedir(), "hq");
132
+ return path.join(home, "hq");
131
133
  }
132
134
  /**
133
135
  * Read the configured HQ root from the installer/menubar records under
@@ -156,8 +158,7 @@ function readJsonStringField(file, field) {
156
158
  }
157
159
  }
158
160
  /** Well-known parent locations an HQ tree commonly lives in (mirrors hq-sync). */
159
- function hqDiscoveryCandidates() {
160
- const home = os.homedir();
161
+ function hqDiscoveryCandidates(home = os.homedir()) {
161
162
  return [
162
163
  path.join(home, "HQ"),
163
164
  path.join(home, "hq"),
@@ -172,8 +173,8 @@ function hqDiscoveryCandidates() {
172
173
  * signature. First match wins; returns undefined if none qualify. Cheap — a
173
174
  * few stats plus one small YAML parse on a hit.
174
175
  */
175
- function discoverHqRootViaCoreYaml() {
176
- return hqDiscoveryCandidates().find(isValidHqRootSignature);
176
+ function discoverHqRootViaCoreYaml(home) {
177
+ return hqDiscoveryCandidates(home).find(isValidHqRootSignature);
177
178
  }
178
179
  /**
179
180
  * True iff `dir` carries a valid hq-core `core.yaml` — canonical
@@ -179,6 +179,8 @@ export interface InstallTreeFs {
179
179
  existsSync(p: string): boolean;
180
180
  statSync(p: string): {
181
181
  isDirectory(): boolean;
182
+ size?: number;
183
+ mtimeMs?: number;
182
184
  };
183
185
  readFileSync(p: string, encoding: "utf-8"): string;
184
186
  readdirSync(p: string): string[];
@@ -225,6 +227,8 @@ export interface WaitForInstallTreeSettledArgs {
225
227
  isPidAlive?: (pid: number) => boolean;
226
228
  pollMs?: number;
227
229
  quietMs?: number;
230
+ /** Short observation window for an unchanged, lock-free broken tree. */
231
+ staleProbeMs?: number;
228
232
  deadlineMs: number;
229
233
  /**
230
234
  * Ceiling on the CUMULATIVE time observed under a FRESH foreign lock held by a
@@ -256,7 +260,12 @@ export interface WaitForInstallTreeSettledResult {
256
260
  lockedMs: number;
257
261
  /** The bounded `tool` of the last fresh foreign lock seen, or "" if none. */
258
262
  lockTool: string;
263
+ /** Whether a relevant install-tree path changed during this wait. */
264
+ sawTreeChange?: boolean;
265
+ /** True when the lock-free tree stayed unchanged for `staleProbeMs`. */
266
+ staleTree?: boolean;
259
267
  }
268
+ export declare const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1500;
260
269
  /**
261
270
  * Poll until the install tree has been continuously READY for `quietMs`, or the
262
271
  * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
@@ -265,14 +274,14 @@ export interface WaitForInstallTreeSettledResult {
265
274
  * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
266
275
  * its condition "not ready" rather than throwing.
267
276
  *
268
- * The wait is LOCK-AWARE and bounded on TWO independent axes, so a live
269
- * cooperating writer (the desktop app, the box's update timer, another `hq`)
270
- * cannot make a legitimately-in-progress reinstall look like a torn install:
277
+ * The wait is LOCK-AWARE and bounded, so a live cooperating writer (the desktop
278
+ * app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
279
+ * reinstall look stale:
271
280
  * - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
272
- * by `deadlineMs`. That is the budget for the TREE to settle once no writer is
273
- * visible; with no lock ever seen, unlocked time equals total wait time and
274
- * the behaviour is byte-for-byte the pre-lock-aware one (unsettled at exactly
275
- * `deadlineMs`, and `deadlineMs === 0` evaluates readiness exactly once).
281
+ * by `deadlineMs`. If the target is still missing, no lock or retired sibling
282
+ * is present, and the observed tree stays unchanged for `staleProbeMs`, the
283
+ * wait returns `staleTree` early. Any observed tree change keeps the ordinary
284
+ * settle deadline in force. `deadlineMs === 0` evaluates readiness once.
276
285
  * - LOCKED time — wall time observed under a fresh foreign lock held by a live
277
286
  * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
278
287
  * 10-minute staleness window) and does NOT consume the settle budget. When
@@ -285,7 +294,8 @@ export interface WaitForInstallTreeSettledResult {
285
294
  * began, so a starved event loop only makes the wait coarser, never unbounded. A
286
295
  * stale lock (dead pid, older than the staleness window, unparseable) or our own
287
296
  * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
288
- * lockWaitMs`), so the caller can never hang.
297
+ * lockWaitMs`), and the unchanged-tree path normally returns after the much
298
+ * shorter `staleProbeMs` observation window.
289
299
  */
290
300
  export declare function waitForInstallTreeSettled(args: WaitForInstallTreeSettledArgs): Promise<WaitForInstallTreeSettledResult>;
291
301
  export interface InstallRewriteSignalArgs {
@@ -310,7 +320,8 @@ export interface InstallRewriteSignalArgs {
310
320
  */
311
321
  export declare function installRewriteInProgress(args: InstallRewriteSignalArgs): boolean;
312
322
  /** The recovery OUTCOME, which — not the dialect — decides capture. */
313
- export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held";
323
+ export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held" | "stale_tree";
324
+ export type InstallTreeState = "in_flight" | "stale";
314
325
  export type InstallTreeTornDiagnostics = {
315
326
  dialect: ModuleErrorDialect;
316
327
  code: ModuleNotFoundCode;
@@ -318,10 +329,12 @@ export type InstallTreeTornDiagnostics = {
318
329
  importer: string;
319
330
  packageRoot: string;
320
331
  outcome: InstallTreeTornOutcome;
332
+ treeState: InstallTreeState;
321
333
  attempt: 1 | 2;
322
334
  waitedMs: number;
323
335
  sawLock: boolean;
324
336
  sawRetired: boolean;
337
+ sawTreeChange: boolean;
325
338
  /** A fresh foreign lock was still held when the wait gave up (→ 'lock-held'). */
326
339
  lockHeldAtEnd: boolean;
327
340
  /** Cumulative wall time the wait spent under a fresh foreign lock. */
@@ -342,6 +355,8 @@ export interface InstallTreeTornInit {
342
355
  waitedMs: number;
343
356
  sawLock: boolean;
344
357
  sawRetired: boolean;
358
+ sawTreeChange?: boolean;
359
+ treeState?: InstallTreeState;
345
360
  /** Whether a fresh foreign lock was held when the wait gave up (default false). */
346
361
  lockHeldAtEnd?: boolean;
347
362
  /** Cumulative time under a fresh foreign lock (default 0). */
@@ -362,11 +377,13 @@ export declare class InstallTreeTornError extends Error {
362
377
  }
363
378
  export declare function isInstallTreeTornError(err: unknown): err is InstallTreeTornError;
364
379
  /** The exact Sentry `contexts` payload for a captured torn-install failure. */
365
- export declare function installTreeTornCaptureContext(err: InstallTreeTornError): {
380
+ export declare function installTreeTornCaptureContext(err: InstallTreeTornError, homeDirectory?: string): {
366
381
  contexts: {
367
382
  install_tree_torn: InstallTreeTornDiagnostics;
368
383
  };
369
384
  };
385
+ /** Replace a home-directory prefix without matching similarly-named sibling folders. */
386
+ export declare function scrubHomeDirectoryPrefix(value: string, homeDirectory?: string): string;
370
387
  /**
371
388
  * The fixed, input-free operator remedy. It names the likely writers (so a
372
389
  * human knows this was not their command's fault) and the reinstall commands,
@@ -374,6 +391,8 @@ export declare function installTreeTornCaptureContext(err: InstallTreeTornError)
374
391
  * appended by {@link installTreeTornStderrLine}.
375
392
  */
376
393
  export declare const INSTALL_TREE_TORN_REMEDY: string;
394
+ /** Fixed repair guidance for an unchanged tree with no active updater signal. */
395
+ export declare const INSTALL_TREE_STALE_REMEDY: string;
377
396
  /**
378
397
  * The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
379
398
  * held the shared update lock past its own staleness ceiling. Telling the user to