@indigoai-us/hq-cli 5.345.160 → 5.345.161

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 (60) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/assets/scaffold/core/scripts/session-journal.sh +116 -24
  3. package/dist/command-catalog.generated.d.ts +218 -5
  4. package/dist/command-catalog.generated.js +269 -5
  5. package/dist/commands/auth.js +6 -5
  6. package/dist/commands/core-session-helper-runtime.d.ts +1 -0
  7. package/dist/commands/core-session-journal-native.d.ts +1 -0
  8. package/dist/commands/core-session-journal-native.js +158 -34
  9. package/dist/commands/feedback.d.ts +12 -0
  10. package/dist/commands/feedback.js +52 -0
  11. package/dist/commands/files-browse.d.ts +8 -0
  12. package/dist/commands/files-browse.js +83 -1
  13. package/dist/commands/install-global.d.ts +2 -0
  14. package/dist/commands/install-global.js +24 -1
  15. package/dist/commands/integrations-connect.js +1 -0
  16. package/dist/commands/integrations-manage.d.ts +2 -0
  17. package/dist/commands/integrations-manage.js +6 -4
  18. package/dist/commands/lanes-link.js +5 -3
  19. package/dist/commands/lanes-session-start.d.ts +12 -0
  20. package/dist/commands/lanes-session-start.js +93 -12
  21. package/dist/commands/meetings.d.ts +11 -0
  22. package/dist/commands/meetings.js +41 -11
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2486 -1794
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/index.js +21 -0
  26. package/dist/lanes-session-start-argv.d.ts +7 -0
  27. package/dist/lanes-session-start-argv.js +9 -0
  28. package/dist/lib/bot/log.d.ts +3 -2
  29. package/dist/lib/bot/progress.d.ts +23 -1
  30. package/dist/lib/bot/progress.js +43 -5
  31. package/dist/lib/bot/run.js +2 -0
  32. package/dist/lib/bot/runtime/claude.d.ts +12 -0
  33. package/dist/lib/bot/runtime/claude.js +20 -0
  34. package/dist/lib/bot/runtime/index.d.ts +10 -1
  35. package/dist/lib/bot/runtime/index.js +14 -2
  36. package/dist/lib/bot/runtime/types.d.ts +8 -0
  37. package/dist/lib/doctor/checks/ps1-shims.d.ts +21 -0
  38. package/dist/lib/doctor/checks/ps1-shims.js +82 -0
  39. package/dist/lib/doctor/fix/apply.d.ts +13 -1
  40. package/dist/lib/doctor/fix/apply.js +38 -3
  41. package/dist/lib/doctor/fix/remediation.d.ts +9 -1
  42. package/dist/lib/doctor/fix/remediation.js +22 -0
  43. package/dist/lib/doctor/registry.js +2 -0
  44. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  45. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  46. package/dist/lib/lanes/links.d.ts +2 -1
  47. package/dist/lib/lanes/links.js +235 -17
  48. package/dist/lib/lanes/registry-flag.d.ts +24 -0
  49. package/dist/lib/lanes/registry-flag.js +72 -6
  50. package/dist/lib/lanes/store.d.ts +4 -2
  51. package/dist/lib/lanes/store.js +8 -8
  52. package/dist/lib/windows/ps1-shims.d.ts +60 -0
  53. package/dist/lib/windows/ps1-shims.js +152 -0
  54. package/dist/utils/self-update.d.ts +2 -0
  55. package/dist/utils/self-update.js +8 -0
  56. package/dist/utils/staged-npm-install.js +13 -2
  57. package/dist/utils/token-keychain.d.ts +11 -8
  58. package/dist/utils/token-keychain.js +36 -15
  59. package/package.json +8 -8
  60. package/scripts/install-hook-engine.mjs +12 -0
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { randomUUID } from "node:crypto";
3
4
  import { fileURLToPath } from "node:url";
4
5
  const USAGE = `session-journal.sh: usage:
5
6
  session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
@@ -163,6 +164,58 @@ function isAgedPidlessCounterLock(lock, now) {
163
164
  return false;
164
165
  }
165
166
  }
167
+ function reclaimStaleCounterTempDirs(lock, now) {
168
+ const parent = path.dirname(lock);
169
+ // Publishers create <lock>.tmp.XXXXXX, matching the shell helper. Older native
170
+ // builds used <lock>.tmp-XXXXXX, so reclaim aged orphans with either prefix.
171
+ const prefixes = [`${path.basename(lock)}.tmp.`, `${path.basename(lock)}.tmp-`];
172
+ let names;
173
+ try {
174
+ names = fs.readdirSync(parent);
175
+ }
176
+ catch {
177
+ return;
178
+ }
179
+ for (const name of names) {
180
+ if (!prefixes.some((prefix) => name.startsWith(prefix)))
181
+ continue;
182
+ const temp = path.join(parent, name);
183
+ if (!isAgedPidlessCounterLock(temp, now))
184
+ continue;
185
+ const read = (file) => {
186
+ try {
187
+ return fs.readFileSync(path.join(temp, file), "utf8").trim();
188
+ }
189
+ catch {
190
+ return "";
191
+ }
192
+ };
193
+ const owner = read("pid");
194
+ const windowsOwner = read("winpid");
195
+ const token = read("owner-token");
196
+ if (owner && !/^\d+$/u.test(owner))
197
+ continue;
198
+ if (windowsOwner && !/^\d+$/u.test(windowsOwner))
199
+ continue;
200
+ if (owner && Number(owner) > 1 && counterLockOwnerIsAlive(temp, Number(owner)))
201
+ continue;
202
+ if (!owner && windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
203
+ continue;
204
+ if (read("pid") !== owner || read("winpid") !== windowsOwner || read("owner-token") !== token
205
+ || !isAgedPidlessCounterLock(temp, now))
206
+ continue;
207
+ if (owner && counterLockOwnerIsAlive(temp, Number(owner)))
208
+ continue;
209
+ if (windowsOwner && Number(windowsOwner) > 1 && processIsAlive(Number(windowsOwner)))
210
+ continue;
211
+ const stale = `${temp}.stale-${process.pid}-${Math.random().toString(16).slice(2)}`;
212
+ try {
213
+ fs.renameSync(temp, stale);
214
+ fs.rmSync(stale, { recursive: true, force: true });
215
+ }
216
+ catch { /* another waiter cleaned it or the owner resumed */ }
217
+ }
218
+ }
166
219
  function reclaimStaleCounterLock(lock, now) {
167
220
  let rawOwner;
168
221
  try {
@@ -184,6 +237,11 @@ function reclaimStaleCounterLock(lock, now) {
184
237
  return false;
185
238
  const owner = rawOwner ? Number.parseInt(rawOwner, 10) : 0;
186
239
  const windowsOwner = rawWindowsPid ? Number.parseInt(rawWindowsPid, 10) : 0;
240
+ let observedToken = "";
241
+ try {
242
+ observedToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
243
+ }
244
+ catch { /* legacy lock */ }
187
245
  if (rawOwner && (owner <= 1 || counterLockOwnerIsAlive(lock, owner)))
188
246
  return false;
189
247
  if (!rawOwner && windowsOwner > 1 && processIsAlive(windowsOwner))
@@ -219,7 +277,12 @@ function reclaimStaleCounterLock(lock, now) {
219
277
  catch {
220
278
  currentRawWindowsPid = "";
221
279
  }
222
- const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid;
280
+ let currentToken = "";
281
+ try {
282
+ currentToken = fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
283
+ }
284
+ catch { /* legacy lock */ }
285
+ const sameOwner = currentRawOwner === rawOwner && currentRawWindowsPid === rawWindowsPid && currentToken === observedToken;
223
286
  const stillStale = rawOwner
224
287
  ? sameOwner && !counterLockOwnerIsAlive(lock, owner)
225
288
  : sameOwner && !(windowsOwner > 1 && processIsAlive(windowsOwner)) && isAgedPidlessCounterLock(lock, now);
@@ -239,7 +302,15 @@ function reclaimStaleCounterLock(lock, now) {
239
302
  catch { /* best effort */ }
240
303
  return false;
241
304
  }
242
- function withCounterLock(lock, env, writers, now, action) {
305
+ function readLockToken(lock) {
306
+ try {
307
+ return fs.readFileSync(path.join(lock, "owner-token"), "utf8").trim();
308
+ }
309
+ catch {
310
+ return "";
311
+ }
312
+ }
313
+ function withCounterLock(lock, env, writers, now, action, beforeCounterAction) {
243
314
  let i = 0;
244
315
  let acquired = false;
245
316
  let tries = env.HQ_JOURNAL_COUNTER_LOCK_TRIES ?? "250";
@@ -247,22 +318,82 @@ function withCounterLock(lock, env, writers, now, action) {
247
318
  tries = "250";
248
319
  const retryLimit = Number.parseInt(tries, 10);
249
320
  while (i < retryLimit) {
321
+ reclaimStaleCounterTempDirs(lock, now);
250
322
  try {
251
- fs.mkdirSync(lock);
252
- acquired = true;
323
+ const temp = fs.mkdtempSync(`${lock}.tmp.`);
324
+ const ownerToken = randomUUID();
253
325
  try {
254
326
  if (process.platform === "win32")
255
- fs.writeFileSync(path.join(lock, "winpid"), `${process.pid}\n`, { flag: "wx" });
256
- fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, { flag: "wx" });
327
+ fs.writeFileSync(path.join(temp, "winpid"), `${process.pid}\n`, { flag: "wx" });
328
+ fs.writeFileSync(path.join(temp, "pid"), `${process.pid}\n`, { flag: "wx" });
329
+ fs.writeFileSync(path.join(temp, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
330
+ const publishClaim = `${lock}.publish`;
331
+ try {
332
+ fs.mkdirSync(publishClaim);
333
+ }
334
+ catch (error) {
335
+ try {
336
+ fs.rmSync(temp, { recursive: true, force: true });
337
+ }
338
+ catch { /* best effort */ }
339
+ if (error.code === "EEXIST" && reclaimStaleCounterLock(publishClaim, now))
340
+ continue;
341
+ throw error;
342
+ }
343
+ try {
344
+ fs.writeFileSync(path.join(publishClaim, "owner-token"), `${ownerToken}\n`, { flag: "wx" });
345
+ fs.writeFileSync(path.join(publishClaim, "pid"), `${process.pid}\n`, { flag: "wx" });
346
+ // renameSync can replace an existing empty PID-less directory on
347
+ // POSIX. Refuse a pre-existing lock while publishers are serialized.
348
+ if (readLockToken(publishClaim) !== ownerToken || fs.existsSync(lock))
349
+ throw new Error("counter lock already exists");
350
+ fs.renameSync(temp, lock);
351
+ }
352
+ finally {
353
+ if (readLockToken(publishClaim) === ownerToken)
354
+ fs.rmSync(publishClaim, { recursive: true, force: true });
355
+ }
356
+ acquired = true;
257
357
  }
258
- catch {
358
+ catch (error) {
359
+ try {
360
+ fs.rmSync(temp, { recursive: true, force: true });
361
+ }
362
+ catch { /* best effort */ }
363
+ throw error;
364
+ }
365
+ if (!acquired)
366
+ continue;
367
+ if (readLockToken(lock) !== ownerToken) {
368
+ acquired = false;
369
+ i++;
370
+ continue;
371
+ }
372
+ beforeCounterAction?.();
373
+ if (readLockToken(lock) !== ownerToken) {
374
+ acquired = false;
375
+ i++;
376
+ continue;
377
+ }
378
+ const completed = action(ownerToken);
379
+ if (!completed) {
380
+ if (readLockToken(lock) === ownerToken) {
381
+ try {
382
+ fs.rmSync(lock, { recursive: true, force: true });
383
+ }
384
+ catch { /* best effort */ }
385
+ }
386
+ acquired = false;
387
+ i++;
388
+ continue;
389
+ }
390
+ if (readLockToken(lock) === ownerToken) {
259
391
  try {
260
392
  fs.rmSync(lock, { recursive: true, force: true });
261
393
  }
262
394
  catch { /* best effort */ }
263
- return;
264
395
  }
265
- break;
396
+ return;
266
397
  }
267
398
  catch {
268
399
  if (reclaimStaleCounterLock(lock, now))
@@ -276,22 +407,6 @@ function withCounterLock(lock, env, writers, now, action) {
276
407
  writers.stderr("session-journal: tool-counter: lock remained busy; skipped update\n");
277
408
  return;
278
409
  }
279
- try {
280
- action();
281
- }
282
- finally {
283
- let owner = 0;
284
- try {
285
- owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10) || 0;
286
- }
287
- catch { /* lock disappeared */ }
288
- if (owner === process.pid) {
289
- try {
290
- fs.rmSync(lock, { recursive: true, force: true });
291
- }
292
- catch { /* best effort */ }
293
- }
294
- }
295
410
  }
296
411
  function nextSequence(dir) {
297
412
  let names = [];
@@ -313,7 +428,7 @@ function options(args, valueFlags) {
313
428
  }
314
429
  return values;
315
430
  }
316
- function runToolCounter(root, action, sessionArg, env, now, writers) {
431
+ function runToolCounter(root, action, sessionArg, env, now, writers, beforeCounterAction) {
317
432
  const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
318
433
  const key = counterKey(rawKey);
319
434
  const date = now().toISOString().slice(0, 10);
@@ -335,27 +450,36 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
335
450
  writers.stderr(`${sessionJournalAsset}: line ${line}: ${printableCounter}: Is a directory\n`);
336
451
  };
337
452
  if (action === "increment") {
338
- withCounterLock(lock, env, writers, now, () => {
453
+ withCounterLock(lock, env, writers, now, (ownerToken) => {
454
+ if (readLockToken(lock) !== ownerToken)
455
+ return false;
456
+ const next = readCounter(counter) + 1;
457
+ if (readLockToken(lock) !== ownerToken)
458
+ return false;
339
459
  try {
340
- fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
460
+ fs.writeFileSync(counter, `${next}\n`);
341
461
  }
342
462
  catch (error) {
343
- reportDirectoryWriteFailure(error, 457);
463
+ reportDirectoryWriteFailure(error, 545);
344
464
  }
345
- });
465
+ return true;
466
+ }, beforeCounterAction);
346
467
  }
347
468
  else if (action === "read") {
348
469
  writers.stdout(`${readCounter(counter)}\n`);
349
470
  }
350
471
  else if (action === "reset") {
351
- withCounterLock(lock, env, writers, now, () => {
472
+ withCounterLock(lock, env, writers, now, (ownerToken) => {
473
+ if (readLockToken(lock) !== ownerToken)
474
+ return false;
352
475
  try {
353
476
  fs.writeFileSync(counter, "0\n");
354
477
  }
355
478
  catch (error) {
356
- reportDirectoryWriteFailure(error, 465);
479
+ reportDirectoryWriteFailure(error, 556);
357
480
  }
358
- });
481
+ return true;
482
+ }, beforeCounterAction);
359
483
  }
360
484
  else {
361
485
  writers.stderr("session-journal: tool-counter: subcommand required (increment|read|reset)\n");
@@ -519,7 +643,7 @@ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
519
643
  if (command === "tool-counter") {
520
644
  const action = rest[0] ?? "";
521
645
  const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
522
- runToolCounter(root, action, session, runtime.env, runtime.now, writers);
646
+ runToolCounter(root, action, session, runtime.env, runtime.now, writers, dependencies.beforeCounterAction);
523
647
  return 0;
524
648
  }
525
649
  writers.stderr(USAGE);
@@ -91,5 +91,17 @@ export declare function postFeedbackUpdate(opts: {
91
91
  publicUrl: string | null;
92
92
  slack: string | null;
93
93
  }>;
94
+ /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
95
+ export declare function normalizeFeedbackAssignee(value: string): string;
96
+ /**
97
+ * Reassign a ticket. A person's own login may move a ticket that another
98
+ * person claimed; agent and operator service tokens get 409
99
+ * TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
100
+ */
101
+ export declare function assignFeedback(opts: {
102
+ id: string;
103
+ token: string;
104
+ assignee: string;
105
+ }): Promise<void>;
94
106
  export declare function registerFeedbackCommand(program: Command): void;
95
107
  //# sourceMappingURL=feedback.d.ts.map
@@ -285,6 +285,57 @@ export async function postFeedbackUpdate(opts) {
285
285
  slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
286
286
  };
287
287
  }
288
+ const ASSIGNEE_RE = /^(?:prs_[A-Za-z0-9]+|[^\s@]+@[^\s@]+\.[^\s@]+)$/;
289
+ /** Mirror of hq-pro normalizeAssignee: a prs_ uid as-is, an email lowercased. */
290
+ export function normalizeFeedbackAssignee(value) {
291
+ const trimmed = value.trim();
292
+ if (!ASSIGNEE_RE.test(trimmed)) {
293
+ throw new Error(`Invalid --to '${value}': expected an email address or a prs_ uid.`);
294
+ }
295
+ return trimmed.startsWith("prs_") ? trimmed : trimmed.toLowerCase();
296
+ }
297
+ /**
298
+ * Reassign a ticket. A person's own login may move a ticket that another
299
+ * person claimed; agent and operator service tokens get 409
300
+ * TICKET_CLAIMED_BY_HUMAN from the server for those tickets.
301
+ */
302
+ export async function assignFeedback(opts) {
303
+ const res = await vaultApiFetch({
304
+ token: opts.token,
305
+ path: `/v1/feedback/${opts.id}`,
306
+ method: "PATCH",
307
+ body: { assignee: opts.assignee },
308
+ });
309
+ if (res.ok)
310
+ return;
311
+ const data = (await res.json().catch(() => ({})));
312
+ const message = typeof data.error === "string" ? data.error : res.statusText;
313
+ if (data.code === "TICKET_CLAIMED_BY_HUMAN") {
314
+ throw new Error(`${message} Run hq feedback assign from your own hq login (hq login), not an agent or operator token.`);
315
+ }
316
+ throw new Error(`Failed to assign ${opts.id}: ${message}`);
317
+ }
318
+ function registerAssignSubcommand(feedbackCmd) {
319
+ feedbackCmd
320
+ .command("assign")
321
+ .description("Reassign a ticket to a person, including one another person has claimed")
322
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
323
+ .requiredOption("--to <assignee>", "New assignee: an email address or a prs_ uid")
324
+ .action(async (id, opts) => {
325
+ try {
326
+ assertFeedbackId(id);
327
+ const assignee = normalizeFeedbackAssignee(opts.to);
328
+ const token = await resolveStaffToken();
329
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
330
+ await assignFeedback({ id, token, assignee });
331
+ console.log(`Assigned: ${id} -> ${assignee}`);
332
+ }
333
+ catch (err) {
334
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
335
+ process.exit(1);
336
+ }
337
+ });
338
+ }
288
339
  function registerLinkSubcommand(feedbackCmd) {
289
340
  feedbackCmd
290
341
  .command("link")
@@ -447,5 +498,6 @@ export function registerFeedbackCommand(program) {
447
498
  registerLinkSubcommand(feedbackCmd);
448
499
  registerUpdateSubcommand(feedbackCmd);
449
500
  registerFeedbackShowCommand(feedbackCmd);
501
+ registerAssignSubcommand(feedbackCmd);
450
502
  }
451
503
  //# sourceMappingURL=feedback.js.map
@@ -459,6 +459,14 @@ export declare const STORAGE_DOWNLOAD_FAILED_CODE = "FILES_STORAGE_DOWNLOAD_FAIL
459
459
  * firewall that does not allow the bucket host, and we say so.
460
460
  */
461
461
  export declare function presignedDownloadError(key: string, url: string, dl: Response): Promise<Error>;
462
+ /**
463
+ * Turn a presigned download that never got an HTTP response (connection
464
+ * refused or reset, DNS failure, timeout, or a TLS failure) into an error that
465
+ * names the storage host and tells the operator to allow it. Returns
466
+ * undefined for any other error so the caller rethrows it unchanged. Only the
467
+ * URL host is used; the signed query string never reaches the message.
468
+ */
469
+ export declare function presignedDownloadTransportError(key: string, url: string, err: unknown): Error | undefined;
462
470
  export declare function createCompanyPresignClient(input: {
463
471
  token: string;
464
472
  companyUid: string;
@@ -760,6 +760,82 @@ export async function presignedDownloadError(key, url, dl) {
760
760
  ...(storageCode ? { storageCode } : {}),
761
761
  });
762
762
  }
763
+ /** Socket-level error codes that mean the storage host could not be reached. */
764
+ const TRANSPORT_BLOCK_CODES = new Set([
765
+ "ECONNREFUSED",
766
+ "ECONNRESET",
767
+ "ETIMEDOUT",
768
+ "EHOSTUNREACH",
769
+ "ENETUNREACH",
770
+ "EPIPE",
771
+ "ENOTFOUND",
772
+ "EAI_AGAIN",
773
+ "UND_ERR_CONNECT_TIMEOUT",
774
+ "UND_ERR_HEADERS_TIMEOUT",
775
+ "UND_ERR_SOCKET",
776
+ ]);
777
+ /** TLS failures, usually a TLS-inspecting proxy whose certificate Node does not trust. */
778
+ const TLS_BLOCK_CODES = new Set([
779
+ "CERT_HAS_EXPIRED",
780
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
781
+ "SELF_SIGNED_CERT_IN_CHAIN",
782
+ "UNABLE_TO_GET_ISSUER_CERT",
783
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
784
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
785
+ "ERR_TLS_CERT_ALTNAME_INVALID",
786
+ "ERR_SSL_WRONG_VERSION_NUMBER",
787
+ "EPROTO",
788
+ ]);
789
+ function errorChainCode(err) {
790
+ let cur = err;
791
+ for (let depth = 0; depth < 5 && cur && typeof cur === "object"; depth++) {
792
+ const code = cur.code;
793
+ if (typeof code === "string" && code.length > 0)
794
+ return code;
795
+ cur = cur.cause;
796
+ }
797
+ return undefined;
798
+ }
799
+ /**
800
+ * Turn a presigned download that never got an HTTP response (connection
801
+ * refused or reset, DNS failure, timeout, or a TLS failure) into an error that
802
+ * names the storage host and tells the operator to allow it. Returns
803
+ * undefined for any other error so the caller rethrows it unchanged. Only the
804
+ * URL host is used; the signed query string never reaches the message.
805
+ */
806
+ export function presignedDownloadTransportError(key, url, err) {
807
+ const name = err?.name;
808
+ const timedOut = name === "TimeoutError";
809
+ const code = timedOut ? "TIMEOUT" : errorChainCode(err);
810
+ if (!code)
811
+ return undefined;
812
+ const tls = TLS_BLOCK_CODES.has(code) || code.startsWith("ERR_TLS_") || code.startsWith("ERR_SSL_");
813
+ if (!timedOut && !tls && !TRANSPORT_BLOCK_CODES.has(code))
814
+ return undefined;
815
+ let host = "the storage host";
816
+ try {
817
+ host = new URL(url).host;
818
+ }
819
+ catch {
820
+ // keep the generic label
821
+ }
822
+ const what = tls
823
+ ? `the TLS connection to ${host} failed (${code}). A proxy that inspects HTTPS traffic is the usual cause`
824
+ : timedOut
825
+ ? `the connection to ${host} timed out. A firewall or egress rule that drops traffic is the usual cause`
826
+ : `the connection to ${host} failed (${code}). A firewall, proxy or egress rule on this network is the usual cause`;
827
+ const message = `Failed to download '${key}': ${what}. Your file access was granted; the ` +
828
+ `network blocked the storage host. Ask your network operator to allow ` +
829
+ `outbound HTTPS (TCP 443) to ${host} (HQ vault files are served from ` +
830
+ `*.s3.us-east-1.amazonaws.com), then retry.`;
831
+ return Object.assign(new Error(message, { cause: err }), {
832
+ code: STORAGE_DOWNLOAD_FAILED_CODE,
833
+ key,
834
+ blockedHost: host,
835
+ transportCode: code,
836
+ fromStorage: false,
837
+ });
838
+ }
763
839
  export function createCompanyPresignClient(input) {
764
840
  const { token, companyUid, companySlug, baseUrl } = input;
765
841
  const denied = (message, key, extra) => Object.assign(new Error(message), {
@@ -829,7 +905,13 @@ export function createCompanyPresignClient(input) {
829
905
  }
830
906
  throw new Error(message);
831
907
  }
832
- const dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
908
+ let dl;
909
+ try {
910
+ dl = await fetch(first.url, { signal: AbortSignal.timeout(120_000) });
911
+ }
912
+ catch (err) {
913
+ throw presignedDownloadTransportError(key, first.url, err) ?? err;
914
+ }
833
915
  if (!dl.ok) {
834
916
  throw await presignedDownloadError(key, first.url, dl);
835
917
  }
@@ -83,6 +83,8 @@ export interface GlobalCommandOpts {
83
83
  trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
84
84
  /** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
85
85
  shimPathCheck?: () => ShimPathCheck | null;
86
+ /** Test seam: the Windows npm prefix holding hq.cmd. Null off Windows. */
87
+ ps1ShimDir?: () => string | null;
86
88
  }
87
89
  export declare function resolveInstallRoot(explicit: string | undefined, options?: {
88
90
  env?: NodeJS.ProcessEnv;
@@ -27,7 +27,8 @@ import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
27
27
  import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
28
28
  import { trustCodexUserHooks } from '../utils/hook-trust.js';
29
29
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
30
- import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
30
+ import { checkShimOnPath, ensureShimOnUserPath, npmShimDir, shimPathWarning } from '../lib/windows/user-path.js';
31
+ import { removeHqCliPs1Shims } from '../lib/windows/ps1-shims.js';
31
32
  import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
32
33
  import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
33
34
  export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
@@ -1156,6 +1157,24 @@ export function resolveInstallRoot(explicit, options = {}) {
1156
1157
  throw Object.assign(new Error('Run from inside your HQ checkout, or pass --hq-root <dir>.'), { expected: true });
1157
1158
  }
1158
1159
  }
1160
+ function defaultPs1ShimDir() {
1161
+ return process.platform === 'win32' ? npmShimDir(process.argv[1], fs.existsSync) : null;
1162
+ }
1163
+ /**
1164
+ * PowerShell picks npm's hq.ps1 over hq.cmd, and a Restricted execution policy
1165
+ * blocks it. Remove only npm's own hq-cli launchers; never change the policy.
1166
+ */
1167
+ function removePs1ShimsReport(dir) {
1168
+ const result = removeHqCliPs1Shims(dir);
1169
+ const lines = [];
1170
+ if (result.removed.length > 0) {
1171
+ lines.push(` windows: removed ${result.removed.map((f) => path.win32.basename(f)).join(', ')} so PowerShell now runs hq.cmd.`);
1172
+ }
1173
+ for (const failure of result.failed) {
1174
+ lines.push(` windows: could not remove ${failure.file} (${failure.reason}). Run hq.cmd instead of hq in PowerShell.`);
1175
+ }
1176
+ return lines;
1177
+ }
1159
1178
  /** Entry used by `hq install --global` (wired from pkg-install.ts). */
1160
1179
  export async function runInstallGlobal(opts) {
1161
1180
  const runtime = assertRuntime(opts.runtime);
@@ -1201,6 +1220,10 @@ export async function runInstallGlobal(opts) {
1201
1220
  if (shimPath)
1202
1221
  for (const line of shimPathWarning(shimPath))
1203
1222
  (opts.log ?? console.log)(line);
1223
+ const ps1Dir = (opts.ps1ShimDir ?? defaultPs1ShimDir)();
1224
+ if (ps1Dir)
1225
+ for (const line of removePs1ShimsReport(ps1Dir))
1226
+ (opts.log ?? console.log)(line);
1204
1227
  }
1205
1228
  }
1206
1229
  /**
@@ -1449,6 +1449,7 @@ export function registerConnectCommands(integrations) {
1449
1449
  .option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
1450
1450
  .option("--client-id <id>", "Client id of an OAuth app you registered with the provider yourself")
1451
1451
  .option("--client-secret-stdin", "Read that app's client secret from stdin")
1452
+ .option("--scopes <list>", "Permissions to request, space- or comma-separated (default: the permissions the app was approved for last time)")
1452
1453
  .option("--no-browser", "Print the sign-in URL instead of opening a browser")
1453
1454
  .option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
1454
1455
  .option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
@@ -26,6 +26,8 @@ interface Principal {
26
26
  * guessing — a misparsed principal silently grants access to the wrong party.
27
27
  */
28
28
  export declare function parsePrincipal(raw: string): Principal;
29
+ /** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
30
+ export declare const PRINCIPAL_HELP = "Email, uid, vault group (grp_\u2026), or `everyone`";
29
31
  export declare function registerManageCommands(integrations: Command): void;
30
32
  /** Order-insensitive identity of an allowlist, for change detection. */
31
33
  export declare function allowlistFingerprint(grants: WriteAllowlistGrant[]): string;
@@ -76,6 +76,8 @@ export function parsePrincipal(raw) {
76
76
  }
77
77
  throw new IntegrationsCliError(`Could not tell what '${raw}' refers to. Use an email address, a uid (prs_… / agt_… / grp_…), \`everyone\`, or an explicit \`person:<id>\` / \`group:<id>\` form.`, { expected: true });
78
78
  }
79
+ /** `--with` / `--to` / `--from` help. Groups resolve membership at call time. */
80
+ export const PRINCIPAL_HELP = "Email, uid, vault group (grp_…), or `everyone`";
79
81
  /** How a principal reads back in output, without leaking a raw uid as a name. */
80
82
  function describePrincipal(entry) {
81
83
  if (entry.granteeType === "company-wide")
@@ -379,7 +381,7 @@ export function registerManageCommands(integrations) {
379
381
  .option("--provider <slug>", "Connected app (e.g. linear)")
380
382
  .option("--connection <id>", "Connection id (acct_…)")
381
383
  .requiredOption("--tool <name>", "The tool to authorize")
382
- .requiredOption("--to <principal>", "Email, uid, or `everyone`")
384
+ .requiredOption("--to <principal>", PRINCIPAL_HELP)
383
385
  .option("--permission <level>", "read, write, or admin", "write")
384
386
  .option("--json", "Machine-readable output")
385
387
  .action(async (app, opts) => {
@@ -418,7 +420,7 @@ export function registerManageCommands(integrations) {
418
420
  .option("--provider <slug>", "Connected app (e.g. linear)")
419
421
  .option("--connection <id>", "Connection id (acct_…)")
420
422
  .requiredOption("--tool <name>", "The tool to stop authorizing")
421
- .requiredOption("--from <principal>", "Email, uid, or `everyone`")
423
+ .requiredOption("--from <principal>", PRINCIPAL_HELP)
422
424
  .option("--json", "Machine-readable output")
423
425
  .action(async (app, opts) => {
424
426
  const principal = parsePrincipal(opts.from);
@@ -465,7 +467,7 @@ export function registerManageCommands(integrations) {
465
467
  .option("--company <slug>", "Company slug, e.g. indigo")
466
468
  .option("--provider <slug>", "Connected app (e.g. linear)")
467
469
  .option("--connection <id>", "Connection id (acct_…)")
468
- .requiredOption("--with <principal>", "Email, uid, or `everyone`")
470
+ .requiredOption("--with <principal>", PRINCIPAL_HELP)
469
471
  .option("--permission <level>", "read, write, or admin", "write")
470
472
  .option("--json", "Machine-readable output")
471
473
  .action(async (app, opts) => {
@@ -495,7 +497,7 @@ export function registerManageCommands(integrations) {
495
497
  .option("--company <slug>", "Company slug, e.g. indigo")
496
498
  .option("--provider <slug>", "Connected app (e.g. linear)")
497
499
  .option("--connection <id>", "Connection id (acct_…)")
498
- .requiredOption("--from <principal>", "Email, uid, or `everyone`")
500
+ .requiredOption("--from <principal>", PRINCIPAL_HELP)
499
501
  .option("--json", "Machine-readable output")
500
502
  .action(async (app, opts) => {
501
503
  const principal = parsePrincipal(opts.from);
@@ -14,6 +14,7 @@ function addLinkOptions(command) {
14
14
  .option("--engine <engine>", "Session engine")
15
15
  .option("--title <title>", "Session title")
16
16
  .option("--host-session <id>", "Host session id")
17
+ .option("--claims <ids>", "Exact comma-separated recipient receipt IDs")
17
18
  .option("--text <message>", "Message text")
18
19
  .option("--text-file <path>", "Read message text from a file")
19
20
  .option("--state <state>", "Child state")
@@ -35,7 +36,7 @@ function commandError(error) {
35
36
  }
36
37
  export function registerLanesLinkCommand(lanes) {
37
38
  const link = lanes.command("link").description("Link live sessions to a conductor through session inboxes");
38
- const commands = ["open", "join", "send", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
39
+ const commands = ["open", "join", "send", "ack", "capabilities", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
39
40
  for (const subcommand of commands) {
40
41
  addLinkOptions(link.command(subcommand)).action(async (options) => {
41
42
  try {
@@ -56,7 +57,8 @@ export function registerLanesLinkCommand(lanes) {
56
57
  }
57
58
  const deliver = link.command("_deliver", { hidden: true })
58
59
  .description("Internal linked-session hook delivery")
59
- .requiredOption("--event <event>", "Hook event");
60
+ .requiredOption("--event <event>", "Hook event")
61
+ .option("--ack-mode <mode>", "Internal explicit recipient acknowledgement");
60
62
  deliver.action(async (options) => {
61
63
  // Hook commands must not update/re-exec the CLI inside the provider's time budget.
62
64
  process.env.HQ_NO_UPDATE_CHECK = "1";
@@ -70,7 +72,7 @@ export function registerLanesLinkCommand(lanes) {
70
72
  env: process.env,
71
73
  defaultValue: true,
72
74
  });
73
- const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, input }, enabled);
75
+ const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, ackMode: options.ackMode, input }, enabled);
74
76
  if (result.output)
75
77
  writeJson(result.output);
76
78
  }
@@ -8,9 +8,21 @@ export interface SessionStartOptions {
8
8
  stderr?: Pick<NodeJS.WriteStream, "write">;
9
9
  resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
10
10
  startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
11
+ /** Starts the off-hot-path registry read when an offline flag value is missing or old. */
12
+ refreshFlags?: (env: NodeJS.ProcessEnv) => void;
11
13
  }
14
+ /** Offline flag values older than this trigger one background registry read. */
15
+ export declare const SESSION_START_FLAG_REFRESH_AFTER_MS: number;
12
16
  /** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
13
17
  export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, rootScoped?: boolean): void;
18
+ /**
19
+ * Start a detached `hq lanes session-start --refresh-flags` so the registry
20
+ * read never runs inside the SessionStart hook. A short lease keeps a burst of
21
+ * session starts to one refresh. An env without a state dir never spawns.
22
+ */
23
+ export declare function startSessionStartFlagRefresh(env: NodeJS.ProcessEnv, spawnDetached?: typeof spawn, now?: number): boolean;
24
+ /** Read the session-start flags from the registry, refreshing the offline cache. */
25
+ export declare function refreshSessionStartFlags(env?: NodeJS.ProcessEnv): Promise<void>;
14
26
  /** SessionStart-compatible, fail-open conduct bootstrap. */
15
27
  export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
16
28
  export declare function registerLaneSessionStartCommand(lanes: Command): void;