@indigoai-us/hq-cli 5.345.156 → 5.345.160

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 (188) hide show
  1. package/CHANGELOG.md +255 -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 +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  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/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. 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;
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { aclLockedBy, describeAclLock } from "./acl-lock.js";
2
3
  import open from "open";
3
4
  import * as readline from "node:readline";
4
5
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
@@ -9,6 +10,7 @@ import { looksLikeCompanyUid } from "../utils/vault-api.js";
9
10
  import { AuthError } from "../utils/auth-error.js";
10
11
  import { formatAccessDenied, offerAccessLadder, } from "../utils/access-denied-hint.js";
11
12
  import { fetchChannels, matchChannelsByName } from "./dm.js";
13
+ import { suggestChannels, withChannelSuggestions } from "./channel-suggest.js";
12
14
  import { resolveCompanySlug, resolvePersonWithRosterFallback, } from "./people.js";
13
15
  import { findHqRoot } from "../utils/manifest.js";
14
16
  import { maxLength } from "../utils/table-width.js";
@@ -55,6 +57,39 @@ function privateFolderExplanation(prefix) {
55
57
  return (`Private folder '${prefix}' — write can create only a new direct child. ` +
56
58
  "Children are creator-only; this grant does not read, list, overwrite, or delete them.");
57
59
  }
60
+ function describeMaskingGrantees(row) {
61
+ if (row.open)
62
+ return "open (all members read)";
63
+ if (row.grantees.length === 0)
64
+ return "none (creator only)";
65
+ return row.grantees
66
+ .map((g) => g.granteeType === "company-wide"
67
+ ? `everyone:${g.permission}`
68
+ : `${g.granteeId}:${g.permission}`)
69
+ .join(", ");
70
+ }
71
+ function printMaskingTable(rows) {
72
+ const PATH_W = Math.max(4, ...rows.map((r) => displayAclPattern(r.path).length));
73
+ const CREATOR_W = Math.max(7, ...rows.map((r) => r.creatorUid.length));
74
+ console.log(chalk.bold(["PATH".padEnd(PATH_W), "CREATOR".padEnd(CREATOR_W), "GRANTS"].join(" ")));
75
+ for (const row of rows) {
76
+ console.log([
77
+ displayAclPattern(row.path).padEnd(PATH_W),
78
+ row.creatorUid.padEnd(CREATOR_W),
79
+ describeMaskingGrantees(row),
80
+ ].join(" "));
81
+ }
82
+ }
83
+ /** The warning `hq files share` prints when a grant is shadowed by more specific rows. */
84
+ export function formatShadowedGrantWarning(input) {
85
+ const paths = input.maskedBy.map((row) => `'${row.path}'`).join(", ");
86
+ const company = input.companySlug ? ` --company ${input.companySlug}` : "";
87
+ return (`Warning: this grant takes no effect under ${input.maskedBy.length} more specific rule(s) ` +
88
+ `that do not include ${input.principal}: ${paths}. ` +
89
+ "Access under those paths is unchanged (the most specific rule wins). " +
90
+ `Share them too, for example: hq files share '${input.maskedBy[0]?.path ?? "<path>"}' --with ${input.principal} --permission ${input.permission}${company}` +
91
+ " — or remove the rule with hq files unshare.");
92
+ }
58
93
  function formatAclPatternConflict(prefix, serverMessage) {
59
94
  const privatePattern = prefix.endsWith("/*")
60
95
  ? prefix.slice(0, -1)
@@ -455,17 +490,21 @@ export function registerFilesCommand(program) {
455
490
  throw new Error('Invalid ACL tree response');
456
491
  }
457
492
  const row = tree.directRow;
493
+ // The most-specific row has no grants, so broader grants do not apply.
494
+ const lockedBy = aclLockedBy(tree);
458
495
  if (opts.json) {
459
- console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
496
+ console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null, lockedBy }, null, 2));
460
497
  return;
461
498
  }
462
499
  // No own row AND nothing inherited or granted below — preserve the
463
500
  // original "no ACL record" exit path.
464
- if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
501
+ if (!row && !lockedBy && tree.inherited.length === 0 && tree.children.length === 0) {
465
502
  console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
466
503
  process.exit(1);
467
504
  }
468
- const aclStatus = row?.open ? "open" : "restricted";
505
+ const aclStatus = lockedBy
506
+ ? lockedBy.readUnknown ? "locked unless open" : "locked"
507
+ : row?.open ? "open" : "restricted";
469
508
  console.log(chalk.green(`ACL for ${displayAclPattern(tree.prefix)} (${aclStatus})`));
470
509
  if (isPrivateFolderPattern(tree.prefix)) {
471
510
  console.log(chalk.gray(privateFolderExplanation(tree.prefix)));
@@ -473,9 +512,12 @@ export function registerFilesCommand(program) {
473
512
  if (row) {
474
513
  console.log(`Creator: ${row.creatorUid}`);
475
514
  }
476
- else {
515
+ else if (!lockedBy) {
477
516
  console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
478
517
  }
518
+ if (lockedBy) {
519
+ console.log(chalk.yellow(describeAclLock(lockedBy, displayAclPattern(lockedBy.path))));
520
+ }
479
521
  if (tree.effectivePermission) {
480
522
  console.log(`Your effective permission: ${tree.effectivePermission}`);
481
523
  }
@@ -526,9 +568,9 @@ export function registerFilesCommand(program) {
526
568
  const directEntries = tree.direct;
527
569
  if (directEntries.length === 0) {
528
570
  if (row?.open) {
529
- console.log(chalk.gray("Open ACL — all active members have read access."));
571
+ console.log(chalk.gray("No grants on this row (open: broader grants still apply). All active members have at least read access."));
530
572
  }
531
- else if (row) {
573
+ else if (row && !lockedBy) {
532
574
  console.log(chalk.gray("No explicit grants on this prefix — only creator has access."));
533
575
  }
534
576
  }
@@ -537,13 +579,25 @@ export function registerFilesCommand(program) {
537
579
  printEntryTable(directEntries, false);
538
580
  }
539
581
  if (tree.inherited.length > 0) {
540
- console.log("\nInherited (granted on an ancestor prefix):");
582
+ console.log(lockedBy
583
+ ? lockedBy.readUnknown
584
+ ? `\nGranted on ancestor prefixes (apply only if ${displayAclPattern(lockedBy.path)} is open):`
585
+ : `\nGranted on ancestor prefixes (do not apply: locked by ${displayAclPattern(lockedBy.path)}):`
586
+ : "\nInherited (granted on an ancestor prefix):");
541
587
  printEntryTable(tree.inherited, true);
542
588
  }
543
589
  if (tree.children.length > 0) {
544
590
  console.log("\nGranted on descendant prefixes (do not affect this prefix's access):");
545
591
  printEntryTable(tree.children, true);
546
592
  }
593
+ if (Array.isArray(tree.masking) && tree.masking.length > 0) {
594
+ console.log(`\nMore specific rules under ${displayAclPattern(tree.prefix)} (each replaces this prefix's grants for its own paths):`);
595
+ printMaskingTable(tree.masking);
596
+ if (tree.maskingTruncated) {
597
+ console.log(chalk.gray(" ...list truncated; use --json for the first page."));
598
+ }
599
+ 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.`));
600
+ }
547
601
  const allEntries = [...tree.direct, ...tree.inherited, ...tree.children];
548
602
  if (allEntries.some((entry) => entry.granteeType === "company-wide")) {
549
603
  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 +758,7 @@ async function resolveShareChannelTarget(params) {
704
758
  const channels = await fetchChannels(token);
705
759
  const matches = matchChannelsByName(channels, params.channelName);
706
760
  if (matches.length === 0) {
707
- console.error(chalk.red(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`));
761
+ console.error(chalk.red(withChannelSuggestions(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`, suggestChannels(channels, params.channelName))));
708
762
  process.exit(1);
709
763
  }
710
764
  if (matches.length > 1) {
@@ -894,6 +948,15 @@ async function runDirectGrant(params) {
894
948
  if (isPrivateFolderPattern(printedPrefix)) {
895
949
  console.log(chalk.gray(privateFolderExplanation(printedPrefix)));
896
950
  }
951
+ if (Array.isArray(data.maskedBy) && data.maskedBy.length > 0) {
952
+ console.warn(chalk.yellow(formatShadowedGrantWarning({
953
+ prefix: printedPrefix,
954
+ principal: isAll ? "@all" : principal,
955
+ permission: params.permission,
956
+ ...(params.companySlug ? { companySlug: params.companySlug } : {}),
957
+ maskedBy: data.maskedBy,
958
+ })));
959
+ }
897
960
  if (typeof data.record?.error === "string" &&
898
961
  typeof data.record.status === "number" &&
899
962
  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"