@indigoai-us/hq-cli 5.106.3 → 5.107.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.107.0] — 2026-09-03
6
+
7
+ ### Added
8
+
9
+ - The CLI now reports client health to HQ Cloud, so support can see which
10
+ versions a person is running and when their sync last actually succeeded —
11
+ without asking them for a screenshot. Each authenticated invocation reports
12
+ the installed CLI, HQ Core, and desktop versions, and sync commands report
13
+ both the attempt and its real outcome. The contribution is bounded, is never
14
+ allowed to delay or change a command's result or exit code, and is skipped
15
+ entirely on machines that have no HQ installation.
16
+ - `hq doctor` gained a `sync` check family covering installed versions,
17
+ available updates, and per-company sync journal staleness. An unreadable
18
+ journal store is now reported as `UNKNOWN` rather than being mistaken for
19
+ "cloud sync not in use".
20
+ - Reindexing now trusts the HQ folder for Claude Code, not only Codex and Grok.
21
+
22
+ ### Fixed
23
+
24
+ - Sync commands that exit early on failure now record that failure instead of
25
+ leaving the run marked as still in progress.
26
+
5
27
  ## [5.106.3] — 2026-09-02
6
28
 
7
29
  ## [5.106.2] — 2026-09-02
@@ -15,6 +15,34 @@
15
15
  import { Command } from "commander";
16
16
  import { type ConflictStrategy, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant } from "@indigoai-us/hq-cloud";
17
17
  import { type BannerLevel } from "../lib/narrow-hint-banner.js";
18
+ /**
19
+ * Terminal-failure signal for the sync fanout runners. The runners
20
+ * (`runPushAll` / `runPullAll` / `runPullPersonal` / `runNowSingle` /
21
+ * `runNowAll`) NEVER call `process.exit` themselves — they print their own
22
+ * diagnostics and throw this instead, so control always unwinds back into
23
+ * {@link withSyncHealthReport}, which records the terminal client-health
24
+ * outcome BEFORE the process exits. A direct `process.exit` inside a runner
25
+ * would skip the wrapper's catch, leaving a dangling `sync_attempt`
26
+ * ("syncing") heartbeat with no terminal success/failure and a wrong local
27
+ * `consecutiveFailures` counter (US-003 review fix).
28
+ */
29
+ export declare class SyncExitError extends Error {
30
+ readonly code: number;
31
+ constructor(code: number);
32
+ }
33
+ /**
34
+ * US-003: run one sync fanout under a client-health attempt/outcome report.
35
+ * The report is best-effort, bounded, and swallowed — it can only append a
36
+ * short bounded wait, never change the run's result, thrown error, or exit
37
+ * code. Direct reporting from the sync command itself (never reconstructed
38
+ * from cli_session_started analytics).
39
+ *
40
+ * Outcome reporting cannot be skipped: runners signal failure exits via
41
+ * {@link SyncExitError} (never a direct `process.exit`), so EVERY terminal
42
+ * path — return, throw, or exit — flows through `succeeded()`/`failed()`
43
+ * before this wrapper exits the process on the runner's behalf.
44
+ */
45
+ export declare function withSyncHealthReport<T>(run: () => Promise<T>): Promise<T>;
18
46
  /**
19
47
  * Build a loud, human-readable warning when a push dropped files because
20
48
  * they fell outside the caller's granted write scope. Returns null when
@@ -302,4 +330,6 @@ export interface PerCompanyPullResolveResult {
302
330
  }
303
331
  export declare function resolvePerCompanyPullPlan(client: PerCompanyPullResolveClient, targetCompany: string | undefined): Promise<PerCompanyPullResolveResult>;
304
332
  export declare function registerCloudCommands(program: Command): void;
333
+ /** Exported for the US-003 sync-health exit-path regression test. */
334
+ export declare function runPullPersonal(hqRoot: string, onConflict?: ConflictStrategy): Promise<void>;
305
335
  //# sourceMappingURL=cloud.d.ts.map
@@ -18,6 +18,52 @@ import * as path from "path";
18
18
  import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, } from "@indigoai-us/hq-cloud";
19
19
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
20
20
  import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
21
+ import { beginSyncHealthReport, } from "../utils/client-health.js";
22
+ /**
23
+ * Terminal-failure signal for the sync fanout runners. The runners
24
+ * (`runPushAll` / `runPullAll` / `runPullPersonal` / `runNowSingle` /
25
+ * `runNowAll`) NEVER call `process.exit` themselves — they print their own
26
+ * diagnostics and throw this instead, so control always unwinds back into
27
+ * {@link withSyncHealthReport}, which records the terminal client-health
28
+ * outcome BEFORE the process exits. A direct `process.exit` inside a runner
29
+ * would skip the wrapper's catch, leaving a dangling `sync_attempt`
30
+ * ("syncing") heartbeat with no terminal success/failure and a wrong local
31
+ * `consecutiveFailures` counter (US-003 review fix).
32
+ */
33
+ export class SyncExitError extends Error {
34
+ code;
35
+ constructor(code) {
36
+ super(`sync command exit ${code}`);
37
+ this.code = code;
38
+ this.name = "SyncExitError";
39
+ }
40
+ }
41
+ /**
42
+ * US-003: run one sync fanout under a client-health attempt/outcome report.
43
+ * The report is best-effort, bounded, and swallowed — it can only append a
44
+ * short bounded wait, never change the run's result, thrown error, or exit
45
+ * code. Direct reporting from the sync command itself (never reconstructed
46
+ * from cli_session_started analytics).
47
+ *
48
+ * Outcome reporting cannot be skipped: runners signal failure exits via
49
+ * {@link SyncExitError} (never a direct `process.exit`), so EVERY terminal
50
+ * path — return, throw, or exit — flows through `succeeded()`/`failed()`
51
+ * before this wrapper exits the process on the runner's behalf.
52
+ */
53
+ export async function withSyncHealthReport(run) {
54
+ const health = beginSyncHealthReport();
55
+ try {
56
+ const result = await run();
57
+ await health.succeeded();
58
+ return result;
59
+ }
60
+ catch (err) {
61
+ await health.failed();
62
+ if (err instanceof SyncExitError)
63
+ process.exit(err.code);
64
+ throw err;
65
+ }
66
+ }
21
67
  /**
22
68
  * Build a loud, human-readable warning when a push dropped files because
23
69
  * they fell outside the caller's granted write scope. Returns null when
@@ -601,7 +647,7 @@ export function registerCloudCommands(program) {
601
647
  // the fanout" — wired through to `pushAll.skipPersonal`. Outside
602
648
  // `--all` the flag has no effect (logged above as part of the
603
649
  // option's help text).
604
- await runPushAll(options.hqRoot, options.message, options.onConflict, options.personal === false);
650
+ await withSyncHealthReport(() => runPushAll(options.hqRoot, options.message, options.onConflict, options.personal === false));
605
651
  return;
606
652
  }
607
653
  const jsonMode = options.json === true;
@@ -614,6 +660,9 @@ export function registerCloudCommands(program) {
614
660
  const emitJson = (event) => {
615
661
  process.stderr.write(JSON.stringify(event) + "\n");
616
662
  };
663
+ // Assigned right before the sync engine runs, so option-validation
664
+ // failures never count as a sync attempt (US-003).
665
+ let syncHealth;
617
666
  try {
618
667
  if (options.personal && options.credsFromStdin) {
619
668
  throw new Error("`--personal` cannot be combined with --creds-from-stdin: " +
@@ -708,6 +757,7 @@ export function registerCloudCommands(program) {
708
757
  const scope = await resolveCliPullScope(scopeClient, targetCompany, options.hqRoot);
709
758
  pushPrefixSet = scope?.prefixSet;
710
759
  }
760
+ syncHealth = beginSyncHealthReport();
711
761
  const result = await share({
712
762
  paths: targetPaths,
713
763
  company: targetCompany,
@@ -738,6 +788,7 @@ export function registerCloudCommands(program) {
738
788
  }
739
789
  if (result.aborted) {
740
790
  log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
791
+ await syncHealth.failed();
741
792
  process.exit(1);
742
793
  }
743
794
  if (result.filesExcludedByScope > 0) {
@@ -751,8 +802,11 @@ export function registerCloudCommands(program) {
751
802
  else {
752
803
  log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
753
804
  }
805
+ await syncHealth.succeeded();
754
806
  }
755
807
  catch (err) {
808
+ if (syncHealth)
809
+ await syncHealth.failed();
756
810
  const message = err instanceof Error ? err.message : String(err);
757
811
  if (jsonMode) {
758
812
  // In JSON mode, the parent process is parsing stderr for ndjson —
@@ -806,13 +860,14 @@ export function registerCloudCommands(program) {
806
860
  // `options.personal === false` is Commander's auto-negation of
807
861
  // `--personal`; in `--all` mode that means "drop the personal
808
862
  // leg from the fanout" (see `--no-personal` option above).
809
- await runPullAll(options.hqRoot, options.onConflict, options.modeAll === true, options.personal === false, options.forceScopeShrink === true);
863
+ await withSyncHealthReport(() => runPullAll(options.hqRoot, options.onConflict, options.modeAll === true, options.personal === false, options.forceScopeShrink === true));
810
864
  return;
811
865
  }
812
866
  if (options.personal) {
813
- await runPullPersonal(options.hqRoot, options.onConflict);
867
+ await withSyncHealthReport(() => runPullPersonal(options.hqRoot, options.onConflict));
814
868
  return;
815
869
  }
870
+ let syncHealth;
816
871
  try {
817
872
  console.log(chalk.bold("\nHQ Sync — Pull"));
818
873
  console.log(` HQ root: ${options.hqRoot}`);
@@ -856,6 +911,7 @@ export function registerCloudCommands(program) {
856
911
  process.exit(1);
857
912
  }
858
913
  const excludePrefixes = readScopeExcludePrefixes(pullScope);
914
+ syncHealth = beginSyncHealthReport();
859
915
  const result = await sync({
860
916
  company: options.company,
861
917
  onConflict: options.onConflict,
@@ -874,6 +930,7 @@ export function registerCloudCommands(program) {
874
930
  });
875
931
  if (result.aborted) {
876
932
  console.log(chalk.yellow(`\n⚠ Pull aborted (${result.filesDownloaded} downloaded, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
933
+ await syncHealth.failed();
877
934
  process.exit(1);
878
935
  }
879
936
  console.log(chalk.green(`\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
@@ -887,8 +944,11 @@ export function registerCloudCommands(program) {
887
944
  level: narrowHintLevel,
888
945
  });
889
946
  }
947
+ await syncHealth.succeeded();
890
948
  }
891
949
  catch (err) {
950
+ if (syncHealth)
951
+ await syncHealth.failed();
892
952
  console.error(chalk.red("\n✗ Pull failed:"), err instanceof Error ? err.message : String(err));
893
953
  process.exit(1);
894
954
  }
@@ -999,10 +1059,10 @@ export function registerCloudCommands(program) {
999
1059
  // of `--personal`; in `--all` mode that means "drop the
1000
1060
  // personal leg from both legs of the bidirectional fanout"
1001
1061
  // (see `--no-personal` option above).
1002
- await runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, options.personal === false, options.forceScopeShrink === true);
1062
+ await withSyncHealthReport(() => runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, options.personal === false, options.forceScopeShrink === true));
1003
1063
  return;
1004
1064
  }
1005
- await runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true);
1065
+ await withSyncHealthReport(() => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true));
1006
1066
  }
1007
1067
  catch (err) {
1008
1068
  console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
@@ -1074,7 +1134,9 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1074
1134
  }
1075
1135
  catch (err) {
1076
1136
  console.error(chalk.red("\n✗ Pull-all failed:"), err instanceof Error ? err.message : String(err));
1077
- process.exit(1);
1137
+ // Unwind (never process.exit) so withSyncHealthReport records the
1138
+ // terminal sync_failure before exiting on our behalf.
1139
+ throw new SyncExitError(1);
1078
1140
  }
1079
1141
  for (const row of result.perCompany) {
1080
1142
  if (row.error) {
@@ -1095,9 +1157,10 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1095
1157
  `target(s); ${result.conflicts} conflict(s); ${errored} error(s)`;
1096
1158
  console.log(errored > 0 ? chalk.yellow(summary) : chalk.green(summary));
1097
1159
  if (errored > 0)
1098
- process.exit(1);
1160
+ throw new SyncExitError(1);
1099
1161
  }
1100
- async function runPullPersonal(hqRoot, onConflict) {
1162
+ /** Exported for the US-003 sync-health exit-path regression test. */
1163
+ export async function runPullPersonal(hqRoot, onConflict) {
1101
1164
  console.log(chalk.bold("\nHQ Sync — Pull (personal)"));
1102
1165
  console.log(` HQ root: ${hqRoot}`);
1103
1166
  console.log(` Strategy: ${onConflict ?? "(interactive)"}\n`);
@@ -1120,13 +1183,15 @@ async function runPullPersonal(hqRoot, onConflict) {
1120
1183
  });
1121
1184
  if (result.aborted) {
1122
1185
  console.log(chalk.yellow(`\n⚠ Pull aborted (${result.filesDownloaded} downloaded, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
1123
- process.exit(1);
1186
+ throw new SyncExitError(1);
1124
1187
  }
1125
1188
  console.log(chalk.green(`\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
1126
1189
  }
1127
1190
  catch (err) {
1191
+ if (err instanceof SyncExitError)
1192
+ throw err;
1128
1193
  console.error(chalk.red("\n✗ Pull (personal) failed:"), err instanceof Error ? err.message : String(err));
1129
- process.exit(1);
1194
+ throw new SyncExitError(1);
1130
1195
  }
1131
1196
  }
1132
1197
  async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
@@ -1192,7 +1257,9 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1192
1257
  }
1193
1258
  catch (err) {
1194
1259
  console.error(chalk.red("\n✗ Push-all failed:"), err instanceof Error ? err.message : String(err));
1195
- process.exit(1);
1260
+ // Unwind (never process.exit) so withSyncHealthReport records the
1261
+ // terminal sync_failure before exiting on our behalf.
1262
+ throw new SyncExitError(1);
1196
1263
  }
1197
1264
  for (const row of result.perCompany) {
1198
1265
  if (row.error) {
@@ -1222,7 +1289,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1222
1289
  console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
1223
1290
  }
1224
1291
  if (errored > 0)
1225
- process.exit(1);
1292
+ throw new SyncExitError(1);
1226
1293
  }
1227
1294
  async function runNowSingle(hqRoot, company, personal, message, onConflict, modeAllOverride, forceScopeShrink) {
1228
1295
  console.log(chalk.bold("\nHQ Sync — Now"));
@@ -1304,7 +1371,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1304
1371
  }
1305
1372
  if (pushResult.aborted) {
1306
1373
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
1307
- process.exit(1);
1374
+ throw new SyncExitError(1);
1308
1375
  }
1309
1376
  // US-011: resolve membership sync-config so we can either nudge an
1310
1377
  // all-mode owner or refuse the pull when strict-mode is on. Skipped
@@ -1352,7 +1419,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1352
1419
  console.error(chalk.red("\n✗ Sync now refused: strict narrow-hint mode is on and this " +
1353
1420
  "company's local folder has grown large. Run `hq sync narrow --apply` " +
1354
1421
  "to migrate, or re-run with --mode-all."));
1355
- process.exit(1);
1422
+ throw new SyncExitError(1);
1356
1423
  }
1357
1424
  // DEV-1768: resolve the membership's real scope and thread it into the
1358
1425
  // pull leg, so `hq sync now` stops seeding all-mode PullRecords. Personal
@@ -1392,7 +1459,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1392
1459
  (pullResult.aborted ? " — aborted" : ""));
1393
1460
  if (pullResult.aborted) {
1394
1461
  console.log(chalk.yellow("\n⚠ Sync now finished with pull leg aborted."));
1395
- process.exit(1);
1462
+ throw new SyncExitError(1);
1396
1463
  }
1397
1464
  // Emit the hint banner after a successful pull so it appears at the
1398
1465
  // bottom of the summary rather than mid-stream. Size-gated: only a large
@@ -1407,8 +1474,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1407
1474
  console.log(chalk.green("\n✓ Sync now complete"));
1408
1475
  }
1409
1476
  catch (err) {
1477
+ if (err instanceof SyncExitError)
1478
+ throw err;
1410
1479
  console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
1411
- process.exit(1);
1480
+ throw new SyncExitError(1);
1412
1481
  }
1413
1482
  }
1414
1483
  async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPersonal, forceScopeShrink) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * hq reindex — surface namespaced skills, mirror the personal overlay into
3
3
  * core/, regenerate the workers registry, and converge HQ-owned hook trust for
4
- * Codex and Grok.
4
+ * Codex, Grok, and Claude Code.
5
5
  *
6
6
  * Thin wrapper over @indigoai-us/hq-cloud's reindex(), which execs the bundled
7
7
  * scripts/reindex.sh against the HQ root. This command is what the hq-core
@@ -493,7 +493,7 @@ export function registerReindexCommand(program) {
493
493
  program
494
494
  .command('reindex')
495
495
  .alias('master-sync')
496
- .description('Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex and Grok')
496
+ .description('Surface namespaced skills, materialize legacy knowledge repos, regenerate the workers registry, and trust HQ hooks for Codex, Grok, and Claude Code')
497
497
  .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
498
498
  .option('--from-hook', 'Invoked from a Claude/Codex lifecycle hook: never wait on the per-root operation lock — if a sync/rescue holds it, skip this reindex instead of blocking the session (equivalent to --lock-timeout 0)')
499
499
  .option('--lock-timeout <seconds>', 'Bound the wait for the per-root operation lock (seconds). 0 = refuse immediately; omitted = wait indefinitely (the interactive default). --from-hook implies 0; an explicit --lock-timeout wins.')
@@ -518,13 +518,23 @@ export function registerReindexCommand(program) {
518
518
  if (lockTimeoutSec !== undefined && process.env.HQ_OP_LOCK_TIMEOUT === undefined) {
519
519
  process.env.HQ_OP_LOCK_TIMEOUT = String(lockTimeoutSec);
520
520
  }
521
- const hqRoot = resolveHqRoot(opts.repoRoot);
521
+ // Keep the pre-realpath spelling: `resolveHqRoot` canonicalizes, and
522
+ // Claude keys its trusted projects by session cwd, so a user who lives
523
+ // in a symlinked HQ root would otherwise never get that spelling trusted.
524
+ const literalHqRoot = path.resolve(opts.repoRoot ?? findHqRoot());
525
+ const hqRoot = resolveHqRoot(literalHqRoot);
522
526
  const sweepStartedAt = Date.now();
523
527
  removeStaleHqWorktrees(hqRoot, sweepStartedAt, opts.fromHook ? sweepStartedAt + WORKTREE_HOOK_SWEEP_BUDGET_MS : Number.POSITIVE_INFINITY);
524
528
  const { status } = reindex({ repoRoot: opts.repoRoot });
525
529
  repairExtremeHookDrift(hqRoot, status === 0);
526
- if (status === 0)
527
- await trustHqRuntimeHooks(hqRoot);
530
+ if (status === 0) {
531
+ await trustHqRuntimeHooks(hqRoot, undefined, {
532
+ rootAliases: [literalHqRoot],
533
+ // A lifecycle hook must never block the agent waiting on another HQ
534
+ // process's config write; trust converges on the next reindex.
535
+ lockWaitMs: opts.fromHook ? 0 : undefined,
536
+ });
537
+ }
528
538
  // Runs even when reindex failed: a failed reindex does not make an
529
539
  // oversized blob any less likely to wedge the next push.
530
540
  reportLargeFileGuard(hqRoot);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Versions & sync health — the doctor family the registry docblock always
3
+ * anticipated (client-sync-health-control-plane US-015).
4
+ *
5
+ * Reports, per install:
6
+ * - the CLI / Core / desktop (hq-sync menubar) component versions, from the
7
+ * same collectors the feedback and client-health paths use (US-003), so
8
+ * `hq doctor --json` shows exactly what a heartbeat would report;
9
+ * - whether a newer hq-core release is known to be available — read OFFLINE
10
+ * from the cache stamped by the `check-hq-update` SessionStart hook
11
+ * (`workspace/.hq-update-check/last-check.json`), never from the network,
12
+ * preserving the doctor's offline contract (no cache → UNTESTED, not PASS);
13
+ * - per-company sync journal staleness via the engine's `listJournals()` —
14
+ * the ONLY correct enumeration of per-scope journal shards
15
+ * (single-path reconstruction regressed before: feedback_9fbf1f82 /
16
+ * feedback_46288b7b).
17
+ *
18
+ * Caution paid for in blood (bridge-health false positives): a machine with no
19
+ * journals at all is NA, not WARN — CLI-only installs never sync locally and
20
+ * must not read as degraded. Staleness warns only on a corroborated signal: a
21
+ * journal that EXISTS and carries a parseable, old `lastSync`.
22
+ *
23
+ * Every dependency is injectable so the family is unit-testable without an HQ
24
+ * tree, a state dir, or the wall clock.
25
+ */
26
+ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
27
+ /** The id of the versions/sync family. */
28
+ export declare const SYNC_FAMILY_ID = "sync";
29
+ /** Human title for grouped output. */
30
+ export declare const SYNC_FAMILY_TITLE = "Versions & sync";
31
+ /**
32
+ * A journal shard older than this is reported stale. Seven days: long enough
33
+ * that a laptop shut over a weekend never warns, short enough that a silently
34
+ * dead sync runner surfaces well before data divergence becomes painful.
35
+ */
36
+ export declare const STALE_JOURNAL_THRESHOLD_MS: number;
37
+ /** The offline update cache written by the check-hq-update SessionStart hook. */
38
+ export declare const UPDATE_CACHE_RELPATH: string;
39
+ /** The minimal journal shape this family reads. Matches `JournalSummary`. */
40
+ export interface SyncJournalSummary {
41
+ slug: string;
42
+ path: string;
43
+ journal: {
44
+ lastSync?: string | null;
45
+ };
46
+ }
47
+ /** The component versions this family reports. */
48
+ export interface SyncVersionInfo {
49
+ cli: string;
50
+ core: string | null;
51
+ desktop: string | null;
52
+ }
53
+ /** Injectable dependencies — defaults are the real collectors. */
54
+ export interface SyncHealthDeps {
55
+ versions: (hqRoot: string) => SyncVersionInfo;
56
+ journals: () => readonly SyncJournalSummary[];
57
+ now: () => Date;
58
+ }
59
+ /** The versions/sync check family. Registered in `createDefaultRegistry`. */
60
+ export declare const syncHealthFamily: CheckFamily;
61
+ /** Run every versions/sync check. A thrown check degrades to UNKNOWN. */
62
+ export declare function checkSyncHealth(context: CheckContext, deps?: SyncHealthDeps): CheckResult[];
63
+ /** True when `a` > `b` for plain X.Y.Z versions. Non-numeric parts compare 0. */
64
+ export declare function semverGt(a: string, b: string): boolean;
65
+ //# sourceMappingURL=sync-health.d.ts.map