@indigoai-us/hq-cli 5.183.0 → 5.185.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 (45) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/command-catalog.generated.d.ts +3 -0
  3. package/dist/command-catalog.generated.js +4 -0
  4. package/dist/commands/doctor.d.ts +2 -0
  5. package/dist/commands/doctor.js +100 -6
  6. package/dist/commands/mesh.js +18 -1
  7. package/dist/lib/bot/progress.d.ts +10 -0
  8. package/dist/lib/bot/progress.js +55 -0
  9. package/dist/lib/bot/prompt.js +3 -1
  10. package/dist/lib/bot/run.d.ts +4 -1
  11. package/dist/lib/bot/run.js +60 -5
  12. package/dist/lib/doctor/checks/sync-health.js +13 -0
  13. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +26 -0
  14. package/dist/lib/doctor/client-health-reason-codes-flag.js +94 -0
  15. package/dist/lib/doctor/fix/apply.d.ts +5 -0
  16. package/dist/lib/doctor/fix/apply.js +28 -3
  17. package/dist/lib/doctor/fix/json-output.d.ts +20 -0
  18. package/dist/lib/doctor/fix/json-output.js +22 -0
  19. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +12 -0
  20. package/dist/lib/doctor/fix/refresh-sync-outcome.js +25 -0
  21. package/dist/lib/doctor/json-output.d.ts +7 -1
  22. package/dist/lib/doctor/json-output.js +6 -2
  23. package/dist/lib/doctor/types.d.ts +5 -0
  24. package/dist/lib/doctor/types.js +15 -0
  25. package/dist/lib/mesh/live/daemon/index.d.ts +1 -1
  26. package/dist/lib/mesh/live/daemon/index.js +1 -1
  27. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +7 -0
  28. package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -3
  29. package/dist/lib/mesh/live/emit-client.d.ts +6 -0
  30. package/dist/lib/mesh/live/emit-client.js +8 -0
  31. package/dist/lib/mesh/live/enqueue.d.ts +5 -0
  32. package/dist/lib/mesh/live/enqueue.js +19 -0
  33. package/dist/lib/mesh/live/flush.js +5 -3
  34. package/dist/lib/mesh/live/format-spool-line.d.ts +5 -2
  35. package/dist/lib/mesh/live/format-spool-line.js +10 -0
  36. package/dist/lib/mesh/live/report-summary.d.ts +45 -0
  37. package/dist/lib/mesh/live/report-summary.js +222 -0
  38. package/dist/lib/mesh/live/session-event.schema.json +24 -1
  39. package/dist/lib/mesh/live/validate-session-event.d.ts +1 -1
  40. package/dist/lib/mesh/live/validate-session-event.js +1 -1
  41. package/dist/lib/search-index/collection-path-repair-flag.d.ts +4 -0
  42. package/dist/lib/search-index/collection-path-repair-flag.js +7 -0
  43. package/dist/lib/search-index/index.d.ts +4 -0
  44. package/dist/lib/search-index/index.js +106 -1
  45. package/package.json +1 -1
@@ -223,6 +223,7 @@ function versionResults(context, deps) {
223
223
  results.push({
224
224
  status: "WARN",
225
225
  checkId: "sync.versions.core",
226
+ reasonCode: "core-version-unavailable",
226
227
  target: path.join(context.hqRoot, "core", "core.yaml"),
227
228
  message: "hq-core version could not be read from core/core.yaml — the scaffold may be missing or pre-v12.",
228
229
  remediation: "Run /update-hq to (re)install the hq-core scaffold.",
@@ -282,6 +283,7 @@ function updateAvailabilityResult(context, deps) {
282
283
  {
283
284
  status: "WARN",
284
285
  checkId: "sync.update.core",
286
+ reasonCode: "core-update-available",
285
287
  message: `hq-core update available: local v${core}, latest v${latest}.`,
286
288
  remediation: "Run /update-hq in a fresh session to upgrade.",
287
289
  },
@@ -373,6 +375,7 @@ function journalResults(deps, journals) {
373
375
  return {
374
376
  status: "NA",
375
377
  checkId,
378
+ reasonCode: "no-local-tree",
376
379
  target: entry.path,
377
380
  message: `Sync journal '${entry.slug}' is unused on this machine — there is no companies/${entry.slug} directory, so this journal is not a live sync target.`,
378
381
  };
@@ -382,6 +385,7 @@ function journalResults(deps, journals) {
382
385
  return {
383
386
  status: "WARN",
384
387
  checkId,
388
+ reasonCode: "never-synced",
385
389
  target: entry.path,
386
390
  message: `Sync journal '${entry.slug}' exists but has never recorded a successful sync.`,
387
391
  remediation: "Run /hq-sync (or `hq sync`) to complete a first sync.",
@@ -392,6 +396,7 @@ function journalResults(deps, journals) {
392
396
  return {
393
397
  status: "UNKNOWN",
394
398
  checkId,
399
+ reasonCode: "invalid-last-sync",
395
400
  target: entry.path,
396
401
  message: `Sync journal '${entry.slug}' has an unparseable lastSync (${lastSync}).`,
397
402
  };
@@ -402,6 +407,7 @@ function journalResults(deps, journals) {
402
407
  return {
403
408
  status: "WARN",
404
409
  checkId,
410
+ reasonCode: "stale-threshold",
405
411
  target: entry.path,
406
412
  message: `Sync journal '${entry.slug}' is stale: last successful sync ${days} day${days === 1 ? "" : "s"} ago (${lastSync}).`,
407
413
  remediation: "Run /hq-sync (or `hq sync`) and check the sync runner.",
@@ -443,6 +449,7 @@ function manifestResults(deps, journals) {
443
449
  {
444
450
  status: "NA",
445
451
  checkId: "sync.manifest",
452
+ reasonCode: "manifest-unavailable",
446
453
  message: `${MANIFEST_UNAVAILABLE_REASON} — upgrade hq-cloud to audit manifest uploads.`,
447
454
  },
448
455
  ];
@@ -456,6 +463,7 @@ function manifestResults(deps, journals) {
456
463
  {
457
464
  status: "UNKNOWN",
458
465
  checkId: "sync.manifest",
466
+ reasonCode: "manifest-state-unavailable",
459
467
  message: `Manifest upload state could not be read: ${error instanceof Error ? error.message : String(error)}`,
460
468
  remediation: "Check that the HQ state directory is readable, then re-run `hq doctor`.",
461
469
  },
@@ -469,6 +477,7 @@ function manifestResults(deps, journals) {
469
477
  {
470
478
  status: "NA",
471
479
  checkId: "sync.manifest",
480
+ reasonCode: "no-manifest-scopes",
472
481
  message: "No manifest-auditable sync scopes on this machine — nothing to reconcile.",
473
482
  },
474
483
  ];
@@ -480,6 +489,7 @@ function manifestResults(deps, journals) {
480
489
  return {
481
490
  status: "UNTESTED",
482
491
  checkId,
492
+ reasonCode: "never-uploaded",
483
493
  message: `Scope '${entry.scopeKey}' has never uploaded a sync manifest — expected until the first pass runs.`,
484
494
  remediation: "Run `hq sync manifest` to upload one now.",
485
495
  };
@@ -489,6 +499,7 @@ function manifestResults(deps, journals) {
489
499
  return {
490
500
  status: "UNKNOWN",
491
501
  checkId,
502
+ reasonCode: "invalid-upload-time",
492
503
  message: `Scope '${entry.scopeKey}' has an unparseable last manifest upload time (${entry.lastUploadAt}).`,
493
504
  };
494
505
  }
@@ -501,6 +512,7 @@ function manifestResults(deps, journals) {
501
512
  return {
502
513
  status: "WARN",
503
514
  checkId,
515
+ reasonCode: "stale-threshold",
504
516
  message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
505
517
  remediation: "Run `hq sync manifest` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.",
506
518
  };
@@ -532,6 +544,7 @@ function unresolvedResults(deps, journals) {
532
544
  return slugs.map((slug) => ({
533
545
  status: "UNTESTED",
534
546
  checkId: `sync.manifest.unresolved.${slug}`,
547
+ reasonCode: "unresolved-company-uid",
535
548
  message: `Company scope '${slug}' has a sync journal but its companyUid could not be resolved locally — its manifest uploads were not checked.`,
536
549
  remediation: `Run \`hq sync manifest --scope ${slug}\` to audit it explicitly.`,
537
550
  }));
@@ -0,0 +1,26 @@
1
+ import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
2
+ export declare const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
3
+ export declare const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
4
+ export declare const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
5
+ type ClientHealthFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
6
+ export interface ClientHealthReasonCodesFlagDependencies {
7
+ env?: NodeJS.ProcessEnv;
8
+ createClient?: (options: FlagClientOptions) => ClientHealthFlagClient;
9
+ getToken?: FlagClientOptions["getToken"];
10
+ fetch?: typeof fetch;
11
+ reportFailure?: (error: unknown) => void;
12
+ }
13
+ export interface ClientHealthReasonCodesCompany {
14
+ companyUid: string;
15
+ companySlug?: string;
16
+ }
17
+ /**
18
+ * Resolve the privacy gate from hq-flags with a bounded request. Missing
19
+ * identity, service configuration, and every lookup failure keep diagnostics
20
+ * off so a flag outage cannot delay the doctor's health result.
21
+ */
22
+ export declare function clientHealthReasonCodesEnabled(dependencies?: ClientHealthReasonCodesFlagDependencies, callerSignal?: AbortSignal): Promise<boolean>;
23
+ /** Resolve the same default-off gate for one explicitly identified company. */
24
+ export declare function clientHealthReasonCodesEnabledForCompany(company: ClientHealthReasonCodesCompany, dependencies?: ClientHealthReasonCodesFlagDependencies, callerSignal?: AbortSignal): Promise<boolean>;
25
+ export {};
26
+ //# sourceMappingURL=client-health-reason-codes-flag.d.ts.map
@@ -0,0 +1,94 @@
1
+ import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
+ import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
+ export const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
4
+ export const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
5
+ export const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
6
+ function safeErrorClass(error) {
7
+ const name = error instanceof Error ? error.name : "UnknownError";
8
+ return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
9
+ }
10
+ function reportFlagFailure(error) {
11
+ process.stderr.write(`HQ client-health reason-code flag lookup failed (${safeErrorClass(error)}); using default off.\n`);
12
+ }
13
+ function combineSignals(first, second) {
14
+ return first ? AbortSignal.any([first, second]) : second;
15
+ }
16
+ /**
17
+ * Resolve the privacy gate from hq-flags with a bounded request. Missing
18
+ * identity, service configuration, and every lookup failure keep diagnostics
19
+ * off so a flag outage cannot delay the doctor's health result.
20
+ */
21
+ export async function clientHealthReasonCodesEnabled(dependencies = {}, callerSignal) {
22
+ const env = dependencies.env ?? process.env;
23
+ const companyUid = env.HQ_COMPANY_UID?.trim() || "";
24
+ if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
25
+ return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
26
+ }
27
+ return clientHealthReasonCodesEnabledForCompany({
28
+ companyUid,
29
+ companySlug: env.HQ_COMPANY_SLUG?.trim() || undefined,
30
+ }, dependencies, callerSignal);
31
+ }
32
+ /** Resolve the same default-off gate for one explicitly identified company. */
33
+ export async function clientHealthReasonCodesEnabledForCompany(company, dependencies = {}, callerSignal) {
34
+ const env = dependencies.env ?? process.env;
35
+ const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
36
+ const companyUid = company.companyUid.trim();
37
+ if (!endpoint || !/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
38
+ return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
39
+ }
40
+ const companySlug = company.companySlug?.trim() || "";
41
+ const deadline = AbortSignal.timeout(CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS);
42
+ const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
43
+ const doFetch = dependencies.fetch ?? globalThis.fetch;
44
+ let client;
45
+ let failed = false;
46
+ let failureReported = false;
47
+ let enabled = CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
48
+ const reportFailureOnce = (error) => {
49
+ failed = true;
50
+ if (failureReported)
51
+ return;
52
+ failureReported = true;
53
+ (dependencies.reportFailure ?? reportFlagFailure)(error);
54
+ };
55
+ try {
56
+ client = (dependencies.createClient ?? createFlagClient)({
57
+ endpoint,
58
+ companyUid,
59
+ ...(companySlug && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
60
+ ? { companyIdentifiers: [companySlug] }
61
+ : {}),
62
+ env: {},
63
+ getToken: dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? ""),
64
+ fetch: (input, init) => doFetch(input, {
65
+ ...init,
66
+ signal: combineSignals(init?.signal ?? undefined, signal),
67
+ }),
68
+ refreshIntervalMs: 0,
69
+ requestTimeoutMs: CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS,
70
+ onError: reportFailureOnce,
71
+ });
72
+ await client.ready();
73
+ if (!failed && !signal.aborted) {
74
+ enabled = client.isEnabled(CLIENT_HEALTH_REASON_CODES_FLAG, {
75
+ fallback: CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK,
76
+ });
77
+ }
78
+ }
79
+ catch (error) {
80
+ reportFailureOnce(error);
81
+ }
82
+ finally {
83
+ try {
84
+ client?.close();
85
+ }
86
+ catch (error) {
87
+ reportFailureOnce(error);
88
+ }
89
+ }
90
+ return failed || signal.aborted
91
+ ? CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK
92
+ : enabled;
93
+ }
94
+ //# sourceMappingURL=client-health-reason-codes-flag.js.map
@@ -31,6 +31,7 @@
31
31
  import type { CheckResult, DoctorStatus } from "../types.js";
32
32
  import { type GateProfile } from "../hook-gate-profiles.js";
33
33
  import { type FixClass } from "./remediation.js";
34
+ import { type RefreshSyncOutcome } from "./refresh-sync-outcome.js";
34
35
  /** The tree subtrees whose uncommitted changes block a `--fix` run. */
35
36
  export declare const HOOK_CONFIG_DIRS: readonly [".claude", ".codex", ".grok"];
36
37
  /** Options for {@link applyFixes}. Everything is injectable for hermetic tests. */
@@ -91,6 +92,8 @@ export interface RefreshSyncTarget {
91
92
  export interface RefreshSyncResult {
92
93
  /** Whether the pull exited 0. */
93
94
  ok: boolean;
95
+ /** Closed enum for machine-readable client-health diagnostics. */
96
+ outcome: RefreshSyncOutcome;
94
97
  /** One-line summary for the post-fix report. */
95
98
  message: string;
96
99
  }
@@ -108,6 +111,8 @@ export interface AppliedFix {
108
111
  summary: string;
109
112
  /** The status of the re-run check for this target, or null if not found. */
110
113
  postStatus: DoctorStatus | null;
114
+ /** Present for refresh-sync plans; contains no command output or path data. */
115
+ refreshSyncOutcome?: RefreshSyncOutcome;
111
116
  }
112
117
  /** The outcome of an {@link applyFixes} run. */
113
118
  export interface ApplyFixesResult {
@@ -40,6 +40,7 @@ import { deriveRemediation } from "./remediation.js";
40
40
  import { botDir } from "../../bot/paths.js";
41
41
  import { readBotConfig } from "../../bot/config.js";
42
42
  import { buildBotDaemonPaths, rewriteBotDaemonNodePath } from "../../bot/daemon.js";
43
+ import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
43
44
  /** The tree subtrees whose uncommitted changes block a `--fix` run. */
44
45
  export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
45
46
  /** Per-company bound for the default `hq sync pull` repair. */
@@ -130,6 +131,9 @@ export async function applyFixes(options) {
130
131
  target: plan.target,
131
132
  summary: plan.summary,
132
133
  postStatus: plan.postStatus(after),
134
+ ...(plan.refreshSyncOutcome?.()
135
+ ? { refreshSyncOutcome: plan.refreshSyncOutcome() }
136
+ : {}),
133
137
  }));
134
138
  write("\nPost-fix status:\n");
135
139
  for (const fix of applied) {
@@ -197,23 +201,41 @@ export function defaultRefreshSync(target) {
197
201
  cwd: target.hqRoot,
198
202
  });
199
203
  if (result.error) {
200
- const timedOut = result.error.message.includes("ETIMEDOUT") || result.signal === "SIGTERM";
204
+ const spawnError = result.error;
205
+ const outcome = classifyRefreshSyncOutcome({
206
+ errorCode: spawnError.code,
207
+ ...(result.error.message.includes("ETIMEDOUT")
208
+ ? { errorCode: "ETIMEDOUT" }
209
+ : {}),
210
+ signal: result.signal,
211
+ status: result.status,
212
+ stdout: result.stdout,
213
+ stderr: result.stderr,
214
+ });
201
215
  return Promise.resolve({
202
216
  ok: false,
203
- message: timedOut
217
+ outcome,
218
+ message: outcome === "pull-timeout"
204
219
  ? `timed out pulling '${target.slug}'`
205
220
  : `could not start pull for '${target.slug}': ${result.error.message}`,
206
221
  });
207
222
  }
208
223
  if (result.status !== 0) {
224
+ const outcome = classifyRefreshSyncOutcome({
225
+ status: result.status,
226
+ stdout: result.stdout,
227
+ stderr: result.stderr,
228
+ });
209
229
  const detail = (result.stderr || result.stdout || "").trim().split("\n").at(-1) ?? "";
210
230
  return Promise.resolve({
211
231
  ok: false,
232
+ outcome,
212
233
  message: `pull of '${target.slug}' exited ${result.status}${detail ? `: ${detail}` : ""}`,
213
234
  });
214
235
  }
215
236
  return Promise.resolve({
216
237
  ok: true,
238
+ outcome: "pull-succeeded",
217
239
  message: `pulled '${target.slug}'`,
218
240
  });
219
241
  }
@@ -287,6 +309,7 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
287
309
  const slug = rem.fixTarget;
288
310
  if (!slug)
289
311
  return null;
312
+ let outcome;
290
313
  return {
291
314
  checkId: result.checkId,
292
315
  fixClass: "refresh-sync",
@@ -297,8 +320,10 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
297
320
  ` refresh the stale sync journal (keeps local conflict copies)`,
298
321
  summary: `pull ${slug} to refresh its sync journal`,
299
322
  apply: async () => {
300
- await refreshSync({ slug, hqRoot });
323
+ const result = await refreshSync({ slug, hqRoot });
324
+ outcome = result.outcome;
301
325
  },
326
+ refreshSyncOutcome: () => outcome,
302
327
  postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
303
328
  };
304
329
  }
@@ -0,0 +1,20 @@
1
+ import type { AppliedFix, ApplyFixesResult } from "./apply.js";
2
+ export declare const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
3
+ export interface DoctorFixJsonResult {
4
+ schemaVersion: number;
5
+ exitCode: number;
6
+ refused: ApplyFixesResult["refused"] | "outside-hq-tree";
7
+ refreshSync: {
8
+ checkId: string;
9
+ outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
10
+ postStatus: AppliedFix["postStatus"];
11
+ }[];
12
+ }
13
+ /** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
14
+ export declare function buildDoctorFixJson(input: {
15
+ exitCode: number;
16
+ refused: ApplyFixesResult["refused"] | "outside-hq-tree";
17
+ applied: readonly AppliedFix[];
18
+ }): DoctorFixJsonResult;
19
+ export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
20
+ //# sourceMappingURL=json-output.d.ts.map
@@ -0,0 +1,22 @@
1
+ export const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
2
+ /** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
3
+ export function buildDoctorFixJson(input) {
4
+ return {
5
+ schemaVersion: DOCTOR_FIX_JSON_SCHEMA_VERSION,
6
+ exitCode: input.exitCode,
7
+ refused: input.refused,
8
+ refreshSync: input.applied.flatMap((fix) => fix.fixClass === "refresh-sync" && fix.refreshSyncOutcome
9
+ ? [
10
+ {
11
+ checkId: fix.checkId,
12
+ outcome: fix.refreshSyncOutcome,
13
+ postStatus: fix.postStatus,
14
+ },
15
+ ]
16
+ : []),
17
+ };
18
+ }
19
+ export function renderDoctorFixJson(input) {
20
+ return JSON.stringify(buildDoctorFixJson(input), null, 2) + "\n";
21
+ }
22
+ //# sourceMappingURL=json-output.js.map
@@ -0,0 +1,12 @@
1
+ /** Privacy-safe outcomes for a targeted refresh-sync attempt. */
2
+ export type RefreshSyncOutcome = "pull-succeeded" | "pull-timeout" | "pull-failed:4xx" | "pull-failed:5xx" | "pull-failed:network" | "pull-failed:other";
3
+ export interface RefreshSyncObservation {
4
+ errorCode?: string;
5
+ signal?: string | null;
6
+ status?: number | null;
7
+ stdout?: string;
8
+ stderr?: string;
9
+ }
10
+ /** Classify process/HTTP results without retaining their free-text details. */
11
+ export declare function classifyRefreshSyncOutcome(observation: RefreshSyncObservation): RefreshSyncOutcome;
12
+ //# sourceMappingURL=refresh-sync-outcome.d.ts.map
@@ -0,0 +1,25 @@
1
+ /** Classify process/HTTP results without retaining their free-text details. */
2
+ export function classifyRefreshSyncOutcome(observation) {
3
+ const details = String(observation.stdout ?? "") + "\n" + String(observation.stderr ?? "");
4
+ const errorCode = observation.errorCode ??
5
+ /\b(ETIMEDOUT|ETIME|EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)\b/.exec(details)?.[1];
6
+ if (errorCode === "ETIMEDOUT" ||
7
+ errorCode === "ETIME" ||
8
+ observation.signal === "SIGTERM") {
9
+ return "pull-timeout";
10
+ }
11
+ if (!errorCode && observation.status === 0) {
12
+ return "pull-succeeded";
13
+ }
14
+ const httpStatus = /(?:HTTP(?:\/\d(?:\.\d)?)?\s+|status(?:\s+code)?\s*[=: ]\s*)([45]\d\d)\b/i.exec(details)?.[1] ?? "";
15
+ if (httpStatus.startsWith("4"))
16
+ return "pull-failed:4xx";
17
+ if (httpStatus.startsWith("5"))
18
+ return "pull-failed:5xx";
19
+ if (errorCode &&
20
+ /^(?:EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)$/.test(errorCode)) {
21
+ return "pull-failed:network";
22
+ }
23
+ return "pull-failed:other";
24
+ }
25
+ //# sourceMappingURL=refresh-sync-outcome.js.map
@@ -18,7 +18,7 @@
18
18
  * per-item result shape where both are optional.
19
19
  */
20
20
  import { type DoctorPlatform, type StatusCounts } from "./report.js";
21
- import type { DoctorStatus, FamilyRun } from "./types.js";
21
+ import type { DoctorStatus, FamilyRun, SyncReasonCode } from "./types.js";
22
22
  /**
23
23
  * The `--json` schema version. Bump on any breaking change to the document
24
24
  * shape; the snapshot test in report.test.ts fails until the bump is
@@ -50,6 +50,10 @@ export interface DoctorJsonResult {
50
50
  family: string;
51
51
  /** Stable machine id for the check, e.g. `hooks.settings-present`. */
52
52
  checkId: string;
53
+ /** Stable classification for the sync family; contains no customer data. */
54
+ reasonCode?: SyncReasonCode;
55
+ /** Present only when this result's company opted in through hq-flags. */
56
+ clientHealthReasonCodesEnabled?: true;
53
57
  /** What was inspected. Present only when the result carries one. */
54
58
  target?: string;
55
59
  /** Human-readable explanation. */
@@ -92,6 +96,8 @@ export interface BuildDoctorJsonInput {
92
96
  platform?: DoctorPlatform;
93
97
  /** Families this run was restricted to. Omitted on a full run. */
94
98
  scope?: readonly string[];
99
+ /** Check ids whose own company enabled diagnostic reason codes in hq-flags. */
100
+ clientHealthReasonCodesEnabledCheckIds?: ReadonlySet<string>;
95
101
  }
96
102
  /** Assemble the machine-readable document from a completed run. */
97
103
  export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
@@ -34,7 +34,7 @@ export function buildDoctorJson(input) {
34
34
  const results = [];
35
35
  for (const run of input.families) {
36
36
  for (const result of run.results) {
37
- results.push(toJsonResult(run.family.id, result));
37
+ results.push(toJsonResult(run.family.id, result, input.clientHealthReasonCodesEnabledCheckIds));
38
38
  }
39
39
  }
40
40
  return {
@@ -48,12 +48,16 @@ export function buildDoctorJson(input) {
48
48
  };
49
49
  }
50
50
  /** Convert one {@link CheckResult} into its JSON form, omitting empty optionals. */
51
- function toJsonResult(familyId, result) {
51
+ function toJsonResult(familyId, result, clientHealthReasonCodesEnabledCheckIds) {
52
52
  const remediation = deriveRemediation(result);
53
53
  return {
54
54
  status: result.status,
55
55
  family: familyId,
56
56
  checkId: result.checkId,
57
+ ...(result.reasonCode !== undefined ? { reasonCode: result.reasonCode } : {}),
58
+ ...(clientHealthReasonCodesEnabledCheckIds?.has(result.checkId)
59
+ ? { clientHealthReasonCodesEnabled: true }
60
+ : {}),
57
61
  ...(result.target !== undefined ? { target: result.target } : {}),
58
62
  message: result.message,
59
63
  ...(result.remediation !== undefined
@@ -18,6 +18,9 @@ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTE
18
18
  * the type system rejects any other string at every call site.
19
19
  */
20
20
  export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
21
+ /** Stable, privacy-safe diagnostics emitted by the sync health family. */
22
+ export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid"];
23
+ export type SyncReasonCode = (typeof SYNC_REASON_CODES)[number];
21
24
  /** Runtime type guard: true only for a member of the fixed vocabulary. */
22
25
  export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
23
26
  /** A single per-item result produced by a check family. */
@@ -26,6 +29,8 @@ export interface CheckResult {
26
29
  status: DoctorStatus;
27
30
  /** Stable machine id for this item, e.g. `hooks.settings-present`. */
28
31
  checkId: string;
32
+ /** Closed diagnostic enum; never contains paths, company slugs, or prose. */
33
+ reasonCode?: SyncReasonCode;
29
34
  /** What was inspected — a path, hook id, etc. Optional. */
30
35
  target?: string;
31
36
  /** Human-readable one-line explanation of the result. */
@@ -21,6 +21,21 @@ export const DOCTOR_STATUSES = [
21
21
  "UNKNOWN",
22
22
  "KNOWN-DEFECT",
23
23
  ];
24
+ /** Stable, privacy-safe diagnostics emitted by the sync health family. */
25
+ export const SYNC_REASON_CODES = [
26
+ "core-version-unavailable",
27
+ "core-update-available",
28
+ "no-local-tree",
29
+ "never-synced",
30
+ "invalid-last-sync",
31
+ "stale-threshold",
32
+ "manifest-unavailable",
33
+ "manifest-state-unavailable",
34
+ "no-manifest-scopes",
35
+ "never-uploaded",
36
+ "invalid-upload-time",
37
+ "unresolved-company-uid",
38
+ ];
24
39
  /** Runtime type guard: true only for a member of the fixed vocabulary. */
25
40
  export function isDoctorStatus(value) {
26
41
  return (typeof value === "string" &&
@@ -22,6 +22,6 @@ export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
22
22
  export { scopeBundleToAgentIdentity, } from "./credentials.js";
23
23
  export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
24
24
  export type { DaemonHandle, DaemonRunDeps } from "./run.js";
25
- export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
25
+ export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
26
26
  export type { DiscoveredTranscript, DiscoverScanMeta, DiscoverTranscriptsResult, TranscriptDiscoverCache, TranscriptFs, TranscriptKind, TranscriptWatchDeps, TranscriptWatchTickResult, } from "./transcript-watch.js";
27
27
  //# sourceMappingURL=index.d.ts.map
@@ -11,5 +11,5 @@ export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonCo
11
11
  export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatCliIdentityLines, formatCliMismatchLine, formatDaemonDoctor, listHqBinariesOnPath, } from "./doctor.js";
12
12
  export { scopeBundleToAgentIdentity, } from "./credentials.js";
13
13
  export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
14
- export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
14
+ export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
15
15
  //# sourceMappingURL=index.js.map
@@ -62,6 +62,13 @@ export declare const TRANSCRIPT_START_RETRY_BASE_MS = 30000;
62
62
  export declare const TRANSCRIPT_START_RETRY_MAX_MS = 120000;
63
63
  /** Ignore cold transcripts whose mtime is older than this on first sight. */
64
64
  export declare const TRANSCRIPT_FRESH_MS: number;
65
+ /**
66
+ * Hook-covered sessions skipped as "stale" on first sight never got a quiet
67
+ * session_end (live: skippedHook=411, ended=0). Adopt them for this long after
68
+ * last activity so a 10–120 min quiet window still reports after a daemon
69
+ * restart or a missed track. Older than this stays silent (the 6h reaper).
70
+ */
71
+ export declare const TRANSCRIPT_HOOK_QUIET_ADOPT_MS: number;
65
72
  export declare const TRANSCRIPT_MARKER: "from transcript";
66
73
  export declare const TRANSCRIPT_ADAPTER_VERSION: string;
67
74
  /** Network `source` stamped on every synthesized event (AMEND-001). */
@@ -67,6 +67,13 @@ export const TRANSCRIPT_START_RETRY_BASE_MS = 30_000;
67
67
  export const TRANSCRIPT_START_RETRY_MAX_MS = 120_000;
68
68
  /** Ignore cold transcripts whose mtime is older than this on first sight. */
69
69
  export const TRANSCRIPT_FRESH_MS = TRANSCRIPT_SESSION_END_MS;
70
+ /**
71
+ * Hook-covered sessions skipped as "stale" on first sight never got a quiet
72
+ * session_end (live: skippedHook=411, ended=0). Adopt them for this long after
73
+ * last activity so a 10–120 min quiet window still reports after a daemon
74
+ * restart or a missed track. Older than this stays silent (the 6h reaper).
75
+ */
76
+ export const TRANSCRIPT_HOOK_QUIET_ADOPT_MS = 2 * 60 * 60 * 1000;
70
77
  export const TRANSCRIPT_MARKER = "from transcript";
71
78
  export const TRANSCRIPT_ADAPTER_VERSION = CLI_VERSION;
72
79
  /** Network `source` stamped on every synthesized event (AMEND-001). */
@@ -521,13 +528,24 @@ export function noteHookSessionsFromSpoolFile(spoolPath, lastHookEventAt, atMs,
521
528
  if (!line.trim())
522
529
  continue;
523
530
  try {
524
- const ev = JSON.parse(line);
531
+ const raw = JSON.parse(line);
532
+ const ev = raw.event && typeof raw.event === "object" ? raw.event : raw;
525
533
  if (typeof ev.sessionId !== "string" || !ev.sessionId)
526
534
  continue;
527
535
  // Synthesized transcript events carry source=transcript — not hooks.
528
536
  if (ev.source === TRANSCRIPT_EVENT_SOURCE)
529
537
  continue;
530
- lastHookEventAt.set(ev.sessionId, atMs);
538
+ let eventAt = atMs;
539
+ const atRaw = typeof ev.at === "string" ? ev.at : undefined;
540
+ if (atRaw) {
541
+ const parsed = Date.parse(atRaw);
542
+ if (Number.isFinite(parsed))
543
+ eventAt = parsed;
544
+ }
545
+ const prev = lastHookEventAt.get(ev.sessionId);
546
+ if (prev === undefined || eventAt > prev) {
547
+ lastHookEventAt.set(ev.sessionId, eventAt);
548
+ }
531
549
  noted.push(ev.sessionId);
532
550
  }
533
551
  catch {
@@ -739,7 +757,10 @@ export class TranscriptWatcher {
739
757
  const lastActivity = Math.max(d.mtimeMs, hookAt ?? 0);
740
758
  let track = this.tracked.get(d.path);
741
759
  if (!track) {
742
- if (nowMs - lastActivity > TRANSCRIPT_FRESH_MS)
760
+ // Cold transcripts stay silent. Hook-covered sessions that already sat
761
+ // quiet past TRANSCRIPT_SESSION_END_MS still need one session_end — the
762
+ // live skip path returned here and never tracked them (US-051).
763
+ if (nowMs - lastActivity > TRANSCRIPT_HOOK_QUIET_ADOPT_MS)
743
764
  return;
744
765
  track = {
745
766
  path: d.path,
@@ -857,6 +878,7 @@ export class TranscriptWatcher {
857
878
  root: this.deps.workMeshRoot,
858
879
  env: this.deps.env,
859
880
  now: () => new Date(nowMs),
881
+ ...(kind === "session_end" ? { transcriptPath: track.path } : {}),
860
882
  };
861
883
  this.enqueueFn(opts);
862
884
  }
@@ -74,6 +74,9 @@ export interface MeshEmitEvent {
74
74
  status?: "queued" | "in_progress" | "review" | "done";
75
75
  reason?: string;
76
76
  summary?: string;
77
+ repo?: string;
78
+ gitBranch?: string;
79
+ prUrls?: string[];
77
80
  evidence?: EmitEvidence;
78
81
  }
79
82
  export interface BuildMeshEmitEventInput {
@@ -90,6 +93,9 @@ export interface BuildMeshEmitEventInput {
90
93
  status?: "queued" | "in_progress" | "review" | "done";
91
94
  reason?: string;
92
95
  summary?: string;
96
+ repo?: string;
97
+ gitBranch?: string;
98
+ prUrls?: string[];
93
99
  evidence?: EmitEvidence;
94
100
  }
95
101
  /**
@@ -192,6 +192,14 @@ export function buildMeshEmitEvent(input) {
192
192
  const summary = keep(input.summary);
193
193
  if (summary)
194
194
  event.summary = summary;
195
+ const repo = keep(input.repo);
196
+ if (repo)
197
+ event.repo = repo;
198
+ const gitBranch = keep(input.gitBranch);
199
+ if (gitBranch)
200
+ event.gitBranch = gitBranch;
201
+ if (input.prUrls && input.prUrls.length > 0)
202
+ event.prUrls = input.prUrls;
195
203
  if (input.evidence && Object.keys(input.evidence).length > 0) {
196
204
  event.evidence = input.evidence;
197
205
  }