@indigoai-us/hq-cli 5.310.0 → 5.311.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 (91) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/assets/scaffold/core/scripts/jobs-validate.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +2 -2
  4. package/dist/command-catalog.generated.js +2 -2
  5. package/dist/commands/channels.d.ts +1 -9
  6. package/dist/commands/channels.js +2 -2
  7. package/dist/commands/company-transfer.d.ts +4 -28
  8. package/dist/commands/company-transfer.js +5 -49
  9. package/dist/commands/core-checkpoint-stop-gate-native.d.ts +34 -0
  10. package/dist/commands/core-checkpoint-stop-gate-native.js +713 -0
  11. package/dist/commands/core-checkpoint.d.ts +0 -24
  12. package/dist/commands/core-checkpoint.js +4 -4
  13. package/dist/commands/core-command-catalog.js +1 -1
  14. package/dist/commands/core-hq-session-native.d.ts +39 -0
  15. package/dist/commands/core-hq-session-native.js +579 -0
  16. package/dist/commands/core-jobs-native.js +1 -4
  17. package/dist/commands/core-native-session-flag.d.ts +4 -0
  18. package/dist/commands/core-native-session-flag.js +7 -0
  19. package/dist/commands/core-native-worker-skills-flag.d.ts +4 -0
  20. package/dist/commands/core-native-worker-skills-flag.js +7 -0
  21. package/dist/commands/core-worker-lint-native.d.ts +4 -0
  22. package/dist/commands/core-worker-lint-native.js +160 -0
  23. package/dist/commands/core-worker-share-native.d.ts +5 -0
  24. package/dist/commands/core-worker-share-native.js +260 -0
  25. package/dist/commands/core-worker-skills-command.d.ts +8 -0
  26. package/dist/commands/core-worker-skills-command.js +33 -0
  27. package/dist/commands/core-worker-skills-runtime.d.ts +33 -0
  28. package/dist/commands/core-worker-skills-runtime.js +86 -0
  29. package/dist/commands/core.js +4 -2
  30. package/dist/commands/dm.d.ts +3 -7
  31. package/dist/commands/dm.js +3 -3
  32. package/dist/commands/integrations-import.d.ts +1 -1
  33. package/dist/commands/integrations-import.js +34 -19
  34. package/dist/commands/lanes-accounts.js +3 -1
  35. package/dist/commands/lanes-message.js +84 -10
  36. package/dist/commands/lanes-shared.d.ts +4 -1
  37. package/dist/commands/lanes.d.ts +4 -1
  38. package/dist/commands/lanes.js +48 -7
  39. package/dist/commands/personal-connector-settings-flag.d.ts +4 -0
  40. package/dist/commands/personal-connector-settings-flag.js +7 -0
  41. package/dist/commands/report.d.ts +1 -26
  42. package/dist/commands/report.js +6 -6
  43. package/dist/fast-core.js +15 -2
  44. package/dist/fast-scaffold.js +25 -2
  45. package/dist/index.js +8 -0
  46. package/dist/lib/bot/continuity.d.ts +2 -1
  47. package/dist/lib/bot/job-completions.d.ts +0 -2
  48. package/dist/lib/bot/job-completions.js +2 -2
  49. package/dist/lib/bot/local-config.d.ts +0 -10
  50. package/dist/lib/bot/local-config.js +3 -9
  51. package/dist/lib/bot/owner-context.d.ts +2 -6
  52. package/dist/lib/bot/owner-context.js +3 -3
  53. package/dist/lib/bot/runtime/windows.d.ts +0 -2
  54. package/dist/lib/bot/runtime/windows.js +1 -1
  55. package/dist/lib/bot/setup-explain.d.ts +0 -4
  56. package/dist/lib/bot/setup-explain.js +2 -2
  57. package/dist/lib/daemon/catalog.d.ts +3 -2
  58. package/dist/lib/daemon/hq-cloud.d.ts +2 -2
  59. package/dist/lib/daemon/hq-cloud.js +1 -1
  60. package/dist/lib/daemon/machine-type.d.ts +3 -4
  61. package/dist/lib/daemon/machine-type.js +2 -2
  62. package/dist/lib/daemon/remote-control.d.ts +0 -1
  63. package/dist/lib/daemon/remote-control.js +1 -1
  64. package/dist/lib/daemon/supervisor.d.ts +4 -9
  65. package/dist/lib/daemon/supervisor.js +5 -5
  66. package/dist/lib/lanes/admission-host.d.ts +3 -0
  67. package/dist/lib/lanes/admission-host.js +4 -0
  68. package/dist/lib/lanes/autopilot-registry.d.ts +62 -0
  69. package/dist/lib/lanes/autopilot-registry.js +192 -0
  70. package/dist/lib/lanes/autopilot.d.ts +2 -0
  71. package/dist/lib/lanes/autopilot.js +18 -5
  72. package/dist/lib/lanes/claude-balance.d.ts +6 -0
  73. package/dist/lib/lanes/claude-balance.js +17 -2
  74. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  75. package/dist/lib/lanes/keep-alive.js +3 -0
  76. package/dist/lib/lanes/reconcile.js +14 -6
  77. package/dist/lib/lanes/spawn.js +2 -0
  78. package/dist/lib/mesh/live/daemon/credentials.d.ts +2 -17
  79. package/dist/lib/mesh/live/daemon/credentials.js +9 -9
  80. package/dist/lib/mesh/live/daemon/index.d.ts +5 -5
  81. package/dist/lib/mesh/live/daemon/index.js +2 -2
  82. package/dist/lib/mesh/live/daemon/state.d.ts +1 -1
  83. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +4 -11
  84. package/dist/lib/mesh/live/daemon/transcript-watch.js +5 -5
  85. package/dist/lib/work-context/company.d.ts +2 -25
  86. package/dist/lib/work-context/company.js +3 -3
  87. package/dist/lib/work-context/config.d.ts +3 -8
  88. package/dist/lib/work-context/config.js +2 -13
  89. package/dist/lib/work-context/state.d.ts +4 -13
  90. package/dist/lib/work-context/state.js +2 -30
  91. package/package.json +1 -1
@@ -892,7 +892,7 @@ export function callerFromToken(token) {
892
892
  ...(emailNorm ? { email: emailNorm } : {}),
893
893
  };
894
894
  }
895
- export function isCallerMessage(m, me) {
895
+ function isCallerMessage(m, me) {
896
896
  if (me.personUid && m.fromPersonUid === me.personUid)
897
897
  return true;
898
898
  if (me.email && (m.fromEmail ?? "").trim().toLowerCase() === me.email) {
@@ -1060,7 +1060,7 @@ export function agentCallerUid(token, machine = isMachineIdentity) {
1060
1060
  export const AGENT_THREAD_UNAVAILABLE = "`hq dm thread` reads /v1/notify/thread, which is not available to agent identities. " +
1061
1061
  "Use `hq dm inbox` — for agents it reads GET /v1/agents/{uid}/inbox.";
1062
1062
  /** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
1063
- export function formatAgentInboxItem(item) {
1063
+ function formatAgentInboxItem(item) {
1064
1064
  const str = (k) => (typeof item[k] === "string" ? item[k] : "");
1065
1065
  const id = str("messageId") || str("id") || "?";
1066
1066
  const channel = str("channel") || "inbox";
@@ -1070,7 +1070,7 @@ export function formatAgentInboxItem(item) {
1070
1070
  return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
1071
1071
  }
1072
1072
  /** Remove delegated execution context before agent inbox items can be emitted. */
1073
- export function redactAgentInboxItem(item) {
1073
+ function redactAgentInboxItem(item) {
1074
1074
  const { actingContext: _actingContext, ...safe } = item;
1075
1075
  return safe;
1076
1076
  }
@@ -3,7 +3,7 @@ import { type McpManifest } from "./mcp-registration.js";
3
3
  export type DesktopConnector = McpManifest & {
4
4
  [key: string]: unknown;
5
5
  };
6
- export type ImportStatus = "imported" | "needs-signin" | "shared-for-local" | "skipped";
6
+ export type ImportStatus = "imported" | "needs-signin" | "shared-for-local" | "personal-for-local" | "skipped";
7
7
  export interface ImportOutcome {
8
8
  name: string;
9
9
  status: ImportStatus;
@@ -15,6 +15,7 @@ import { bareProvider, IntegrationsCliError, printJson } from "./integrations-co
15
15
  import { installIntegration, startOAuth } from "./integrations-api.js";
16
16
  import { loadRevealedSecrets } from "./secrets.js";
17
17
  import { registerServer } from "./mcp-registration.js";
18
+ import { personalConnectorSettingsEnabled } from "./personal-connector-settings-flag.js";
18
19
  const OAUTH_REQUIRED_CODE = "INTEGRATION_FACTORY_OAUTH_REQUIRED";
19
20
  /** Claude Desktop's documented connector-config location for the current OS. */
20
21
  export function claudeDesktopConfigPath(platform = process.platform, home = os.homedir(), env = process.env) {
@@ -149,7 +150,13 @@ function oauthRequired(error) {
149
150
  function reconnectCommand(name, provider) {
150
151
  return `hq integrations reconnect ${name} --provider ${bareProvider(provider)}`;
151
152
  }
152
- function connectorPath(hqRoot, company, name) {
153
+ function connectorPath(hqRoot, name, company, personal) {
154
+ if (personal) {
155
+ return path.join(hqRoot, "personal", "settings", "connectors", `${name}.json`);
156
+ }
157
+ if (!company) {
158
+ throw new IntegrationsCliError("A company is required for a shared connector manifest.", { expected: true });
159
+ }
153
160
  return path.join(hqRoot, "companies", company, "settings", "connectors", `${name}.json`);
154
161
  }
155
162
  /**
@@ -172,8 +179,8 @@ async function companyFolderSlug(token, companyUid) {
172
179
  }
173
180
  return folderSlug;
174
181
  }
175
- function writeLocalConnector(hqRoot, company, name, entry) {
176
- const target = connectorPath(hqRoot, company, name);
182
+ function writeLocalConnector(hqRoot, name, company, personal, entry) {
183
+ const target = connectorPath(hqRoot, name, company, personal);
177
184
  fs.mkdirSync(path.dirname(target), { recursive: true });
178
185
  fs.writeFileSync(target, `${JSON.stringify(stripConnectorSecrets(entry), null, 2)}\n`, { mode: 0o600 });
179
186
  return target;
@@ -225,11 +232,13 @@ function renderOutcomes(outcomes) {
225
232
  for (const outcome of outcomes) {
226
233
  console.log(`${redactUrlsInText(outcome.name).padEnd(25)} ${outcome.status.padEnd(19)} ${redactUrlsInText(outcome.reason ?? outcome.path ?? "")}`);
227
234
  }
228
- for (const outcome of outcomes.filter((item) => item.status === "shared-for-local")) {
229
- console.log(chalk.dim(` Shared manifest: ${redactUrlsInText(outcome.path ?? "")}`));
230
- console.log(chalk.dim(` Teammate install: ${redactUrlsInText(outcome.installCommand ?? "")}`));
235
+ for (const outcome of outcomes.filter((item) => item.status === "shared-for-local" || item.status === "personal-for-local")) {
236
+ const label = outcome.status === "personal-for-local" ? "Personal manifest" : "Shared manifest";
237
+ const installLabel = outcome.status === "personal-for-local" ? "Personal install" : "Teammate install";
238
+ console.log(chalk.dim(` ${label}: ${redactUrlsInText(outcome.path ?? "")}`));
239
+ console.log(chalk.dim(` ${installLabel}: ${redactUrlsInText(outcome.installCommand ?? "")}`));
231
240
  }
232
- if (outcomes.some((item) => item.status === "shared-for-local")) {
241
+ if (outcomes.some((item) => item.status === "shared-for-local" || item.status === "personal-for-local")) {
233
242
  console.log(chalk.yellow(" Local connector credentials were replaced with ${secret:ENV_NAME}. Set them with `hq secrets` before installing."));
234
243
  }
235
244
  if (outcomes.some((item) => item.redactedArgumentCredentials)) {
@@ -245,7 +254,7 @@ function renderOutcomes(outcomes) {
245
254
  export function registerImportCommands(integrations) {
246
255
  integrations
247
256
  .command("import")
248
- .description("Import Claude Desktop connectors into company Integrations")
257
+ .description("Import Claude Desktop connectors into HQ Integrations")
249
258
  .option("--company <slug>", "Company slug (defaults to your single active company)")
250
259
  .option("--dry-run", "Detect and classify connectors without writing or connecting")
251
260
  .option("--only <name[,name...]>", "Import only these Claude Desktop connector names")
@@ -272,6 +281,7 @@ export function registerImportCommands(integrations) {
272
281
  const outcomes = [];
273
282
  let company;
274
283
  let hqRoot;
284
+ const personalSettings = personalConnectorSettingsEnabled();
275
285
  const localCompany = async () => company ??= await companyFolderSlug(token, companyUid);
276
286
  const localHqRoot = () => hqRoot ??= resolveDefaultHqRoot({ onMissing: "throw" });
277
287
  const normalizedNames = new Map();
@@ -297,13 +307,17 @@ export function registerImportCommands(integrations) {
297
307
  continue;
298
308
  }
299
309
  if (isLocal(entry)) {
300
- const resolvedCompany = await localCompany();
301
- const target = connectorPath(localHqRoot(), resolvedCompany, safeName);
310
+ const resolvedCompany = personalSettings ? undefined : await localCompany();
311
+ const target = connectorPath(localHqRoot(), safeName, resolvedCompany, personalSettings);
302
312
  outcomes.push({
303
313
  name,
304
- status: "shared-for-local",
305
- ...(opts.dryRun ? { reason: "would write secret-stripped local manifest" } : { path: writeLocalConnector(localHqRoot(), resolvedCompany, safeName, entry) }),
306
- installCommand: `hq integrations install-local ${safeName} --company ${resolvedCompany}`,
314
+ status: personalSettings ? "personal-for-local" : "shared-for-local",
315
+ ...(opts.dryRun
316
+ ? { reason: personalSettings ? "would write secret-stripped personal manifest" : "would write secret-stripped local manifest" }
317
+ : { path: writeLocalConnector(localHqRoot(), safeName, resolvedCompany, personalSettings, entry) }),
318
+ installCommand: personalSettings
319
+ ? `hq integrations install-local ${safeName}`
320
+ : `hq integrations install-local ${safeName} --company ${resolvedCompany}`,
307
321
  ...(hasRedactedArgumentCredentials(entry) ? { redactedArgumentCredentials: true } : {}),
308
322
  });
309
323
  // Keep dry-run output useful without leaking a filesystem write target.
@@ -324,7 +338,7 @@ export function registerImportCommands(integrations) {
324
338
  });
325
339
  integrations
326
340
  .command("install-local <name>")
327
- .description("Register a company-shared local connector in this machine's Claude/Codex MCP config")
341
+ .description("Install local connector settings in this machine's Claude/Codex MCP config")
328
342
  .option("--company <slug>", "Company slug (defaults to your single active company)")
329
343
  .option("--json", "Machine-readable registration result")
330
344
  .action(async (name, opts) => {
@@ -334,11 +348,12 @@ export function registerImportCommands(integrations) {
334
348
  }
335
349
  const token = await ensureCognitoToken();
336
350
  const companyUid = await getCompanyUid(token, opts.company);
337
- const company = await companyFolderSlug(token, companyUid);
351
+ const personalSettings = personalConnectorSettingsEnabled();
352
+ const company = personalSettings ? undefined : await companyFolderSlug(token, companyUid);
338
353
  const hqRoot = resolveDefaultHqRoot({ onMissing: "throw" });
339
- const manifestPath = connectorPath(hqRoot, company, safeName);
354
+ const manifestPath = connectorPath(hqRoot, safeName, company, personalSettings);
340
355
  if (!fs.existsSync(manifestPath)) {
341
- throw new IntegrationsCliError(`Shared connector not found: ${manifestPath}`, { expected: true });
356
+ throw new IntegrationsCliError(`Connector settings not found: ${manifestPath}`, { expected: true });
342
357
  }
343
358
  let manifest;
344
359
  try {
@@ -346,7 +361,7 @@ export function registerImportCommands(integrations) {
346
361
  }
347
362
  catch (error) {
348
363
  const message = error instanceof Error ? error.message : String(error);
349
- throw new IntegrationsCliError(`Could not parse shared connector ${manifestPath}: ${message}`, { expected: true });
364
+ throw new IntegrationsCliError(`Could not parse connector settings ${manifestPath}: ${message}`, { expected: true });
350
365
  }
351
366
  const secretNames = [...Object.values({ ...(manifest.env ?? {}), ...(manifest.headers ?? {}) }), ...(manifest.args ?? [])]
352
367
  .flatMap((value) => [...value.matchAll(/\$\{secret:([A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]*)*)\}/g)].map((match) => match[1]));
@@ -356,7 +371,7 @@ export function registerImportCommands(integrations) {
356
371
  const result = registerServer({
357
372
  name: safeName,
358
373
  manifest,
359
- pack: `company-${company}-connectors`,
374
+ pack: personalSettings ? "personal-connectors" : `company-${company}-connectors`,
360
375
  resolveSecret: (secret) => secrets.get(secret) ?? null,
361
376
  });
362
377
  if (opts.json)
@@ -165,12 +165,13 @@ export function createLaneAccountsHandlers(deps) {
165
165
  }
166
166
  const hqRoot = ctx.hqRoot();
167
167
  const config = loadLanesConfig(hqRoot);
168
+ const quotaState = loadQuotaState(hqRoot);
168
169
  const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
169
170
  // Retain the established response field for clients; balancing is now
170
171
  // unconditional, so this compatibility value is always true.
171
172
  const balanceGateEnabled = true;
172
173
  const recentUsageEnabled = await deps.readClaudeAccountBalanceSignalsEnabled(ctx);
173
- const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
174
+ const loads = accountLoadsForReport({ hosts: loaded.snapshots, config, quotaState });
174
175
  const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
175
176
  const selections = loaded.snapshots.map((host) => {
176
177
  try {
@@ -178,6 +179,7 @@ export function createLaneAccountsHandlers(deps) {
178
179
  hosts: loaded.snapshots,
179
180
  targetHost: host.capacity.host,
180
181
  config,
182
+ quotaState,
181
183
  recentUsageEnabled,
182
184
  now: ctx.now(),
183
185
  unavailableHosts: loaded.unavailableHosts,
@@ -396,9 +396,31 @@ export function createLaneMessageHandlers(deps) {
396
396
  if (opts.model !== undefined && opts.account === undefined) {
397
397
  throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
398
398
  }
399
- const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
400
- ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
401
- : undefined;
399
+ let quotaHoldDeferred;
400
+ let automaticAccountSelection = undefined;
401
+ if (opts.account === undefined && mechanism === "resume") {
402
+ try {
403
+ automaticAccountSelection = await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile);
404
+ }
405
+ catch (err) {
406
+ const reasons = err instanceof LanesError && err.error === "claude_account_balance_no_eligible_account" &&
407
+ Array.isArray(err.details.account_reasons)
408
+ ? err.details.account_reasons.filter((reason) => reason !== null && typeof reason === "object" && !Array.isArray(reason))
409
+ : [];
410
+ const held = reasons.filter((reason) => reason.reason === "quota_held");
411
+ if (held.length === 0)
412
+ throw err;
413
+ const resetTimes = held
414
+ .map((reason) => reason.quota_reset_at)
415
+ .filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
416
+ .sort((a, b) => Date.parse(a) - Date.parse(b));
417
+ quotaHoldDeferred = {
418
+ ...(resetTimes[0] !== undefined ? { quota_reset_at: resetTimes[0] } : {}),
419
+ ...(lane.model !== undefined ? { model: lane.model } : {}),
420
+ };
421
+ automaticAccountSelection = undefined;
422
+ }
423
+ }
402
424
  const resumeEffort = resolveResumeEffort(ctx, lane, {
403
425
  cliEffort,
404
426
  profile: resumeProfile,
@@ -488,7 +510,49 @@ export function createLaneMessageHandlers(deps) {
488
510
  let resumed = null;
489
511
  let move = null;
490
512
  let queuedOnAdmission = false;
491
- if (opts.account !== undefined && !keepCurrentAccount) {
513
+ let queuedOnQuotaHold = false;
514
+ if (quotaHoldDeferred !== undefined) {
515
+ const sessionId = lane.provider_session_id;
516
+ if (!sessionId) {
517
+ throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message`, {
518
+ lane_id: laneId,
519
+ provider: lane.provider,
520
+ });
521
+ }
522
+ lane = casUpdateLane(hqRoot, laneId, (current) => {
523
+ if (current.version !== lane.version || current.state !== lane.state) {
524
+ throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while its quota-held message was queued`, {
525
+ lane_id: laneId,
526
+ prepared_version: lane.version,
527
+ version: current.version,
528
+ state: current.state,
529
+ });
530
+ }
531
+ return {
532
+ ...current,
533
+ state: "queued",
534
+ queued_reason: "account_retry",
535
+ queued_details: undefined,
536
+ queued_at: at,
537
+ admission_reserved_at: undefined,
538
+ pending_resume: {
539
+ session_id: sessionId,
540
+ trigger: "message",
541
+ caller: ctx.env.HQ_LANE_ID?.trim() || "user",
542
+ text: deliveryText,
543
+ ...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
544
+ message_id: messageId,
545
+ compact: opts.compact !== false,
546
+ at,
547
+ },
548
+ };
549
+ });
550
+ queuedOnQuotaHold = true;
551
+ storySwitchDeliveryStarted = true;
552
+ markAccepted();
553
+ deps.onLaneMessageQueued?.(laneId, "quota_hold");
554
+ }
555
+ else if (opts.account !== undefined && !keepCurrentAccount) {
492
556
  // A moved message is always a resume on the target account: a running
493
557
  // lane (the dropbox mechanism) is refused inside prepareAccountMove.
494
558
  move = await prepareAccountMove(ctx, lane, {
@@ -556,7 +620,7 @@ export function createLaneMessageHandlers(deps) {
556
620
  });
557
621
  }
558
622
  let questionCancellation = null;
559
- if (move?.queued) {
623
+ if (move?.queued || queuedOnQuotaHold) {
560
624
  // Parked at the target's cap: reconcile resumes the session there.
561
625
  markAccepted();
562
626
  questionCancellation = awaitingInputFallback
@@ -678,11 +742,13 @@ export function createLaneMessageHandlers(deps) {
678
742
  const drainHook = drainHookStatusFor(ctx, next);
679
743
  const deliveryStatus = move?.queued
680
744
  ? "queued_on_target_account"
681
- : queuedOnAdmission
682
- ? "queued_on_admission"
683
- : mechanism === "dropbox"
684
- ? "awaiting_next_tool_call"
685
- : "worker_spawned";
745
+ : queuedOnQuotaHold
746
+ ? "queued_on_quota_hold"
747
+ : queuedOnAdmission
748
+ ? "queued_on_admission"
749
+ : mechanism === "dropbox"
750
+ ? "awaiting_next_tool_call"
751
+ : "worker_spawned";
686
752
  const warning = [
687
753
  eligibility.supersededMessageWarning,
688
754
  mechanism === "dropbox" && drainHook !== null && !drainHook.registered
@@ -702,6 +768,14 @@ export function createLaneMessageHandlers(deps) {
702
768
  delivery: mechanism,
703
769
  delivered_to: { host: lane.host.id, lane_id: laneId, mechanism },
704
770
  delivery_status: deliveryStatus,
771
+ ...(queuedOnQuotaHold ? {
772
+ queued_reason: "account_quota_held",
773
+ quota_hold: {
774
+ reason: "account_quota_held",
775
+ quota_reset_at: quotaHoldDeferred?.quota_reset_at ?? null,
776
+ model: quotaHoldDeferred?.model ?? null,
777
+ },
778
+ } : {}),
705
779
  inbox_path: mechanism === "dropbox" ? dropboxInboxPath(hqRoot, laneId) : null,
706
780
  drain_hook: drainHook,
707
781
  warning,
@@ -156,7 +156,7 @@ export interface LaneMessageHandlerDeps {
156
156
  /** Test seam invoked after a provider worker has been started. */
157
157
  onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
158
158
  /** Test seam invoked after a message has been durably queued for a lane. */
159
- onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move") => void;
159
+ onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move" | "quota_hold") => void;
160
160
  archiveResumeMessage(hqRoot: string, laneId: string, message: {
161
161
  message_id: string;
162
162
  [key: string]: unknown;
@@ -264,6 +264,9 @@ export interface Ctx {
264
264
  backfillOutpostIdentity: (expectedId: string) => "created" | "unchanged";
265
265
  fetchText: (url: string) => Promise<string>;
266
266
  lanePlacementEnabled: (company: LaneCompanyRef | undefined) => Promise<boolean>;
267
+ readLeadAutopilotFlag: (flag: string, options: {
268
+ company: LaneCompanyRef;
269
+ }) => Promise<boolean>;
267
270
  storyLedgerEnabled: () => boolean;
268
271
  isSsmOwnerDispatch: () => boolean;
269
272
  withGrantHireLocks: typeof withLaneHireLocks;
@@ -32,7 +32,7 @@ export interface LanesCommandDeps {
32
32
  /** Test seam for failures after a lane provider worker is started. */
33
33
  onLaneWorkerStarted?: (laneId: string, command: "message" | "resume") => void;
34
34
  /** Test seam for failures after a message is durably queued. */
35
- onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move") => void;
35
+ onLaneMessageQueued?: (laneId: string, path: "admission" | "account_move" | "quota_hold") => void;
36
36
  hqRoot?: () => string;
37
37
  /** Start HQ-root walk here instead of process.cwd(). Tests only. */
38
38
  cwd?: () => string;
@@ -40,6 +40,9 @@ export interface LanesCommandDeps {
40
40
  now?: () => Date;
41
41
  hostname?: () => string;
42
42
  resolveCompany?: (company?: string) => Promise<LaneCompanyRef>;
43
+ readLeadAutopilotFlag?: (flag: string, options: {
44
+ company: LaneCompanyRef;
45
+ }) => Promise<boolean>;
43
46
  /** Membership-backed explicit company selection for account-home writers. */
44
47
  resolveActiveCompany?: (company: string) => Promise<LaneCompanyRef | null>;
45
48
  listOutpostIds?: () => Promise<string[]>;
@@ -33,7 +33,9 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
33
33
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
34
34
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
35
35
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
36
+ import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
36
37
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
38
+ import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
37
39
  import { OutpostSnapshotInvocationMemo } from "../lib/lanes/outpost-snapshot-memo.js";
38
40
  import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
39
41
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
@@ -253,6 +255,7 @@ function resolveDeps(deps = {}) {
253
255
  now: deps.now ?? (() => new Date()),
254
256
  hostname: deps.hostname ?? (() => os.hostname()),
255
257
  resolveCompany: deps.resolveCompany ?? defaultResolveCompany,
258
+ readLeadAutopilotFlag: deps.readLeadAutopilotFlag ?? readLanesRegistryFlag,
256
259
  resolveActiveCompany: deps.resolveActiveCompany ?? defaultResolveActiveCompany,
257
260
  listOutpostIds: deps.listOutpostIds ?? defaultListOutpostIds,
258
261
  listOutpostRoutes,
@@ -1112,8 +1115,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
1112
1115
  .map((id) => id.trim())
1113
1116
  // `primary` aliases the controller when running on the primary box. From
1114
1117
  // another Outpost it identifies a separate host and must be measured.
1115
- .filter((id) => id !== "primary" ||
1116
- (localHost.kind === "outpost" && localHost.id !== "primary"))
1118
+ .filter((id) => !isControllerAliasOutpostId(id, localHost))
1117
1119
  .filter((id) => localHost.kind !== "outpost" || id !== localHost.id)
1118
1120
  .sort();
1119
1121
  const local = buildHostCapacitySnapshot({
@@ -1676,7 +1678,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1676
1678
  });
1677
1679
  }
1678
1680
  const remoteIds = outpostIds.map((id) => id.trim())
1679
- .filter((id) => id !== "primary" || (localHost.kind === "outpost" && localHost.id !== "primary"))
1681
+ .filter((id) => !isControllerAliasOutpostId(id, localHost))
1680
1682
  .filter((id) => localHost.kind !== "outpost" || id !== localHost.id)
1681
1683
  .sort();
1682
1684
  const remoteResults = await mapWithConcurrency(remoteIds, OUTPOST_HIRE_SUMMARY_CONCURRENCY, async (id) => {
@@ -3282,6 +3284,8 @@ async function runCreate(ctx, opts) {
3282
3284
  hosts: balanceHostLoad.snapshots,
3283
3285
  targetHost,
3284
3286
  config,
3287
+ quotaState: loadQuotaState(hqRoot),
3288
+ ...(modelToValidate !== undefined ? { model: modelToValidate } : {}),
3285
3289
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
3286
3290
  now: ctx.now(),
3287
3291
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
@@ -3329,6 +3333,8 @@ async function runCreate(ctx, opts) {
3329
3333
  hosts,
3330
3334
  targetHost: balanceTargetHost,
3331
3335
  config,
3336
+ quotaState: loadQuotaState(hqRoot),
3337
+ ...(modelToValidate !== undefined ? { model: modelToValidate } : {}),
3332
3338
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
3333
3339
  now: ctx.now(),
3334
3340
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
@@ -6391,9 +6397,8 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6391
6397
  probe: ctx.probeProvider,
6392
6398
  env: ctx.env,
6393
6399
  }).logged_in;
6394
- const accountNeedsRepair = lane.account !== undefined &&
6400
+ const recordedAccountUnavailable = lane.account !== undefined &&
6395
6401
  (currentConfig?.enabled !== true || currentLogin === false);
6396
- const repairUnavailableAccount = options.repairUnavailableAccount === true && accountNeedsRepair;
6397
6402
  const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
6398
6403
  ? loadWorkerProfile(hqRoot, lane.worker_profile)
6399
6404
  : undefined);
@@ -6413,6 +6418,16 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6413
6418
  (lane.model_source === "cli" || lane.model_source === "preserved")
6414
6419
  ? lane.account
6415
6420
  : undefined;
6421
+ const quotaModel = resolveLaneModel({
6422
+ cliModel: pinnedModel ?? (lane.model_source === "account" ? lane.model : undefined),
6423
+ profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
6424
+ accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
6425
+ providerModel: config.providers.claude.model,
6426
+ });
6427
+ const recordedAccountQuotaHeld = lane.account !== undefined &&
6428
+ quotaBlockFor(loadQuotaState(hqRoot), "claude", lane.account, quotaModel, ctx.now()).blocked;
6429
+ const accountNeedsRepair = recordedAccountUnavailable || recordedAccountQuotaHeld;
6430
+ const repairUnavailableAccount = options.repairUnavailableAccount === true && accountNeedsRepair;
6416
6431
  const preservedModel = repairUnavailableAccount && lane.model_source === "account"
6417
6432
  ? lane.model ?? currentConfig?.model
6418
6433
  : undefined;
@@ -6430,7 +6445,8 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6430
6445
  const currentTargetAccount = lane.account === undefined
6431
6446
  ? undefined
6432
6447
  : targetHost?.capacity.providers.claude.accounts[lane.account];
6433
- if (profilePin !== undefined &&
6448
+ if (!recordedAccountQuotaHeld &&
6449
+ profilePin !== undefined &&
6434
6450
  lane.account !== undefined &&
6435
6451
  allowedAccounts?.includes(lane.account) &&
6436
6452
  currentConfig?.enabled === true &&
@@ -6443,6 +6459,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
6443
6459
  hosts: balanceHosts.snapshots,
6444
6460
  targetHost: lane.host,
6445
6461
  config,
6462
+ quotaState: loadQuotaState(hqRoot),
6446
6463
  recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
6447
6464
  now: ctx.now(),
6448
6465
  ...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
@@ -7810,6 +7827,29 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7810
7827
  summary.ok = false;
7811
7828
  }
7812
7829
  const terminalizedRemoteIds = new Set(remoteLost.map(({ id }) => id));
7830
+ let autopilotRegistrySync;
7831
+ const reconcileHost = reconcileHostFromCtx(ctx);
7832
+ const localHost = laneHostFromCtx(ctx);
7833
+ if (!opts.dryRun && (reconcileHost.outpostId === null || isControllerAliasOutpostId(reconcileHost.outpostId, localHost))) {
7834
+ try {
7835
+ autopilotRegistrySync = await syncOutpostLeadRegistries({
7836
+ hqRoot,
7837
+ outpostIds: await ctx.listOutpostIds(),
7838
+ generatedAt: ctx.now().toISOString(),
7839
+ localHost,
7840
+ resolveCompany: (slug) => ctx.resolveCompany(slug),
7841
+ readFlag: (flag, options) => ctx.readLeadAutopilotFlag(flag, options),
7842
+ dispatch: ctx.dispatchOutpostExec,
7843
+ });
7844
+ }
7845
+ catch (error) {
7846
+ const code = error instanceof LanesError ? error.error : "lead_autopilot_registry_sync_failed";
7847
+ const message = error instanceof Error ? error.message : String(error);
7848
+ summary.ok = false;
7849
+ summary.errors.push({ code, message });
7850
+ process.stderr.write(`hq lanes autopilot registry: ${message}\n`);
7851
+ }
7852
+ }
7813
7853
  let autopilot;
7814
7854
  try {
7815
7855
  const host = reconcileHostFromCtx(ctx);
@@ -7842,11 +7882,12 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7842
7882
  catch (error) {
7843
7883
  const message = error instanceof Error ? error.message : String(error);
7844
7884
  summary.ok = false;
7845
- summary.errors.push({ code: "lead_autopilot_failed", message });
7885
+ summary.errors.push({ code: error instanceof LanesError ? error.error : "lead_autopilot_failed", message });
7846
7886
  process.stderr.write(`hq lanes autopilot: ${message}\n`);
7847
7887
  }
7848
7888
  printJson({
7849
7889
  ...summary,
7890
+ ...(autopilotRegistrySync ? { autopilot_registry_sync: autopilotRegistrySync } : {}),
7850
7891
  ...(autopilot ? { autopilot } : {}),
7851
7892
  ...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
7852
7893
  ...(mirrorRefresh.unverifiedRemote.size > 0
@@ -0,0 +1,4 @@
1
+ export declare const CLI_PERSONAL_CONNECTOR_SETTINGS_FLAG = "cli.personal-connector-settings";
2
+ /** Keep company-shared connector storage unchanged until the flag is explicitly enabled. */
3
+ export declare function personalConnectorSettingsEnabled(): boolean;
4
+ //# sourceMappingURL=personal-connector-settings-flag.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
2
+ export const CLI_PERSONAL_CONNECTOR_SETTINGS_FLAG = "cli.personal-connector-settings";
3
+ /** Keep company-shared connector storage unchanged until the flag is explicitly enabled. */
4
+ export function personalConnectorSettingsEnabled() {
5
+ return resolveProcessFlagGate(CLI_PERSONAL_CONNECTOR_SETTINGS_FLAG, { fallback: false }, () => false);
6
+ }
7
+ //# sourceMappingURL=personal-connector-settings-flag.js.map
@@ -1,44 +1,19 @@
1
1
  import { Command } from "commander";
2
- import { buildReportTarball, deployOrgContext, type ChannelReport, type DeployAccessPolicy, type ReportDeployment, type ReportPatch, ReportPublishError } from "../lib/report-publish.js";
3
- type ChannelRow = {
4
- channelId?: string;
5
- scope?: string;
6
- companyUid?: string | null;
7
- };
2
+ import { deployOrgContext, type ChannelReport, type ReportPatch } from "../lib/report-publish.js";
8
3
  type ReportPatchInput = ReportPatch | ((current: ChannelReport) => ReportPatch);
9
- /** Any company-owned channel (company or project scope) can carry reports; personal channels cannot. */
10
- export declare function isCompanyReportChannel(channel: ChannelRow): boolean;
11
- /**
12
- * Records created before hq-pro stamped companyUid carry null. The channel the
13
- * caller can see knows its company, and publish needs it to pick the deploy
14
- * org, so fill it from there rather than failing every publish.
15
- */
16
- export declare function withChannelCompany(report: ChannelReport, channels: ChannelRow[]): ChannelReport;
17
4
  /** Explicit context has priority; discovery is the authenticated fallback for normal CLI use. */
18
5
  export declare function resolveReport(token: string, reportId: string, requestedChannelId?: string): Promise<ChannelReport>;
19
6
  export declare function patchReport(token: string, report: ChannelReport, patch: ReportPatchInput): Promise<ChannelReport>;
20
- /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
21
- export declare class SubdomainTakenError extends ReportPublishError {
22
- constructor();
23
- }
24
7
  /**
25
8
  * Subdomains to try, in order. The first is the report's own slug; the rest
26
9
  * are used only when an app the caller cannot see already holds it (for
27
10
  * example, one left by an earlier publish under a different account).
28
11
  */
29
12
  export declare function candidateSlugs(slug: string, attempts?: number): string[];
30
- export declare function resolveCompanySlug(token: string, companyUid: string): Promise<string>;
31
13
  export declare function findOrCreateApp(token: string, slug: string, org: ReturnType<typeof deployOrgContext>): Promise<{
32
14
  id: string;
33
15
  subdomain: string;
34
16
  }>;
35
- export declare function deployReportStatic(token: string, input: {
36
- slug: string;
37
- files: Parameters<typeof buildReportTarball>[0];
38
- access: DeployAccessPolicy;
39
- org: ReturnType<typeof deployOrgContext>;
40
- }): Promise<ReportDeployment>;
41
17
  export declare function registerReportCommand(program: Command): void;
42
- export declare function runReportCommand(action: () => Promise<void>): Promise<void>;
43
18
  export {};
44
19
  //# sourceMappingURL=report.d.ts.map
@@ -99,7 +99,7 @@ async function listReportChannels(token) {
99
99
  return Array.isArray(listed.channels) ? listed.channels : [];
100
100
  }
101
101
  /** Any company-owned channel (company or project scope) can carry reports; personal channels cannot. */
102
- export function isCompanyReportChannel(channel) {
102
+ function isCompanyReportChannel(channel) {
103
103
  if (channel.scope === "personal")
104
104
  return false;
105
105
  return channel.scope === "company" || channel.scope === "project" || Boolean(channel.companyUid);
@@ -125,7 +125,7 @@ async function readReportInChannel(token, channelId, reportId) {
125
125
  * caller can see knows its company, and publish needs it to pick the deploy
126
126
  * org, so fill it from there rather than failing every publish.
127
127
  */
128
- export function withChannelCompany(report, channels) {
128
+ function withChannelCompany(report, channels) {
129
129
  if (report.companyUid)
130
130
  return report;
131
131
  const companyUid = channels.find((row) => row.channelId === report.channelId)?.companyUid;
@@ -232,7 +232,7 @@ async function deployJson(token, route, method, body, extraHeaders = {}) {
232
232
  return await res.json();
233
233
  }
234
234
  /** hq-deploy refused the subdomain: another app, not visible to this caller, holds it. */
235
- export class SubdomainTakenError extends ReportPublishError {
235
+ class SubdomainTakenError extends ReportPublishError {
236
236
  constructor() {
237
237
  super("hq-deploy rejected the report deployment.");
238
238
  }
@@ -245,7 +245,7 @@ export class SubdomainTakenError extends ReportPublishError {
245
245
  export function candidateSlugs(slug, attempts = 3) {
246
246
  return [slug, ...Array.from({ length: attempts }, (_, i) => `${slug}-${i + 2}`)];
247
247
  }
248
- export async function resolveCompanySlug(token, companyUid) {
248
+ async function resolveCompanySlug(token, companyUid) {
249
249
  const res = await vaultApiFetch({ token, path: `/entity/${encodeURIComponent(companyUid)}` });
250
250
  if (!res.ok)
251
251
  throw new ReportPublishError("Could not resolve the report's company for deploy.");
@@ -290,7 +290,7 @@ export async function findOrCreateApp(token, slug, org) {
290
290
  throw new ReportPublishError("hq-deploy returned an incomplete app record.");
291
291
  return { id, subdomain };
292
292
  }
293
- export async function deployReportStatic(token, input) {
293
+ async function deployReportStatic(token, input) {
294
294
  const app = await findOrCreateApp(token, input.slug, input.org);
295
295
  await deployJson(token, `/api/apps/${encodeURIComponent(app.id)}/access-policy`, "PUT", input.access, input.org.headers);
296
296
  const tarball = buildReportTarball(input.files);
@@ -525,7 +525,7 @@ export function registerReportCommand(program) {
525
525
  print(templates, options.json === true);
526
526
  });
527
527
  }
528
- export async function runReportCommand(action) {
528
+ async function runReportCommand(action) {
529
529
  try {
530
530
  await action();
531
531
  }