@indigoai-us/hq-cli 5.345.61 → 5.345.62

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.62] — 2026-10-09
6
+ - Repair the scope capability from the validated metadata set when `hq core hq-session add company` is repeated for a company already listed in the session.
7
+
8
+ - `hq daemon sync status --json` reports daemon-owned hq-cloud updater checks, target, outcome, closed failure class, and pin status. Registry lookups are bounded to 15 seconds and installs to 2 minutes; success is reported only after the installed package converges to the selected version.
9
+
5
10
  ## [5.345.61] — 2026-10-09
6
11
 
7
12
  ## [5.345.60] — 2026-10-09
@@ -657,8 +657,12 @@ export function executeHqSession(argv, context, overrides = {}) {
657
657
  const oldText = existed ? deps.readFile(target, "utf8") : `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
658
658
  const rawCompanies = metaCompanySlugs(oldText);
659
659
  const companies = rawCompanies.filter((company) => company === "personal" || exactCompanyDir(root, company, deps));
660
- if (subcommand === "add" && companies.includes(slug))
660
+ if (subcommand === "add" && companies.includes(slug)) {
661
+ if (!mintScopeCapability(root, sessionId, companies, deps, true)) {
662
+ return { code: 1, stdout: "", stderr: `hq-session: failed to repair scope-capability for session ${sessionId}; metadata was unchanged\n` };
663
+ }
661
664
  return { code: 0, stdout: "", stderr: "" };
665
+ }
662
666
  const next = subcommand === "add"
663
667
  ? [...(companies[0] === "personal" ? [] : companies), slug]
664
668
  : companies.filter((value) => value !== slug);
@@ -24,7 +24,7 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
24
24
  import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
25
25
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
26
26
  import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
27
- import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy } from "../lib/daemon/hq-cloud.js";
27
+ import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } 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
30
  import { processCommandLine } from "../lib/daemon/lock.js";
@@ -510,6 +510,7 @@ function daemonSyncStatus(d) {
510
510
  const runnerSource = syncRunnerSource(runnerPath, paths.root, d.platform);
511
511
  const managedCopy = managedHqCloud(paths.root);
512
512
  const managedVersion = managedCopy?.version ?? null;
513
+ const hqCloudUpdate = readHqCloudUpdateState(paths.root);
513
514
  let hqCloudVersion = ownerStatus?.version ?? null;
514
515
  let hqCloudVersionSource = hqCloudVersion ? "owner-status" : "unknown";
515
516
  if (!hqCloudVersion && runnerPath) {
@@ -547,6 +548,10 @@ function daemonSyncStatus(d) {
547
548
  runnerSource,
548
549
  managedHqCloudVersion: managedVersion,
549
550
  pinnedHqCloudVersion: config.hqCloudVersion ?? null,
551
+ hqCloudUpdateLastCheckAt: hqCloudUpdate?.lastCheckAt ?? null,
552
+ hqCloudUpdateOutcome: hqCloudUpdate?.outcome ?? null,
553
+ hqCloudUpdateTarget: hqCloudUpdate?.target ?? null,
554
+ hqCloudUpdateErrorClass: hqCloudUpdate?.errorClass ?? null,
550
555
  lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
551
556
  lastPassResult: syncUnit?.lastPassResult ?? null,
552
557
  unitStatus: syncUnit?.status ?? "not-running",
@@ -71,8 +71,19 @@ export interface HqCloudUpdateOutcome {
71
71
  ok: boolean;
72
72
  from: string | null;
73
73
  to: string | null;
74
+ outcome?: HqCloudUpdateState["outcome"];
75
+ target?: string | null;
76
+ errorClass?: HqCloudUpdateState["errorClass"];
77
+ lastCheckAt?: string;
74
78
  detail?: string;
75
79
  }
80
+ export interface HqCloudUpdateState {
81
+ lastCheckAt: string;
82
+ outcome: "updated" | "up_to_date" | "pinned" | "failed";
83
+ target: string | null;
84
+ errorClass: "registry" | "install" | "convergence" | "filesystem" | "unknown" | null;
85
+ }
86
+ export declare function readHqCloudUpdateState(daemonRoot: string): HqCloudUpdateState | null;
76
87
  interface NpmRunResult {
77
88
  status: number | null;
78
89
  stdout?: string | null;
@@ -80,11 +91,12 @@ interface NpmRunResult {
80
91
  error?: Error;
81
92
  }
82
93
  export interface UpdateHqCloudDeps {
83
- run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv) => NpmRunResult;
94
+ run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv, timeoutMs: number) => NpmRunResult;
84
95
  bundled?: HqCloudCopy | null;
85
96
  platform?: NodeJS.Platform;
86
97
  /** Test seam for bounded install retry delays. */
87
98
  sleep?: (ms: number) => void;
99
+ now?: () => Date;
88
100
  }
89
101
  /** Install or update the updater's copy, as this (the login) user. */
90
102
  export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv, deps?: UpdateHqCloudDeps): HqCloudUpdateOutcome;
@@ -161,6 +161,47 @@ export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
161
161
  return null;
162
162
  return { ...managed, source: "managed" };
163
163
  }
164
+ const HQ_CLOUD_UPDATE_STATE_FILE = "hq-cloud-update-state.json";
165
+ const HQ_CLOUD_UPDATE_STATE_MAX_BYTES = 4_096;
166
+ const HQ_CLOUD_UPDATE_OUTCOMES = new Set(["updated", "up_to_date", "pinned", "failed"]);
167
+ const HQ_CLOUD_UPDATE_ERROR_CLASSES = new Set([
168
+ "registry", "install", "convergence", "filesystem", "unknown",
169
+ ]);
170
+ export function readHqCloudUpdateState(daemonRoot) {
171
+ try {
172
+ const file = path.join(daemonRoot, HQ_CLOUD_UPDATE_STATE_FILE);
173
+ const stat = fs.lstatSync(file);
174
+ if (!stat.isFile() || stat.size > HQ_CLOUD_UPDATE_STATE_MAX_BYTES) {
175
+ throw new Error("updater state is not a small regular file");
176
+ }
177
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
178
+ if (typeof raw.lastCheckAt !== "string"
179
+ || !HQ_CLOUD_UPDATE_OUTCOMES.has(raw.outcome)
180
+ || !(raw.target === null || typeof raw.target === "string")
181
+ || !(raw.errorClass === null || HQ_CLOUD_UPDATE_ERROR_CLASSES.has(raw.errorClass)))
182
+ return null;
183
+ return raw;
184
+ }
185
+ catch (error) {
186
+ if (error.code !== "ENOENT") {
187
+ console.warn("Could not read daemon hq-cloud updater state; status will omit it:", error instanceof Error ? error.message : String(error));
188
+ }
189
+ return null;
190
+ }
191
+ }
192
+ function writeHqCloudUpdateState(daemonRoot, state) {
193
+ const file = path.join(daemonRoot, HQ_CLOUD_UPDATE_STATE_FILE);
194
+ const temporary = `${file}.tmp-${process.pid}`;
195
+ fs.writeFileSync(temporary, JSON.stringify(state) + "\n", { mode: 0o600 });
196
+ fs.renameSync(temporary, file);
197
+ }
198
+ function updateErrorClass(detail, fallback) {
199
+ if (/could not look up|npm listed no/i.test(detail))
200
+ return "registry";
201
+ return fallback;
202
+ }
203
+ const HQ_CLOUD_REGISTRY_TIMEOUT_MS = 15_000;
204
+ const HQ_CLOUD_INSTALL_TIMEOUT_MS = 120_000;
164
205
  function isTransientNpmFailure(result) {
165
206
  // spawnSync's ETIMEDOUT means this attempt hit our full 10-minute process
166
207
  // timeout. Retrying that result would multiply one stall across every attempt.
@@ -172,11 +213,11 @@ function isTransientNpmFailure(result) {
172
213
  function sleepSync(ms) {
173
214
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
174
215
  }
175
- function runNpm(file, args, shell, env) {
216
+ function runNpm(file, args, shell, env, timeoutMs) {
176
217
  return spawnSync(file, args, {
177
218
  encoding: "utf8",
178
219
  env,
179
- timeout: 10 * 60_000,
220
+ timeout: timeoutMs,
180
221
  stdio: ["ignore", "pipe", "pipe"],
181
222
  windowsHide: true,
182
223
  shell,
@@ -193,24 +234,40 @@ export function updateManagedHqCloud(daemonRoot, env, deps = {}) {
193
234
  const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
194
235
  const bundled = deps.bundled !== undefined ? deps.bundled : bundledHqCloud();
195
236
  const from = managedHqCloud(daemonRoot)?.version ?? bundled?.version ?? null;
237
+ const checkedAt = (deps.now ?? (() => new Date()))().toISOString();
238
+ const finish = (result, outcome, target, errorClass) => {
239
+ const state = { lastCheckAt: checkedAt, outcome, target, errorClass };
240
+ try {
241
+ fs.mkdirSync(daemonRoot, { recursive: true, mode: 0o700 });
242
+ writeHqCloudUpdateState(daemonRoot, state);
243
+ }
244
+ catch (error) {
245
+ const detail = error instanceof Error ? error.message : String(error);
246
+ console.warn("Could not persist daemon hq-cloud updater state; preserving update result:", detail);
247
+ }
248
+ return { ...result, outcome, target, errorClass, lastCheckAt: checkedAt };
249
+ };
196
250
  if (!bundled)
197
- return { ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" };
251
+ return finish({ ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" }, "failed", pinnedVersion ?? null, "unknown");
198
252
  const prefix = managedHqCloudDir(daemonRoot);
199
253
  try {
200
254
  fs.mkdirSync(prefix, { recursive: true, mode: 0o700 });
201
255
  }
202
256
  catch (err) {
203
- return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
257
+ return finish({ ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` }, "failed", pinnedVersion ?? null, "filesystem");
204
258
  }
205
259
  let version = pinnedVersion;
206
260
  if (!version) {
207
261
  const view = hqCloudLatestCommand(bundled.version, platform);
208
- const r = run(view.file, view.args, view.shell, env);
209
- if (r.status !== 0)
210
- return { ok: false, from, to: null, detail: `could not look up the newest hq-cloud: ${npmFailure(r)}` };
262
+ const r = run(view.file, view.args, view.shell, env, HQ_CLOUD_REGISTRY_TIMEOUT_MS);
263
+ if (r.status !== 0) {
264
+ const detail = `could not look up the newest hq-cloud: ${npmFailure(r)}`;
265
+ return finish({ ok: false, from, to: null, detail }, "failed", null, updateErrorClass(detail, "registry"));
266
+ }
211
267
  const latest = latestSameMajor(r.stdout ?? "", bundled.version);
212
268
  if (!latest) {
213
- return { ok: false, from, to: null, detail: `npm listed no hq-cloud release at or above ${bundled.version} in the same major version` };
269
+ const detail = `npm listed no hq-cloud release at or above ${bundled.version} in the same major version`;
270
+ return finish({ ok: false, from, to: null, detail }, "failed", null, "registry");
214
271
  }
215
272
  version = latest;
216
273
  }
@@ -219,22 +276,28 @@ export function updateManagedHqCloud(daemonRoot, env, deps = {}) {
219
276
  const sleep = deps.sleep ?? sleepSync;
220
277
  let r;
221
278
  for (let attempt = 0;; attempt++) {
222
- r = run(cmd.file, cmd.args, cmd.shell, env);
279
+ r = run(cmd.file, cmd.args, cmd.shell, env, HQ_CLOUD_INSTALL_TIMEOUT_MS);
223
280
  if (r.status === 0)
224
281
  break;
225
282
  const retryable = pinnedVersion !== undefined && pinnedVersion !== null && isTransientNpmFailure(r);
226
283
  const delay = retryDelays[attempt];
227
284
  if (!retryable || delay === undefined) {
228
285
  const retries = retryable ? attempt : 0;
229
- return {
230
- ok: false,
231
- from,
232
- to: null,
233
- detail: retries > 0 ? `pinned hq-cloud install failed after ${retries + 1} attempts: ${npmFailure(r)}` : npmFailure(r),
234
- };
286
+ const detail = retries > 0 ? `pinned hq-cloud install failed after ${retries + 1} attempts: ${npmFailure(r)}` : npmFailure(r);
287
+ return finish({ ok: false, from, to: null, detail }, "failed", version, retryable ? "registry" : "install");
235
288
  }
236
289
  sleep(delay);
237
290
  }
238
- return { ok: true, from, to: managedHqCloud(daemonRoot)?.version ?? null };
291
+ const installedPackage = managedHqCloud(daemonRoot);
292
+ const installedVersion = installedPackage?.version ?? null;
293
+ const syncRunner = installedPackage ? hqCloudRunners(installedPackage.root).syncRunner : null;
294
+ if (installedVersion !== version || !syncRunner) {
295
+ const detail = installedVersion !== version
296
+ ? `hq-cloud install exited successfully but installed ${installedVersion ?? "no readable package"}, expected ${version}`
297
+ : `hq-cloud install exited successfully at ${version} but has no dist/bin/sync-runner.js`;
298
+ return finish({ ok: false, from, to: installedVersion, detail }, "failed", version, "convergence");
299
+ }
300
+ const outcome = pinnedVersion ? "pinned" : installedVersion === from ? "up_to_date" : "updated";
301
+ return finish({ ok: true, from, to: installedVersion }, outcome, version, null);
239
302
  }
240
303
  //# sourceMappingURL=hq-cloud.js.map
@@ -53,6 +53,13 @@ export type ClientHealthSyncState = (typeof CLIENT_HEALTH_SYNC_STATES)[number];
53
53
  */
54
54
  export declare const CLIENT_HEALTH_UPDATER_STATES: readonly ["unchecked", "up_to_date", "update_available", "update_downloading", "update_ready", "update_failed", "unsupported"];
55
55
  export type ClientHealthUpdaterState = (typeof CLIENT_HEALTH_UPDATER_STATES)[number];
56
+ /** Origin of the runner currently used by desktop sync. */
57
+ export declare const CLIENT_HEALTH_SYNC_RUNNER_SOURCES: readonly ["npx", "daemon_managed", "daemon_staged", "daemon_bundled", "unknown"];
58
+ export type ClientHealthSyncRunnerSource = (typeof CLIENT_HEALTH_SYNC_RUNNER_SOURCES)[number];
59
+ export declare const CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_OUTCOMES: readonly ["updated", "up_to_date", "pinned", "failed"];
60
+ export type ClientHealthDaemonHqCloudUpdateOutcome = (typeof CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_OUTCOMES)[number];
61
+ export declare const CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_ERROR_CLASSES: readonly ["registry", "install", "convergence", "filesystem", "unknown"];
62
+ export type ClientHealthDaemonHqCloudUpdateErrorClass = (typeof CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_ERROR_CLASSES)[number];
56
63
  /** Closed failure/blocker reason codes — the ONLY reasons that cross the wire. */
57
64
  export declare const CLIENT_HEALTH_FAILURE_REASONS: readonly ["SYNC_PAUSED", "CONFLICT_BLOCKED", "DESKTOP_OUTDATED", "CLI_OUTDATED", "CORE_OUTDATED", "AUTH_EXPIRED", "UPDATE_FAILED", "RUNNER_FAILED", "PERMISSION_DENIED", "DISK_FULL", "HEARTBEAT_STALE"];
58
65
  export type ClientHealthFailureReason = (typeof CLIENT_HEALTH_FAILURE_REASONS)[number];
@@ -139,6 +146,11 @@ export interface ClientHealthHeartbeat {
139
146
  conflictCount?: number;
140
147
  updaterState?: ClientHealthUpdaterState;
141
148
  failureReason?: ClientHealthFailureReason;
149
+ syncRunnerSource?: ClientHealthSyncRunnerSource;
150
+ daemonHqCloudUpdateLastCheckAt?: string;
151
+ daemonHqCloudUpdateOutcome?: ClientHealthDaemonHqCloudUpdateOutcome;
152
+ daemonHqCloudUpdateTarget?: string;
153
+ daemonHqCloudUpdateErrorClass?: ClientHealthDaemonHqCloudUpdateErrorClass;
142
154
  /** Absent on older clients / non-sync-outcome events. See {@link ClientHealthLocalFilesOverview}. */
143
155
  localFilesOverview?: ClientHealthLocalFilesOverview;
144
156
  }
@@ -65,6 +65,27 @@ export const CLIENT_HEALTH_UPDATER_STATES = [
65
65
  "update_failed",
66
66
  "unsupported",
67
67
  ];
68
+ /** Origin of the runner currently used by desktop sync. */
69
+ export const CLIENT_HEALTH_SYNC_RUNNER_SOURCES = [
70
+ "npx",
71
+ "daemon_managed",
72
+ "daemon_staged",
73
+ "daemon_bundled",
74
+ "unknown",
75
+ ];
76
+ export const CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_OUTCOMES = [
77
+ "updated",
78
+ "up_to_date",
79
+ "pinned",
80
+ "failed",
81
+ ];
82
+ export const CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_ERROR_CLASSES = [
83
+ "registry",
84
+ "install",
85
+ "convergence",
86
+ "filesystem",
87
+ "unknown",
88
+ ];
68
89
  /** Closed failure/blocker reason codes — the ONLY reasons that cross the wire. */
69
90
  export const CLIENT_HEALTH_FAILURE_REASONS = [
70
91
  "SYNC_PAUSED",
@@ -193,6 +214,23 @@ function assertEnum(field, value, allowed) {
193
214
  }
194
215
  return safe;
195
216
  }
217
+ function copyValidOptionalDiagnostic(field, value, validate) {
218
+ try {
219
+ return validate(field, value);
220
+ }
221
+ catch (error) {
222
+ if (error instanceof ClientHealthContractError)
223
+ return undefined;
224
+ throw error;
225
+ }
226
+ }
227
+ function copyKnownOptionalEnum(target, field, value, allowed) {
228
+ if (value === undefined)
229
+ return;
230
+ const parsed = copyValidOptionalDiagnostic(field, value, (name, input) => assertEnum(name, input, allowed));
231
+ if (parsed !== undefined)
232
+ Object.assign(target, { [field]: parsed });
233
+ }
196
234
  function assertBoundedInt(field, value, max) {
197
235
  if (value === undefined || value === null)
198
236
  throw new ClientHealthContractError("MISSING_FIELD", field);
@@ -264,6 +302,19 @@ export function parseClientHealthHeartbeat(input) {
264
302
  if (raw.failureReason !== undefined) {
265
303
  heartbeat.failureReason = assertEnum("failureReason", raw.failureReason, CLIENT_HEALTH_FAILURE_REASONS);
266
304
  }
305
+ copyKnownOptionalEnum(heartbeat, "syncRunnerSource", raw.syncRunnerSource, CLIENT_HEALTH_SYNC_RUNNER_SOURCES);
306
+ if (raw.daemonHqCloudUpdateLastCheckAt !== undefined) {
307
+ const lastCheckAt = copyValidOptionalDiagnostic("daemonHqCloudUpdateLastCheckAt", raw.daemonHqCloudUpdateLastCheckAt, assertIsoUtc);
308
+ if (lastCheckAt !== undefined)
309
+ heartbeat.daemonHqCloudUpdateLastCheckAt = lastCheckAt;
310
+ }
311
+ copyKnownOptionalEnum(heartbeat, "daemonHqCloudUpdateOutcome", raw.daemonHqCloudUpdateOutcome, CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_OUTCOMES);
312
+ if (raw.daemonHqCloudUpdateTarget !== undefined) {
313
+ const target = copyValidOptionalDiagnostic("daemonHqCloudUpdateTarget", raw.daemonHqCloudUpdateTarget, assertVersion);
314
+ if (target !== undefined)
315
+ heartbeat.daemonHqCloudUpdateTarget = target;
316
+ }
317
+ copyKnownOptionalEnum(heartbeat, "daemonHqCloudUpdateErrorClass", raw.daemonHqCloudUpdateErrorClass, CLIENT_HEALTH_DAEMON_HQ_CLOUD_UPDATE_ERROR_CLASSES);
267
318
  if (raw.localFilesOverview !== undefined) {
268
319
  heartbeat.localFilesOverview = parseClientHealthLocalFilesOverview("localFilesOverview", raw.localFilesOverview);
269
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.61",
3
+ "version": "5.345.62",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {