@indigoai-us/hq-cli 5.345.156 → 5.345.159

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 (144) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +531 -6
  6. package/dist/command-catalog.generated.js +674 -27
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-kit.js +8 -6
  12. package/dist/commands/bot.js +7 -4
  13. package/dist/commands/channel-suggest.d.ts +56 -0
  14. package/dist/commands/channel-suggest.js +170 -0
  15. package/dist/commands/channels.js +7 -1
  16. package/dist/commands/cloud.d.ts +45 -0
  17. package/dist/commands/cloud.js +150 -10
  18. package/dist/commands/company.js +4 -3
  19. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  20. package/dist/commands/core-checkpoint-native.js +67 -2
  21. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  22. package/dist/commands/core-pipeline-driver-native.js +131 -5
  23. package/dist/commands/core.js +12 -0
  24. package/dist/commands/crm.js +83 -6
  25. package/dist/commands/deploy-access.d.ts +33 -0
  26. package/dist/commands/deploy-access.js +322 -0
  27. package/dist/commands/dm-attach.d.ts +106 -0
  28. package/dist/commands/dm-attach.js +184 -0
  29. package/dist/commands/dm.d.ts +37 -0
  30. package/dist/commands/dm.js +172 -10
  31. package/dist/commands/feedback-show.d.ts +40 -0
  32. package/dist/commands/feedback-show.js +261 -0
  33. package/dist/commands/feedback.d.ts +21 -0
  34. package/dist/commands/feedback.js +152 -2
  35. package/dist/commands/files.d.ts +24 -0
  36. package/dist/commands/files.js +52 -1
  37. package/dist/commands/integrations-connect.js +7 -3
  38. package/dist/commands/lanes-accounts.js +16 -1
  39. package/dist/commands/lanes-loop.js +14 -4
  40. package/dist/commands/lanes-message.js +6 -4
  41. package/dist/commands/lanes-resume.js +36 -13
  42. package/dist/commands/lanes-shared.d.ts +5 -0
  43. package/dist/commands/lanes-shared.js +13 -3
  44. package/dist/commands/lanes-watch.js +1 -0
  45. package/dist/commands/lanes.d.ts +20 -1
  46. package/dist/commands/lanes.js +201 -51
  47. package/dist/commands/mesh.js +149 -1
  48. package/dist/commands/monitor.d.ts +1 -0
  49. package/dist/commands/monitor.js +4 -1
  50. package/dist/commands/repo.d.ts +41 -0
  51. package/dist/commands/repo.js +331 -0
  52. package/dist/commands/scan.d.ts +3 -1
  53. package/dist/commands/scan.js +7 -3
  54. package/dist/commands/secrets.d.ts +21 -0
  55. package/dist/commands/secrets.js +114 -14
  56. package/dist/commands/signals.js +17 -5
  57. package/dist/commands/staff/listings.d.ts +41 -0
  58. package/dist/commands/staff/listings.js +160 -0
  59. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  60. package/dist/commands/staff/reset-test-account.js +4 -3
  61. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  62. package/dist/hq-cloud-sync-runner/package.json +1 -1
  63. package/dist/lib/agent-kit/run/inbox.js +2 -1
  64. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  65. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  66. package/dist/lib/agent-kit/wake.d.ts +4 -0
  67. package/dist/lib/agent-kit/wake.js +49 -3
  68. package/dist/lib/bot/inflight.d.ts +7 -1
  69. package/dist/lib/bot/inflight.js +10 -2
  70. package/dist/lib/bot/prompt.js +3 -0
  71. package/dist/lib/bot/run.js +72 -20
  72. package/dist/lib/bot/session.d.ts +14 -0
  73. package/dist/lib/bot/session.js +70 -0
  74. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  75. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  76. package/dist/lib/capacity/lane-login.d.ts +6 -0
  77. package/dist/lib/capacity/lane-login.js +5 -0
  78. package/dist/lib/core-utils/worktree.js +2 -0
  79. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  80. package/dist/lib/daemon/exit-cause.js +88 -0
  81. package/dist/lib/daemon/handoff.d.ts +2 -0
  82. package/dist/lib/daemon/handoff.js +6 -1
  83. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  84. package/dist/lib/daemon/paths.d.ts +2 -0
  85. package/dist/lib/daemon/paths.js +1 -0
  86. package/dist/lib/daemon/run.js +13 -0
  87. package/dist/lib/daemon/supervisor.d.ts +54 -1
  88. package/dist/lib/daemon/supervisor.js +297 -21
  89. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  90. package/dist/lib/daemon/unit-holds.js +86 -0
  91. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  92. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  93. package/dist/lib/index-render/company-knowledge.js +1 -3
  94. package/dist/lib/index-render/projects.js +1 -3
  95. package/dist/lib/lanes/account-company.d.ts +28 -0
  96. package/dist/lib/lanes/account-company.js +53 -0
  97. package/dist/lib/lanes/accounts.d.ts +22 -3
  98. package/dist/lib/lanes/accounts.js +38 -2
  99. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  100. package/dist/lib/lanes/claude-balance.js +6 -1
  101. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  102. package/dist/lib/lanes/claude-credential.js +163 -0
  103. package/dist/lib/lanes/compaction.d.ts +2 -0
  104. package/dist/lib/lanes/compaction.js +9 -0
  105. package/dist/lib/lanes/config.js +6 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  107. package/dist/lib/lanes/hooks-install.js +2 -1
  108. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  109. package/dist/lib/lanes/host-capacity.js +1 -0
  110. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  111. package/dist/lib/lanes/keep-alive.js +3 -1
  112. package/dist/lib/lanes/links.js +10 -4
  113. package/dist/lib/lanes/loop.js +16 -1
  114. package/dist/lib/lanes/node-binary.d.ts +17 -0
  115. package/dist/lib/lanes/node-binary.js +30 -0
  116. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  117. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  118. package/dist/lib/lanes/reconcile.js +4 -0
  119. package/dist/lib/lanes/remote-control-lane.js +2 -0
  120. package/dist/lib/lanes/spawn.d.ts +3 -0
  121. package/dist/lib/lanes/spawn.js +119 -9
  122. package/dist/lib/lanes/store.d.ts +6 -0
  123. package/dist/lib/lanes/store.js +107 -18
  124. package/dist/lib/lanes/types.d.ts +2 -0
  125. package/dist/lib/lanes/watch.js +28 -5
  126. package/dist/lib/lanes/watcher-main.js +4 -3
  127. package/dist/lib/lanes/watcher.d.ts +4 -0
  128. package/dist/lib/lanes/watcher.js +110 -21
  129. package/dist/lib/mesh/api.d.ts +32 -0
  130. package/dist/lib/mesh/api.js +75 -0
  131. package/dist/lib/secret-scan/detect.js +9 -0
  132. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  133. package/dist/lib/secret-scan/repo-config.js +237 -0
  134. package/dist/lib/secret-scan/rules.d.ts +7 -1
  135. package/dist/lib/secret-scan/rules.js +3 -1
  136. package/dist/lib/secret-scan/staged.d.ts +5 -0
  137. package/dist/lib/secret-scan/staged.js +19 -2
  138. package/dist/lib/secret-scan/types.d.ts +2 -0
  139. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  140. package/dist/lib/sync/server-owned-local-only.js +75 -0
  141. package/dist/utils/package-use-lease.js +27 -9
  142. package/dist/utils/version-gate.d.ts +7 -0
  143. package/dist/utils/version-gate.js +66 -0
  144. package/package.json +8 -8
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import chalk from "chalk";
3
- import { ensureCognitoToken } from "../utils/cognito-session.js";
3
+ import { ensureCognitoToken, loadCachedTokens } from "../utils/cognito-session.js";
4
4
  import { assertVaultApiRequestSupported, vaultApiFetch, } from "../utils/vault-api.js";
5
5
  import { collectDiagnostics } from "../utils/feedback-diagnostics.js";
6
6
  import { MAX_SCREENSHOTS, uploadScreenshots } from "../utils/feedback-screenshots.js";
@@ -8,6 +8,7 @@ import { FEEDBACK_LOGS_MAX_TOTAL_BYTES, FEEDBACK_LOGS_MIN_USEFUL_BYTES, collectF
8
8
  import { uploadLogBundle } from "../utils/feedback-log-bundle.js";
9
9
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
10
10
  import { cachedLatestVersion } from "../utils/version-check.js";
11
+ import { registerFeedbackShowCommand } from "./feedback-show.js";
11
12
  export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
12
13
  export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
13
14
  export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
@@ -199,7 +200,151 @@ export async function submitFeedback(opts) {
199
200
  throw new Error(`Failed to submit feedback: ${errMsg}`);
200
201
  }
201
202
  const data = (await res.json());
202
- return { id: data.id };
203
+ return {
204
+ id: data.id,
205
+ ...(typeof data.publicUrl === "string" && data.publicUrl.length > 0
206
+ ? { publicUrl: data.publicUrl }
207
+ : {}),
208
+ };
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Public ticket pages: `hq feedback link <id>` and `hq feedback update <id>`
212
+ // ---------------------------------------------------------------------------
213
+ /** Customer-visible states accepted by `hq feedback update --state`. */
214
+ export const PUBLIC_TICKET_STATES = [
215
+ "triaged",
216
+ "in-progress",
217
+ "shipped",
218
+ "verified",
219
+ "resolved",
220
+ ];
221
+ const FEEDBACK_ID_RE = /^feedback_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
222
+ export function assertFeedbackId(id) {
223
+ if (!FEEDBACK_ID_RE.test(id)) {
224
+ throw new Error(`Invalid feedback id '${id}': expected the full feedback_<uuid> id printed by hq feedback.`);
225
+ }
226
+ }
227
+ /**
228
+ * The staff feedback routes gate on the id token's email claim, so send the
229
+ * cached id token when a person session has one. Machine identities already
230
+ * get their id token back from ensureCognitoToken.
231
+ */
232
+ async function resolveStaffToken() {
233
+ const token = await ensureCognitoToken({ interactive: false });
234
+ const cached = loadCachedTokens();
235
+ return typeof cached?.idToken === "string" && cached.idToken.length > 0
236
+ ? cached.idToken
237
+ : token;
238
+ }
239
+ async function readFeedbackError(res) {
240
+ const data = await res.json().catch(() => ({}));
241
+ return data &&
242
+ typeof data === "object" &&
243
+ !Array.isArray(data) &&
244
+ typeof data.error === "string"
245
+ ? data.error
246
+ : res.statusText;
247
+ }
248
+ function publicUrlFrom(data) {
249
+ if (!data || typeof data !== "object" || Array.isArray(data))
250
+ return null;
251
+ const url = data.publicUrl;
252
+ return typeof url === "string" && url.length > 0 ? url : null;
253
+ }
254
+ /** Resolve (and lazily mint) the public tracking link for a ticket. */
255
+ export async function fetchFeedbackLink(opts) {
256
+ const res = await vaultApiFetch({
257
+ token: opts.token,
258
+ path: `/v1/feedback/${opts.id}`,
259
+ method: "PATCH",
260
+ body: { ensurePublicToken: true },
261
+ });
262
+ if (!res.ok) {
263
+ throw new Error(`Failed to get the public link for ${opts.id}: ${await readFeedbackError(res)}`);
264
+ }
265
+ const url = publicUrlFrom(await res.json());
266
+ if (!url) {
267
+ throw new Error(`The server did not return a public link for ${opts.id}. Public ticket pages may not be deployed yet.`);
268
+ }
269
+ return url;
270
+ }
271
+ /** Post a customer-visible update; the server also mirrors it into #hq-feedback. */
272
+ export async function postFeedbackUpdate(opts) {
273
+ const res = await vaultApiFetch({
274
+ token: opts.token,
275
+ path: `/v1/feedback/${opts.id}`,
276
+ method: "PATCH",
277
+ body: { publicUpdate: { state: opts.state, note: opts.note } },
278
+ });
279
+ if (!res.ok) {
280
+ throw new Error(`Failed to update ${opts.id}: ${await readFeedbackError(res)}`);
281
+ }
282
+ const data = (await res.json());
283
+ return {
284
+ publicUrl: publicUrlFrom(data),
285
+ slack: typeof data.publicUpdateSlack === "string" ? data.publicUpdateSlack : null,
286
+ };
287
+ }
288
+ function registerLinkSubcommand(feedbackCmd) {
289
+ feedbackCmd
290
+ .command("link")
291
+ .description("Print the public tracking link for a ticket (status.hqforwork.com)")
292
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
293
+ .action(async (id) => {
294
+ try {
295
+ assertFeedbackId(id);
296
+ const token = await resolveStaffToken();
297
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
298
+ const url = await fetchFeedbackLink({ id, token });
299
+ console.log(`Track: ${url}`);
300
+ }
301
+ catch (err) {
302
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
303
+ process.exit(1);
304
+ }
305
+ });
306
+ }
307
+ function registerUpdateSubcommand(feedbackCmd) {
308
+ feedbackCmd
309
+ .command("update")
310
+ .description("Post a customer-visible status update on a ticket's public page and its #hq-feedback thread")
311
+ .argument("<id>", "Feedback id (feedback_<uuid>)")
312
+ .requiredOption("--state <state>", `One of: ${PUBLIC_TICKET_STATES.join(", ")}`)
313
+ .requiredOption("--note <text>", "What the customer should read (plain text, up to 2000 characters)")
314
+ .action(async (id, opts) => {
315
+ try {
316
+ assertFeedbackId(id);
317
+ if (!PUBLIC_TICKET_STATES.includes(opts.state)) {
318
+ throw new Error(`Invalid --state '${opts.state}': expected one of ${PUBLIC_TICKET_STATES.join(", ")}.`);
319
+ }
320
+ const note = opts.note.trim();
321
+ if (note.length === 0)
322
+ throw new Error("--note must not be empty.");
323
+ if (note.length > 2000)
324
+ throw new Error("--note must be at most 2000 characters.");
325
+ const token = await resolveStaffToken();
326
+ assertVaultApiRequestSupported({ token, path: `/v1/feedback/${id}` });
327
+ const result = await postFeedbackUpdate({
328
+ id,
329
+ token,
330
+ state: opts.state,
331
+ note,
332
+ });
333
+ console.log(`Updated: ${id} -> ${opts.state}`);
334
+ if (result.publicUrl)
335
+ console.log(`Track: ${result.publicUrl}`);
336
+ if (result.slack === "failed") {
337
+ console.error(chalk.yellow("Warning:"), "the update was saved but the #hq-feedback thread post failed.");
338
+ }
339
+ else if (result.slack === "skipped") {
340
+ console.error(chalk.yellow("Note:"), "the ticket has no #hq-feedback thread, so nothing was posted to Slack.");
341
+ }
342
+ }
343
+ catch (err) {
344
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
345
+ process.exit(1);
346
+ }
347
+ });
203
348
  }
204
349
  function registerSubcommand(feedbackCmd, type) {
205
350
  feedbackCmd
@@ -284,6 +429,8 @@ function registerSubcommand(feedbackCmd, type) {
284
429
  logsEnabled,
285
430
  });
286
431
  console.log(`Submitted: ${result.id}`);
432
+ if (result.publicUrl)
433
+ console.log(`Track: ${result.publicUrl}`);
287
434
  }
288
435
  catch (err) {
289
436
  console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
@@ -297,5 +444,8 @@ export function registerFeedbackCommand(program) {
297
444
  .description("Submit a bug report or feature request to HQ");
298
445
  registerSubcommand(feedbackCmd, "bug");
299
446
  registerSubcommand(feedbackCmd, "feature");
447
+ registerLinkSubcommand(feedbackCmd);
448
+ registerUpdateSubcommand(feedbackCmd);
449
+ registerFeedbackShowCommand(feedbackCmd);
300
450
  }
301
451
  //# sourceMappingURL=feedback.js.map
@@ -9,6 +9,30 @@ export declare const FILES_SHARE_EMPTY_PREFIX_WARNING_FLAG = "files.share-empty-
9
9
  export declare function parseDuration(input: string): number | null;
10
10
  /** PRD upper bound on browser-launch share-session expiry. */
11
11
  export declare const MAX_SHARE_SESSION_EXPIRY_MS: number;
12
+ /**
13
+ * A more specific ACL row under a prefix, as hq-pro reports it. File ACL
14
+ * resolution picks the single most specific row that covers a key, so each of
15
+ * these rows replaces the broader prefix's grants for the keys it covers
16
+ * (feedback_466fe8b8).
17
+ */
18
+ export interface AclMaskingRow {
19
+ path: string;
20
+ open: boolean;
21
+ creatorUid: string;
22
+ grantees: Array<{
23
+ granteeType: string;
24
+ granteeId: string;
25
+ permission: string;
26
+ }>;
27
+ }
28
+ /** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
29
+ export declare function formatShadowedGrantWarning(input: {
30
+ prefix: string;
31
+ principal: string;
32
+ permission: string;
33
+ companySlug?: string;
34
+ maskedBy: AclMaskingRow[];
35
+ }): string;
12
36
  export interface ShareSessionResponse {
13
37
  url: string;
14
38
  token?: string;
@@ -9,6 +9,7 @@ import { looksLikeCompanyUid } from "../utils/vault-api.js";
9
9
  import { AuthError } from "../utils/auth-error.js";
10
10
  import { formatAccessDenied, offerAccessLadder, } from "../utils/access-denied-hint.js";
11
11
  import { fetchChannels, matchChannelsByName } from "./dm.js";
12
+ import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
12
13
  import { resolveCompanySlug, resolvePersonWithRosterFallback, } from "./people.js";
13
14
  import { findHqRoot } from "../utils/manifest.js";
14
15
  import { maxLength } from "../utils/table-width.js";
@@ -55,6 +56,39 @@ function privateFolderExplanation(prefix) {
55
56
  return (`Private folder '${prefix}' — write can create only a new direct child. ` +
56
57
  "Children are creator-only; this grant does not read, list, overwrite, or delete them.");
57
58
  }
59
+ function describeMaskingGrantees(row) {
60
+ if (row.open)
61
+ return "open (all members read)";
62
+ if (row.grantees.length === 0)
63
+ return "none (creator only)";
64
+ return row.grantees
65
+ .map((g) => g.granteeType === "company-wide"
66
+ ? `everyone:${g.permission}`
67
+ : `${g.granteeId}:${g.permission}`)
68
+ .join(", ");
69
+ }
70
+ function printMaskingTable(rows) {
71
+ const PATH_W = Math.max(4, ...rows.map((r) => displayAclPattern(r.path).length));
72
+ const CREATOR_W = Math.max(7, ...rows.map((r) => r.creatorUid.length));
73
+ console.log(chalk.bold(["PATH".padEnd(PATH_W), "CREATOR".padEnd(CREATOR_W), "GRANTS"].join(" ")));
74
+ for (const row of rows) {
75
+ console.log([
76
+ displayAclPattern(row.path).padEnd(PATH_W),
77
+ row.creatorUid.padEnd(CREATOR_W),
78
+ describeMaskingGrantees(row),
79
+ ].join(" "));
80
+ }
81
+ }
82
+ /** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
83
+ export function formatShadowedGrantWarning(input) {
84
+ const paths = input.maskedBy.map((row) => `'${row.path}'`).join(", ");
85
+ const company = input.companySlug ? ` --company ${input.companySlug}` : "";
86
+ return (`Warning: this grant takes no effect under ${input.maskedBy.length} more specific rule(s) ` +
87
+ `that do not include ${input.principal}: ${paths}. ` +
88
+ "Access under those paths is unchanged (the most specific rule wins). " +
89
+ `Share them too, for example: hq files share '${input.maskedBy[0]?.path ?? "<path>"}' --with ${input.principal} --permission ${input.permission}${company}` +
90
+ " — or remove the rule with hq files unshare.");
91
+ }
58
92
  function formatAclPatternConflict(prefix, serverMessage) {
59
93
  const privatePattern = prefix.endsWith("/*")
60
94
  ? prefix.slice(0, -1)
@@ -544,6 +578,14 @@ export function registerFilesCommand(program) {
544
578
  console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
545
579
  printEntryTable(tree.children, true);
546
580
  }
581
+ if (Array.isArray(tree.masking) && tree.masking.length > 0) {
582
+ console.log(`\nMore specific rules under ${displayAclPattern(tree.prefix)} (each replaces this prefix's grants for its own paths):`);
583
+ printMaskingTable(tree.masking);
584
+ if (tree.maskingTruncated) {
585
+ console.log(chalk.gray(" ...list truncated; use --json for the first page."));
586
+ }
587
+ console.log(chalk.gray(`A grant on ${displayAclPattern(tree.prefix)} does not reach these paths unless they grant the same principal. Share each path too, or remove its rule with hq files unshare.`));
588
+ }
547
589
  const allEntries = [...tree.direct, ...tree.inherited, ...tree.children];
548
590
  if (allEntries.some((entry) => entry.granteeType === "company-wide")) {
549
591
  console.log(chalk.gray("\nCompany-wide grants apply to every active member, including guests. Demoting someone to guest does not exclude them. Use hq members remove to end membership, or hq access <path> --as <email> to see effective access."));
@@ -704,7 +746,7 @@ async function resolveShareChannelTarget(params) {
704
746
  const channels = await fetchChannels(token);
705
747
  const matches = matchChannelsByName(channels, params.channelName);
706
748
  if (matches.length === 0) {
707
- console.error(chalk.red(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`));
749
+ console.error(chalk.red(withChannelSuggestions(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`, suggestChannels(channels, params.channelName))));
708
750
  process.exit(1);
709
751
  }
710
752
  if (matches.length > 1) {
@@ -894,6 +936,15 @@ async function runDirectGrant(params) {
894
936
  if (isPrivateFolderPattern(printedPrefix)) {
895
937
  console.log(chalk.gray(privateFolderExplanation(printedPrefix)));
896
938
  }
939
+ if (Array.isArray(data.maskedBy) && data.maskedBy.length > 0) {
940
+ console.warn(chalk.yellow(formatShadowedGrantWarning({
941
+ prefix: printedPrefix,
942
+ principal: isAll ? "@all" : principal,
943
+ permission: params.permission,
944
+ ...(params.companySlug ? { companySlug: params.companySlug } : {}),
945
+ maskedBy: data.maskedBy,
946
+ })));
947
+ }
897
948
  if (typeof data.record?.error === "string" &&
898
949
  typeof data.record.status === "number" &&
899
950
  data.record.status >= 400) {
@@ -548,12 +548,16 @@ async function connectViaOAuth(token, companyUid, target, opts) {
548
548
  ...target.ref,
549
549
  ...(requestedScopes ? { scopes: requestedScopes } : {}),
550
550
  };
551
- if (opts.linkOnly)
552
- return await linkOnlySignIn(token, companyUid, target, opts, startInput);
551
+ const userClient = await resolveUserOAuthClient(opts, target.label);
552
+ if (opts.linkOnly) {
553
+ return await linkOnlySignIn(token, companyUid, target, opts, {
554
+ ...startInput,
555
+ ...(userClient ?? {}),
556
+ });
557
+ }
553
558
  // A user-registered app pins the console callback and skips the loopback
554
559
  // entirely — see `userClientSignIn` for why an ephemeral port cannot be the
555
560
  // redirect URI here.
556
- const userClient = await resolveUserOAuthClient(opts, target.label);
557
561
  if (userClient)
558
562
  return await userClientSignIn(token, companyUid, target, opts, userClient, requestedScopes);
559
563
  // A loopback listener only works if the browser runs on THIS machine. Over
@@ -9,9 +9,11 @@ import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/c
9
9
  import { resolveLaneEffort } from "../lib/lanes/effort.js";
10
10
  import { LanesError } from "../lib/lanes/errors.js";
11
11
  import { CAPACITY_ONLY_REASON } from "../lib/capacity/accounts.js";
12
+ import { claudeOpenCommand } from "../lib/capacity/ledger.js";
12
13
  import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
13
14
  import { resolveLaneModel } from "../lib/lanes/spawn.js";
14
15
  import { leadLaneIds } from "../lib/lanes/admission.js";
16
+ import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
15
17
  export function createLaneAccountsHandlers(deps) {
16
18
  const { admitQueuedFromCtx, automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
17
19
  const VENDOR_LOGIN = {
@@ -124,6 +126,7 @@ export function createLaneAccountsHandlers(deps) {
124
126
  hostname: ctx.hostname(),
125
127
  homedir: ctx.homedir,
126
128
  probe: ctx.probeProvider,
129
+ readClaudeCredential: ctx.readClaudeCredential,
127
130
  env: ctx.env,
128
131
  }).map((status) => {
129
132
  const accountCfg = config.providers[provider].accounts[status.id];
@@ -146,6 +149,10 @@ export function createLaneAccountsHandlers(deps) {
146
149
  error: "home_missing",
147
150
  fix_command: `hq lanes accounts set-home ${provider} ${status.id} <host-home>`,
148
151
  } : {}),
152
+ ...(provider === "claude" && status.sign_in_state === "expired" ? {
153
+ error: "sign_in_expired",
154
+ fix_command: `${claudeOpenCommand(status.home, ctx.homedir())} then run /login`,
155
+ } : {}),
149
156
  ...(provider === "claude" ? {
150
157
  identity: status.identity ?? null,
151
158
  credential_matches_interactive: differs === null ? null : !differs,
@@ -192,7 +199,11 @@ export function createLaneAccountsHandlers(deps) {
192
199
  const balanceGateEnabled = true;
193
200
  const recentUsageEnabled = await deps.readClaudeAccountBalanceSignalsEnabled(ctx);
194
201
  const loads = accountLoadsForReport({ hosts: loaded.snapshots, config, quotaState });
195
- const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
202
+ const suggestions = planClaudeBalanceMoves({
203
+ hosts: loaded.snapshots,
204
+ config,
205
+ laneCompany: (lane) => resolveLaneCompany(hqRoot, lane).company,
206
+ });
196
207
  const selections = loaded.snapshots.map((host) => {
197
208
  try {
198
209
  const selection = selectClaudeAccount({
@@ -428,6 +439,7 @@ export function createLaneAccountsHandlers(deps) {
428
439
  existsSync: ctx.existsSync,
429
440
  hostname,
430
441
  probe: ctx.probeProvider,
442
+ readClaudeCredential: ctx.readClaudeCredential,
431
443
  env: ctx.env,
432
444
  });
433
445
  if (status.logged_in !== true) {
@@ -567,6 +579,7 @@ export function createLaneAccountsHandlers(deps) {
567
579
  existsSync: ctx.existsSync,
568
580
  hostname: ctx.hostname(),
569
581
  probe: ctx.probeProvider,
582
+ readClaudeCredential: ctx.readClaudeCredential,
570
583
  env: ctx.env,
571
584
  });
572
585
  if (status.logged_in !== true) {
@@ -584,6 +597,7 @@ export function createLaneAccountsHandlers(deps) {
584
597
  existsSync: ctx.existsSync,
585
598
  hostname: ctx.hostname(),
586
599
  probe: ctx.probeProvider,
600
+ readClaudeCredential: ctx.readClaudeCredential,
587
601
  env: ctx.env,
588
602
  }),
589
603
  });
@@ -613,6 +627,7 @@ export function createLaneAccountsHandlers(deps) {
613
627
  existsSync: ctx.existsSync,
614
628
  hostname: ctx.hostname(),
615
629
  probe: ctx.probeProvider,
630
+ readClaudeCredential: ctx.readClaudeCredential,
616
631
  env: ctx.env,
617
632
  }),
618
633
  });
@@ -101,7 +101,16 @@ export async function runLaneLoopController(ctx, laneId, deps) {
101
101
  execute: async ({ envelope, signal, fresh, sessionId, repairError }) => {
102
102
  let current = readLane(hqRoot, laneId);
103
103
  const data = envelope;
104
- const storyId = typeof data.story_id === "string" && data.story_id.trim() ? data.story_id.trim() : current.story_id;
104
+ const envelopeStory = typeof data.story_id === "string" && data.story_id.trim() ? data.story_id.trim() : undefined;
105
+ // A lane created with --no-story keeps no_story_reason and an empty
106
+ // story_id; the record schema rejects both together. Ignore an
107
+ // envelope story on such a lane instead of writing a record that can
108
+ // no longer be loaded or cancelled.
109
+ const ignoreEnvelopeStory = envelopeStory !== undefined && !current.story_id && !!current.no_story_reason;
110
+ if (ignoreEnvelopeStory) {
111
+ process.stderr.write(`hq lanes: ignoring envelope story_id ${envelopeStory} for no-story lane ${laneId}\n`);
112
+ }
113
+ const storyId = envelopeStory !== undefined && !ignoreEnvelopeStory ? envelopeStory : current.story_id;
105
114
  if (storyId !== current.story_id) {
106
115
  current = retryUpdateLane(hqRoot, laneId, "command", (record) => {
107
116
  const stories = laneStories(record);
@@ -153,8 +162,8 @@ export async function runLaneLoopController(ctx, laneId, deps) {
153
162
  fs.writeFileSync(briefPath, [
154
163
  "Follow the lane worker instructions and complete this single bounded phase.",
155
164
  handoffRepairEnabled
156
- ? "Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts. The lane envelope decision uses done/ask/blocked; handoff status uses passed/failed/blocked. Allowed handoff status values are passed, failed, blocked. Allowed back_pressure values for tests, lint, typecheck, and build are pass, fail, skip. Include story_id, phase, worker_id, summary, files_changed, commits, back_pressure, and context_for_next."
157
- : "Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts; it must identify the requested story_id and phase and include worker_id, status, summary, files_changed, commits, back_pressure (tests/lint/typecheck/build), and context_for_next.",
165
+ ? "Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts. The lane envelope decision uses done/ask/blocked; handoff status uses passed/failed/blocked. Allowed handoff status values are passed, failed, blocked. Allowed back_pressure values for tests, lint, typecheck, and build are pass, fail, skip. Include story_id, phase, worker_id, summary, files_changed, commits, back_pressure, and context_for_next. summary, context_for_next, and each ac_evidence evidence are single strings, never arrays. Copy each acceptance criterion verbatim into ac_evidence[].criterion; keep its 0-based index and do not shorten or paraphrase the text."
166
+ : "Return the standard hq-lane-envelope.v1 envelope when the phase is complete. For a section 8 phase envelope, put a complete hq-phase-handoff/v1 object in artifacts; it must identify the requested story_id and phase and include worker_id, status, summary, files_changed, commits, back_pressure (tests/lint/typecheck/build), and context_for_next. summary, context_for_next, and each ac_evidence evidence are single strings, never arrays. Copy each acceptance criterion verbatim into ac_evidence[].criterion; keep its 0-based index and do not shorten or paraphrase the text.",
158
167
  repairInstructions,
159
168
  "",
160
169
  prompt,
@@ -173,6 +182,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
173
182
  }
174
183
  await spawnLaneWorker(hqRoot, current, {
175
184
  env: ctx.env,
185
+ readClaudeCredential: ctx.readClaudeCredential,
176
186
  modelDeps: { readFileSync: ctx.readFileSync, probe: ctx.probeProvider, env: ctx.env },
177
187
  existsSync: ctx.existsSync,
178
188
  spawnProcess: ctx.spawnProcess,
@@ -201,7 +211,7 @@ export async function runLaneLoopController(ctx, laneId, deps) {
201
211
  await deps.resumeLane(laneId, {
202
212
  text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
203
213
  project: current.project_id,
204
- story: storyId,
214
+ ...(storyId ? { story: storyId } : {}),
205
215
  freshThread: fresh,
206
216
  ...(sessionId ? { loopSessionId: sessionId } : {}),
207
217
  ...(repairError !== undefined ? { loopHandoffRepair: true } : {}),
@@ -560,16 +560,17 @@ export function createLaneMessageHandlers(deps) {
560
560
  ? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
561
561
  : [];
562
562
  const held = reasons.filter((reason) => reason.reason === "quota_held");
563
- if (held.length === 0)
563
+ const companyUnavailable = reasons.some((reason) => reason.reason === "company_mismatch");
564
+ if (held.length === 0 && !companyUnavailable)
564
565
  throw err;
565
566
  const resetTimes = held
566
567
  .map((reason) => reason.quota_reset_at)
567
568
  .filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
568
569
  .sort((a, b) => Date.parse(a) - Date.parse(b));
569
- if (resetTimes.length === 0)
570
+ if (resetTimes.length === 0 && !companyUnavailable)
570
571
  throw err;
571
572
  quotaHoldDeferred = {
572
- quota_reset_at: resetTimes[0],
573
+ ...(resetTimes[0] !== undefined ? { quota_reset_at: resetTimes[0] } : {}),
573
574
  ...(lane.model !== undefined ? { model: lane.model } : {}),
574
575
  };
575
576
  automaticAccountSelection = undefined;
@@ -911,6 +912,7 @@ export function createLaneMessageHandlers(deps) {
911
912
  }
912
913
  const started = spawnLaneWorker(hqRoot, lane, {
913
914
  env: ctx.env,
915
+ readClaudeCredential: ctx.readClaudeCredential,
914
916
  modelDeps: {
915
917
  readFileSync: ctx.readFileSync,
916
918
  probe: ctx.probeProvider,
@@ -974,7 +976,7 @@ export function createLaneMessageHandlers(deps) {
974
976
  const deliveryStatus = move?.queued
975
977
  ? "queued_on_target_account"
976
978
  : queuedOnQuotaHold
977
- ? "queued_on_quota_hold"
979
+ ? quotaHoldDeferred?.quota_reset_at === undefined ? "queued_on_account" : "queued_on_quota_hold"
978
980
  : queuedOnAdmission
979
981
  ? "queued_on_admission"
980
982
  : mechanism === "dropbox"
@@ -98,6 +98,8 @@ export function createLaneResumeHandlers(deps) {
98
98
  const hqRoot = ctx.hqRoot();
99
99
  const resolution = await deps.resolveCommandLane(ctx, laneId);
100
100
  let lane = resolution.lane;
101
+ // Use the resolved identity for every later exact-path write and audit.
102
+ laneId = lane.lane_id;
101
103
  if (opts.freshThread === true && lane.loop_mode !== true) {
102
104
  throw new LanesError("fresh_thread_loop_only", "--fresh-thread is reserved for loop-lane phases", { lane_id: laneId });
103
105
  }
@@ -333,9 +335,10 @@ export function createLaneResumeHandlers(deps) {
333
335
  .map((reason) => reason.quota_reset_at)
334
336
  .filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
335
337
  .sort((a, b) => Date.parse(a) - Date.parse(b));
336
- if (resets.length === 0)
338
+ const companyUnavailable = reasons.some((reason) => reason.reason === "company_mismatch");
339
+ if (resets.length === 0 && !companyUnavailable)
337
340
  throw err;
338
- quotaHoldDeferred = { quota_reset_at: resets[0], account_reasons: reasons };
341
+ quotaHoldDeferred = { ...(resets[0] !== undefined ? { quota_reset_at: resets[0] } : {}), account_reasons: reasons };
339
342
  }
340
343
  }
341
344
  const profileRefreshAccount = opts.account === undefined && refreshProfileSettings && resumeProfile !== undefined &&
@@ -555,12 +558,24 @@ export function createLaneResumeHandlers(deps) {
555
558
  ...(opts.workingDirectory ? { working_directory: opts.workingDirectory } : {}),
556
559
  at: quotaHoldAt,
557
560
  };
558
- resumed = markSpawnFailure(hqRoot, laneId, "account_quota_exhausted", {
559
- provider: lane.provider,
560
- account: lane.account ?? null,
561
- quota_reset_at: quotaHoldDeferred.quota_reset_at,
562
- account_reasons: quotaHoldDeferred.account_reasons,
563
- }, { resumeTrigger: "resume", pendingResume, at: quotaHoldAt });
561
+ resumed = quotaHoldDeferred.quota_reset_at === undefined
562
+ ? casUpdateLane(hqRoot, laneId, (current) => ({
563
+ ...current,
564
+ state: "queued",
565
+ queued_reason: "account_retry",
566
+ queued_details: undefined,
567
+ queued_at: quotaHoldAt,
568
+ admission_reserved_at: undefined,
569
+ terminal_reason: undefined,
570
+ terminal_details: undefined,
571
+ pending_resume: pendingResume,
572
+ }))
573
+ : markSpawnFailure(hqRoot, laneId, "account_quota_exhausted", {
574
+ provider: lane.provider,
575
+ account: lane.account ?? null,
576
+ quota_reset_at: quotaHoldDeferred.quota_reset_at,
577
+ account_reasons: quotaHoldDeferred.account_reasons,
578
+ }, { resumeTrigger: "resume", pendingResume, at: quotaHoldAt });
564
579
  queuedOnQuotaHold = true;
565
580
  }
566
581
  else if (move?.queued) {
@@ -617,6 +632,7 @@ export function createLaneResumeHandlers(deps) {
617
632
  }
618
633
  const started = spawnLaneWorker(hqRoot, lane, {
619
634
  env: ctx.env,
635
+ readClaudeCredential: ctx.readClaudeCredential,
620
636
  modelDeps: {
621
637
  readFileSync: ctx.readFileSync,
622
638
  probe: ctx.probeProvider,
@@ -624,6 +640,7 @@ export function createLaneResumeHandlers(deps) {
624
640
  },
625
641
  existsSync: ctx.existsSync,
626
642
  spawnProcess: ctx.spawnProcess,
643
+ statfs: ctx.statfs,
627
644
  proveDetached: ctx.proveDetached,
628
645
  killProcess: ctx.killProcess,
629
646
  now: ctx.now,
@@ -647,8 +664,14 @@ export function createLaneResumeHandlers(deps) {
647
664
  compact: opts.compact !== false,
648
665
  effort: resumeEffort ?? recordedResumeEffort(lane),
649
666
  });
650
- storySwitchWorkerStarted = true;
651
- deps.onLaneWorkerStarted?.(laneId, "resume");
667
+ if (started.state === "running") {
668
+ storySwitchWorkerStarted = true;
669
+ deps.onLaneWorkerStarted?.(laneId, "resume");
670
+ }
671
+ else if (started.state === "queued") {
672
+ // The spawn preflight can hold on disk even after admission passed.
673
+ queuedOnAdmission = true;
674
+ }
652
675
  return started;
653
676
  }, hireGrantContext);
654
677
  }, ctx.sleep);
@@ -658,7 +681,7 @@ export function createLaneResumeHandlers(deps) {
658
681
  : null;
659
682
  if (deliveryText !== undefined && messageId !== undefined) {
660
683
  // Same audit copy a message resume leaves; nothing drains it. Archive only
661
- // after a successful spawn so a concurrent resume cannot leave a phantom.
684
+ // after the launch starts or durably queues its resume instruction.
662
685
  archiveResumeMessage(hqRoot, laneId, {
663
686
  at,
664
687
  message_id: messageId,
@@ -703,10 +726,10 @@ export function createLaneResumeHandlers(deps) {
703
726
  delivery_status: move?.queued
704
727
  ? "queued_on_target_account"
705
728
  : queuedOnQuotaHold
706
- ? "queued_on_quota_hold"
729
+ ? quotaHoldDeferred?.quota_reset_at === undefined ? "queued_on_account" : "queued_on_quota_hold"
707
730
  : queuedOnAdmission
708
731
  ? "queued_on_admission"
709
- : "worker_spawned",
732
+ : next.state === "running" ? "worker_spawned" : "not_started",
710
733
  account_move: move ? accountMoveSummary(move) : null,
711
734
  resumed_session_id: resumed.last_resume?.session_id ?? null,
712
735
  last_resume: next.last_resume ?? null,
@@ -1,8 +1,10 @@
1
+ import type { StatFs } from "../lib/lanes/disk-guard.js";
1
2
  import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
2
3
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
3
4
  import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
4
5
  import type { CpuBusySource, MemoryHold } from "../lib/lanes/throttle.js";
5
6
  import type { ProbeCommand } from "../lib/lanes/models.js";
7
+ import type { ClaudeCredentialReader } from "../lib/lanes/claude-credential.js";
6
8
  import type { FocusGuardReason } from "../lib/lanes/focus-guard.js";
7
9
  import type { KillProcessFn, ProveDetachedFn, SpawnProcessFn } from "../lib/lanes/spawn.js";
8
10
  import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
@@ -284,6 +286,7 @@ export interface Ctx {
284
286
  mesh: LanesMesh;
285
287
  sleep: (ms: number) => Promise<void>;
286
288
  spawnProcess?: SpawnProcessFn;
289
+ statfs?: StatFs;
287
290
  proveDetached?: ProveDetachedFn;
288
291
  killProcess?: KillProcessFn;
289
292
  isAlive?: (pid: number) => boolean;
@@ -296,6 +299,8 @@ export interface Ctx {
296
299
  runSystemctl?: RunCommand;
297
300
  /** Runs a provider CLI probe (`grok models`, `claude --help`) for --model validation. */
298
301
  probeProvider?: ProbeCommand;
302
+ /** Claude access-token expiry check for account status. Absent in tests that fake the probe. */
303
+ readClaudeCredential?: ClaudeCredentialReader;
299
304
  resolveMachineIdentity: () => MachineIdentity;
300
305
  runCompaction?: RunCompactionFn;
301
306
  locateTranscript?: (home: string, sessionId: string) => string | null;
@@ -25,13 +25,23 @@ export function assertCurrentResumeAccountAvailable(ctx, lane) {
25
25
  ? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`
26
26
  : `Account ${JSON.stringify(lane.account)} is disabled${account.disabled_reason ? `: ${account.disabled_reason}` : ""}`, { provider: lane.provider, account: lane.account ?? "", ...(account?.disabled_reason !== undefined ? { reason: account.disabled_reason } : {}) });
27
27
  }
28
- if (accountStatus(lane.provider, lane.account, config, {
28
+ const status = accountStatus(lane.provider, lane.account, config, {
29
29
  existsSync: ctx.existsSync,
30
30
  hostname: ctx.hostname(),
31
31
  probe: ctx.probeProvider,
32
+ readClaudeCredential: ctx.readClaudeCredential,
32
33
  env: ctx.env,
33
- }).logged_in !== true) {
34
- throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: lane.account, lane_id: lane.lane_id });
34
+ });
35
+ if (status.logged_in !== true) {
36
+ const expired = status.sign_in_state === "expired";
37
+ throw new LanesError("account_not_logged_in", expired
38
+ ? `Account ${lane.account} for ${lane.provider} has an expired Claude sign-in; open Claude Code on this account and run /login`
39
+ : `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, {
40
+ provider: lane.provider,
41
+ account: lane.account,
42
+ lane_id: lane.lane_id,
43
+ ...(expired ? { sign_in_state: "expired", sign_in_expires_at: status.sign_in_expires_at ?? null } : {}),
44
+ });
35
45
  }
36
46
  }
37
47
  export function accountMoveSummary(move) {