@indigoai-us/hq-cli 5.130.0 → 5.132.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +155 -1
  2. package/contracts/preflight/v1/fixtures.json +87 -0
  3. package/contracts/preflight/v1/schema.json +58 -0
  4. package/dist/command-catalog.generated.d.ts +39 -0
  5. package/dist/command-catalog.generated.js +50 -0
  6. package/dist/commands/dm.d.ts +2 -0
  7. package/dist/commands/dm.js +1 -1
  8. package/dist/commands/files.js +137 -9
  9. package/dist/commands/lanes.d.ts +13 -2
  10. package/dist/commands/lanes.js +394 -42
  11. package/dist/commands/mesh.js +66 -13
  12. package/dist/commands/update.js +1 -1
  13. package/dist/lib/bot/paths.d.ts +7 -5
  14. package/dist/lib/bot/paths.js +8 -1
  15. package/dist/lib/bot/prompt.d.ts +0 -2
  16. package/dist/lib/bot/prompt.js +5 -11
  17. package/dist/lib/bot/room-policy.d.ts +1 -24
  18. package/dist/lib/bot/room-policy.js +2 -35
  19. package/dist/lib/bot/run.d.ts +7 -8
  20. package/dist/lib/bot/run.js +21 -33
  21. package/dist/lib/bot/session.d.ts +1 -1
  22. package/dist/lib/bot/session.js +1 -1
  23. package/dist/lib/lanes/accounts.d.ts +6 -0
  24. package/dist/lib/lanes/accounts.js +2 -1
  25. package/dist/lib/lanes/config.d.ts +4 -0
  26. package/dist/lib/lanes/config.js +9 -0
  27. package/dist/lib/lanes/index.d.ts +10 -3
  28. package/dist/lib/lanes/index.js +5 -1
  29. package/dist/lib/lanes/mesh-events.js +4 -0
  30. package/dist/lib/lanes/models.d.ts +78 -0
  31. package/dist/lib/lanes/models.js +205 -0
  32. package/dist/lib/lanes/move-account.d.ts +56 -0
  33. package/dist/lib/lanes/move-account.js +193 -0
  34. package/dist/lib/lanes/provider-rejection.d.ts +86 -0
  35. package/dist/lib/lanes/provider-rejection.js +258 -0
  36. package/dist/lib/lanes/quota.d.ts +17 -0
  37. package/dist/lib/lanes/quota.js +24 -0
  38. package/dist/lib/lanes/reconcile-install.d.ts +24 -3
  39. package/dist/lib/lanes/reconcile-install.js +48 -12
  40. package/dist/lib/lanes/reconcile.js +41 -11
  41. package/dist/lib/lanes/spawn.js +26 -5
  42. package/dist/lib/lanes/store.js +10 -0
  43. package/dist/lib/lanes/types.d.ts +49 -2
  44. package/dist/lib/lanes/types.js +4 -3
  45. package/dist/lib/lanes/watcher.d.ts +27 -0
  46. package/dist/lib/lanes/watcher.js +68 -5
  47. package/dist/lib/mesh/api.d.ts +2 -0
  48. package/dist/lib/mesh/api.js +15 -2
  49. package/dist/lib/mesh/live/backfill-held.d.ts +4 -3
  50. package/dist/lib/mesh/live/backfill-held.js +37 -14
  51. package/dist/lib/mesh/live/daemon/doctor.d.ts +4 -0
  52. package/dist/lib/mesh/live/daemon/doctor.js +15 -4
  53. package/dist/lib/mesh/live/daemon/install.d.ts +5 -0
  54. package/dist/lib/mesh/live/daemon/install.js +84 -1
  55. package/dist/lib/mesh/live/daemon/run.d.ts +5 -0
  56. package/dist/lib/mesh/live/daemon/run.js +17 -4
  57. package/dist/lib/mesh/live/emit-client.d.ts +2 -0
  58. package/dist/lib/mesh/live/emit-client.js +15 -8
  59. package/dist/lib/mesh/live/emit-evidence.d.ts +19 -0
  60. package/dist/lib/mesh/live/emit-evidence.js +20 -2
  61. package/dist/lib/mesh/live/emit.d.ts +9 -0
  62. package/dist/lib/mesh/live/emit.js +28 -2
  63. package/dist/lib/mesh/live/flush.js +9 -3
  64. package/dist/lib/search-index/index.js +22 -3
  65. package/dist/lib/work-context/company.d.ts +16 -0
  66. package/dist/lib/work-context/company.js +46 -14
  67. package/dist/lib/work-context/preflight-contract.d.ts +28 -0
  68. package/dist/lib/work-context/preflight-contract.js +85 -0
  69. package/dist/main.js +11 -1
  70. package/dist/utils/cognito-session.d.ts +39 -0
  71. package/dist/utils/cognito-session.js +119 -3
  72. package/dist/utils/environmental-error.d.ts +18 -0
  73. package/dist/utils/environmental-error.js +240 -7
  74. package/dist/utils/expected-cli-error.d.ts +2 -0
  75. package/dist/utils/expected-cli-error.js +4 -0
  76. package/dist/utils/self-update.js +4 -1
  77. package/dist/utils/vault-api.d.ts +1 -0
  78. package/dist/utils/vault-api.js +11 -3
  79. package/package.json +3 -2
@@ -8,6 +8,13 @@ import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_pattern
8
8
  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
+ import { fetchChannels, matchChannelsByName } from "./dm.js";
12
+ import { resolveCompanySlug, resolvePersonWithRosterFallback, } from "./people.js";
13
+ import { findHqRoot } from "../utils/manifest.js";
14
+ const PERSON_UID_PATTERN = /^(prs|agt)_[A-Za-z0-9_-]+$/;
15
+ function collectMentions(value, previous) {
16
+ return [...previous, value];
17
+ }
11
18
  // ---------------------------------------------------------------------------
12
19
  // Pure helpers (exported for unit tests)
13
20
  // ---------------------------------------------------------------------------
@@ -129,8 +136,23 @@ export function registerFilesCommand(program) {
129
136
  .option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
130
137
  .option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
131
138
  .option("--no-open", "Print the share-session URL but do not launch the browser")
139
+ .option("--channel <name>", "Post the share record to this named channel instead of a DM")
140
+ .option("--mention <person>", "Mention a person on the channel share record (repeatable)", collectMentions, [])
132
141
  .action(async (paths, opts) => {
133
142
  try {
143
+ const mentions = opts.mention ?? [];
144
+ if (mentions.length > 0 && opts.channel === undefined) {
145
+ console.error(chalk.red("--mention requires --channel <name>."));
146
+ process.exit(1);
147
+ }
148
+ const companySlug = files.opts().company;
149
+ const channelTarget = opts.channel !== undefined
150
+ ? await resolveShareChannelTarget({
151
+ channelName: opts.channel,
152
+ mentions,
153
+ companySlug,
154
+ })
155
+ : undefined;
134
156
  // Full-vault grant: a glob-safe affordance for "give this person the
135
157
  // whole vault" so admins never have to quote a `*` (an unquoted glob
136
158
  // expands to local filenames and instantly fails the one-prefix
@@ -150,8 +172,9 @@ export function registerFilesCommand(program) {
150
172
  prefix: "*",
151
173
  principal: opts.with,
152
174
  permission: opts.permission ?? "write",
153
- companySlug: files.opts().company,
175
+ companySlug,
154
176
  fullVault: true,
177
+ ...channelTarget,
155
178
  });
156
179
  return;
157
180
  }
@@ -159,9 +182,9 @@ export function registerFilesCommand(program) {
159
182
  console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
160
183
  process.exit(1);
161
184
  }
162
- // Fork: --with present → existing direct-grant path. No --with →
163
- // browser-launch share-session path. The direct-grant path
164
- // operates on a single prefix.
185
+ // Fork: --with present → existing direct-grant path. --channel without
186
+ // --with grants each --mention as a person. No --with and no --channel
187
+ // → browser-launch share-session path.
165
188
  if (opts.with !== undefined) {
166
189
  if (paths.length !== 1) {
167
190
  console.error(chalk.red("Direct grant (--with) takes exactly one prefix. Pass multiple paths only when minting a share-session URL."));
@@ -171,15 +194,42 @@ export function registerFilesCommand(program) {
171
194
  prefix: paths[0],
172
195
  principal: opts.with,
173
196
  permission: opts.permission,
174
- companySlug: files.opts().company,
197
+ companySlug,
198
+ ...channelTarget,
175
199
  });
176
200
  return;
177
201
  }
202
+ if (channelTarget) {
203
+ if (channelTarget.mentionPersonUids.length === 0) {
204
+ console.error(chalk.red("--channel requires --with <principal> or --mention <person>."));
205
+ process.exit(1);
206
+ }
207
+ if (paths.length !== 1) {
208
+ console.error(chalk.red("Channel share takes exactly one prefix. Pass multiple paths only when minting a share-session URL."));
209
+ process.exit(1);
210
+ }
211
+ const permission = opts.permission ?? "read";
212
+ for (const [index, uid] of channelTarget.mentionPersonUids.entries()) {
213
+ await runDirectGrant({
214
+ prefix: paths[0],
215
+ principal: uid,
216
+ permission,
217
+ companySlug,
218
+ ...(index === 0
219
+ ? {
220
+ channelId: channelTarget.channelId,
221
+ mentionPersonUids: channelTarget.mentionPersonUids,
222
+ }
223
+ : {}),
224
+ });
225
+ }
226
+ return;
227
+ }
178
228
  await runShareSession({
179
229
  paths,
180
230
  expires: opts.expires,
181
231
  launchBrowser: opts.open,
182
- companySlug: files.opts().company,
232
+ companySlug,
183
233
  });
184
234
  }
185
235
  catch (err) {
@@ -511,6 +561,64 @@ export function registerFilesCommand(program) {
511
561
  // files-browse.ts) onto the same group without re-creating it.
512
562
  return files;
513
563
  }
564
+ async function resolveShareChannelTarget(params) {
565
+ const token = (await resolveVaultCredential()).token;
566
+ const channels = await fetchChannels(token);
567
+ const matches = matchChannelsByName(channels, params.channelName);
568
+ if (matches.length === 0) {
569
+ console.error(chalk.red(`No channel named '${params.channelName}' — run \`hq channels\` to see your channels.`));
570
+ process.exit(1);
571
+ }
572
+ if (matches.length > 1) {
573
+ const scopes = matches
574
+ .map((c) => c.scope ?? "unknown")
575
+ .join(", ");
576
+ console.error(chalk.red(`'${params.channelName}' matches ${matches.length} channels (${scopes}) — this is ambiguous.`));
577
+ process.exit(1);
578
+ }
579
+ const channelId = matches[0].channelId;
580
+ const mentionPersonUids = [];
581
+ for (const mention of params.mentions) {
582
+ mentionPersonUids.push(await resolveMentionPersonUid(mention, params.companySlug));
583
+ }
584
+ return { channelId, mentionPersonUids };
585
+ }
586
+ async function resolveMentionPersonUid(mention, companySlug) {
587
+ const trimmed = mention.trim();
588
+ if (PERSON_UID_PATTERN.test(trimmed))
589
+ return trimmed;
590
+ const hqRoot = findHqRoot();
591
+ const slug = resolveCompanySlug(hqRoot, companySlug);
592
+ const result = await resolvePersonWithRosterFallback({
593
+ hqRoot,
594
+ slug,
595
+ name: trimmed,
596
+ });
597
+ if (result.status === "found" && result.person.personUid) {
598
+ return result.person.personUid;
599
+ }
600
+ if (result.status === "ambiguous") {
601
+ const names = result.matches.map((p) => p.name).join(", ");
602
+ console.error(chalk.red(`'${trimmed}' matches multiple people (${names}) — use a person uid or email.`));
603
+ process.exit(1);
604
+ }
605
+ const membership = await resolvePersonWithRosterFallback({
606
+ hqRoot,
607
+ slug,
608
+ name: trimmed,
609
+ opts: { membershipOnly: true },
610
+ });
611
+ if (membership.status === "found" && membership.person.personUid) {
612
+ return membership.person.personUid;
613
+ }
614
+ if (membership.status === "ambiguous") {
615
+ const names = membership.matches.map((p) => p.name).join(", ");
616
+ console.error(chalk.red(`'${trimmed}' matches multiple people (${names}) — use a person uid or email.`));
617
+ process.exit(1);
618
+ }
619
+ console.error(chalk.red(`Could not resolve --mention '${trimmed}' to a person.`));
620
+ process.exit(1);
621
+ }
514
622
  async function runDirectGrant(params) {
515
623
  const canonicalPrefix = normalizeCompanyFilePrefix(params.prefix, {
516
624
  mutating: true,
@@ -535,11 +643,18 @@ async function runDirectGrant(params) {
535
643
  const isAll = principal === "@all";
536
644
  const isEmail = !isAll && EMAIL_PATTERN.test(principal);
537
645
  const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
538
- if (!isAll && !isEmail && !isGroup) {
539
- console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, or a group id matching grp_<alphanumeric>`));
646
+ const isPerson = !isAll && !isEmail && !isGroup && PERSON_UID_PATTERN.test(principal);
647
+ if (!isAll && !isEmail && !isGroup && !isPerson) {
648
+ console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, a person uid, or a group id matching grp_<alphanumeric>`));
540
649
  process.exit(1);
541
650
  }
542
- const granteeType = isAll ? "company-wide" : isEmail ? "email" : "group";
651
+ const granteeType = isAll
652
+ ? "company-wide"
653
+ : isEmail
654
+ ? "email"
655
+ : isPerson
656
+ ? "person"
657
+ : "group";
543
658
  const granteeId = isAll
544
659
  ? ""
545
660
  : isEmail
@@ -558,6 +673,10 @@ async function runDirectGrant(params) {
558
673
  granteeType,
559
674
  granteeId,
560
675
  permission: params.permission,
676
+ ...(params.channelId ? { channelId: params.channelId } : {}),
677
+ ...(params.mentionPersonUids && params.mentionPersonUids.length > 0
678
+ ? { mentionPersonUids: params.mentionPersonUids }
679
+ : {}),
561
680
  },
562
681
  });
563
682
  // No ACL row exists yet for this prefix. Auto-create one with this
@@ -572,6 +691,10 @@ async function runDirectGrant(params) {
572
691
  body: {
573
692
  prefix: canonicalPrefix,
574
693
  entries: [{ granteeType, granteeId, permission: params.permission }],
694
+ ...(params.channelId ? { channelId: params.channelId } : {}),
695
+ ...(params.mentionPersonUids && params.mentionPersonUids.length > 0
696
+ ? { mentionPersonUids: params.mentionPersonUids }
697
+ : {}),
575
698
  },
576
699
  });
577
700
  autoCreated = res.ok;
@@ -610,6 +733,11 @@ async function runDirectGrant(params) {
610
733
  else {
611
734
  console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
612
735
  }
736
+ if (typeof data.record?.error === "string" &&
737
+ typeof data.record.status === "number" &&
738
+ data.record.status >= 400) {
739
+ console.log(data.record.error);
740
+ }
613
741
  }
614
742
  async function runShareSession(params) {
615
743
  // Validate every path before minting a session; an anchored path must never
@@ -10,6 +10,7 @@ import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.
10
10
  import { type LaneCompanyRef, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
11
11
  import { questionBodyAbsPath } from "../lib/lanes/questions.js";
12
12
  import { type RunCompactionFn } from "../lib/lanes/compaction.js";
13
+ import { type KnownModelsDeps, type ProbeCommand } from "../lib/lanes/models.js";
13
14
  import { spawnLaneWorker, type KillProcessFn, type ProveDetachedFn, type SpawnProcessFn } from "../lib/lanes/spawn.js";
14
15
  export { spawnLaneWorker };
15
16
  export { questionBodyAbsPath };
@@ -62,6 +63,8 @@ export interface LanesCommandDeps {
62
63
  presenceConnected?: boolean;
63
64
  systemdRoot?: string;
64
65
  runSystemctl?: RunCommand;
66
+ /** Provider CLI probe for --model validation; tests inject a fake. */
67
+ probeProvider?: ProbeCommand;
65
68
  resolveMachineIdentity?: () => MachineIdentity;
66
69
  stageOutpostPayload?: (outpostId: string, bytes: Uint8Array) => Promise<{
67
70
  getUrl: string;
@@ -100,14 +103,22 @@ export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief
100
103
  */
101
104
  export declare function composeGeneralBrief(taskBrief: string): string;
102
105
  export declare function parseMaxRuntimeSeconds(raw: unknown, workerId: string): number | undefined;
103
- export declare const PICKUP_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled"];
106
+ export declare const PICKUP_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "model_unsupported"];
104
107
  export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
105
108
  export type PickupTerminalReason = (typeof PICKUP_TERMINAL_REASONS)[number];
109
+ /**
110
+ * The CAS updater a pickup failure applies. A lane that is already terminal
111
+ * for any reason (a cancel that landed after the caller's last read, an
112
+ * earlier pickup failure) is returned unchanged: the guard lives inside the
113
+ * same compare-and-swap that writes the failure, so no read-then-write gap
114
+ * can overwrite another writer's terminal decision.
115
+ */
116
+ export declare function pickupFailureUpdater(reasonRaw: string, details: Record<string, unknown>): (current: LaneRecord) => LaneRecord;
106
117
  /**
107
118
  * Outpost-side account bind (spec §5.2). Writes a named terminal reason,
108
119
  * never a plain `failed`.
109
120
  */
110
- export declare function applyOutpostAccountPickup(hqRoot: string, laneId: string): LaneRecord;
121
+ export declare function applyOutpostAccountPickup(hqRoot: string, laneId: string, modelDeps?: KnownModelsDeps): LaneRecord;
111
122
  export declare function cancelPendingQuestionsForLane(hqRoot: string, laneId: string, nowIso: string, operatorReason?: string): QuestionRecord[];
112
123
  export declare function stopCommittedState(state: LaneRecord["state"]): "cancelled" | "killed";
113
124
  export declare function registerLanesCommand(parent: Command, deps?: LanesCommandDeps): Command;