@indigoai-us/hq-cli 5.225.0 → 5.226.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 (33) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/command-catalog.generated.d.ts +94 -0
  3. package/dist/command-catalog.generated.js +120 -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.d.ts +5 -0
  11. package/dist/commands/lanes.js +498 -312
  12. package/dist/commands/staff/reset-test-account.d.ts +110 -0
  13. package/dist/commands/staff/reset-test-account.js +268 -0
  14. package/dist/commands/staff/test-account.d.ts +51 -0
  15. package/dist/commands/staff/test-account.js +196 -0
  16. package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
  17. package/dist/lib/doctor/client-health-flag-token.js +66 -0
  18. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
  19. package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
  20. package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
  21. package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
  22. package/dist/lib/lanes/dropbox.d.ts +9 -0
  23. package/dist/lib/lanes/dropbox.js +70 -1
  24. package/dist/lib/lanes/reconcile.d.ts +2 -0
  25. package/dist/lib/lanes/reconcile.js +12 -0
  26. package/dist/lib/lanes/watcher-main.js +16 -0
  27. package/dist/lib/lanes/watcher.d.ts +4 -1
  28. package/dist/lib/lanes/watcher.js +77 -3
  29. package/dist/utils/cognito-session.d.ts +8 -0
  30. package/dist/utils/cognito-session.js +198 -3
  31. package/dist/utils/upload-author.d.ts +15 -0
  32. package/dist/utils/upload-author.js +39 -0
  33. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.226.0] — 2026-09-26
6
+
7
+ ### Added
8
+
9
+ - `hq staff reset-test-account <email>` wipes a designated test account through
10
+ the hq-pro admin API. Defaults to a dry-run that lists what would be deleted;
11
+ `--execute` prompts for the typed email confirmation before wiping, and
12
+ `--yes` skips the prompt. Refuses to run for callers whose stored email is
13
+ not on the staff list.
14
+ - `hq staff test-account add|remove|list` manages the designated-test-account
15
+ registry through the same admin API. `add` and `remove` require staff
16
+ callers; `list` returns the current registry entries.
17
+
18
+ ### Fixed
19
+
20
+ - `hq lanes message` now probes the worker PID and start time before queueing.
21
+ Watcher and reconcile recover accepted inbox messages through the same
22
+ resume path after classifying an exited worker; undrained messages on done,
23
+ killed, retired, or superseded lanes are archived as undeliverable.
24
+ - Failed queued-message recovery restores unaccepted claims to the inbox and
25
+ records the failure. Recovery also sees leftover draining and inflight files,
26
+ and synchronous classification shares watcher ownership to prevent duplicate
27
+ repair and resume work.
28
+ - `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.
29
+ - 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.
30
+ - 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.
31
+
5
32
  ## [5.225.0] — 2026-09-26
6
33
 
7
34
  ### Changed
@@ -20,7 +47,13 @@
20
47
 
21
48
  ### Fixed
22
49
 
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.
50
+ - When `hq daemon pin-hq-cloud` names a version that is not installed yet, the
51
+ daemon keeps running the newest installed hq-cloud instead of falling back
52
+ to the older copy bundled with the CLI, and logs a warning naming the
53
+ missing version. The updater then installs the pinned version and the
54
+ daemon switches to it. Before, a pin that did not match the installed copy
55
+ silently ran the bundled copy, which on one outpost meant a months-old sync
56
+ runner without recent fixes.
24
57
 
25
58
  ## [5.223.0] — 2026-09-26
26
59
 
@@ -34,6 +67,10 @@
34
67
  shares one local account probe across matching lanes per poll. Quota and
35
68
  model failures keep their existing account-selection path and reason.
36
69
 
70
+ - Client-health flag checks refresh expired person identity and use machine
71
+ identity when available. Identity refresh has a separate bound, and failed
72
+ lookups keep the existing doctor fallback path.
73
+
37
74
  ## [5.222.0] — 2026-09-26
38
75
 
39
76
  - Checkpoint ontology, behind `checkpoint.ontology.multi-writer` (off by
@@ -7961,6 +7961,22 @@ export declare const COMMAND_CATALOG: readonly [{
7961
7961
  readonly description: "JSON on stdout (always)";
7962
7962
  }];
7963
7963
  readonly subcommands: readonly [];
7964
+ }, {
7965
+ readonly name: "_recover-queued-messages";
7966
+ readonly description: "";
7967
+ readonly aliases: readonly [];
7968
+ readonly hidden: true;
7969
+ readonly usage: "[options] <lane-id>";
7970
+ readonly arguments: readonly [{
7971
+ readonly name: "lane-id";
7972
+ readonly required: true;
7973
+ readonly variadic: false;
7974
+ }];
7975
+ readonly options: readonly [{
7976
+ readonly flags: "--json";
7977
+ readonly description: "JSON on stdout (always)";
7978
+ }];
7979
+ readonly subcommands: readonly [];
7964
7980
  }, {
7965
7981
  readonly name: "_monitor-message";
7966
7982
  readonly description: "";
@@ -9490,5 +9506,83 @@ export declare const COMMAND_CATALOG: readonly [{
9490
9506
  }];
9491
9507
  readonly subcommands: readonly [];
9492
9508
  }];
9509
+ }, {
9510
+ readonly name: "staff";
9511
+ readonly description: "Staff-only commands (super-admin required)";
9512
+ readonly aliases: readonly [];
9513
+ readonly hidden: false;
9514
+ readonly usage: "[options] [command]";
9515
+ readonly arguments: readonly [];
9516
+ readonly options: readonly [];
9517
+ readonly subcommands: readonly [{
9518
+ readonly name: "reset-test-account";
9519
+ readonly description: "Reset a designated real-email test account (dry-run by default). Wraps POST /v1/admin/test-account/reset — super-admin only.";
9520
+ readonly aliases: readonly [];
9521
+ readonly hidden: false;
9522
+ readonly usage: "[options] <email>";
9523
+ readonly arguments: readonly [{
9524
+ readonly name: "email";
9525
+ readonly required: true;
9526
+ readonly variadic: false;
9527
+ }];
9528
+ readonly options: readonly [{
9529
+ readonly flags: "--execute";
9530
+ readonly description: "Actually delete artifacts. Prompts for a typed-email confirmation.";
9531
+ }, {
9532
+ readonly flags: "-y, --yes";
9533
+ readonly description: "Skip the typed-email confirmation prompt (still sends confirmEmail).";
9534
+ }];
9535
+ readonly subcommands: readonly [];
9536
+ }, {
9537
+ readonly name: "test-account";
9538
+ readonly description: "Manage the designated test-account registry (super-admin only). Wraps /v1/admin/test-account-registry.";
9539
+ readonly aliases: readonly [];
9540
+ readonly hidden: false;
9541
+ readonly usage: "[options] [command]";
9542
+ readonly arguments: readonly [];
9543
+ readonly options: readonly [];
9544
+ readonly subcommands: readonly [{
9545
+ readonly name: "add";
9546
+ readonly description: "Add an email to the registry so `hq staff reset-test-account` will sweep it.";
9547
+ readonly aliases: readonly [];
9548
+ readonly hidden: false;
9549
+ readonly usage: "[options] <email>";
9550
+ readonly arguments: readonly [{
9551
+ readonly name: "email";
9552
+ readonly required: true;
9553
+ readonly variadic: false;
9554
+ }];
9555
+ readonly options: readonly [{
9556
+ readonly flags: "--note <text>";
9557
+ readonly description: "Short reason recorded on the row";
9558
+ }];
9559
+ readonly subcommands: readonly [];
9560
+ }, {
9561
+ readonly name: "remove";
9562
+ readonly description: "Soft-delete a registry entry (row stays with active: false).";
9563
+ readonly aliases: readonly [];
9564
+ readonly hidden: false;
9565
+ readonly usage: "[options] <email>";
9566
+ readonly arguments: readonly [{
9567
+ readonly name: "email";
9568
+ readonly required: true;
9569
+ readonly variadic: false;
9570
+ }];
9571
+ readonly options: readonly [];
9572
+ readonly subcommands: readonly [];
9573
+ }, {
9574
+ readonly name: "list";
9575
+ readonly description: "List active registry entries.";
9576
+ readonly aliases: readonly [];
9577
+ readonly hidden: false;
9578
+ readonly usage: "[options]";
9579
+ readonly arguments: readonly [];
9580
+ readonly options: readonly [{
9581
+ readonly flags: "--include-inactive";
9582
+ readonly description: "Also list soft-deleted rows (active: false).";
9583
+ }];
9584
+ readonly subcommands: readonly [];
9585
+ }];
9586
+ }];
9493
9587
  }];
9494
9588
  //# sourceMappingURL=command-catalog.generated.d.ts.map
@@ -10296,6 +10296,27 @@ export const COMMAND_CATALOG = [
10296
10296
  ],
10297
10297
  "subcommands": []
10298
10298
  },
10299
+ {
10300
+ "name": "_recover-queued-messages",
10301
+ "description": "",
10302
+ "aliases": [],
10303
+ "hidden": true,
10304
+ "usage": "[options] <lane-id>",
10305
+ "arguments": [
10306
+ {
10307
+ "name": "lane-id",
10308
+ "required": true,
10309
+ "variadic": false
10310
+ }
10311
+ ],
10312
+ "options": [
10313
+ {
10314
+ "flags": "--json",
10315
+ "description": "JSON on stdout (always)"
10316
+ }
10317
+ ],
10318
+ "subcommands": []
10319
+ },
10299
10320
  {
10300
10321
  "name": "_monitor-message",
10301
10322
  "description": "",
@@ -12273,6 +12294,105 @@ export const COMMAND_CATALOG = [
12273
12294
  "subcommands": []
12274
12295
  }
12275
12296
  ]
12297
+ },
12298
+ {
12299
+ "name": "staff",
12300
+ "description": "Staff-only commands (super-admin required)",
12301
+ "aliases": [],
12302
+ "hidden": false,
12303
+ "usage": "[options] [command]",
12304
+ "arguments": [],
12305
+ "options": [],
12306
+ "subcommands": [
12307
+ {
12308
+ "name": "reset-test-account",
12309
+ "description": "Reset a designated real-email test account (dry-run by default). Wraps POST /v1/admin/test-account/reset — super-admin only.",
12310
+ "aliases": [],
12311
+ "hidden": false,
12312
+ "usage": "[options] <email>",
12313
+ "arguments": [
12314
+ {
12315
+ "name": "email",
12316
+ "required": true,
12317
+ "variadic": false
12318
+ }
12319
+ ],
12320
+ "options": [
12321
+ {
12322
+ "flags": "--execute",
12323
+ "description": "Actually delete artifacts. Prompts for a typed-email confirmation."
12324
+ },
12325
+ {
12326
+ "flags": "-y, --yes",
12327
+ "description": "Skip the typed-email confirmation prompt (still sends confirmEmail)."
12328
+ }
12329
+ ],
12330
+ "subcommands": []
12331
+ },
12332
+ {
12333
+ "name": "test-account",
12334
+ "description": "Manage the designated test-account registry (super-admin only). Wraps /v1/admin/test-account-registry.",
12335
+ "aliases": [],
12336
+ "hidden": false,
12337
+ "usage": "[options] [command]",
12338
+ "arguments": [],
12339
+ "options": [],
12340
+ "subcommands": [
12341
+ {
12342
+ "name": "add",
12343
+ "description": "Add an email to the registry so `hq staff reset-test-account` will sweep it.",
12344
+ "aliases": [],
12345
+ "hidden": false,
12346
+ "usage": "[options] <email>",
12347
+ "arguments": [
12348
+ {
12349
+ "name": "email",
12350
+ "required": true,
12351
+ "variadic": false
12352
+ }
12353
+ ],
12354
+ "options": [
12355
+ {
12356
+ "flags": "--note <text>",
12357
+ "description": "Short reason recorded on the row"
12358
+ }
12359
+ ],
12360
+ "subcommands": []
12361
+ },
12362
+ {
12363
+ "name": "remove",
12364
+ "description": "Soft-delete a registry entry (row stays with active: false).",
12365
+ "aliases": [],
12366
+ "hidden": false,
12367
+ "usage": "[options] <email>",
12368
+ "arguments": [
12369
+ {
12370
+ "name": "email",
12371
+ "required": true,
12372
+ "variadic": false
12373
+ }
12374
+ ],
12375
+ "options": [],
12376
+ "subcommands": []
12377
+ },
12378
+ {
12379
+ "name": "list",
12380
+ "description": "List active registry entries.",
12381
+ "aliases": [],
12382
+ "hidden": false,
12383
+ "usage": "[options]",
12384
+ "arguments": [],
12385
+ "options": [
12386
+ {
12387
+ "flags": "--include-inactive",
12388
+ "description": "Also list soft-deleted rows (active: false)."
12389
+ }
12390
+ ],
12391
+ "subcommands": []
12392
+ }
12393
+ ]
12394
+ }
12395
+ ]
12276
12396
  }
12277
12397
  ];
12278
12398
  //# 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
@@ -15,6 +15,7 @@ import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.
15
15
  import { LANE_HIRE_GRANTS_FLAG, MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
16
16
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
17
17
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
18
+ import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
18
19
  import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
19
20
  import { spawnLaneWorker, type KillProcessFn, type ProveDetachedFn, type SpawnProcessFn } from "../lib/lanes/spawn.js";
20
21
  export { spawnLaneWorker };
@@ -56,6 +57,10 @@ export interface LanesCommandDeps {
56
57
  proveDetached?: ProveDetachedFn;
57
58
  killProcess?: KillProcessFn;
58
59
  isAlive?: (pid: number) => boolean;
60
+ /** Test seam for worker PID identity checks. */
61
+ readStarttime?: (pid: number) => number | null;
62
+ /** Test seams for synchronous dead-worker classification and repair. */
63
+ watcherLoopDeps?: Pick<WatcherLoopDeps, "sleep" | "spawnRepair">;
59
64
  stopSleepMs?: (ms: number) => void;
60
65
  livenessDeps?: LivenessDeps;
61
66
  presenceConnected?: boolean;