@indigoai-us/hq-cli 5.225.0 → 5.227.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 (50) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/dist/command-catalog.generated.d.ts +100 -0
  3. package/dist/command-catalog.generated.js +128 -0
  4. package/dist/command-registration-plan.d.ts +18 -0
  5. package/dist/command-registration-plan.js +6 -0
  6. package/dist/commands/agent-enroll.d.ts +2 -0
  7. package/dist/commands/agent-enroll.js +17 -2
  8. package/dist/commands/cloud.d.ts +10 -0
  9. package/dist/commands/cloud.js +52 -47
  10. package/dist/commands/lanes-checks.d.ts +2 -0
  11. package/dist/commands/lanes-checks.js +13 -11
  12. package/dist/commands/lanes-import.d.ts +1 -0
  13. package/dist/commands/lanes-import.js +12 -11
  14. package/dist/commands/lanes-report.js +21 -14
  15. package/dist/commands/lanes-tracker.d.ts +1 -0
  16. package/dist/commands/lanes-tracker.js +39 -31
  17. package/dist/commands/lanes.d.ts +8 -0
  18. package/dist/commands/lanes.js +818 -314
  19. package/dist/commands/staff/reset-test-account.d.ts +110 -0
  20. package/dist/commands/staff/reset-test-account.js +268 -0
  21. package/dist/commands/staff/test-account.d.ts +51 -0
  22. package/dist/commands/staff/test-account.js +196 -0
  23. package/dist/lib/checkpoint-ontology/plan.d.ts +4 -6
  24. package/dist/lib/checkpoint-ontology/plan.js +8 -26
  25. package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
  26. package/dist/lib/doctor/client-health-flag-token.js +66 -0
  27. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
  28. package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
  29. package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
  30. package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
  31. package/dist/lib/lanes/dropbox.d.ts +9 -0
  32. package/dist/lib/lanes/dropbox.js +70 -1
  33. package/dist/lib/lanes/reconcile.d.ts +4 -0
  34. package/dist/lib/lanes/reconcile.js +28 -0
  35. package/dist/lib/lanes/store.js +9 -1
  36. package/dist/lib/lanes/tracker-mesh-questions.d.ts +54 -0
  37. package/dist/lib/lanes/tracker-mesh-questions.js +138 -0
  38. package/dist/lib/lanes/tracker-mesh.d.ts +19 -0
  39. package/dist/lib/lanes/tracker-mesh.js +228 -0
  40. package/dist/lib/lanes/tracker-store.d.ts +13 -2
  41. package/dist/lib/lanes/tracker-store.js +1 -1
  42. package/dist/lib/lanes/types.d.ts +4 -0
  43. package/dist/lib/lanes/watcher-main.js +16 -0
  44. package/dist/lib/lanes/watcher.d.ts +4 -1
  45. package/dist/lib/lanes/watcher.js +77 -3
  46. package/dist/utils/cognito-session.d.ts +8 -0
  47. package/dist/utils/cognito-session.js +198 -3
  48. package/dist/utils/upload-author.d.ts +15 -0
  49. package/dist/utils/upload-author.js +39 -0
  50. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.227.0] — 2026-09-27
6
+
7
+ ### Added
8
+
9
+ - `hq lanes tracker`, `checks`, `import`, and `report` use the Work Mesh tracker
10
+ store when `lanes.tracker-mesh` is enabled. Company questions are indexed and
11
+ shared across hosts; reconcile polls for remote answers, wakes the owning lane,
12
+ and stamps delivery once.
13
+
14
+ ## [5.226.0] — 2026-09-26
15
+
16
+ ### Added
17
+
18
+ - `hq staff reset-test-account <email>` wipes a designated test account through
19
+ the hq-pro admin API. Defaults to a dry-run that lists what would be deleted;
20
+ `--execute` prompts for the typed email confirmation before wiping, and
21
+ `--yes` skips the prompt. Refuses to run for callers whose stored email is
22
+ not on the staff list.
23
+ - `hq staff test-account add|remove|list` manages the designated-test-account
24
+ registry through the same admin API. `add` and `remove` require staff
25
+ callers; `list` returns the current registry entries.
26
+
27
+ ### Fixed
28
+
29
+ - `hq lanes message` now probes the worker PID and start time before queueing.
30
+ Watcher and reconcile recover accepted inbox messages through the same
31
+ resume path after classifying an exited worker; undrained messages on done,
32
+ killed, retired, or superseded lanes are archived as undeliverable.
33
+ - Failed queued-message recovery restores unaccepted claims to the inbox and
34
+ records the failure. Recovery also sees leftover draining and inflight files,
35
+ and synchronous classification shares watcher ownership to prevent duplicate
36
+ repair and resume work.
37
+ - `hq agent enroll --name <bot>` now works on a computer where a person is signed in. It enrolls into `~/.hq-agent/<bot>/` and refuses only if that folder already holds an agent. Before, it refused because of the person's login and told you to pass `--name`, which you already had. Enrolling with no name on such a computer (the default local folder) had the same bug.
38
+ - Pushes made as an agent now record the agent as the uploader. Before, on a computer where a person was also signed in, the files were stamped with the person's email and id.
39
+ - When the server refuses some paths in a push (for example, another member's file in a private folder `foo/`), the warning now says the server refused them, in `hq sync push`, `hq sync now` and `hq sync push --all`. Before, it said the files were outside your write access and to ask an admin for a grant.
40
+
5
41
  ## [5.225.0] — 2026-09-26
6
42
 
7
43
  ### Changed
@@ -20,7 +56,13 @@
20
56
 
21
57
  ### Fixed
22
58
 
23
- - When `hq daemon pin-hq-cloud` names a version that is not installed yet, the daemon keeps running the newest installed hq-cloud instead of falling back to the older copy bundled with the CLI, and logs a warning naming the missing version. The updater then installs the pinned version and the daemon switches to it. Before, a pin that did not match the installed copy silently ran the bundled copy, which on one outpost meant a months-old sync runner without recent fixes.
59
+ - When `hq daemon pin-hq-cloud` names a version that is not installed yet, the
60
+ daemon keeps running the newest installed hq-cloud instead of falling back
61
+ to the older copy bundled with the CLI, and logs a warning naming the
62
+ missing version. The updater then installs the pinned version and the
63
+ daemon switches to it. Before, a pin that did not match the installed copy
64
+ silently ran the bundled copy, which on one outpost meant a months-old sync
65
+ runner without recent fixes.
24
66
 
25
67
  ## [5.223.0] — 2026-09-26
26
68
 
@@ -34,6 +76,10 @@
34
76
  shares one local account probe across matching lanes per poll. Quota and
35
77
  model failures keep their existing account-selection path and reason.
36
78
 
79
+ - Client-health flag checks refresh expired person identity and use machine
80
+ identity when available. Identity refresh has a separate bound, and failed
81
+ lookups keep the existing doctor fallback path.
82
+
37
83
  ## [5.222.0] — 2026-09-26
38
84
 
39
85
  - Checkpoint ontology, behind `checkpoint.ontology.multi-writer` (off by
@@ -7069,6 +7069,9 @@ export declare const COMMAND_CATALOG: readonly [{
7069
7069
  }, {
7070
7070
  readonly flags: "--company <slug>";
7071
7071
  readonly description: "Select the company by slug";
7072
+ }, {
7073
+ readonly flags: "--question <id>";
7074
+ readonly description: "Answered owner question authorizing this approval";
7072
7075
  }, {
7073
7076
  readonly flags: "--json";
7074
7077
  readonly description: "JSON on stdout (always)";
@@ -7145,6 +7148,9 @@ export declare const COMMAND_CATALOG: readonly [{
7145
7148
  }, {
7146
7149
  readonly flags: "--company <slug>";
7147
7150
  readonly description: "Select the company by slug";
7151
+ }, {
7152
+ readonly flags: "--question <id>";
7153
+ readonly description: "Answered owner question authorizing this approval";
7148
7154
  }, {
7149
7155
  readonly flags: "--json";
7150
7156
  readonly description: "JSON on stdout (always)";
@@ -7961,6 +7967,22 @@ export declare const COMMAND_CATALOG: readonly [{
7961
7967
  readonly description: "JSON on stdout (always)";
7962
7968
  }];
7963
7969
  readonly subcommands: readonly [];
7970
+ }, {
7971
+ readonly name: "_recover-queued-messages";
7972
+ readonly description: "";
7973
+ readonly aliases: readonly [];
7974
+ readonly hidden: true;
7975
+ readonly usage: "[options] <lane-id>";
7976
+ readonly arguments: readonly [{
7977
+ readonly name: "lane-id";
7978
+ readonly required: true;
7979
+ readonly variadic: false;
7980
+ }];
7981
+ readonly options: readonly [{
7982
+ readonly flags: "--json";
7983
+ readonly description: "JSON on stdout (always)";
7984
+ }];
7985
+ readonly subcommands: readonly [];
7964
7986
  }, {
7965
7987
  readonly name: "_monitor-message";
7966
7988
  readonly description: "";
@@ -9490,5 +9512,83 @@ export declare const COMMAND_CATALOG: readonly [{
9490
9512
  }];
9491
9513
  readonly subcommands: readonly [];
9492
9514
  }];
9515
+ }, {
9516
+ readonly name: "staff";
9517
+ readonly description: "Staff-only commands (super-admin required)";
9518
+ readonly aliases: readonly [];
9519
+ readonly hidden: false;
9520
+ readonly usage: "[options] [command]";
9521
+ readonly arguments: readonly [];
9522
+ readonly options: readonly [];
9523
+ readonly subcommands: readonly [{
9524
+ readonly name: "reset-test-account";
9525
+ readonly description: "Reset a designated real-email test account (dry-run by default). Wraps POST /v1/admin/test-account/reset — super-admin only.";
9526
+ readonly aliases: readonly [];
9527
+ readonly hidden: false;
9528
+ readonly usage: "[options] <email>";
9529
+ readonly arguments: readonly [{
9530
+ readonly name: "email";
9531
+ readonly required: true;
9532
+ readonly variadic: false;
9533
+ }];
9534
+ readonly options: readonly [{
9535
+ readonly flags: "--execute";
9536
+ readonly description: "Actually delete artifacts. Prompts for a typed-email confirmation.";
9537
+ }, {
9538
+ readonly flags: "-y, --yes";
9539
+ readonly description: "Skip the typed-email confirmation prompt (still sends confirmEmail).";
9540
+ }];
9541
+ readonly subcommands: readonly [];
9542
+ }, {
9543
+ readonly name: "test-account";
9544
+ readonly description: "Manage the designated test-account registry (super-admin only). Wraps /v1/admin/test-account-registry.";
9545
+ readonly aliases: readonly [];
9546
+ readonly hidden: false;
9547
+ readonly usage: "[options] [command]";
9548
+ readonly arguments: readonly [];
9549
+ readonly options: readonly [];
9550
+ readonly subcommands: readonly [{
9551
+ readonly name: "add";
9552
+ readonly description: "Add an email to the registry so `hq staff reset-test-account` will sweep it.";
9553
+ readonly aliases: readonly [];
9554
+ readonly hidden: false;
9555
+ readonly usage: "[options] <email>";
9556
+ readonly arguments: readonly [{
9557
+ readonly name: "email";
9558
+ readonly required: true;
9559
+ readonly variadic: false;
9560
+ }];
9561
+ readonly options: readonly [{
9562
+ readonly flags: "--note <text>";
9563
+ readonly description: "Short reason recorded on the row";
9564
+ }];
9565
+ readonly subcommands: readonly [];
9566
+ }, {
9567
+ readonly name: "remove";
9568
+ readonly description: "Soft-delete a registry entry (row stays with active: false).";
9569
+ readonly aliases: readonly [];
9570
+ readonly hidden: false;
9571
+ readonly usage: "[options] <email>";
9572
+ readonly arguments: readonly [{
9573
+ readonly name: "email";
9574
+ readonly required: true;
9575
+ readonly variadic: false;
9576
+ }];
9577
+ readonly options: readonly [];
9578
+ readonly subcommands: readonly [];
9579
+ }, {
9580
+ readonly name: "list";
9581
+ readonly description: "List active registry entries.";
9582
+ readonly aliases: readonly [];
9583
+ readonly hidden: false;
9584
+ readonly usage: "[options]";
9585
+ readonly arguments: readonly [];
9586
+ readonly options: readonly [{
9587
+ readonly flags: "--include-inactive";
9588
+ readonly description: "Also list soft-deleted rows (active: false).";
9589
+ }];
9590
+ readonly subcommands: readonly [];
9591
+ }];
9592
+ }];
9493
9593
  }];
9494
9594
  //# sourceMappingURL=command-catalog.generated.d.ts.map
@@ -9137,6 +9137,10 @@ export const COMMAND_CATALOG = [
9137
9137
  "flags": "--company <slug>",
9138
9138
  "description": "Select the company by slug"
9139
9139
  },
9140
+ {
9141
+ "flags": "--question <id>",
9142
+ "description": "Answered owner question authorizing this approval"
9143
+ },
9140
9144
  {
9141
9145
  "flags": "--json",
9142
9146
  "description": "JSON on stdout (always)"
@@ -9235,6 +9239,10 @@ export const COMMAND_CATALOG = [
9235
9239
  "flags": "--company <slug>",
9236
9240
  "description": "Select the company by slug"
9237
9241
  },
9242
+ {
9243
+ "flags": "--question <id>",
9244
+ "description": "Answered owner question authorizing this approval"
9245
+ },
9238
9246
  {
9239
9247
  "flags": "--json",
9240
9248
  "description": "JSON on stdout (always)"
@@ -10296,6 +10304,27 @@ export const COMMAND_CATALOG = [
10296
10304
  ],
10297
10305
  "subcommands": []
10298
10306
  },
10307
+ {
10308
+ "name": "_recover-queued-messages",
10309
+ "description": "",
10310
+ "aliases": [],
10311
+ "hidden": true,
10312
+ "usage": "[options] <lane-id>",
10313
+ "arguments": [
10314
+ {
10315
+ "name": "lane-id",
10316
+ "required": true,
10317
+ "variadic": false
10318
+ }
10319
+ ],
10320
+ "options": [
10321
+ {
10322
+ "flags": "--json",
10323
+ "description": "JSON on stdout (always)"
10324
+ }
10325
+ ],
10326
+ "subcommands": []
10327
+ },
10299
10328
  {
10300
10329
  "name": "_monitor-message",
10301
10330
  "description": "",
@@ -12273,6 +12302,105 @@ export const COMMAND_CATALOG = [
12273
12302
  "subcommands": []
12274
12303
  }
12275
12304
  ]
12305
+ },
12306
+ {
12307
+ "name": "staff",
12308
+ "description": "Staff-only commands (super-admin required)",
12309
+ "aliases": [],
12310
+ "hidden": false,
12311
+ "usage": "[options] [command]",
12312
+ "arguments": [],
12313
+ "options": [],
12314
+ "subcommands": [
12315
+ {
12316
+ "name": "reset-test-account",
12317
+ "description": "Reset a designated real-email test account (dry-run by default). Wraps POST /v1/admin/test-account/reset — super-admin only.",
12318
+ "aliases": [],
12319
+ "hidden": false,
12320
+ "usage": "[options] <email>",
12321
+ "arguments": [
12322
+ {
12323
+ "name": "email",
12324
+ "required": true,
12325
+ "variadic": false
12326
+ }
12327
+ ],
12328
+ "options": [
12329
+ {
12330
+ "flags": "--execute",
12331
+ "description": "Actually delete artifacts. Prompts for a typed-email confirmation."
12332
+ },
12333
+ {
12334
+ "flags": "-y, --yes",
12335
+ "description": "Skip the typed-email confirmation prompt (still sends confirmEmail)."
12336
+ }
12337
+ ],
12338
+ "subcommands": []
12339
+ },
12340
+ {
12341
+ "name": "test-account",
12342
+ "description": "Manage the designated test-account registry (super-admin only). Wraps /v1/admin/test-account-registry.",
12343
+ "aliases": [],
12344
+ "hidden": false,
12345
+ "usage": "[options] [command]",
12346
+ "arguments": [],
12347
+ "options": [],
12348
+ "subcommands": [
12349
+ {
12350
+ "name": "add",
12351
+ "description": "Add an email to the registry so `hq staff reset-test-account` will sweep it.",
12352
+ "aliases": [],
12353
+ "hidden": false,
12354
+ "usage": "[options] <email>",
12355
+ "arguments": [
12356
+ {
12357
+ "name": "email",
12358
+ "required": true,
12359
+ "variadic": false
12360
+ }
12361
+ ],
12362
+ "options": [
12363
+ {
12364
+ "flags": "--note <text>",
12365
+ "description": "Short reason recorded on the row"
12366
+ }
12367
+ ],
12368
+ "subcommands": []
12369
+ },
12370
+ {
12371
+ "name": "remove",
12372
+ "description": "Soft-delete a registry entry (row stays with active: false).",
12373
+ "aliases": [],
12374
+ "hidden": false,
12375
+ "usage": "[options] <email>",
12376
+ "arguments": [
12377
+ {
12378
+ "name": "email",
12379
+ "required": true,
12380
+ "variadic": false
12381
+ }
12382
+ ],
12383
+ "options": [],
12384
+ "subcommands": []
12385
+ },
12386
+ {
12387
+ "name": "list",
12388
+ "description": "List active registry entries.",
12389
+ "aliases": [],
12390
+ "hidden": false,
12391
+ "usage": "[options]",
12392
+ "arguments": [],
12393
+ "options": [
12394
+ {
12395
+ "flags": "--include-inactive",
12396
+ "description": "Also list soft-deleted rows (active: false)."
12397
+ }
12398
+ ],
12399
+ "subcommands": []
12400
+ }
12401
+ ]
12402
+ }
12403
+ ]
12276
12404
  }
12277
12405
  ];
12278
12406
  //# sourceMappingURL=command-catalog.generated.js.map
@@ -432,6 +432,24 @@ export declare const REGISTRATION_PLAN: readonly [{
432
432
  readonly parent: "program";
433
433
  readonly module: "./commands/report.js";
434
434
  readonly exportName: "registerReportCommand";
435
+ }, {
436
+ readonly type: "group";
437
+ readonly root: "staff";
438
+ readonly id: "staff";
439
+ readonly name: "staff";
440
+ readonly description: "Staff-only commands (super-admin required)";
441
+ }, {
442
+ readonly type: "registrar";
443
+ readonly root: "staff";
444
+ readonly parent: "staff";
445
+ readonly module: "./commands/staff/reset-test-account.js";
446
+ readonly exportName: "registerResetTestAccountCommand";
447
+ }, {
448
+ readonly type: "registrar";
449
+ readonly root: "staff";
450
+ readonly parent: "staff";
451
+ readonly module: "./commands/staff/test-account.js";
452
+ readonly exportName: "registerTestAccountCommand";
435
453
  }];
436
454
  export declare function registerAllCommands(program: Command): Promise<void>;
437
455
  export declare function registerCommandRoot(program: Command, root: string): Promise<boolean>;
@@ -74,6 +74,12 @@ export const REGISTRATION_PLAN = [
74
74
  { type: "registrar", root: "daemon", parent: "program", module: "./commands/daemon.js", exportName: "registerDaemonCommand", hidden: true },
75
75
  { type: "registrar", root: "bot", parent: "program", module: "./commands/bot.js", exportName: "registerBotCommand" },
76
76
  { type: "registrar", root: "report", parent: "program", module: "./commands/report.js", exportName: "registerReportCommand" },
77
+ // onboarding-test-accounts US-007 — super-admin staff CLI wrappers over the
78
+ // hq-pro admin surface. New wrappers land under this group; the reset
79
+ // command mirrors POST /v1/admin/test-account/reset.
80
+ { type: "group", root: "staff", id: "staff", name: "staff", description: "Staff-only commands (super-admin required)" },
81
+ { type: "registrar", root: "staff", parent: "staff", module: "./commands/staff/reset-test-account.js", exportName: "registerResetTestAccountCommand" },
82
+ { type: "registrar", root: "staff", parent: "staff", module: "./commands/staff/test-account.js", exportName: "registerTestAccountCommand" },
77
83
  ];
78
84
  async function executePlan(program, steps) {
79
85
  const parents = new Map([["program", program]]);
@@ -87,6 +87,8 @@ export interface EnrollDeps {
87
87
  env?: NodeJS.ProcessEnv;
88
88
  hostInfo?: HostInfo;
89
89
  generateKeyPair?: typeof generateHostKeyPair;
90
+ /** Home directory the enroll target resolves under (defaults to os.homedir()). */
91
+ home?: string;
90
92
  }
91
93
  export interface EnrollOptions {
92
94
  code: string;
@@ -59,6 +59,17 @@ export function detectExistingIdentity(paths, env = process.env) {
59
59
  }
60
60
  return { kind: "none" };
61
61
  }
62
+ /** Machine identity already enrolled in a local agent directory, if any. */
63
+ function existingLocalIdentity(paths) {
64
+ if (!machineCredsFileExists(paths))
65
+ return { kind: "none" };
66
+ const existing = readExternalMachineCreds(paths);
67
+ return {
68
+ kind: "machine",
69
+ file: paths.machineCredsPath,
70
+ ...(existing ? { agentUid: existing.entityUid } : {}),
71
+ };
72
+ }
62
73
  export function refusalMessage(existing) {
63
74
  switch (existing.kind) {
64
75
  case "none":
@@ -221,7 +232,7 @@ export async function enrollHost(opts, deps = {}) {
221
232
  const env = deps.env ?? process.env;
222
233
  const target = deps.paths
223
234
  ? { paths: deps.paths, local: false }
224
- : resolveEnrollTarget({ name: opts.name, replace: opts.replace }, env);
235
+ : resolveEnrollTarget({ name: opts.name, replace: opts.replace }, env, deps.home);
225
236
  const paths = target.paths;
226
237
  const doFetch = deps.fetch ?? fetch;
227
238
  const apiBaseUrl = (opts.apiBaseUrl ?? env.HQ_VAULT_API_URL ?? DEFAULT_VAULT_API_URL).replace(/\/+$/, "");
@@ -229,7 +240,11 @@ export async function enrollHost(opts, deps = {}) {
229
240
  if (!isWellFormedEnrollmentCode(code)) {
230
241
  throw new EnrollError(`Enrollment code must be 24 base32 characters (shown as XXXX-XXXX-XXXX-XXXX-XXXX-XXXX); got ${code.length}.`);
231
242
  }
232
- const existing = detectExistingIdentity(paths, env);
243
+ // A local target sits in its own ~/.hq-agent/<name>/ beside the person's
244
+ // login by design, so only an identity already in that directory blocks it.
245
+ const existing = target.local
246
+ ? existingLocalIdentity(paths)
247
+ : detectExistingIdentity(paths, env);
233
248
  const refusal = refusalMessage(existing);
234
249
  if (refusal && !opts.replace)
235
250
  throw new EnrollError(refusal);
@@ -61,6 +61,14 @@ export declare function scopeExcludedWarning(count: number): string | null;
61
61
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
62
62
  */
63
63
  export declare function scopeExcludedPrefixDetail(prefixSet: readonly string[] | undefined): string | null;
64
+ /**
65
+ * Every warning line for a push that dropped files, shared by `push`, `now`
66
+ * and `push --all`. With a client prefix scope the files fell outside the
67
+ * caller's write prefixes; without one (prefixSet undefined) the server
68
+ * refused them path by path — typically another member's child in a private
69
+ * folder foo/ — and grant advice would point the reader the wrong way.
70
+ */
71
+ export declare function scopeExcludedLines(count: number, prefixSet: readonly string[] | undefined): string[];
64
72
  /**
65
73
  * Bound foreground waits for a watcher/manual sync that currently owns the
66
74
  * per-root operation lock. The cloud engine reads this environment value on
@@ -231,6 +239,8 @@ export interface PushAllOptions {
231
239
  export interface PushAllRow {
232
240
  slug: string;
233
241
  result?: ShareCallResult;
242
+ /** Client write-prefix scope this push ran under; undefined = none applied. */
243
+ prefixSet?: readonly string[];
234
244
  error?: string;
235
245
  }
236
246
  export interface PushAllResult {
@@ -16,8 +16,9 @@ import { isAccessDeniedError, accessDeniedKeyOf, formatAccessDenied, } from "../
16
16
  import chalk from "chalk";
17
17
  import * as fs from "fs";
18
18
  import * as path from "path";
19
- import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
19
+ import { share, sync, getStateDir, listJournals, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
20
20
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
21
+ import { resolveUploadAuthor } from "../utils/upload-author.js";
21
22
  import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
22
23
  import { beginSyncHealthReport, } from "../utils/client-health.js";
23
24
  /**
@@ -115,11 +116,39 @@ export function scopeExcludedWarning(count) {
115
116
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
116
117
  */
117
118
  export function scopeExcludedPrefixDetail(prefixSet) {
118
- if (!prefixSet || prefixSet.length === 0) {
119
+ // No client-side prefix scope applied: the server refused the paths one by
120
+ // one, and scopeExcludedLines' primary line already says so.
121
+ if (prefixSet === undefined)
122
+ return null;
123
+ if (prefixSet.length === 0) {
119
124
  return " Granted write prefixes: (none — shared-mode membership with no explicit grants)";
120
125
  }
121
126
  return ` Granted write prefixes: ${prefixSet.join(", ")}`;
122
127
  }
128
+ /**
129
+ * Every warning line for a push that dropped files, shared by `push`, `now`
130
+ * and `push --all`. With a client prefix scope the files fell outside the
131
+ * caller's write prefixes; without one (prefixSet undefined) the server
132
+ * refused them path by path — typically another member's child in a private
133
+ * folder foo/ — and grant advice would point the reader the wrong way.
134
+ */
135
+ export function scopeExcludedLines(count, prefixSet) {
136
+ if (count <= 0)
137
+ return [];
138
+ if (prefixSet === undefined) {
139
+ return [
140
+ `⚠ ${count} file(s) were NOT uploaded — the server refused them. ` +
141
+ `A common cause is a file someone else created in a private folder ` +
142
+ `(foo/): only its creator can change it. They were skipped, not ` +
143
+ `synced. Re-run with --json to list them.`,
144
+ ];
145
+ }
146
+ const lines = [scopeExcludedWarning(count)];
147
+ const detail = scopeExcludedPrefixDetail(prefixSet);
148
+ if (detail)
149
+ lines.push(detail);
150
+ return lines;
151
+ }
123
152
  /**
124
153
  * Resolve the `propagateDeletePolicy` for share() calls.
125
154
  *
@@ -445,7 +474,13 @@ export async function pushAll(options, deps) {
445
474
  result.bytesUploaded += r.bytesUploaded;
446
475
  result.filesDeleted += r.filesDeleted;
447
476
  result.filesExcludedByScope += r.filesExcludedByScope;
448
- result.perCompany.push({ slug: entry.slug, result: r });
477
+ result.perCompany.push({
478
+ slug: entry.slug,
479
+ result: r,
480
+ ...(entry.shareOptions.prefixSet !== undefined
481
+ ? { prefixSet: entry.shareOptions.prefixSet }
482
+ : {}),
483
+ });
449
484
  }
450
485
  catch (err) {
451
486
  const message = formatSyncFailure(err);
@@ -803,7 +838,7 @@ export function registerCloudCommands(program) {
803
838
  // every row renders `—`. Resolved best-effort from the cached
804
839
  // idToken — pre-vended `--creds-from-stdin` paths still get author
805
840
  // attribution as long as the caller is logged in locally.
806
- const author = resolveUploadAuthorFromCache();
841
+ const author = resolveUploadAuthor();
807
842
  let pushPrefixSet;
808
843
  if (!personalMode && scopeVaultConfig) {
809
844
  const scopeClient = new VaultClient(scopeVaultConfig);
@@ -850,10 +885,9 @@ export function registerCloudCommands(program) {
850
885
  if (result.filesExcludedByScope > 0) {
851
886
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
852
887
  `${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
853
- log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
854
- const prefixDetail = scopeExcludedPrefixDetail(pushPrefixSet);
855
- if (prefixDetail)
856
- log(chalk.yellow(prefixDetail));
888
+ for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
889
+ log(chalk.yellow(line));
890
+ }
857
891
  }
858
892
  else {
859
893
  log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
@@ -1342,7 +1376,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1342
1376
  const accessToken = await ensureCognitoToken();
1343
1377
  const vaultConfig = buildVaultConfig(accessToken);
1344
1378
  const realClient = new VaultClient(vaultConfig);
1345
- const author = resolveUploadAuthorFromCache();
1379
+ const author = resolveUploadAuthor();
1346
1380
  const adapter = {
1347
1381
  listMyMemberships: () => realClient.listMyMemberships(),
1348
1382
  listPersonEntities: () => realClient.entity.listByType("person"),
@@ -1420,8 +1454,11 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
1420
1454
  console.log(errored > 0 || result.filesExcludedByScope > 0
1421
1455
  ? chalk.yellow(summary)
1422
1456
  : chalk.green(summary));
1423
- if (result.filesExcludedByScope > 0) {
1424
- console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
1457
+ for (const row of result.perCompany) {
1458
+ const excluded = row.result?.filesExcludedByScope ?? 0;
1459
+ for (const line of scopeExcludedLines(excluded, row.prefixSet)) {
1460
+ console.log(chalk.yellow(`${row.slug}: ${line}`));
1461
+ }
1425
1462
  }
1426
1463
  if (errored > 0)
1427
1464
  throw new SyncExitError(1);
@@ -1434,7 +1471,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1434
1471
  try {
1435
1472
  const accessToken = await ensureCognitoToken();
1436
1473
  const vaultConfig = buildVaultConfig(accessToken);
1437
- const author = resolveUploadAuthorFromCache();
1474
+ const author = resolveUploadAuthor();
1438
1475
  // Resolve the target. For --personal, look up the canonical person and
1439
1476
  // route paths/journal through personalMode. Otherwise use the company arg
1440
1477
  // (or fall back to the active company inside share()/sync()).
@@ -1499,10 +1536,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1499
1536
  `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
1500
1537
  (pushResult.aborted ? " — aborted" : ""));
1501
1538
  if (pushResult.filesExcludedByScope > 0) {
1502
- console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
1503
- const prefixDetail = scopeExcludedPrefixDetail(nowPushPrefixSet);
1504
- if (prefixDetail)
1505
- console.log(chalk.yellow(prefixDetail));
1539
+ for (const line of scopeExcludedLines(pushResult.filesExcludedByScope, nowPushPrefixSet)) {
1540
+ console.log(chalk.yellow(line));
1541
+ }
1506
1542
  }
1507
1543
  if (pushResult.aborted) {
1508
1544
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
@@ -1689,35 +1725,4 @@ async function readAllStdin() {
1689
1725
  }
1690
1726
  return Buffer.concat(chunks).toString("utf8");
1691
1727
  }
1692
- /**
1693
- * Resolve the syncing user's `UploadAuthor` (sub + email) from the cached
1694
- * Cognito idToken. Returns `undefined` when no tokens are cached or the
1695
- * token is missing the required claims — share() then skips the metadata
1696
- * stamp gracefully (not an error).
1697
- *
1698
- * We deliberately decode the JWT here instead of verifying it: Cognito
1699
- * already verified at issuance, and we only use the public claims to
1700
- * label the upload's S3 user metadata (no auth decision rides on it).
1701
- */
1702
- function resolveUploadAuthorFromCache() {
1703
- const tokens = loadCachedTokens();
1704
- if (!tokens?.idToken)
1705
- return undefined;
1706
- const parts = tokens.idToken.split(".");
1707
- if (parts.length !== 3)
1708
- return undefined;
1709
- try {
1710
- const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1711
- const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4);
1712
- const json = Buffer.from(padded, "base64").toString("utf-8");
1713
- const claims = JSON.parse(json);
1714
- if (claims.sub && claims.email) {
1715
- return { userSub: claims.sub, email: claims.email };
1716
- }
1717
- return undefined;
1718
- }
1719
- catch {
1720
- return undefined;
1721
- }
1722
- }
1723
1728
  //# sourceMappingURL=cloud.js.map
@@ -1,5 +1,6 @@
1
1
  import type { Command } from "commander";
2
2
  import { type TrackerKind, type TrackerRecord } from "../lib/lanes/tracker-store.js";
3
+ import type { TrackerStore } from "../lib/lanes/tracker-store.js";
3
4
  import type { LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
4
5
  export interface LaneChecksDeps {
5
6
  hqRoot: () => string;
@@ -7,6 +8,7 @@ export interface LaneChecksDeps {
7
8
  trackerEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
8
9
  trackerMeshEnabled: (company: LaneCompanyRef) => boolean | Promise<boolean>;
9
10
  now: () => Date;
11
+ meshStore?: (company: LaneCompanyRef, projectId: string) => TrackerStore;
10
12
  }
11
13
  type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
12
14
  export interface ProjectMapping {