@indigoai-us/hq-cli 5.18.0 → 5.18.1

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.
@@ -163,5 +163,42 @@ export declare function assertSingleSelector(opts: {
163
163
  personal?: boolean;
164
164
  company?: string;
165
165
  }, command: string): void;
166
+ /**
167
+ * Per-company pull resolution helper used by `hq sync pull --company <slug>`
168
+ * (US-011 fix, 2026-05-21). Mirrors the inline lookup that `runNowSingle`
169
+ * does for sync-now. Pulled out so the action handler stays thin AND so
170
+ * unit tests can exercise the banner / strict-refusal decision without
171
+ * spinning up commander + a real VaultClient.
172
+ *
173
+ * Input shape:
174
+ * - `targetCompany` — slug or UID the caller passed to `--company`. If
175
+ * undefined, the helper short-circuits to a "no resolution" result
176
+ * (the action handler falls back to .hq/config.json via sync()).
177
+ * - `client` — minimal VaultClient surface: listMyMemberships + entity.get
178
+ * + getMembershipSyncConfig.
179
+ *
180
+ * Output: `{ resolvedCompanyUid, resolvedMode }` — either may be undefined
181
+ * if the membership / sync-config call failed. Both undefined is a clean
182
+ * degradation — the caller pulls without a banner.
183
+ */
184
+ export interface PerCompanyPullResolveClient {
185
+ listMyMemberships(): Promise<Array<{
186
+ companyUid: string;
187
+ membershipKey: string;
188
+ }>>;
189
+ getMembershipSyncConfig(membershipKey: string): Promise<{
190
+ syncMode: MembershipSyncConfig["syncMode"];
191
+ }>;
192
+ entity: {
193
+ get(uid: string): Promise<{
194
+ slug?: string;
195
+ }>;
196
+ };
197
+ }
198
+ export interface PerCompanyPullResolveResult {
199
+ resolvedCompanyUid: string | undefined;
200
+ resolvedMode: MembershipSyncConfig["syncMode"] | undefined;
201
+ }
202
+ export declare function resolvePerCompanyPullPlan(client: PerCompanyPullResolveClient, targetCompany: string | undefined): Promise<PerCompanyPullResolveResult>;
166
203
  export declare function registerCloudCommands(program: Command): void;
167
204
  //# sourceMappingURL=cloud.d.ts.map
@@ -13,7 +13,7 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
 
16
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2eb7da1d-e84f-591c-b930-b84c5c96986b")}catch(e){}}();
16
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="4672d875-1dc8-56a2-bef1-49c7f4ff6c76")}catch(e){}}();
17
17
  import chalk from "chalk";
18
18
  import * as fs from "fs";
19
19
  import * as path from "path";
@@ -252,6 +252,54 @@ export function assertSingleSelector(opts, command) {
252
252
  `--company; got: ${selectors.join(", ")}.`);
253
253
  }
254
254
  }
255
+ export async function resolvePerCompanyPullPlan(client, targetCompany) {
256
+ if (!targetCompany)
257
+ return { resolvedCompanyUid: undefined, resolvedMode: undefined };
258
+ try {
259
+ const memberships = await client.listMyMemberships();
260
+ // Direct UID / membershipKey match first (cheapest).
261
+ const direct = memberships.find((m) => m.companyUid === targetCompany || m.membershipKey === targetCompany);
262
+ if (direct) {
263
+ let mode;
264
+ try {
265
+ const cfg = await client.getMembershipSyncConfig(direct.membershipKey);
266
+ mode = cfg.syncMode;
267
+ }
268
+ catch {
269
+ mode = undefined;
270
+ }
271
+ return { resolvedCompanyUid: direct.companyUid, resolvedMode: mode };
272
+ }
273
+ // Slug match — listMyMemberships returns companyUid only, so fan out
274
+ // entity.get to find the row whose slug matches the caller's input.
275
+ for (const m of memberships) {
276
+ try {
277
+ const entity = await client.entity.get(m.companyUid);
278
+ if (entity.slug === targetCompany) {
279
+ let mode;
280
+ try {
281
+ const cfg = await client.getMembershipSyncConfig(m.membershipKey);
282
+ mode = cfg.syncMode;
283
+ }
284
+ catch {
285
+ mode = undefined;
286
+ }
287
+ return { resolvedCompanyUid: m.companyUid, resolvedMode: mode };
288
+ }
289
+ }
290
+ catch {
291
+ // Entity not visible — skip and continue. Worst case the loop ends
292
+ // with no match and we return undefined for both — the pull still
293
+ // proceeds, banner just stays quiet.
294
+ }
295
+ }
296
+ }
297
+ catch {
298
+ // listMyMemberships failed — degrade silently. Sync still works without
299
+ // the banner; this matches the runPullAll catch behavior.
300
+ }
301
+ return { resolvedCompanyUid: undefined, resolvedMode: undefined };
302
+ }
255
303
  export function registerCloudCommands(program) {
256
304
  program
257
305
  .command("push")
@@ -482,10 +530,37 @@ export function registerCloudCommands(program) {
482
530
  console.log(` HQ root: ${options.hqRoot}`);
483
531
  console.log(` Company: ${options.company ?? "(from .hq/config.json)"}\n`);
484
532
  const accessToken = await ensureCognitoToken();
533
+ const vaultConfig = buildVaultConfig(accessToken);
534
+ // US-011 (2026-05-21 fix): resolve the caller's sync-config for
535
+ // the targeted membership BEFORE the pull, so we can (a) emit the
536
+ // narrow-hint banner after success if still on all-mode and
537
+ // (b) respect strict-mode refusal mirror of the --all + sync-now
538
+ // paths. Failure to resolve degrades silently — pull still works,
539
+ // banner just stays quiet (same as the catch in runPullAll).
540
+ const narrowHintLevel = resolveBannerLevel();
541
+ const { resolvedCompanyUid, resolvedMode } = await resolvePerCompanyPullPlan(new VaultClient(vaultConfig), options.company);
542
+ // Strict-mode refusal: matches runPullAll + runNowSingle behavior.
543
+ // Default banner level is 'hint' which never triggers refusal —
544
+ // wired now so future hq-core-staging releases can flip the
545
+ // default to 'strict' without re-touching this command.
546
+ if (resolvedMode === "all" &&
547
+ isStrictRefusal(resolvedMode, narrowHintLevel) &&
548
+ options.modeAll !== true &&
549
+ resolvedCompanyUid) {
550
+ emitNarrowHint({
551
+ companyUid: resolvedCompanyUid,
552
+ syncMode: resolvedMode,
553
+ level: narrowHintLevel,
554
+ });
555
+ console.error(chalk.red("\n✗ Pull refused: strict narrow-hint mode is on and this " +
556
+ "membership still pulls everything. Run `hq sync narrow --apply` " +
557
+ "to migrate, or re-run with --mode-all."));
558
+ process.exit(1);
559
+ }
485
560
  const result = await sync({
486
561
  company: options.company,
487
562
  onConflict: options.onConflict,
488
- vaultConfig: buildVaultConfig(accessToken),
563
+ vaultConfig,
489
564
  hqRoot: options.hqRoot,
490
565
  });
491
566
  if (result.aborted) {
@@ -493,6 +568,16 @@ export function registerCloudCommands(program) {
493
568
  process.exit(1);
494
569
  }
495
570
  console.log(chalk.green(`\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`));
571
+ // US-011 (2026-05-21 fix): emit the hint banner after success
572
+ // so it appears alongside the summary line. Mirrors the wiring
573
+ // in runPullAll (cloud.ts:331) and runNowSingle (cloud.ts:1371).
574
+ if (resolvedMode === "all" && resolvedCompanyUid) {
575
+ emitNarrowHint({
576
+ companyUid: resolvedCompanyUid,
577
+ syncMode: resolvedMode,
578
+ level: narrowHintLevel,
579
+ });
580
+ }
496
581
  }
497
582
  catch (err) {
498
583
  console.error(chalk.red("\n✗ Pull failed:"), err instanceof Error ? err.message : String(err));
@@ -978,4 +1063,4 @@ function resolveUploadAuthorFromCache() {
978
1063
  }
979
1064
  }
980
1065
  //# sourceMappingURL=cloud.js.map
981
- //# debugId=2eb7da1d-e84f-591c-b930-b84c5c96986b
1066
+ //# debugId=4672d875-1dc8-56a2-bef1-49c7f4ff6c76
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.18.0",
3
+ "version": "5.18.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Unit tests for `resolvePerCompanyPullPlan` — the helper extracted from the
3
+ * `hq sync pull --company <slug>` action handler so the US-011 banner +
4
+ * strict-refusal wiring can be exercised without commander / a real
5
+ * VaultClient (US-011 fix, 2026-05-21).
6
+ *
7
+ * The action handler itself stays a thin orchestrator. These tests cover
8
+ * the membership-resolution decision tree, which is the part that broke
9
+ * on the pre-fix code path (banner silently never fired because the
10
+ * handler never looked up the sync-config).
11
+ *
12
+ * Coverage:
13
+ * 1. Undefined targetCompany short-circuits to no-op.
14
+ * 2. Direct UID match returns mode + companyUid.
15
+ * 3. Direct membershipKey match returns mode + companyUid.
16
+ * 4. Slug match via entity.get returns mode + companyUid.
17
+ * 5. listMyMemberships error degrades silently to no resolution.
18
+ * 6. getMembershipSyncConfig error degrades to undefined mode but keeps companyUid.
19
+ * 7. Slug iteration stops at first match (doesn't fan out to all entities).
20
+ * 8. No matching membership returns no resolution.
21
+ */
22
+
23
+ import { describe, expect, it, vi } from "vitest";
24
+ import {
25
+ resolvePerCompanyPullPlan,
26
+ type PerCompanyPullResolveClient,
27
+ } from "./cloud.js";
28
+
29
+ function makeClient(opts: {
30
+ memberships?: Array<{ companyUid: string; membershipKey: string }>;
31
+ syncConfigs?: Record<string, { syncMode: "shared" | "all" | "custom" }>;
32
+ entities?: Record<string, { slug?: string }>;
33
+ failListMemberships?: boolean;
34
+ failSyncConfigFor?: Set<string>;
35
+ failEntityFor?: Set<string>;
36
+ }): PerCompanyPullResolveClient & {
37
+ _entityCalls: () => string[];
38
+ } {
39
+ const entityCalls: string[] = [];
40
+ return {
41
+ listMyMemberships: vi.fn(async () => {
42
+ if (opts.failListMemberships) throw new Error("listMyMemberships boom");
43
+ return opts.memberships ?? [];
44
+ }),
45
+ getMembershipSyncConfig: vi.fn(async (key: string) => {
46
+ if (opts.failSyncConfigFor?.has(key))
47
+ throw new Error(`getMembershipSyncConfig boom for ${key}`);
48
+ const cfg = (opts.syncConfigs ?? {})[key];
49
+ if (!cfg) throw new Error(`no fake config for ${key}`);
50
+ return cfg;
51
+ }),
52
+ entity: {
53
+ get: vi.fn(async (uid: string) => {
54
+ entityCalls.push(uid);
55
+ if (opts.failEntityFor?.has(uid))
56
+ throw new Error(`entity.get boom for ${uid}`);
57
+ return (opts.entities ?? {})[uid] ?? {};
58
+ }),
59
+ },
60
+ _entityCalls: () => entityCalls,
61
+ };
62
+ }
63
+
64
+ describe("resolvePerCompanyPullPlan (US-011 per-company pull fix)", () => {
65
+ it("returns undefined+undefined when targetCompany is undefined", async () => {
66
+ const client = makeClient({});
67
+ const result = await resolvePerCompanyPullPlan(client, undefined);
68
+ expect(result.resolvedCompanyUid).toBeUndefined();
69
+ expect(result.resolvedMode).toBeUndefined();
70
+ // Should not call any API — short-circuit.
71
+ expect(client.listMyMemberships).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it("matches by direct companyUid and returns the live syncMode", async () => {
75
+ const client = makeClient({
76
+ memberships: [
77
+ { companyUid: "cmp_personal", membershipKey: "mbr_personal" },
78
+ { companyUid: "cmp_indigo", membershipKey: "mbr_indigo" },
79
+ ],
80
+ syncConfigs: {
81
+ mbr_personal: { syncMode: "all" },
82
+ mbr_indigo: { syncMode: "shared" },
83
+ },
84
+ });
85
+ const result = await resolvePerCompanyPullPlan(client, "cmp_indigo");
86
+ expect(result.resolvedCompanyUid).toBe("cmp_indigo");
87
+ expect(result.resolvedMode).toBe("shared");
88
+ // Slug-fallback loop must not fire — no entity calls.
89
+ expect(client._entityCalls()).toEqual([]);
90
+ });
91
+
92
+ it("matches by direct membershipKey", async () => {
93
+ const client = makeClient({
94
+ memberships: [
95
+ { companyUid: "cmp_x", membershipKey: "mbr_special" },
96
+ ],
97
+ syncConfigs: { mbr_special: { syncMode: "custom" } },
98
+ });
99
+ const result = await resolvePerCompanyPullPlan(client, "mbr_special");
100
+ expect(result.resolvedCompanyUid).toBe("cmp_x");
101
+ expect(result.resolvedMode).toBe("custom");
102
+ });
103
+
104
+ it("matches by slug via entity.get fan-out", async () => {
105
+ const client = makeClient({
106
+ memberships: [
107
+ { companyUid: "cmp_one", membershipKey: "mbr_one" },
108
+ { companyUid: "cmp_two", membershipKey: "mbr_two" },
109
+ ],
110
+ syncConfigs: { mbr_two: { syncMode: "all" } },
111
+ entities: {
112
+ cmp_one: { slug: "foo" },
113
+ cmp_two: { slug: "personal" },
114
+ },
115
+ });
116
+ const result = await resolvePerCompanyPullPlan(client, "personal");
117
+ expect(result.resolvedCompanyUid).toBe("cmp_two");
118
+ expect(result.resolvedMode).toBe("all");
119
+ });
120
+
121
+ it("stops slug iteration at the first match — does not fan out", async () => {
122
+ const client = makeClient({
123
+ memberships: [
124
+ { companyUid: "cmp_a", membershipKey: "mbr_a" },
125
+ { companyUid: "cmp_b", membershipKey: "mbr_b" },
126
+ { companyUid: "cmp_c", membershipKey: "mbr_c" },
127
+ ],
128
+ syncConfigs: { mbr_b: { syncMode: "shared" } },
129
+ entities: {
130
+ cmp_a: { slug: "alpha" },
131
+ cmp_b: { slug: "beta" },
132
+ cmp_c: { slug: "gamma" },
133
+ },
134
+ });
135
+ const result = await resolvePerCompanyPullPlan(client, "beta");
136
+ expect(result.resolvedCompanyUid).toBe("cmp_b");
137
+ expect(result.resolvedMode).toBe("shared");
138
+ // cmp_a was probed, cmp_b matched, cmp_c never touched.
139
+ expect(client._entityCalls()).toEqual(["cmp_a", "cmp_b"]);
140
+ });
141
+
142
+ it("returns undefined for both when no matching membership/slug", async () => {
143
+ const client = makeClient({
144
+ memberships: [
145
+ { companyUid: "cmp_x", membershipKey: "mbr_x" },
146
+ ],
147
+ entities: { cmp_x: { slug: "foo" } },
148
+ });
149
+ const result = await resolvePerCompanyPullPlan(client, "doesnotexist");
150
+ expect(result.resolvedCompanyUid).toBeUndefined();
151
+ expect(result.resolvedMode).toBeUndefined();
152
+ });
153
+
154
+ it("degrades silently when listMyMemberships fails — no banner, pull continues", async () => {
155
+ const client = makeClient({ failListMemberships: true });
156
+ const result = await resolvePerCompanyPullPlan(client, "personal");
157
+ expect(result.resolvedCompanyUid).toBeUndefined();
158
+ expect(result.resolvedMode).toBeUndefined();
159
+ // Sync-config never reached.
160
+ expect(client.getMembershipSyncConfig).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it("on direct match, keeps companyUid even when getMembershipSyncConfig throws", async () => {
164
+ const client = makeClient({
165
+ memberships: [{ companyUid: "cmp_p", membershipKey: "mbr_p" }],
166
+ failSyncConfigFor: new Set(["mbr_p"]),
167
+ });
168
+ const result = await resolvePerCompanyPullPlan(client, "cmp_p");
169
+ expect(result.resolvedCompanyUid).toBe("cmp_p");
170
+ // Mode unknown — caller will skip banner emit (it only fires when mode='all').
171
+ expect(result.resolvedMode).toBeUndefined();
172
+ });
173
+
174
+ it("on slug-fallback match, skips broken entity rows and keeps looking", async () => {
175
+ const client = makeClient({
176
+ memberships: [
177
+ { companyUid: "cmp_broken", membershipKey: "mbr_broken" },
178
+ { companyUid: "cmp_good", membershipKey: "mbr_good" },
179
+ ],
180
+ syncConfigs: { mbr_good: { syncMode: "all" } },
181
+ entities: { cmp_good: { slug: "wanted" } },
182
+ failEntityFor: new Set(["cmp_broken"]),
183
+ });
184
+ const result = await resolvePerCompanyPullPlan(client, "wanted");
185
+ expect(result.resolvedCompanyUid).toBe("cmp_good");
186
+ expect(result.resolvedMode).toBe("all");
187
+ });
188
+ });
@@ -473,6 +473,86 @@ export function assertSingleSelector(opts: {
473
473
  }
474
474
  }
475
475
 
476
+ /**
477
+ * Per-company pull resolution helper used by `hq sync pull --company <slug>`
478
+ * (US-011 fix, 2026-05-21). Mirrors the inline lookup that `runNowSingle`
479
+ * does for sync-now. Pulled out so the action handler stays thin AND so
480
+ * unit tests can exercise the banner / strict-refusal decision without
481
+ * spinning up commander + a real VaultClient.
482
+ *
483
+ * Input shape:
484
+ * - `targetCompany` — slug or UID the caller passed to `--company`. If
485
+ * undefined, the helper short-circuits to a "no resolution" result
486
+ * (the action handler falls back to .hq/config.json via sync()).
487
+ * - `client` — minimal VaultClient surface: listMyMemberships + entity.get
488
+ * + getMembershipSyncConfig.
489
+ *
490
+ * Output: `{ resolvedCompanyUid, resolvedMode }` — either may be undefined
491
+ * if the membership / sync-config call failed. Both undefined is a clean
492
+ * degradation — the caller pulls without a banner.
493
+ */
494
+ export interface PerCompanyPullResolveClient {
495
+ listMyMemberships(): Promise<Array<{ companyUid: string; membershipKey: string }>>;
496
+ getMembershipSyncConfig(
497
+ membershipKey: string,
498
+ ): Promise<{ syncMode: MembershipSyncConfig["syncMode"] }>;
499
+ entity: { get(uid: string): Promise<{ slug?: string }> };
500
+ }
501
+
502
+ export interface PerCompanyPullResolveResult {
503
+ resolvedCompanyUid: string | undefined;
504
+ resolvedMode: MembershipSyncConfig["syncMode"] | undefined;
505
+ }
506
+
507
+ export async function resolvePerCompanyPullPlan(
508
+ client: PerCompanyPullResolveClient,
509
+ targetCompany: string | undefined,
510
+ ): Promise<PerCompanyPullResolveResult> {
511
+ if (!targetCompany) return { resolvedCompanyUid: undefined, resolvedMode: undefined };
512
+ try {
513
+ const memberships = await client.listMyMemberships();
514
+ // Direct UID / membershipKey match first (cheapest).
515
+ const direct = memberships.find(
516
+ (m) => m.companyUid === targetCompany || m.membershipKey === targetCompany,
517
+ );
518
+ if (direct) {
519
+ let mode: MembershipSyncConfig["syncMode"] | undefined;
520
+ try {
521
+ const cfg = await client.getMembershipSyncConfig(direct.membershipKey);
522
+ mode = cfg.syncMode;
523
+ } catch {
524
+ mode = undefined;
525
+ }
526
+ return { resolvedCompanyUid: direct.companyUid, resolvedMode: mode };
527
+ }
528
+ // Slug match — listMyMemberships returns companyUid only, so fan out
529
+ // entity.get to find the row whose slug matches the caller's input.
530
+ for (const m of memberships) {
531
+ try {
532
+ const entity = await client.entity.get(m.companyUid);
533
+ if (entity.slug === targetCompany) {
534
+ let mode: MembershipSyncConfig["syncMode"] | undefined;
535
+ try {
536
+ const cfg = await client.getMembershipSyncConfig(m.membershipKey);
537
+ mode = cfg.syncMode;
538
+ } catch {
539
+ mode = undefined;
540
+ }
541
+ return { resolvedCompanyUid: m.companyUid, resolvedMode: mode };
542
+ }
543
+ } catch {
544
+ // Entity not visible — skip and continue. Worst case the loop ends
545
+ // with no match and we return undefined for both — the pull still
546
+ // proceeds, banner just stays quiet.
547
+ }
548
+ }
549
+ } catch {
550
+ // listMyMemberships failed — degrade silently. Sync still works without
551
+ // the banner; this matches the runPullAll catch behavior.
552
+ }
553
+ return { resolvedCompanyUid: undefined, resolvedMode: undefined };
554
+ }
555
+
476
556
  export function registerCloudCommands(program: Command): void {
477
557
  program
478
558
  .command("push")
@@ -817,10 +897,50 @@ export function registerCloudCommands(program: Command): void {
817
897
  console.log(` Company: ${options.company ?? "(from .hq/config.json)"}\n`);
818
898
 
819
899
  const accessToken = await ensureCognitoToken();
900
+ const vaultConfig = buildVaultConfig(accessToken);
901
+
902
+ // US-011 (2026-05-21 fix): resolve the caller's sync-config for
903
+ // the targeted membership BEFORE the pull, so we can (a) emit the
904
+ // narrow-hint banner after success if still on all-mode and
905
+ // (b) respect strict-mode refusal mirror of the --all + sync-now
906
+ // paths. Failure to resolve degrades silently — pull still works,
907
+ // banner just stays quiet (same as the catch in runPullAll).
908
+ const narrowHintLevel: BannerLevel = resolveBannerLevel();
909
+ const { resolvedCompanyUid, resolvedMode } =
910
+ await resolvePerCompanyPullPlan(
911
+ new VaultClient(vaultConfig),
912
+ options.company,
913
+ );
914
+
915
+ // Strict-mode refusal: matches runPullAll + runNowSingle behavior.
916
+ // Default banner level is 'hint' which never triggers refusal —
917
+ // wired now so future hq-core-staging releases can flip the
918
+ // default to 'strict' without re-touching this command.
919
+ if (
920
+ resolvedMode === "all" &&
921
+ isStrictRefusal(resolvedMode, narrowHintLevel) &&
922
+ options.modeAll !== true &&
923
+ resolvedCompanyUid
924
+ ) {
925
+ emitNarrowHint({
926
+ companyUid: resolvedCompanyUid,
927
+ syncMode: resolvedMode,
928
+ level: narrowHintLevel,
929
+ });
930
+ console.error(
931
+ chalk.red(
932
+ "\n✗ Pull refused: strict narrow-hint mode is on and this " +
933
+ "membership still pulls everything. Run `hq sync narrow --apply` " +
934
+ "to migrate, or re-run with --mode-all.",
935
+ ),
936
+ );
937
+ process.exit(1);
938
+ }
939
+
820
940
  const result = await sync({
821
941
  company: options.company,
822
942
  onConflict: options.onConflict,
823
- vaultConfig: buildVaultConfig(accessToken),
943
+ vaultConfig,
824
944
  hqRoot: options.hqRoot,
825
945
  });
826
946
 
@@ -838,6 +958,17 @@ export function registerCloudCommands(program: Command): void {
838
958
  `\n✓ Pulled ${result.filesDownloaded} file(s) (${formatBytes(result.bytesDownloaded)}, ${result.filesSkipped} skipped, ${result.conflicts} conflicts)`,
839
959
  ),
840
960
  );
961
+
962
+ // US-011 (2026-05-21 fix): emit the hint banner after success
963
+ // so it appears alongside the summary line. Mirrors the wiring
964
+ // in runPullAll (cloud.ts:331) and runNowSingle (cloud.ts:1371).
965
+ if (resolvedMode === "all" && resolvedCompanyUid) {
966
+ emitNarrowHint({
967
+ companyUid: resolvedCompanyUid,
968
+ syncMode: resolvedMode,
969
+ level: narrowHintLevel,
970
+ });
971
+ }
841
972
  } catch (err) {
842
973
  console.error(
843
974
  chalk.red("\n✗ Pull failed:"),