@indigoai-us/hq-cli 5.309.1 → 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 (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/assets/scaffold/core/scripts/hq-dm-bind.sh +585 -0
  3. package/assets/scaffold/core/scripts/jobs-validate.sh +1 -1
  4. package/assets/scaffold/core/scripts/work-mesh-live-bind-trusted.sh +152 -0
  5. package/dist/command-catalog.generated.d.ts +28 -2
  6. package/dist/command-catalog.generated.js +34 -2
  7. package/dist/commands/channels.d.ts +1 -9
  8. package/dist/commands/channels.js +2 -2
  9. package/dist/commands/company-transfer.d.ts +4 -28
  10. package/dist/commands/company-transfer.js +5 -49
  11. package/dist/commands/core-checkpoint-stop-gate-native.d.ts +34 -0
  12. package/dist/commands/core-checkpoint-stop-gate-native.js +713 -0
  13. package/dist/commands/core-checkpoint.d.ts +0 -24
  14. package/dist/commands/core-checkpoint.js +4 -4
  15. package/dist/commands/core-command-catalog.js +1 -1
  16. package/dist/commands/core-dm-bind-native.d.ts +32 -0
  17. package/dist/commands/core-dm-bind-native.js +668 -0
  18. package/dist/commands/core-hq-session-native.d.ts +39 -0
  19. package/dist/commands/core-hq-session-native.js +579 -0
  20. package/dist/commands/core-jobs-native.js +1 -4
  21. package/dist/commands/core-native-session-flag.d.ts +4 -0
  22. package/dist/commands/core-native-session-flag.js +7 -0
  23. package/dist/commands/core-native-utilities.d.ts +2 -1
  24. package/dist/commands/core-native-utilities.js +4 -2
  25. package/dist/commands/core-native-worker-skills-flag.d.ts +4 -0
  26. package/dist/commands/core-native-worker-skills-flag.js +7 -0
  27. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +32 -0
  28. package/dist/commands/core-work-mesh-bind-trusted-native.js +190 -0
  29. package/dist/commands/core-worker-lint-native.d.ts +4 -0
  30. package/dist/commands/core-worker-lint-native.js +160 -0
  31. package/dist/commands/core-worker-share-native.d.ts +5 -0
  32. package/dist/commands/core-worker-share-native.js +260 -0
  33. package/dist/commands/core-worker-skills-command.d.ts +8 -0
  34. package/dist/commands/core-worker-skills-command.js +33 -0
  35. package/dist/commands/core-worker-skills-runtime.d.ts +33 -0
  36. package/dist/commands/core-worker-skills-runtime.js +86 -0
  37. package/dist/commands/core.js +10 -2
  38. package/dist/commands/dm.d.ts +3 -7
  39. package/dist/commands/dm.js +3 -3
  40. package/dist/commands/integrations-import.d.ts +1 -1
  41. package/dist/commands/integrations-import.js +34 -19
  42. package/dist/commands/lanes-accounts.js +3 -1
  43. package/dist/commands/lanes-message.js +84 -10
  44. package/dist/commands/lanes-shared.d.ts +4 -1
  45. package/dist/commands/lanes.d.ts +4 -1
  46. package/dist/commands/lanes.js +48 -7
  47. package/dist/commands/personal-connector-settings-flag.d.ts +4 -0
  48. package/dist/commands/personal-connector-settings-flag.js +7 -0
  49. package/dist/commands/report.d.ts +1 -26
  50. package/dist/commands/report.js +6 -6
  51. package/dist/fast-core.js +15 -2
  52. package/dist/fast-scaffold.js +25 -2
  53. package/dist/index.js +8 -0
  54. package/dist/lib/bot/continuity.d.ts +2 -1
  55. package/dist/lib/bot/job-completions.d.ts +0 -2
  56. package/dist/lib/bot/job-completions.js +2 -2
  57. package/dist/lib/bot/local-config.d.ts +0 -10
  58. package/dist/lib/bot/local-config.js +3 -9
  59. package/dist/lib/bot/owner-context.d.ts +2 -6
  60. package/dist/lib/bot/owner-context.js +3 -3
  61. package/dist/lib/bot/runtime/windows.d.ts +0 -2
  62. package/dist/lib/bot/runtime/windows.js +1 -1
  63. package/dist/lib/bot/setup-explain.d.ts +0 -4
  64. package/dist/lib/bot/setup-explain.js +2 -2
  65. package/dist/lib/daemon/catalog.d.ts +3 -2
  66. package/dist/lib/daemon/hq-cloud.d.ts +2 -2
  67. package/dist/lib/daemon/hq-cloud.js +1 -1
  68. package/dist/lib/daemon/machine-type.d.ts +3 -4
  69. package/dist/lib/daemon/machine-type.js +2 -2
  70. package/dist/lib/daemon/remote-control.d.ts +0 -1
  71. package/dist/lib/daemon/remote-control.js +1 -1
  72. package/dist/lib/daemon/supervisor.d.ts +4 -9
  73. package/dist/lib/daemon/supervisor.js +5 -5
  74. package/dist/lib/lanes/admission-host.d.ts +3 -0
  75. package/dist/lib/lanes/admission-host.js +4 -0
  76. package/dist/lib/lanes/autopilot-registry.d.ts +62 -0
  77. package/dist/lib/lanes/autopilot-registry.js +192 -0
  78. package/dist/lib/lanes/autopilot.d.ts +2 -0
  79. package/dist/lib/lanes/autopilot.js +18 -5
  80. package/dist/lib/lanes/claude-balance.d.ts +6 -0
  81. package/dist/lib/lanes/claude-balance.js +17 -2
  82. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  83. package/dist/lib/lanes/keep-alive.js +3 -0
  84. package/dist/lib/lanes/reconcile.js +14 -6
  85. package/dist/lib/lanes/spawn.js +2 -0
  86. package/dist/lib/mesh/live/daemon/credentials.d.ts +2 -17
  87. package/dist/lib/mesh/live/daemon/credentials.js +9 -9
  88. package/dist/lib/mesh/live/daemon/index.d.ts +5 -5
  89. package/dist/lib/mesh/live/daemon/index.js +2 -2
  90. package/dist/lib/mesh/live/daemon/state.d.ts +1 -1
  91. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +4 -11
  92. package/dist/lib/mesh/live/daemon/transcript-watch.js +5 -5
  93. package/dist/lib/work-context/company.d.ts +2 -25
  94. package/dist/lib/work-context/company.js +3 -3
  95. package/dist/lib/work-context/config.d.ts +3 -8
  96. package/dist/lib/work-context/config.js +2 -13
  97. package/dist/lib/work-context/state.d.ts +4 -13
  98. package/dist/lib/work-context/state.js +2 -30
  99. package/package.json +1 -1
@@ -0,0 +1,33 @@
1
+ import { runBundledScript } from "../utils/run-bundled-script.js";
2
+ import { coreNativeWorkerSkillsEnabled } from "./core-native-worker-skills-flag.js";
3
+ import { runWorkerSkillLint, WORKER_SKILL_LINT_ASSET } from "./core-worker-lint-native.js";
4
+ import { runWorkerSkillShare, WORKER_SKILL_SHARE_ASSET } from "./core-worker-share-native.js";
5
+ const assets = {
6
+ lint: WORKER_SKILL_LINT_ASSET,
7
+ share: WORKER_SKILL_SHARE_ASSET,
8
+ };
9
+ /** Keep the shell rollback route byte-transparent while the family flag is off. */
10
+ export async function runWorkerSkillCommand(name, args, context) {
11
+ if (!(await coreNativeWorkerSkillsEnabled())) {
12
+ const result = runBundledScript({
13
+ asset: assets[name],
14
+ args,
15
+ cwd: context.cwd,
16
+ hqRoot: context.hqRoot,
17
+ });
18
+ if (result.code !== 0)
19
+ process.exit(result.code);
20
+ return result.code;
21
+ }
22
+ const result = name === "lint"
23
+ ? runWorkerSkillLint(args, { cwd: context.cwd })
24
+ : runWorkerSkillShare(args, { cwd: context.cwd });
25
+ if (result.stdout)
26
+ process.stdout.write(result.stdout);
27
+ if (result.stderr)
28
+ process.stderr.write(result.stderr);
29
+ if (result.code !== 0)
30
+ process.exit(result.code);
31
+ return result.code;
32
+ }
33
+ //# sourceMappingURL=core-worker-skills-command.js.map
@@ -0,0 +1,33 @@
1
+ import * as fs from "node:fs";
2
+ export type WorkerSkillResult = {
3
+ code: number;
4
+ stdout: string;
5
+ stderr: string;
6
+ };
7
+ export type WorkerSkillFs = Pick<typeof fs, "lstatSync" | "statSync" | "readdirSync" | "readFileSync" | "readlinkSync" | "existsSync" | "mkdirSync" | "renameSync" | "unlinkSync" | "symlinkSync">;
8
+ export type GitResult = {
9
+ code: number;
10
+ stdout: string;
11
+ };
12
+ export type GitRunner = (args: string[], cwd: string, env: NodeJS.ProcessEnv) => GitResult;
13
+ export type WorkerSkillDependencies = {
14
+ cwd: string;
15
+ env?: NodeJS.ProcessEnv;
16
+ fs?: WorkerSkillFs;
17
+ runGit?: GitRunner;
18
+ /** The bundled shell's absolute $0, used for byte-identical usage text. */
19
+ scriptPath?: string;
20
+ };
21
+ export declare function runGit(args: string[], cwd: string, env: NodeJS.ProcessEnv, injected?: GitRunner): GitResult;
22
+ /** Match `git rev-parse --show-toplevel || pwd` without consulting HQ_ROOT. */
23
+ export declare function resolveWorkerCallerRoot(dependencies: WorkerSkillDependencies): {
24
+ root: string;
25
+ env: NodeJS.ProcessEnv;
26
+ fs: WorkerSkillFs;
27
+ };
28
+ export declare function errorCode(error: unknown): string | undefined;
29
+ export declare function missingOrUnreadable(error: unknown): boolean;
30
+ export declare function isDirectory(filePath: string, fileSystem: WorkerSkillFs): boolean;
31
+ export declare function isSymlink(filePath: string, fileSystem: WorkerSkillFs): boolean;
32
+ export declare function existsFollowingLinks(filePath: string, fileSystem: WorkerSkillFs): boolean;
33
+ //# sourceMappingURL=core-worker-skills-runtime.d.ts.map
@@ -0,0 +1,86 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ const SYSTEM_FS = fs;
4
+ function processGit(args, cwd, env) {
5
+ // Tests that set this marker must inject a runner. This guard catches an
6
+ // accidental escape from a synthetic fixture before any real executable can
7
+ // be reached through PATH.
8
+ if (env.HQ_CORE_STUBS === "1") {
9
+ throw new Error("real git execution is disabled while HQ_CORE_STUBS=1");
10
+ }
11
+ const result = spawnSync("git", args, { cwd, env, encoding: "utf8" });
12
+ return {
13
+ code: result.status ?? 127,
14
+ stdout: result.stdout ?? "",
15
+ };
16
+ }
17
+ export function runGit(args, cwd, env, injected) {
18
+ return (injected ?? processGit)(args, cwd, env);
19
+ }
20
+ function shellPwd(cwd, env, fileSystem) {
21
+ const logical = env.PWD;
22
+ if (!logical || !logical.startsWith("/"))
23
+ return cwd;
24
+ try {
25
+ const logicalStat = fileSystem.statSync(logical);
26
+ const cwdStat = fileSystem.statSync(cwd);
27
+ if (logicalStat.dev === cwdStat.dev && logicalStat.ino === cwdStat.ino)
28
+ return logical;
29
+ }
30
+ catch {
31
+ // Bash falls back to its physical cwd when PWD no longer names this inode.
32
+ }
33
+ return cwd;
34
+ }
35
+ /** Match `git rev-parse --show-toplevel || pwd` without consulting HQ_ROOT. */
36
+ export function resolveWorkerCallerRoot(dependencies) {
37
+ const env = dependencies.env ?? process.env;
38
+ const fileSystem = dependencies.fs ?? SYSTEM_FS;
39
+ const git = runGit(["rev-parse", "--show-toplevel"], dependencies.cwd, env, dependencies.runGit);
40
+ if (git.code === 0) {
41
+ // Command substitution removes every trailing LF and no other whitespace.
42
+ return { root: git.stdout.replace(/\n+$/, ""), env, fs: fileSystem };
43
+ }
44
+ return { root: shellPwd(dependencies.cwd, env, fileSystem), env, fs: fileSystem };
45
+ }
46
+ export function errorCode(error) {
47
+ if (!error || typeof error !== "object" || !("code" in error))
48
+ return undefined;
49
+ const code = error.code;
50
+ return typeof code === "string" ? code : undefined;
51
+ }
52
+ export function missingOrUnreadable(error) {
53
+ return ["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP"].includes(errorCode(error) ?? "");
54
+ }
55
+ export function isDirectory(filePath, fileSystem) {
56
+ try {
57
+ return fileSystem.statSync(filePath).isDirectory();
58
+ }
59
+ catch (error) {
60
+ if (missingOrUnreadable(error))
61
+ return false;
62
+ throw error;
63
+ }
64
+ }
65
+ export function isSymlink(filePath, fileSystem) {
66
+ try {
67
+ return fileSystem.lstatSync(filePath).isSymbolicLink();
68
+ }
69
+ catch (error) {
70
+ if (missingOrUnreadable(error))
71
+ return false;
72
+ throw error;
73
+ }
74
+ }
75
+ export function existsFollowingLinks(filePath, fileSystem) {
76
+ try {
77
+ fileSystem.statSync(filePath);
78
+ return true;
79
+ }
80
+ catch (error) {
81
+ if (missingOrUnreadable(error))
82
+ return false;
83
+ throw error;
84
+ }
85
+ }
86
+ //# sourceMappingURL=core-worker-skills-runtime.js.map
@@ -40,9 +40,11 @@ import { registerCoreJobsNativeCommand } from "./core-job-native.js";
40
40
  import { registerCoreResolveCompanyNativeCommand } from "./core-resolve-company-command.js";
41
41
  import { registerCoreCheckpointGateSettingCommand } from "./core-checkpoint-gate-setting.js";
42
42
  import { registerCoreAuditLogNative } from "./core-audit-log-native.js";
43
+ import { registerCoreDmBindNative } from "./core-dm-bind-native.js";
43
44
  import { registerCoreGitPackExtensionCheckNative } from "./core-git-pack-extension-check-native.js";
44
45
  import { registerCorePinNative } from "./core-pin-native.js";
45
46
  import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-access-native.js";
47
+ import { registerCoreWorkMeshBindTrustedNative } from "./core-work-mesh-bind-trusted-native.js";
46
48
  import { resolveLiveRoot } from "../utils/hq-roots.js";
47
49
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
48
50
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
@@ -63,6 +65,7 @@ import { readSentryReportStdin, reportSentryEvent } from "../lib/core-utils/sent
63
65
  import { CLEAN_HANDOFF_ASSET, RESUME_LOCK_ASSET, VERIFY_DELIVERABLES_ASSET, WORKTREE_GC_ASSET, runCleanWorktreeReconcileHandoff, runResumeThreadLock, runVerifyStoryDeliverables, runWorktreeGc, } from "../lib/core-utils/worktree-resume.js";
64
66
  import { coreNativeHandoffEnabled } from "./core-native-handoff-flag.js";
65
67
  import { coreCompanyIndexScopeEnabled } from "./core-company-index-scope-flag.js";
68
+ import { runWorkerSkillCommand } from "./core-worker-skills-command.js";
66
69
  import { describeCoreCommand, registeredCoreCommandListing, } from "./core-command-catalog.js";
67
70
  /**
68
71
  * These commands are now native implementations. Their retained shell assets
@@ -227,6 +230,8 @@ export const SCAFFOLD_ONLY_ASSETS = [
227
230
  { asset: "core/scripts/hq-job-run.sh", root: "live", summary: "Scheduled jobs runner used by generated systemd units" },
228
231
  { asset: "core/scripts/hq-job-notify.sh", root: "live", summary: "Scheduled jobs failure notification helper" },
229
232
  { asset: "core/scripts/hq-job-remediate.sh", root: "live", summary: "Scheduled jobs bounded remediation helper" },
233
+ { asset: "core/scripts/hq-dm-bind.sh", root: "live", summary: "Rollback shell for native dm-bind" },
234
+ { asset: "core/scripts/work-mesh-live-bind-trusted.sh", root: "live", summary: "Rollback shell for native work-mesh-bind-trusted" },
230
235
  // Shared shell libs sourced by the two relocated scripts (and hooks). Never
231
236
  // dispatched on their own (so no `hq core` command), but they MUST ship in the
232
237
  // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
@@ -353,6 +358,7 @@ export function registerCoreCommands(program) {
353
358
  const core = program
354
359
  .command("core", { hidden: true })
355
360
  .description("HQ scaffold scripts hosted by the CLI (core; not a stable surface)")
361
+ .addHelpText("afterAll", "exact-company-directory-match-v1")
356
362
  // The root override lives on the GROUP, not the subcommands, so it can never
357
363
  // collide with a wrapped script's own flags — everything after the
358
364
  // subcommand name is passed through untouched.
@@ -372,6 +378,8 @@ export function registerCoreCommands(program) {
372
378
  registerCoreGitPackExtensionCheckNative(core);
373
379
  registerCorePinNative(core);
374
380
  registerCoreRefreshVaultAccessNative(core);
381
+ registerCoreDmBindNative(core);
382
+ registerCoreWorkMeshBindTrustedNative(core);
375
383
  // Wave 2 handoff/worktree commands retain their byte-identical shell assets
376
384
  // as a default-off rollback path for one release. Keep the gate helper small
377
385
  // so sibling Wave 2 work can reuse it without editing this command table.
@@ -472,10 +480,10 @@ export function registerCoreCommands(program) {
472
480
  .allowExcessArguments()
473
481
  .helpOption(false)
474
482
  .argument("[args...]", "arguments passed through to the script")
475
- .action((args = [], _opts, cmd) => {
483
+ .action(async (args = [], _opts, cmd) => {
476
484
  const scope = core.opts();
477
485
  const operands = cmd.args.length > 0 ? cmd.args : args;
478
- runEntry(entry, scope, operands);
486
+ await runWorkerSkillCommand(entry.name, operands, resolveEntryRoot(entry, scope));
479
487
  });
480
488
  describeCoreCommand(command, {
481
489
  execution: interpreterFor(entry.asset) === "node" ? "bundled-node" : "bundled-shell",
@@ -71,7 +71,7 @@ export interface ChannelMember {
71
71
  handle?: string;
72
72
  }
73
73
  /** A structured mention as the POST messages route expects it. */
74
- export interface ChannelMention {
74
+ interface ChannelMention {
75
75
  participantUid: string;
76
76
  participantType: "agent" | "human";
77
77
  displayName: string;
@@ -133,7 +133,7 @@ export declare function resolveMentions(tokens: string[], members: ChannelMember
133
133
  * Returns null on anything that doesn't match. Pure → unit-testable.
134
134
  */
135
135
  export declare function parseDuration(input: string): number | null;
136
- export type DmAudience = "human" | "agent" | "both";
136
+ type DmAudience = "human" | "agent" | "both";
137
137
  export interface DmSendBody {
138
138
  toEmail?: string;
139
139
  toPersonUid?: string;
@@ -321,7 +321,6 @@ export interface CallerIdentity {
321
321
  * Pure → unit-testable.
322
322
  */
323
323
  export declare function callerFromToken(token: string): CallerIdentity;
324
- export declare function isCallerMessage(m: ChannelMessageItem, me: CallerIdentity): boolean;
325
324
  /**
326
325
  * Newest-first channel history → the caller's most recent top-level eventId.
327
326
  * Replies (those with rootEventId) are skipped. Pure → unit-testable.
@@ -366,9 +365,6 @@ export declare function formatChannelTranscript(messages: ChannelMessageItem[],
366
365
  */
367
366
  export declare function agentCallerUid(token: string, machine?: () => boolean): string | null;
368
367
  export declare const AGENT_THREAD_UNAVAILABLE: string;
369
- /** One-line summary of an agent inbox item (DM, email, … — shapes vary by channel). Pure. */
370
- export declare function formatAgentInboxItem(item: Record<string, unknown>): string;
371
- /** Remove delegated execution context before agent inbox items can be emitted. */
372
- export declare function redactAgentInboxItem<T extends object>(item: T): Omit<T, "actingContext">;
373
368
  export declare function registerDmCommand(program: Command): void;
369
+ export {};
374
370
  //# sourceMappingURL=dm.d.ts.map
@@ -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[]>;