@indigoai-us/hq-cli 5.345.147 → 5.345.149

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 (168) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +341 -11
  5. package/dist/command-catalog.generated.js +421 -11
  6. package/dist/command-help.js +2 -1
  7. package/dist/commands/access.d.ts +45 -2
  8. package/dist/commands/access.js +537 -223
  9. package/dist/commands/agent-inbox.js +2 -1
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/cloud.js +31 -1
  16. package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
  17. package/dist/commands/core-refresh-vault-access-native.js +356 -28
  18. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  19. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  20. package/dist/commands/core.js +9 -0
  21. package/dist/commands/daemon.d.ts +7 -0
  22. package/dist/commands/daemon.js +37 -3
  23. package/dist/commands/dm.js +18 -3
  24. package/dist/commands/doctor.d.ts +11 -1
  25. package/dist/commands/doctor.js +56 -1
  26. package/dist/commands/install-global.d.ts +8 -8
  27. package/dist/commands/install-global.js +139 -61
  28. package/dist/commands/lanes-shared.d.ts +3 -1
  29. package/dist/commands/lanes.d.ts +4 -4
  30. package/dist/commands/lanes.js +380 -25
  31. package/dist/commands/library-add.d.ts +31 -1
  32. package/dist/commands/library-add.js +69 -2
  33. package/dist/commands/library.js +37 -3
  34. package/dist/commands/pack-install.d.ts +2 -2
  35. package/dist/commands/pack-install.js +133 -35
  36. package/dist/commands/reindex.js +19 -0
  37. package/dist/commands/sync-manifest.d.ts +2 -9
  38. package/dist/commands/sync-manifest.js +4 -15
  39. package/dist/hook-engine-path.d.ts +4 -0
  40. package/dist/hook-engine-path.js +43 -0
  41. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  42. package/dist/hq-cloud-sync-runner/package.json +1 -1
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +76 -5
  45. package/dist/lazy-commands.js +2 -0
  46. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  47. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  48. package/dist/lib/agent-kit/inbox-state.js +11 -2
  49. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  50. package/dist/lib/agent-kit/run/inbox.js +36 -7
  51. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  52. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  53. package/dist/lib/billing/usage-cost.d.ts +31 -1
  54. package/dist/lib/billing/usage-cost.js +46 -0
  55. package/dist/lib/company-skill-wrapper.js +6 -4
  56. package/dist/lib/daemon/install.d.ts +47 -1
  57. package/dist/lib/daemon/install.js +181 -15
  58. package/dist/lib/daemon/lock.d.ts +32 -0
  59. package/dist/lib/daemon/lock.js +55 -2
  60. package/dist/lib/db/local.d.ts +2 -2
  61. package/dist/lib/db/local.js +2 -2
  62. package/dist/lib/db/sql.js +2 -2
  63. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  64. package/dist/lib/db/sqlite-binding.js +168 -0
  65. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  66. package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
  67. package/dist/lib/doctor/checks/anywhere.js +77 -11
  68. package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
  69. package/dist/lib/doctor/checks/cli-path.js +46 -0
  70. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  71. package/dist/lib/doctor/checks/hook-engine.js +17 -0
  72. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  73. package/dist/lib/doctor/checks/sync-health.js +38 -2
  74. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  75. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  76. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  77. package/dist/lib/doctor/fix/apply.js +176 -10
  78. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  79. package/dist/lib/doctor/fix/json-output.js +6 -0
  80. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  81. package/dist/lib/doctor/fix/remediation.js +23 -1
  82. package/dist/lib/doctor/registry.js +10 -0
  83. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  84. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  85. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  86. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  87. package/dist/lib/flag-registry.js +9 -1
  88. package/dist/lib/flag-snapshot-file.js +8 -3
  89. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  90. package/dist/lib/hook-engine/reconcile.js +134 -0
  91. package/dist/lib/hook-engine/registration.d.ts +17 -0
  92. package/dist/lib/hook-engine/registration.js +50 -0
  93. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  94. package/dist/lib/hook-engine/resolve.js +134 -0
  95. package/dist/lib/install/claude-targets.d.ts +2 -1
  96. package/dist/lib/install/claude-targets.js +8 -3
  97. package/dist/lib/install/codex-targets.d.ts +3 -0
  98. package/dist/lib/install/codex-targets.js +15 -4
  99. package/dist/lib/install/hook-target.d.ts +9 -0
  100. package/dist/lib/install/hook-target.js +39 -0
  101. package/dist/lib/lanes/admission-host.d.ts +1 -0
  102. package/dist/lib/lanes/admission-host.js +8 -1
  103. package/dist/lib/lanes/admission.d.ts +4 -0
  104. package/dist/lib/lanes/admission.js +6 -2
  105. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  107. package/dist/lib/lanes/index.d.ts +2 -2
  108. package/dist/lib/lanes/index.js +1 -1
  109. package/dist/lib/lanes/keep-alive.js +4 -1
  110. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  111. package/dist/lib/lanes/local-host-identity.js +314 -0
  112. package/dist/lib/lanes/reaper.d.ts +1 -1
  113. package/dist/lib/lanes/reaper.js +7 -0
  114. package/dist/lib/lanes/reconcile.d.ts +4 -0
  115. package/dist/lib/lanes/reconcile.js +16 -8
  116. package/dist/lib/lanes/spawn.d.ts +20 -0
  117. package/dist/lib/lanes/spawn.js +48 -2
  118. package/dist/lib/lanes/status-fields.js +6 -1
  119. package/dist/lib/lanes/store.js +8 -1
  120. package/dist/lib/lanes/throttle.d.ts +49 -6
  121. package/dist/lib/lanes/throttle.js +154 -17
  122. package/dist/lib/lanes/types.d.ts +22 -1
  123. package/dist/lib/lanes/types.js +27 -3
  124. package/dist/lib/lanes/watcher.js +27 -2
  125. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  126. package/dist/lib/lanes/worker-continuity.js +18 -11
  127. package/dist/lib/manifest-disabled.d.ts +13 -0
  128. package/dist/lib/manifest-disabled.js +19 -0
  129. package/dist/lib/service-manager/launchd.d.ts +1 -0
  130. package/dist/lib/service-manager/launchd.js +1 -1
  131. package/dist/lib/windows/console-encoding.d.ts +24 -0
  132. package/dist/lib/windows/console-encoding.js +83 -0
  133. package/dist/lib/windows/install-followup.d.ts +35 -0
  134. package/dist/lib/windows/install-followup.js +142 -0
  135. package/dist/lib/windows/user-path.d.ts +125 -0
  136. package/dist/lib/windows/user-path.js +275 -0
  137. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  138. package/dist/lib/work-context/auto-bind.js +22 -3
  139. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  140. package/dist/lib/work-context/lane-bind.js +101 -0
  141. package/dist/lib/work-context/organize.js +1 -0
  142. package/dist/lib/work-context/prd-sync.js +7 -0
  143. package/dist/lib/work-context/project.d.ts +36 -10
  144. package/dist/lib/work-context/project.js +130 -41
  145. package/dist/lib/work-context/reconcile.d.ts +6 -0
  146. package/dist/lib/work-context/reconcile.js +25 -5
  147. package/dist/lib/work-context/state.d.ts +6 -0
  148. package/dist/lib/work-context/state.js +29 -1
  149. package/dist/main.js +2 -1
  150. package/dist/unknown-command.d.ts +18 -0
  151. package/dist/unknown-command.js +88 -0
  152. package/dist/utils/cognito-session.d.ts +8 -0
  153. package/dist/utils/cognito-session.js +14 -2
  154. package/dist/utils/copy-tree.d.ts +61 -0
  155. package/dist/utils/copy-tree.js +249 -0
  156. package/dist/utils/environmental-error.js +7 -3
  157. package/dist/utils/hook-trust.d.ts +9 -0
  158. package/dist/utils/hook-trust.js +30 -2
  159. package/dist/utils/npm-invocation.d.ts +27 -0
  160. package/dist/utils/npm-invocation.js +54 -0
  161. package/dist/utils/qmd-native-binding-error.js +3 -1
  162. package/dist/utils/resolve-executable.d.ts +2 -0
  163. package/dist/utils/resolve-executable.js +6 -2
  164. package/dist/utils/safe-link.d.ts +50 -0
  165. package/dist/utils/safe-link.js +300 -0
  166. package/package.json +17 -5
  167. package/scripts/hook-engine-manifest.mjs +22 -0
  168. package/scripts/install-hook-engine.mjs +10 -0
@@ -33,6 +33,7 @@ import * as path from "node:path";
33
33
  import * as hqCloud from "@indigoai-us/hq-cloud";
34
34
  import { loadManifestExports, MANIFEST_UNAVAILABLE_REASON, } from "../../hq-cloud-manifest.js";
35
35
  import { readCompaniesManifestMap } from "../../work-context/repo-remote.js";
36
+ import { MANIFEST_DISABLED_ENV, manifestUploadsDisabled, } from "../../manifest-disabled.js";
36
37
  import { CLI_VERSION } from "../../../cli-version.js";
37
38
  import { readSyncVersion } from "../../../utils/feedback-versions.js";
38
39
  import { readHqVersion } from "../../../utils/pack-contributions.js";
@@ -58,6 +59,7 @@ function withDefaults(deps, hqRoot) {
58
59
  unresolvedScopes: deps.unresolvedScopes ?? ((journals) => defaultUnresolvedScopes(hqRoot, journals)),
59
60
  localCompanyExists: deps.localCompanyExists ??
60
61
  ((slug) => defaultLocalCompanyExists(hqRoot, slug)),
62
+ manifestUploadsDisabled: deps.manifestUploadsDisabled ?? (() => manifestUploadsDisabled()),
61
63
  };
62
64
  }
63
65
  /**
@@ -216,6 +218,22 @@ function boundInvalidScopeToken(token) {
216
218
  return token;
217
219
  return token.slice(0, maxSuffix);
218
220
  }
221
+ /**
222
+ * The `hq sync manifest --scope` value that re-uploads this status's scope:
223
+ * `personal`, the company slug, or undefined when the scope cannot be named
224
+ * safely (a company row whose slug was not resolved locally).
225
+ */
226
+ function manifestUploadScope(entry) {
227
+ if (entry.scopeKey === "personal")
228
+ return "personal";
229
+ if (entry.scopeKey.startsWith("company:") &&
230
+ entry.companySlug &&
231
+ isSafeCompanySlug(entry.companySlug) &&
232
+ !NON_COMPANY_JOURNAL_SLUGS.has(entry.companySlug)) {
233
+ return entry.companySlug;
234
+ }
235
+ return undefined;
236
+ }
219
237
  /**
220
238
  * Doctor JSON `checkId` for a manifest scope. hq-cloud keys company rows
221
239
  * `company:cmp_…`; the colon is replaced so SessionStart health can report
@@ -629,12 +647,30 @@ function manifestResults(deps, journals) {
629
647
  : "next pass will send a full manifest";
630
648
  if (ageMs > STALE_MANIFEST_THRESHOLD_MS) {
631
649
  const days = Math.floor(ageMs / 86_400_000);
650
+ // `target` is the `hq sync manifest --scope` value `--fix` re-uploads.
651
+ // Without it the row stays manual-only. Personal must be explicit: a
652
+ // bare `hq sync manifest` uploads the active company instead.
653
+ const scope = manifestUploadScope(entry);
654
+ const command = `hq sync manifest${scope ? ` --scope ${scope}` : ""}`;
655
+ const message = `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`;
656
+ // With the kill switch set the upload exits 0 and sends nothing, so the
657
+ // row carries no `--fix` target and names the switch instead.
658
+ if (deps.manifestUploadsDisabled()) {
659
+ return {
660
+ status: "WARN",
661
+ checkId,
662
+ reasonCode: "stale-threshold",
663
+ message: `${message} Manifest uploads are turned off on this machine by ${MANIFEST_DISABLED_ENV}.`,
664
+ remediation: `Unset ${MANIFEST_DISABLED_ENV}, then run \`${command}\`.`,
665
+ };
666
+ }
632
667
  return {
633
668
  status: "WARN",
634
669
  checkId,
635
670
  reasonCode: "stale-threshold",
636
- message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
637
- remediation: `Run \`hq sync manifest${entry.companySlug ? ` --scope ${entry.companySlug}` : ""}\` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.`,
671
+ ...(scope ? { target: scope } : {}),
672
+ message,
673
+ remediation: `Run \`${command}\` and check ${MANIFEST_DISABLED_ENV} and the sync runner.`,
638
674
  };
639
675
  }
640
676
  return {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Doctor family: abandoned throwaway `$HOME` folders.
3
+ *
4
+ * Read-only and cheap. It runs the sweeper in shallow mode: no size walk and
5
+ * no process probe, so plain `hq doctor` stays fast. It reports how many idle
6
+ * temp homes it found; `hq doctor --sweep-temp-homes --dry-run` measures them,
7
+ * and `hq doctor --sweep-temp-homes` removes them.
8
+ */
9
+ import { type TempHomeSweepOptions } from "../temp-home-sweeper.js";
10
+ import type { CheckFamily, CheckResult } from "../types.js";
11
+ export declare const TEMP_HOMES_FAMILY_ID = "temp-homes";
12
+ export declare const TEMP_HOMES_FAMILY_TITLE = "Temp homes";
13
+ type TempHomesCheckOptions = Omit<TempHomeSweepOptions, "dryRun" | "shallow">;
14
+ /** Build the family; tests pass fake roots and clock through `options`. */
15
+ export declare function createTempHomesFamily(options?: TempHomesCheckOptions): CheckFamily;
16
+ export declare const tempHomesFamily: CheckFamily;
17
+ export declare function checkTempHomes(options?: TempHomesCheckOptions): CheckResult[];
18
+ export {};
19
+ //# sourceMappingURL=temp-homes.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Doctor family: abandoned throwaway `$HOME` folders.
3
+ *
4
+ * Read-only and cheap. It runs the sweeper in shallow mode: no size walk and
5
+ * no process probe, so plain `hq doctor` stays fast. It reports how many idle
6
+ * temp homes it found; `hq doctor --sweep-temp-homes --dry-run` measures them,
7
+ * and `hq doctor --sweep-temp-homes` removes them.
8
+ */
9
+ import { sweepTempHomes, } from "../temp-home-sweeper.js";
10
+ export const TEMP_HOMES_FAMILY_ID = "temp-homes";
11
+ export const TEMP_HOMES_FAMILY_TITLE = "Temp homes";
12
+ /** Build the family; tests pass fake roots and clock through `options`. */
13
+ export function createTempHomesFamily(options = {}) {
14
+ return {
15
+ id: TEMP_HOMES_FAMILY_ID,
16
+ title: TEMP_HOMES_FAMILY_TITLE,
17
+ run: () => Promise.resolve(checkTempHomes(options)),
18
+ };
19
+ }
20
+ export const tempHomesFamily = createTempHomesFamily();
21
+ export function checkTempHomes(options = {}) {
22
+ let result;
23
+ try {
24
+ result = sweepTempHomes({ ...options, shallow: true });
25
+ }
26
+ catch (error) {
27
+ return [{
28
+ status: "UNKNOWN",
29
+ checkId: "temp-homes.scan-error",
30
+ message: `Could not scan temp folders: ${error instanceof Error ? error.message : String(error)}`,
31
+ }];
32
+ }
33
+ const stale = result.entries.filter((entry) => entry.action === "would-remove");
34
+ if (stale.length === 0) {
35
+ return [{
36
+ status: "PASS",
37
+ checkId: "temp-homes.none",
38
+ message: "No idle temp homes to sweep.",
39
+ details: { roots: result.roots.length },
40
+ }];
41
+ }
42
+ return [{
43
+ status: "WARN",
44
+ checkId: "temp-homes.stale",
45
+ message: `${stale.length} idle temp home${stale.length === 1 ? "" : "s"} left in the temp folder. ` +
46
+ "Run hq doctor --sweep-temp-homes --dry-run to see the disk space they use.",
47
+ details: {
48
+ count: stale.length,
49
+ paths: stale.map((entry) => entry.path),
50
+ },
51
+ remediation: "hq doctor --sweep-temp-homes --dry-run, then hq doctor --sweep-temp-homes",
52
+ }];
53
+ }
54
+ //# sourceMappingURL=temp-homes.js.map
@@ -34,6 +34,7 @@
34
34
  * (the command resolves it once), keeping this module decoupled from the CLI and
35
35
  * trivially testable against a fake tree.
36
36
  */
37
+ import { type SpawnSyncReturns } from "node:child_process";
37
38
  import type { CheckResult, DoctorStatus } from "../types.js";
38
39
  import { type GateProfile } from "../hook-gate-profiles.js";
39
40
  import { type FixClass } from "./remediation.js";
@@ -78,6 +79,12 @@ export interface ApplyFixesOptions {
78
79
  * Tests inject a stub so `--fix` never hits the network.
79
80
  */
80
81
  refreshSync?: (target: RefreshSyncTarget) => Promise<RefreshSyncResult>;
82
+ /**
83
+ * Manifest upload used by the `refresh-manifest` class. Default shells out
84
+ * to `hq sync manifest --scope <slug|personal>`. Tests inject a stub so
85
+ * `--fix` never hits the network.
86
+ */
87
+ refreshManifest?: (target: RefreshManifestTarget) => Promise<RefreshManifestResult>;
81
88
  /**
82
89
  * File-only rewrite used by the `bot-node-path` class. Default rewrites
83
90
  * ~/Library/LaunchAgents/ai.getindigo.hq-bot.<name>.plist and never calls
@@ -117,6 +124,42 @@ export interface RefreshSyncResult {
117
124
  }
118
125
  /** Per-company bound for the default `hq sync pull` repair. */
119
126
  export declare const REFRESH_SYNC_TIMEOUT_MS = 90000;
127
+ /** One manifest scope the `refresh-manifest` class will upload. */
128
+ export interface RefreshManifestTarget {
129
+ /** `personal` or a company slug, as `hq sync manifest --scope` takes it. */
130
+ scope: string;
131
+ /** HQ root the manifest is built from. */
132
+ hqRoot: string;
133
+ }
134
+ /** Outcome of one {@link ApplyFixesOptions.refreshManifest} call. */
135
+ export interface RefreshManifestResult {
136
+ /** Whether the upload command exited 0. The re-check is the real verdict. */
137
+ ok: boolean;
138
+ /** One-line summary. */
139
+ message: string;
140
+ /**
141
+ * Why the upload failed, in plain words with no paths or slugs, for the
142
+ * unresolved-check next step. Absent when the upload exited 0.
143
+ */
144
+ reason?: string;
145
+ }
146
+ /**
147
+ * Why a sync check is still FAIL or WARN after `--fix`:
148
+ *
149
+ * - `manual-only` — no safe automatic repair exists.
150
+ * - `repair-did-not-clear` — `--fix` ran its repair and the re-check still fails.
151
+ * - `not-attempted` — a repair exists but this run did not get to it
152
+ * (per-run cap, declined, dirty tree, pass limit).
153
+ */
154
+ export type UnresolvedSyncReason = "manual-only" | "repair-did-not-clear" | "not-attempted";
155
+ /** A sync check `--fix` left FAIL or WARN, with the next step in plain words. */
156
+ export interface UnresolvedSyncCheck {
157
+ checkId: string;
158
+ status: DoctorStatus;
159
+ reason: UnresolvedSyncReason;
160
+ /** What the user should do next. Never the finding message (it can hold paths). */
161
+ nextStep: string;
162
+ }
120
163
  /** One applied (or attempted) repair, with its post-fix re-check status. */
121
164
  export interface AppliedFix {
122
165
  /** One-based repair pass in this `hq doctor --fix` invocation. */
@@ -145,7 +188,11 @@ export interface AppliedFixBackup {
145
188
  }
146
189
  /** A newly unlocked safe repair can be picked up for at most this many passes. */
147
190
  export declare const MAX_FIX_PASSES = 3;
148
- /** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
191
+ /**
192
+ * Two 90s network repairs leave 120s for startup and check overhead under the
193
+ * hook's 300s ceiling. Journal pulls and manifest uploads share this budget;
194
+ * pulls go first and uploads take any slot left.
195
+ */
149
196
  export declare const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
150
197
  /** The outcome of an {@link applyFixes} run. */
151
198
  export interface ApplyFixesResult {
@@ -165,6 +212,27 @@ export interface ApplyFixesResult {
165
212
  backups: AppliedFixBackup[];
166
213
  /** The applied repairs with their post-fix status. */
167
214
  applied: AppliedFix[];
215
+ /** Sync checks still FAIL or WARN when the run ended, with the next step. */
216
+ unresolved: UnresolvedSyncCheck[];
217
+ }
218
+ /** A concrete, planned change: its preview, the file it touches, and its apply. */
219
+ interface FixPlan {
220
+ checkId: string;
221
+ fixClass: FixClass;
222
+ target: string;
223
+ /** HQ-root-relative path of the single file this plan writes. Empty when none. */
224
+ relpath: string;
225
+ /** Diff-style preview lines. */
226
+ preview: string;
227
+ /** One-line summary for the post-fix report. */
228
+ summary: string;
229
+ /** Apply the change. Reads the current on-disk state each time. */
230
+ apply: () => void | Promise<void>;
231
+ /** The status of `results` this target maps to after the fix. */
232
+ postStatus: (results: CheckResult[]) => DoctorStatus | null;
233
+ refreshSyncOutcome?: () => RefreshSyncOutcome | undefined;
234
+ /** Why the last apply's command failed (path-free), when it reported one. */
235
+ failureReason?: () => string | undefined;
168
236
  }
169
237
  /**
170
238
  * Apply every auto-fixable finding, honouring the dirty-tree refusal, the
@@ -173,6 +241,13 @@ export interface ApplyFixesResult {
173
241
  * assert on it.
174
242
  */
175
243
  export declare function applyFixes(options: ApplyFixesOptions): Promise<ApplyFixesResult>;
244
+ /**
245
+ * The sync checks still FAIL or WARN in `findings`, each with why `--fix` left
246
+ * it and what to do next. Reported on every run, independent of the
247
+ * manual-only flag: the SessionStart health hook files a bug for any sync row
248
+ * left after `--fix`, so the user must see the same rows and a next step.
249
+ */
250
+ export declare function unresolvedSyncChecks(findings: readonly CheckResult[], attempted: readonly Pick<FixPlan, "checkId" | "fixClass" | "summary" | "failureReason">[]): UnresolvedSyncCheck[];
176
251
  /**
177
252
  * Default `refresh-sync` repair: a bounded, pull-only, keep-conflicts
178
253
  * `hq sync pull --company <slug>` (or `--personal`) against the tree
@@ -180,6 +255,19 @@ export declare function applyFixes(options: ApplyFixesOptions): Promise<ApplyFix
180
255
  * non-zero exit is reported rather than thrown so later plans still run.
181
256
  */
182
257
  export declare function defaultRefreshSync(target: RefreshSyncTarget): Promise<RefreshSyncResult>;
258
+ /**
259
+ * Default `refresh-manifest` repair: a bounded `hq sync manifest --scope <s>`
260
+ * against the tree `--fix` is repairing. Exit 0 also covers throttled,
261
+ * disabled, and skipped passes, so the post-fix re-check decides whether the
262
+ * row cleared. A timeout or non-zero exit is reported, not thrown.
263
+ */
264
+ export declare function defaultRefreshManifest(target: RefreshManifestTarget): Promise<RefreshManifestResult>;
265
+ /**
266
+ * Map one `hq sync manifest` spawn result to a {@link RefreshManifestResult}.
267
+ * A spawn killed by the timeout has a null status, so it is reported as a
268
+ * timeout rather than as an exit code.
269
+ */
270
+ export declare function manifestUploadResult(scope: string, result: Pick<SpawnSyncReturns<string>, "error" | "status" | "signal">): RefreshManifestResult;
183
271
  /**
184
272
  * The uncommitted changes under {@link HOOK_CONFIG_DIRS}, one porcelain line
185
273
  * each. Returns [] when the tree is clean OR when `hqRoot` is not a git repo
@@ -218,4 +306,5 @@ export declare function addHookIdToGateProfiles(source: string, hookId: string,
218
306
  * non-object `hooks` value — it is replaced rather than trusted.
219
307
  */
220
308
  export declare function registerHookInSettings(settings: unknown, event: string, command: string): Record<string, unknown>;
309
+ export {};
221
310
  //# sourceMappingURL=apply.d.ts.map
@@ -56,7 +56,11 @@ export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
56
56
  export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
57
57
  /** A newly unlocked safe repair can be picked up for at most this many passes. */
58
58
  export const MAX_FIX_PASSES = 3;
59
- /** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
59
+ /**
60
+ * Two 90s network repairs leave 120s for startup and check overhead under the
61
+ * hook's 300s ceiling. Journal pulls and manifest uploads share this budget;
62
+ * pulls go first and uploads take any slot left.
63
+ */
60
64
  export const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
61
65
  function rotateRefreshSyncPlans(plans, afterSlug) {
62
66
  const ordered = [...plans].sort((a, b) => a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
@@ -85,6 +89,7 @@ export async function applyFixes(options) {
85
89
  restoreCommand: null,
86
90
  backups: [],
87
91
  applied: [],
92
+ unresolved: [],
88
93
  ...over,
89
94
  });
90
95
  // 1. Dirty-tree refusal (AC8). Never write over uncommitted security-layer
@@ -99,7 +104,9 @@ export async function applyFixes(options) {
99
104
  // Journal refreshes never write hook files. Allow them by class, not
100
105
  // only by empty relpath, so a personal-vault pull and a company pull
101
106
  // still run when SessionStart `--fix --yes` cannot pass `--force`.
102
- const allowed = plans.filter((plan) => plan.fixClass === "refresh-sync" || plan.relpath.length === 0);
107
+ const allowed = plans.filter((plan) => plan.fixClass === "refresh-sync" ||
108
+ plan.fixClass === "refresh-manifest" ||
109
+ plan.relpath.length === 0);
103
110
  if (allowed.length === plans.length)
104
111
  return plans;
105
112
  if (allowed.length === 0)
@@ -109,12 +116,14 @@ export async function applyFixes(options) {
109
116
  `re-run with --force to repair hook files.\n`);
110
117
  return allowed;
111
118
  };
112
- const refuseDirtyTree = () => {
119
+ const refuseDirtyTree = (results) => {
113
120
  writeErr(`hq doctor --fix: refusing to run — uncommitted changes under ` +
114
121
  `${HOOK_CONFIG_DIRS.join(", ")}:\n` +
115
122
  dirty.map((line) => ` ${line}`).join("\n") +
116
123
  `\nCommit or stash them, or re-run with --force.\n`);
117
- return empty({ exitCode: 1, refused: "dirty-tree" });
124
+ const unresolved = unresolvedSyncChecks(results, []);
125
+ writeUnresolvedSyncChecks(unresolved, write);
126
+ return empty({ exitCode: 1, refused: "dirty-tree", unresolved });
118
127
  };
119
128
  // Resolve the customer-visible manual-only output gate once per fix run.
120
129
  // Fail open to the pre-change output if the identity is absent or the
@@ -129,6 +138,7 @@ export async function applyFixes(options) {
129
138
  }
130
139
  const collect = options.runChecks ?? defaultRunChecks;
131
140
  const refreshSync = options.refreshSync ?? defaultRefreshSync;
141
+ const refreshManifest = options.refreshManifest ?? defaultRefreshManifest;
132
142
  const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
133
143
  const repairBotHqBinary = options.repairBotHqBinary ?? defaultRepairBotHqBinary;
134
144
  // 2. Collect findings, then plan each newly unlocked allowlisted repair.
@@ -137,6 +147,7 @@ export async function applyFixes(options) {
137
147
  let findings = await collect(hqRoot);
138
148
  let lastAttemptedRefreshSyncSlug = readRefreshSyncProgress(hqRoot)?.lastAttemptedSlug ?? null;
139
149
  let refreshSyncAppliedThisRun = 0;
150
+ let refreshManifestAppliedThisRun = 0;
140
151
  const appliedKeys = new Set();
141
152
  const planned = [];
142
153
  const backups = [];
@@ -155,7 +166,7 @@ export async function applyFixes(options) {
155
166
  const rem = deriveRemediation(result);
156
167
  if (!rem?.autoFixable)
157
168
  continue;
158
- const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary);
169
+ const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath, repairBotHqBinary, refreshManifest);
159
170
  if (!plan)
160
171
  continue;
161
172
  const key = JSON.stringify([plan.checkId, plan.target]);
@@ -171,16 +182,25 @@ export async function applyFixes(options) {
171
182
  seenRefreshTargets.add(plan.target);
172
183
  return true;
173
184
  }).slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN - refreshSyncAppliedThisRun));
185
+ // Manifest uploads take whatever network budget the pulls left.
186
+ const manifestPlans = plans
187
+ .filter((plan) => plan.fixClass === "refresh-manifest")
188
+ .sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
189
+ .slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN -
190
+ refreshSyncAppliedThisRun -
191
+ refreshManifestAppliedThisRun -
192
+ refreshPlans.length));
174
193
  return [
175
- ...plans.filter((plan) => plan.fixClass !== "refresh-sync"),
194
+ ...plans.filter((plan) => plan.fixClass !== "refresh-sync" && plan.fixClass !== "refresh-manifest"),
176
195
  ...refreshPlans,
196
+ ...manifestPlans,
177
197
  ];
178
198
  };
179
199
  for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
180
200
  const plannedThisPass = refuseDirtyHookWrites(buildPlans(findings));
181
201
  if (plannedThisPass === "refuse") {
182
202
  if (planned.length === 0)
183
- return refuseDirtyTree();
203
+ return refuseDirtyTree(findings);
184
204
  write(`Pass ${pass}/${MAX_FIX_PASSES}: remaining repairs write hook files; ` +
185
205
  `stopped because the tree is dirty.\n`);
186
206
  refused = "dirty-tree";
@@ -202,7 +222,9 @@ export async function applyFixes(options) {
202
222
  write("Aborted — no changes written.\n");
203
223
  if (showManualOnlyRemediations)
204
224
  writeManualOnlyRemediations(findings, write);
205
- return empty({ refused, fixableCount });
225
+ const unresolved = unresolvedSyncChecks(findings, []);
226
+ writeUnresolvedSyncChecks(unresolved, write);
227
+ return empty({ refused, fixableCount, unresolved });
206
228
  }
207
229
  write(`Pass ${pass} declined — no further changes written.\n`);
208
230
  break;
@@ -233,6 +255,8 @@ export async function applyFixes(options) {
233
255
  lastAttemptedRefreshSyncSlug = plan.target;
234
256
  refreshSyncAppliedThisRun += 1;
235
257
  }
258
+ if (plan.fixClass === "refresh-manifest")
259
+ refreshManifestAppliedThisRun += 1;
236
260
  appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
237
261
  planned.push({ plan, pass });
238
262
  await plan.apply();
@@ -244,7 +268,9 @@ export async function applyFixes(options) {
244
268
  write("hq doctor --fix: nothing to repair — no auto-fixable findings.\n");
245
269
  if (showManualOnlyRemediations)
246
270
  writeManualOnlyRemediations(findings, write);
247
- return empty({ refused, fixableCount });
271
+ const unresolved = unresolvedSyncChecks(findings, []);
272
+ writeUnresolvedSyncChecks(unresolved, write);
273
+ return empty({ refused, fixableCount, unresolved });
248
274
  }
249
275
  // Show each repair's final post-fix status, including work from earlier passes.
250
276
  const applied = planned.map(({ plan, pass }) => {
@@ -283,6 +309,8 @@ export async function applyFixes(options) {
283
309
  }
284
310
  if (showManualOnlyRemediations)
285
311
  writeManualOnlyRemediations(findings, write);
312
+ const unresolved = unresolvedSyncChecks(findings, planned.map(({ plan }) => plan));
313
+ writeUnresolvedSyncChecks(unresolved, write);
286
314
  return {
287
315
  exitCode: 0,
288
316
  wrote: true,
@@ -292,11 +320,81 @@ export async function applyFixes(options) {
292
320
  restoreCommand,
293
321
  backups,
294
322
  applied,
323
+ unresolved,
295
324
  };
296
325
  }
326
+ /**
327
+ * The sync checks still FAIL or WARN in `findings`, each with why `--fix` left
328
+ * it and what to do next. Reported on every run, independent of the
329
+ * manual-only flag: the SessionStart health hook files a bug for any sync row
330
+ * left after `--fix`, so the user must see the same rows and a next step.
331
+ */
332
+ export function unresolvedSyncChecks(findings, attempted) {
333
+ return findings.flatMap((result) => {
334
+ if (!result.checkId.startsWith("sync."))
335
+ return [];
336
+ const remediation = deriveRemediation(result);
337
+ if (!remediation)
338
+ return [];
339
+ const tried = attempted.find((plan) => plan.checkId === result.checkId);
340
+ const command = remediation.command.trim();
341
+ if (tried) {
342
+ const failure = tried.failureReason?.();
343
+ const outcome = failure
344
+ ? `--fix tried to ${tried.summary}, but ${failure}, so the check still shows ${result.status}.`
345
+ : `--fix tried to ${tried.summary}, but the check still shows ${result.status}.`;
346
+ return [
347
+ {
348
+ checkId: result.checkId,
349
+ status: result.status,
350
+ reason: "repair-did-not-clear",
351
+ nextStep: command
352
+ ? `${outcome} Run \`${command}\` yourself and read its error.`
353
+ : `${outcome} Run \`hq doctor --only sync\` for details.`,
354
+ },
355
+ ];
356
+ }
357
+ if (remediation.autoFixable) {
358
+ return [
359
+ {
360
+ checkId: result.checkId,
361
+ status: result.status,
362
+ reason: "not-attempted",
363
+ nextStep: `--fix did not get to this one in this run. Run \`hq doctor --fix\` again, or run \`${command}\` now.`,
364
+ },
365
+ ];
366
+ }
367
+ // The action is the check's remediation (or a classifier override). When
368
+ // it is only the finding message, give a generic step: messages can hold
369
+ // paths, and the JSON surface carries none.
370
+ const nextStep = remediation.action && remediation.action !== result.message
371
+ ? remediation.action
372
+ : "Run `hq doctor --only sync` to see the details, then fix what it describes.";
373
+ return [
374
+ {
375
+ checkId: result.checkId,
376
+ status: result.status,
377
+ reason: "manual-only",
378
+ nextStep: `--fix cannot repair this automatically. ${nextStep}`,
379
+ },
380
+ ];
381
+ });
382
+ }
383
+ /** Print the sync checks `--fix` left unresolved, or nothing when there are none. */
384
+ function writeUnresolvedSyncChecks(unresolved, write) {
385
+ if (unresolved.length === 0)
386
+ return;
387
+ write("\nSync checks still not resolved after --fix:\n");
388
+ for (const entry of unresolved) {
389
+ write(` ${entry.status} ${entry.checkId}\n Next step: ${entry.nextStep}\n`);
390
+ }
391
+ }
297
392
  /** Print the available user action for findings that `--fix` deliberately skips. */
298
393
  function writeManualOnlyRemediations(findings, write) {
299
394
  const manual = findings.flatMap((result) => {
395
+ // Sync rows are always listed by writeUnresolvedSyncChecks.
396
+ if (result.checkId.startsWith("sync."))
397
+ return [];
300
398
  const remediation = deriveRemediation(result);
301
399
  return remediation && !remediation.autoFixable
302
400
  ? [{ result, remediation }]
@@ -403,6 +501,49 @@ export function defaultRefreshSync(target) {
403
501
  message: `pulled '${target.slug}'`,
404
502
  });
405
503
  }
504
+ /**
505
+ * Default `refresh-manifest` repair: a bounded `hq sync manifest --scope <s>`
506
+ * against the tree `--fix` is repairing. Exit 0 also covers throttled,
507
+ * disabled, and skipped passes, so the post-fix re-check decides whether the
508
+ * row cleared. A timeout or non-zero exit is reported, not thrown.
509
+ */
510
+ export function defaultRefreshManifest(target) {
511
+ const hqBin = process.argv[1];
512
+ const args = ["sync", "manifest", "--scope", target.scope, "--hq-root", target.hqRoot];
513
+ const result = spawnSync(hqBin ? process.execPath : "hq", hqBin ? [hqBin, ...args] : args, {
514
+ encoding: "utf8",
515
+ timeout: REFRESH_SYNC_TIMEOUT_MS,
516
+ cwd: target.hqRoot,
517
+ });
518
+ return Promise.resolve(manifestUploadResult(target.scope, result));
519
+ }
520
+ /**
521
+ * Map one `hq sync manifest` spawn result to a {@link RefreshManifestResult}.
522
+ * A spawn killed by the timeout has a null status, so it is reported as a
523
+ * timeout rather than as an exit code.
524
+ */
525
+ export function manifestUploadResult(scope, result) {
526
+ const errorCode = result.error?.code;
527
+ if (errorCode === "ETIMEDOUT" || (result.status === null && result.signal)) {
528
+ const reason = `the upload timed out after ${REFRESH_SYNC_TIMEOUT_MS / 1000}s`;
529
+ return { ok: false, reason, message: `manifest upload for '${scope}' timed out` };
530
+ }
531
+ if (result.error) {
532
+ return {
533
+ ok: false,
534
+ reason: `the upload could not start${errorCode ? ` (${errorCode})` : ""}`,
535
+ message: `could not upload the manifest for '${scope}': ${result.error.message}`,
536
+ };
537
+ }
538
+ if (result.status !== 0) {
539
+ return {
540
+ ok: false,
541
+ reason: `the upload exited with code ${result.status}`,
542
+ message: `manifest upload for '${scope}' exited ${result.status}`,
543
+ };
544
+ }
545
+ return { ok: true, message: `uploaded the manifest for '${scope}'` };
546
+ }
406
547
  // --- Dirty-tree probe ----------------------------------------------------------
407
548
  /**
408
549
  * The uncommitted changes under {@link HOOK_CONFIG_DIRS}, one porcelain line
@@ -422,7 +563,7 @@ export function uncommittedHookConfigChanges(hqRoot) {
422
563
  }
423
564
  // --- Fix planning --------------------------------------------------------------
424
565
  /** Build the concrete plan for one auto-fixable finding, or null if unplannable. */
425
- function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary) {
566
+ function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultRewriteBotNodePath, repairBotHqBinary = defaultRepairBotHqBinary, refreshManifest = defaultRefreshManifest) {
426
567
  switch (rem.fixClass) {
427
568
  case "bot-node-path":
428
569
  return planBotNodePath(result, rem, rewriteBotNodePath);
@@ -436,6 +577,8 @@ function planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath = defaultR
436
577
  return planRemoveStrayGateEntries(hqRoot, result, rem);
437
578
  case "refresh-sync":
438
579
  return planRefreshSync(hqRoot, result, rem, refreshSync);
580
+ case "refresh-manifest":
581
+ return planRefreshManifest(hqRoot, result, rem, refreshManifest);
439
582
  default:
440
583
  return null;
441
584
  }
@@ -555,6 +698,29 @@ function planRefreshSync(hqRoot, result, rem, refreshSync) {
555
698
  postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
556
699
  };
557
700
  }
701
+ /** Re-upload one stale sync manifest scope. Writes nothing under the HQ tree. */
702
+ function planRefreshManifest(hqRoot, result, rem, refreshManifest) {
703
+ const scope = rem.fixTarget;
704
+ if (!scope)
705
+ return null;
706
+ let failureReason;
707
+ return {
708
+ checkId: result.checkId,
709
+ fixClass: "refresh-manifest",
710
+ target: scope,
711
+ relpath: "",
712
+ preview: ` sync manifest (${scope === "personal" ? "personal vault" : `company ${scope}`})\n` +
713
+ ` ~ hq sync manifest --scope ${scope}\n` +
714
+ ` upload a fresh file manifest for the sync audit (no local files change)`,
715
+ summary: `upload a fresh sync manifest for ${scope}`,
716
+ apply: async () => {
717
+ const outcome = await refreshManifest({ scope, hqRoot });
718
+ failureReason = outcome.ok ? undefined : outcome.reason;
719
+ },
720
+ postStatus: (results) => results.find((entry) => entry.checkId === result.checkId)?.status ?? null,
721
+ failureReason: () => failureReason,
722
+ };
723
+ }
558
724
  /** Restore the execute bit on a hook script. */
559
725
  function planExecutableBit(hqRoot, result, rem) {
560
726
  const abs = rem.fixTarget;
@@ -1,4 +1,4 @@
1
- import type { AppliedFix, AppliedFixBackup, ApplyFixesResult } from "./apply.js";
1
+ import type { AppliedFix, AppliedFixBackup, ApplyFixesResult, UnresolvedSyncCheck } from "./apply.js";
2
2
  export interface DoctorFixJsonResult {
3
3
  schemaVersion: number;
4
4
  exitCode: number;
@@ -15,6 +15,8 @@ export interface DoctorFixJsonResult {
15
15
  outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
16
16
  postStatus: AppliedFix["postStatus"];
17
17
  }[];
18
+ /** Sync checks still FAIL or WARN after the run, with the next step. */
19
+ unresolved: Pick<UnresolvedSyncCheck, "checkId" | "status" | "reason" | "nextStep">[];
18
20
  }
19
21
  /** Build the intentionally narrow JSON surface for `hq doctor --fix --json`. */
20
22
  export declare function buildDoctorFixJson(input: {
@@ -22,6 +24,7 @@ export declare function buildDoctorFixJson(input: {
22
24
  refused: ApplyFixesResult["refused"] | "outside-hq-tree";
23
25
  applied: readonly AppliedFix[];
24
26
  backups?: readonly AppliedFixBackup[];
27
+ unresolved?: readonly UnresolvedSyncCheck[];
25
28
  }): DoctorFixJsonResult;
26
29
  export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
27
30
  //# sourceMappingURL=json-output.d.ts.map
@@ -25,6 +25,12 @@ export function buildDoctorFixJson(input) {
25
25
  },
26
26
  ]
27
27
  : []),
28
+ unresolved: (input.unresolved ?? []).map(({ checkId, status, reason, nextStep }) => ({
29
+ checkId,
30
+ status,
31
+ reason,
32
+ nextStep,
33
+ })),
28
34
  };
29
35
  }
30
36
  export function renderDoctorFixJson(input) {
@@ -29,6 +29,12 @@
29
29
  * keep`, no hook-file rewrite. Leftover shards
30
30
  * without a local tree stay unfixed (the check reports
31
31
  * NA).
32
+ * - `refresh-manifest` — re-run `hq sync manifest --scope <slug|personal>`
33
+ * for a manifest scope whose last upload is past the
34
+ * stale threshold. Upload-only metadata (file paths
35
+ * and hashes); it never writes the HQ tree. A company
36
+ * scope whose slug did not resolve locally stays
37
+ * manual-only.
32
38
  *
33
39
  * EVERYTHING else — content drift between platform copies, a missing script, a
34
40
  * missing Codex counterpart, an unquoted `$CLAUDE_PROJECT_DIR`, a stale
@@ -40,7 +46,7 @@
40
46
  */
41
47
  import type { CheckResult } from "../types.js";
42
48
  /** The allowlisted safe repair classes `--fix` is permitted to apply. */
43
- export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "bot-node-path" | "bot-hq-binary";
49
+ export type FixClass = "executable-bit" | "gate-profile" | "remove-stray-gate-entries" | "refresh-sync" | "refresh-manifest" | "bot-node-path" | "bot-hq-binary";
44
50
  /**
45
51
  * A finding's structured remediation. `autoFixable`, `action`, and `command`
46
52
  * are the `--json` surface (AC1); `fixTarget`/`fixClass` are the internal handle