@indigoai-us/hq-cli 5.275.0 → 5.277.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 (120) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +12 -0
  3. package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
  4. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  9. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  10. package/dist/command-catalog.generated.d.ts +239 -1
  11. package/dist/command-catalog.generated.js +302 -1
  12. package/dist/commands/core-checkpoint.d.ts +4 -2
  13. package/dist/commands/core-checkpoint.js +34 -3
  14. package/dist/commands/core-command-catalog.d.ts +4 -2
  15. package/dist/commands/core-command-catalog.js +5 -2
  16. package/dist/commands/core-delegate.d.ts +6 -0
  17. package/dist/commands/core-delegate.js +987 -0
  18. package/dist/commands/core-ontology.js +3 -3
  19. package/dist/commands/core.js +11 -1
  20. package/dist/commands/daemon.d.ts +4 -0
  21. package/dist/commands/daemon.js +175 -2
  22. package/dist/commands/dm.d.ts +2 -0
  23. package/dist/commands/dm.js +11 -4
  24. package/dist/commands/feedback.d.ts +6 -0
  25. package/dist/commands/feedback.js +86 -37
  26. package/dist/commands/lanes-accounts.js +44 -2
  27. package/dist/commands/lanes-message.js +69 -12
  28. package/dist/commands/lanes-resume.js +2 -0
  29. package/dist/commands/lanes-shared.d.ts +3 -0
  30. package/dist/commands/lanes-shared.js +12 -2
  31. package/dist/commands/lanes.d.ts +5 -1
  32. package/dist/commands/lanes.js +402 -49
  33. package/dist/commands/run-feature-telemetry.d.ts +3 -0
  34. package/dist/commands/run-feature-telemetry.js +6 -0
  35. package/dist/commands/run.js +3 -2
  36. package/dist/commands/secrets.d.ts +16 -1
  37. package/dist/commands/secrets.js +80 -45
  38. package/dist/commands/sync-mode.d.ts +11 -0
  39. package/dist/commands/sync-mode.js +30 -0
  40. package/dist/commands/whoami.d.ts +4 -6
  41. package/dist/commands/whoami.js +39 -8
  42. package/dist/fast-core.d.ts +6 -7
  43. package/dist/fast-core.js +22 -8
  44. package/dist/fast-scaffold.d.ts +33 -7
  45. package/dist/fast-scaffold.js +104 -8
  46. package/dist/index.js +32 -1
  47. package/dist/lanes-drain-hook.js +154 -7
  48. package/dist/lib/agent-kit/run/inbox.js +3 -2
  49. package/dist/lib/billing/plan-lock.d.ts +3 -1
  50. package/dist/lib/billing/plan-lock.js +7 -1
  51. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  52. package/dist/lib/checkpoint-ontology/apply.js +60 -3
  53. package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
  54. package/dist/lib/checkpoint-ontology/company.js +15 -7
  55. package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
  56. package/dist/lib/checkpoint-ontology/plan.js +66 -4
  57. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
  58. package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
  59. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  60. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  61. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  62. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  63. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  64. package/dist/lib/core-utils/secret-patterns.js +33 -0
  65. package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
  66. package/dist/lib/core-utils/timeout-guard-native.js +40 -0
  67. package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
  68. package/dist/lib/core-utils/timeout-guard.js +28 -13
  69. package/dist/lib/daemon/catalog.d.ts +18 -2
  70. package/dist/lib/daemon/catalog.js +18 -2
  71. package/dist/lib/daemon/config.d.ts +6 -0
  72. package/dist/lib/daemon/config.js +15 -2
  73. package/dist/lib/daemon/requests.d.ts +4 -0
  74. package/dist/lib/daemon/requests.js +2 -0
  75. package/dist/lib/daemon/run.js +94 -8
  76. package/dist/lib/daemon/supervisor.d.ts +8 -0
  77. package/dist/lib/daemon/supervisor.js +101 -1
  78. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  79. package/dist/lib/daemon/sync-runner.js +277 -0
  80. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  81. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  82. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  83. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  84. package/dist/lib/flag-registry.d.ts +12 -7
  85. package/dist/lib/flag-registry.js +66 -75
  86. package/dist/lib/flag-snapshot-file.d.ts +20 -0
  87. package/dist/lib/flag-snapshot-file.js +91 -0
  88. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  89. package/dist/lib/lanes/claude-balance.js +46 -8
  90. package/dist/lib/lanes/config.js +15 -0
  91. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  92. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  93. package/dist/lib/lanes/index.d.ts +1 -1
  94. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  95. package/dist/lib/lanes/keep-alive.js +3 -0
  96. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  97. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  98. package/dist/lib/lanes/reconcile.js +20 -4
  99. package/dist/lib/lanes/spawn.js +17 -10
  100. package/dist/lib/lanes/store.js +45 -1
  101. package/dist/lib/lanes/types.d.ts +30 -1
  102. package/dist/lib/lanes/types.js +2 -2
  103. package/dist/lib/lanes/watcher.js +3 -2
  104. package/dist/lib/process-flag-company-context.d.ts +8 -0
  105. package/dist/lib/process-flag-company-context.js +67 -0
  106. package/dist/main.js +3 -1
  107. package/dist/native-fast-route.d.ts +3 -0
  108. package/dist/native-fast-route.js +39 -0
  109. package/dist/secrets-env-fast-path.d.ts +5 -0
  110. package/dist/secrets-env-fast-path.js +16 -0
  111. package/dist/secrets-env-fast-route.d.ts +18 -0
  112. package/dist/secrets-env-fast-route.js +54 -0
  113. package/dist/utils/cli-telemetry.d.ts +26 -2
  114. package/dist/utils/cli-telemetry.js +178 -3
  115. package/dist/utils/hq-root-resolver.d.ts +13 -0
  116. package/dist/utils/hq-root-resolver.js +103 -0
  117. package/dist/utils/scaffold-fast-import-guard.js +6 -1
  118. package/dist/utils/vault-api.d.ts +8 -0
  119. package/dist/utils/vault-api.js +13 -1
  120. package/package.json +2 -2
@@ -0,0 +1,3 @@
1
+ /** Attribute hq run's batch secret read to its resolved company UID. */
2
+ export declare function withRunSecretReadTelemetry<T>(companyUid: string, action: () => Promise<T>): Promise<T>;
3
+ //# sourceMappingURL=run-feature-telemetry.d.ts.map
@@ -0,0 +1,6 @@
1
+ import { withCliFeatureUsed } from '../utils/cli-telemetry.js';
2
+ /** Attribute hq run's batch secret read to its resolved company UID. */
3
+ export function withRunSecretReadTelemetry(companyUid, action) {
4
+ return withCliFeatureUsed('secret_read', action, companyUid);
5
+ }
6
+ //# sourceMappingURL=run-feature-telemetry.js.map
@@ -11,6 +11,7 @@ import { discoverSchemas } from '../run/discover-schemas.js';
11
11
  import { installHqPlugin, prewarmHqSecrets } from '../run/hq-plugin.js';
12
12
  import { assertEnvGraphLoaded, resolveEnvValuesOrThrow, EnvGraphLoadError, EnvResolutionError, } from '../run/env-graph-guard.js';
13
13
  import { hadFatalRejection } from '../unhandled-rejection-boundary.js';
14
+ import { withRunSecretReadTelemetry } from './run-feature-telemetry.js';
14
15
  const SECRET_LOAD_TIMEOUT_MS = 30_000;
15
16
  export async function buildRunUsage(scriptPath, scriptId) {
16
17
  if (scriptId && !scriptPath) {
@@ -87,7 +88,7 @@ export function registerRunCommand(program) {
87
88
  allowOfflineScopeFallback: true,
88
89
  });
89
90
  const usage = await buildRunUsage(opts.script, opts.scriptId);
90
- const fetchBatch = async (companyUid, names, requestUsage) => {
91
+ const fetchBatch = async (companyUid, names, requestUsage) => withRunSecretReadTelemetry(companyUid, async () => {
91
92
  const res = await vaultApiFetch({
92
93
  token,
93
94
  path: `/secrets/${encodeURIComponent(companyUid)}/load`,
@@ -113,7 +114,7 @@ export function registerRunCommand(program) {
113
114
  throw new Error(`Failed to batch-load secrets: ${message}`);
114
115
  }
115
116
  return res.json();
116
- };
117
+ });
117
118
  const pluginOpts = {
118
119
  companyOverride: opts.company,
119
120
  principalUid: cognitoCachePrincipalUid(token) ?? undefined,
@@ -1,4 +1,4 @@
1
- import { Command } from "commander";
1
+ import type { Command } from "commander";
2
2
  import { writeSync } from "node:fs";
3
3
  import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
4
4
  export type { VaultApiOptions } from "../utils/vault-api.js";
@@ -88,5 +88,20 @@ export declare function loadRevealedSecrets(token: string, companyUid: string, k
88
88
  * `exit` and `write` are injectable for tests; production uses the real ones.
89
89
  */
90
90
  export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
91
+ /**
92
+ * Shared `secrets env` action for Commander and the default-off lightweight
93
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
94
+ * server authorization, cache-fallback, redaction, and output code.
95
+ */
96
+ export declare function runSecretsEnvCommand(opts: {
97
+ only: string[];
98
+ script?: string;
99
+ scriptId?: string;
100
+ }, parentScope: {
101
+ company?: string;
102
+ personal?: boolean;
103
+ }, options?: {
104
+ fallbackOnAuthFailure?: boolean;
105
+ }): Promise<"handled" | "fallback">;
91
106
  export declare function registerSecretsCommand(program: Command): void;
92
107
  //# sourceMappingURL=secrets.d.ts.map
@@ -1,9 +1,8 @@
1
1
  import chalk from "chalk";
2
- import * as readline from "node:readline";
3
- import { spawn } from "node:child_process";
4
2
  import { writeSync } from "node:fs";
5
3
  import * as nodePath from "node:path";
6
4
  import { cognitoCachePrincipalUid, ensureCognitoToken } from "../utils/cognito-session.js";
5
+ import { isAuthError } from "../utils/auth-error.js";
7
6
  import { DEFAULT_SECRETS_CACHE_TTL_MS, hasCacheEntry, readCache, writeCache, removeCacheEntry, clearAllCache, } from "../utils/secrets-cache.js";
8
7
  import { computeSha256 } from "../utils/integrity.js";
9
8
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN, EMAIL_PATTERN } from "./_patterns.js";
@@ -11,7 +10,6 @@ import { describeSecretsScope, formatSecretSaved, formatSecretsListEmpty, format
11
10
  import { vaultApiFetch, getCompanyUid, getEntityUid, looksLikeCompanyUid, } from "../utils/vault-api.js";
12
11
  import { HQ_API_KEY_PREFIX, formatApiKeyCapabilityDenial, requireApiKeyCapability, resolveVaultCredentialForCapability, } from "../utils/resolve-vault-credential.js";
13
12
  import { CACHED_SECRETS_SERVER_UNAVAILABLE_MESSAGE, isSecretLoadUnavailable, isUnavailableSecretLoadHttpStatus, SecretLoadUnavailableError, } from "../utils/secret-load-availability.js";
14
- import { SandboxRunnerClient, } from "../utils/sandbox-runner-client.js";
15
13
  export { vaultApiFetch, getCompanyUid, getEntityUid };
16
14
  /**
17
15
  * Mirrors `RESOURCE_SURFACES` in hq-pro
@@ -158,14 +156,16 @@ function parseDuration(input) {
158
156
  return null;
159
157
  }
160
158
  function confirmPrompt(message) {
161
- const rl = readline.createInterface({
162
- input: process.stdin,
163
- output: process.stdout,
164
- });
165
- return new Promise((resolve) => {
166
- rl.question(`${message} [y/N] `, (answer) => {
167
- rl.close();
168
- resolve(answer.trim().toLowerCase() === "y");
159
+ return import("node:readline").then(({ createInterface }) => {
160
+ const rl = createInterface({
161
+ input: process.stdin,
162
+ output: process.stdout,
163
+ });
164
+ return new Promise((resolve) => {
165
+ rl.question(`${message} [y/N] `, (answer) => {
166
+ rl.close();
167
+ resolve(answer.trim().toLowerCase() === "y");
168
+ });
169
169
  });
170
170
  });
171
171
  }
@@ -820,6 +820,61 @@ export function emitEnvExportsAndExit(payload, exit = process.exit, write = writ
820
820
  }
821
821
  return exit(0);
822
822
  }
823
+ /**
824
+ * Shared `secrets env` action for Commander and the default-off lightweight
825
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
826
+ * server authorization, cache-fallback, redaction, and output code.
827
+ */
828
+ export async function runSecretsEnvCommand(opts, parentScope, options = {}) {
829
+ let secretLoadStarted = false;
830
+ try {
831
+ const redact = process.stdout.isTTY;
832
+ if (redact) {
833
+ console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
834
+ }
835
+ const keys = parseSecretNameList(opts.only);
836
+ const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
837
+ const companyUid = cred.kind === "api-key"
838
+ ? "__api_key__"
839
+ : await getEntityUid(cred.token, scopeOpts(parentScope), {
840
+ allowOfflineScopeFallback: true,
841
+ });
842
+ const usage = cred.kind === "cognito"
843
+ ? await buildSecretUsage("env", opts.script, opts.scriptId)
844
+ : undefined;
845
+ secretLoadStarted = true;
846
+ const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, usage, { allowOfflineFallback: true });
847
+ let payload = "";
848
+ for (const key of keys) {
849
+ const value = revealed.get(key);
850
+ // loadRevealedSecrets throws on any unresolved key, so a miss here is
851
+ // unreachable — guard rather than print an `undefined` export line.
852
+ if (value === undefined) {
853
+ throw new Error(`Failed to fetch secret '${key}': not returned by vault`);
854
+ }
855
+ const out = redact ? "[REDACTED]" : value;
856
+ payload += `export ${key}=${shellSingleQuote(out)}\n`;
857
+ }
858
+ // Write synchronously and exit unconditionally — never depend on the
859
+ // event loop draining or the stdout write callback firing. See
860
+ // emitEnvExportsAndExit for the full rationale (orphaned-process leak).
861
+ emitEnvExportsAndExit(payload);
862
+ return "handled";
863
+ }
864
+ catch (err) {
865
+ if (options.fallbackOnAuthFailure &&
866
+ !secretLoadStarted &&
867
+ (isAuthError(err) ||
868
+ (err instanceof Error &&
869
+ (/^No valid HQ session\b/.test(err.message) ||
870
+ /^HQ_API_KEY must start with /.test(err.message))))) {
871
+ return "fallback";
872
+ }
873
+ console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
874
+ process.exit(1);
875
+ return "handled";
876
+ }
877
+ }
823
878
  export function registerSecretsCommand(program) {
824
879
  const secrets = program
825
880
  .command("secrets")
@@ -1444,6 +1499,7 @@ export function registerSecretsCommand(program) {
1444
1499
  const token = await requireCognitoTokenForSecrets("secrets sandbox", "secrets:read");
1445
1500
  const scope = scopeOpts(mergeScopeOpts(secrets.opts(), opts));
1446
1501
  const companyUid = await getEntityUid(token, scope);
1502
+ const { SandboxRunnerClient } = await import("../utils/sandbox-runner-client.js");
1447
1503
  const client = new SandboxRunnerClient();
1448
1504
  const started = await client.startJob(token, {
1449
1505
  companyUid,
@@ -1498,9 +1554,19 @@ export function registerSecretsCommand(program) {
1498
1554
  const companyUid = cred.kind === "api-key"
1499
1555
  ? "__api_key__"
1500
1556
  : await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
1501
- const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1557
+ const readSecrets = async () => loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1502
1558
  ? await buildSecretUsage("exec", _opts.script, _opts.scriptId)
1503
1559
  : undefined, { allowOfflineFallback: true });
1560
+ // API-key auth has no signed-in person identity for the CLI telemetry
1561
+ // endpoint. Do not attribute that read to an unrelated cached session.
1562
+ let revealed;
1563
+ if (cred.kind === "cognito") {
1564
+ const { withCliFeatureUsed } = await import("../utils/cli-telemetry.js");
1565
+ revealed = await withCliFeatureUsed("secret_read", readSecrets, companyUid);
1566
+ }
1567
+ else {
1568
+ revealed = await readSecrets();
1569
+ }
1504
1570
  const secretEnv = {};
1505
1571
  for (const key of keys) {
1506
1572
  const value = revealed.get(key);
@@ -1512,6 +1578,7 @@ export function registerSecretsCommand(program) {
1512
1578
  secretEnv[key] = value;
1513
1579
  }
1514
1580
  const [childCmd, ...childCmdArgs] = childArgs;
1581
+ const { spawn } = await import("node:child_process");
1515
1582
  const child = spawn(childCmd, childCmdArgs, {
1516
1583
  stdio: "inherit",
1517
1584
  env: { ...process.env, ...secretEnv },
@@ -1539,39 +1606,7 @@ export function registerSecretsCommand(program) {
1539
1606
  .option("--script <path>", "Attach local script identity for script-locked secrets")
1540
1607
  .option("--script-id <id>", "Stable script identifier approved by policy")
1541
1608
  .action(async (opts) => {
1542
- try {
1543
- const redact = process.stdout.isTTY;
1544
- if (redact) {
1545
- console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
1546
- }
1547
- const keys = parseSecretNameList(opts.only);
1548
- const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
1549
- const companyUid = cred.kind === "api-key"
1550
- ? "__api_key__"
1551
- : await getEntityUid(cred.token, scopeOpts(secrets.opts()), { allowOfflineScopeFallback: true });
1552
- const revealed = await loadRevealedSecrets(cred.token, companyUid, keys, cred.kind === "cognito"
1553
- ? await buildSecretUsage("env", opts.script, opts.scriptId)
1554
- : undefined, { allowOfflineFallback: true });
1555
- let payload = "";
1556
- for (const key of keys) {
1557
- const value = revealed.get(key);
1558
- // loadRevealedSecrets throws on any unresolved key, so a miss here is
1559
- // unreachable — guard rather than print an `undefined` export line.
1560
- if (value === undefined) {
1561
- throw new Error(`Failed to fetch secret '${key}': not returned by vault`);
1562
- }
1563
- const out = redact ? "[REDACTED]" : value;
1564
- payload += `export ${key}=${shellSingleQuote(out)}\n`;
1565
- }
1566
- // Write synchronously and exit unconditionally — never depend on the
1567
- // event loop draining or the stdout write callback firing. See
1568
- // emitEnvExportsAndExit for the full rationale (orphaned-process leak).
1569
- emitEnvExportsAndExit(payload);
1570
- }
1571
- catch (err) {
1572
- console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
1573
- process.exit(1);
1574
- }
1609
+ await runSecretsEnvCommand(opts, secrets.opts());
1575
1610
  });
1576
1611
  secrets
1577
1612
  .command("generate-link <name>")
@@ -70,6 +70,15 @@ export interface SetSyncModeResult {
70
70
  export interface ShowSyncModesOptions {
71
71
  vaultClient: SyncModeVaultClient;
72
72
  }
73
+ export interface GetSyncModeOptions {
74
+ companySlug: string;
75
+ vaultClient: SyncModeVaultClient;
76
+ }
77
+ export interface GetSyncModeResult {
78
+ membershipId: string;
79
+ companySlug: string;
80
+ config: MembershipSyncConfig;
81
+ }
73
82
  export interface ShowSyncModesRow {
74
83
  companySlug: string;
75
84
  companyName?: string;
@@ -100,6 +109,8 @@ export declare function setSyncMode(options: SetSyncModeOptions): Promise<SetSyn
100
109
  * stable table output.
101
110
  */
102
111
  export declare function showSyncModes(options: ShowSyncModesOptions): Promise<ShowSyncModesRow[]>;
112
+ /** Read one company's effective sync mode without hiding API read failures. */
113
+ export declare function getSyncModeForCompany(options: GetSyncModeOptions): Promise<GetSyncModeResult>;
103
114
  /**
104
115
  * Render the `--show` table as plain text. No external dep — hq-cli doesn't
105
116
  * use cli-table3, so we hand-format columns to match the existing chalk +
@@ -163,6 +163,36 @@ export async function showSyncModes(options) {
163
163
  rows.sort((a, b) => a.companySlug.localeCompare(b.companySlug));
164
164
  return rows;
165
165
  }
166
+ /** Read one company's effective sync mode without hiding API read failures. */
167
+ export async function getSyncModeForCompany(options) {
168
+ const { companySlug, vaultClient } = options;
169
+ const memberships = await vaultClient.listMyMemberships();
170
+ const enriched = await Promise.all(memberships.map(async (membership) => {
171
+ try {
172
+ const entity = await vaultClient.entity.get(membership.companyUid);
173
+ return { ok: true, membership, slug: entity.slug };
174
+ }
175
+ catch (error) {
176
+ return { ok: false, membership, error };
177
+ }
178
+ }));
179
+ const match = enriched.find((row) => row.ok && row.slug === companySlug);
180
+ if (!match) {
181
+ const failedLookup = enriched.find((row) => !row.ok);
182
+ if (failedLookup && !failedLookup.ok)
183
+ throw failedLookup.error;
184
+ throw new Error(`No membership found for company '${companySlug}'.`);
185
+ }
186
+ for (const failedLookup of enriched) {
187
+ if (!failedLookup.ok) {
188
+ const detail = failedLookup.error instanceof Error ? failedLookup.error.message : String(failedLookup.error);
189
+ console.error(`getSyncModeForCompany: entity lookup failed for membership ${failedLookup.membership.membershipKey} while resolving '${companySlug}': ${detail}`);
190
+ }
191
+ }
192
+ const membershipId = match.membership.membershipKey;
193
+ const config = await vaultClient.getMembershipSyncConfig(membershipId);
194
+ return { membershipId, companySlug, config };
195
+ }
166
196
  // ── Table rendering (used by CLI action, separable for tests) ──────────────
167
197
  /**
168
198
  * Render the `--show` table as plain text. No external dep — hq-cli doesn't
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { Command } from 'commander';
5
5
  import { type PlanLimitStatus } from "../lib/billing/plan-lock.js";
6
+ export declare const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
6
7
  export interface WhoamiTokenIdentity {
7
8
  email?: string;
8
9
  sub?: string;
@@ -20,11 +21,8 @@ export interface WhoamiDisplayIdentity {
20
21
  * email with the token subject of the delegating machine.
21
22
  */
22
23
  export declare function resolveWhoamiIdentity(identity: WhoamiTokenIdentity): WhoamiDisplayIdentity;
23
- /**
24
- * Human rendering of the plan state: the full over-limit block when the
25
- * workspace is over its Starter limits, the one-line `Plan: Starter — …`
26
- * orientation when it is Starter and healthy, and nothing at all otherwise.
27
- */
28
- export declare function renderWhoamiPlanBlock(status: PlanLimitStatus): string | null;
24
+ export declare function renderWhoamiPlanBlock(status: PlanLimitStatus, opts?: {
25
+ inviteTeammateEnabled?: boolean;
26
+ }): string | null;
29
27
  export declare function registerWhoamiCommand(program: Command): void;
30
28
  //# sourceMappingURL=whoami.d.ts.map
@@ -6,6 +6,9 @@ import { loadCachedTokens, isExpiring, loadMachineCreds, } from '@indigoai-us/hq
6
6
  import { peekIdToken as decodeIdToken } from "../utils/id-token.js";
7
7
  import { ensureCognitoToken, loadMachineCachedTokens, resolveCognitoTokenSource, } from "../utils/cognito-session.js";
8
8
  import { colorizePlanLimitNotice, fetchPlanLimitStatus, renderPlanLine, renderPlanLimitNotice, } from "../lib/billing/plan-lock.js";
9
+ import { resolveCompanyFlagGate, } from "../lib/flag-registry.js";
10
+ import { queuePlanLimitInviteTeammateExposure } from "../utils/cli-telemetry.js";
11
+ export const CLI_LIMIT_INVITE_TEAMMATE_FLAG = "cli.limit-invite-teammate-v1";
9
12
  /**
10
13
  * Resolves the person represented by an ID token without pairing a delegated
11
14
  * email with the token subject of the delegating machine.
@@ -62,7 +65,13 @@ async function readPlanLimits(company) {
62
65
  // default `interactive: true`, a box with no cached session would launch a
63
66
  // browser sign-in and block the caller instead of resolving to UNKNOWN.
64
67
  const token = await ensureCognitoToken({ interactive: false });
65
- return await fetchPlanLimitStatus(token, slug);
68
+ const status = await fetchPlanLimitStatus(token, slug);
69
+ if (!status)
70
+ return null;
71
+ return {
72
+ status,
73
+ idToken: loadCachedTokens()?.idToken,
74
+ };
66
75
  }
67
76
  catch {
68
77
  return null;
@@ -73,19 +82,41 @@ async function readPlanLimits(company) {
73
82
  * workspace is over its Starter limits, the one-line `Plan: Starter — …`
74
83
  * orientation when it is Starter and healthy, and nothing at all otherwise.
75
84
  */
76
- export function renderWhoamiPlanBlock(status) {
85
+ function shouldOfferInviteTeammateSuggestion(status) {
86
+ if (!status.over ||
87
+ status.plan !== "free" ||
88
+ status.members?.used !== 1 ||
89
+ !status.companyUid) {
90
+ return false;
91
+ }
92
+ return true;
93
+ }
94
+ async function inviteTeammateSuggestionEnabled(status, idToken) {
95
+ if (!shouldOfferInviteTeammateSuggestion(status) || !status.companyUid) {
96
+ return false;
97
+ }
98
+ return resolveCompanyFlagGate(status.companyUid, idToken ?? "", CLI_LIMIT_INVITE_TEAMMATE_FLAG, { fallback: false }, () => false);
99
+ }
100
+ export function renderWhoamiPlanBlock(status, opts = {}) {
77
101
  if (status.over) {
78
- return colorizePlanLimitNotice(renderPlanLimitNotice(status));
102
+ const inviteTeammateEnabled = opts.inviteTeammateEnabled === true;
103
+ return colorizePlanLimitNotice(renderPlanLimitNotice(status, { inviteTeammateEnabled }));
79
104
  }
80
105
  return renderPlanLine(status);
81
106
  }
82
107
  async function printPlanBlock(company) {
83
- const status = await readPlanLimits(company);
84
- if (!status)
108
+ const planLimits = await readPlanLimits(company);
109
+ if (!planLimits)
110
+ return;
111
+ const { status, idToken } = planLimits;
112
+ const inviteTeammateEnabled = await inviteTeammateSuggestionEnabled(status, idToken);
113
+ const block = renderWhoamiPlanBlock(status, { inviteTeammateEnabled });
114
+ if (!block)
85
115
  return;
86
- const block = renderWhoamiPlanBlock(status);
87
- if (block)
88
- console.log(block);
116
+ console.log(block);
117
+ if (inviteTeammateEnabled && status.companyUid) {
118
+ queuePlanLimitInviteTeammateExposure(status.companyUid);
119
+ }
89
120
  }
90
121
  export function registerWhoamiCommand(program) {
91
122
  program
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Fast-path dispatcher for relocated scaffold scripts.
2
+ * Fast-path dispatcher for selected core commands.
3
3
  *
4
4
  * Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
5
- * ever imported, so a hot `hq core hq-session` / `hq core checkpoint-stop-gate`
6
- * dispatches in roughly node's own startup time. It imports only the light root
7
- * resolver and the bundled-script runner. The slower commander registration in
8
- * `core.ts` remains the complete path for any invocation this does not match.
5
+ * ever imported, so hot hook commands avoid the full commander graph. Shell
6
+ * entries use the bundled-script runner; native entries import their one handler
7
+ * module. The registration in `core.ts` remains the complete fallback path.
9
8
  */
10
9
  /**
11
- * Run the fast-path scaffold script named in `argv` and exit with its code.
10
+ * Run the fast-path core command named in `argv` and exit with its code.
12
11
  * Only call when {@link isFastCoreRequest} is true; a non-match returns without
13
12
  * doing anything.
14
13
  */
15
- export declare function runFastCore(argv: readonly string[]): void;
14
+ export declare function runFastCore(argv: readonly string[]): Promise<void>;
16
15
  //# sourceMappingURL=fast-core.d.ts.map
package/dist/fast-core.js CHANGED
@@ -1,21 +1,20 @@
1
1
  /**
2
- * Fast-path dispatcher for relocated scaffold scripts.
2
+ * Fast-path dispatcher for selected core commands.
3
3
  *
4
4
  * Reached from the entrypoint BEFORE `main.ts` (and its heavy command graph) is
5
- * ever imported, so a hot `hq core hq-session` / `hq core checkpoint-stop-gate`
6
- * dispatches in roughly node's own startup time. It imports only the light root
7
- * resolver and the bundled-script runner. The slower commander registration in
8
- * `core.ts` remains the complete path for any invocation this does not match.
5
+ * ever imported, so hot hook commands avoid the full commander graph. Shell
6
+ * entries use the bundled-script runner; native entries import their one handler
7
+ * module. The registration in `core.ts` remains the complete fallback path.
9
8
  */
10
9
  import { resolveLiveRoot } from "./utils/hq-roots.js";
11
10
  import { runBundledScript } from "./utils/run-bundled-script.js";
12
11
  import { parseFastCore } from "./fast-scaffold.js";
13
12
  /**
14
- * Run the fast-path scaffold script named in `argv` and exit with its code.
13
+ * Run the fast-path core command named in `argv` and exit with its code.
15
14
  * Only call when {@link isFastCoreRequest} is true; a non-match returns without
16
15
  * doing anything.
17
16
  */
18
- export function runFastCore(argv) {
17
+ export async function runFastCore(argv) {
19
18
  const parsed = parseFastCore(argv);
20
19
  if (!parsed)
21
20
  return;
@@ -31,12 +30,27 @@ export function runFastCore(argv) {
31
30
  catch {
32
31
  hqRoot = undefined;
33
32
  }
33
+ if (parsed.entry.kind === "native") {
34
+ const handlerModule = await import(parsed.entry.modulePath);
35
+ const handler = handlerModule[parsed.entry.exportName];
36
+ if (typeof handler !== "function") {
37
+ throw new Error(`Native fast handler ${parsed.entry.exportName} was not exported`);
38
+ }
39
+ const chunks = [];
40
+ for await (const chunk of process.stdin)
41
+ chunks.push(chunk);
42
+ const code = await handler(argv, Buffer.concat(chunks).toString("utf8"), process.cwd(), hqRoot);
43
+ // Let Node drain handler output before terminating. The timeout guard's
44
+ // stderr message must survive the native route's process boundary.
45
+ process.exitCode = code;
46
+ return;
47
+ }
34
48
  // Keep the CHILD in the caller's cwd, unlike the generic "live" dispatch. The
35
49
  // relocated scripts read their data tree from the injected root, not cwd, and
36
50
  // hq-session's Work Mesh registration serializes $PWD — so changing directory
37
51
  // to the HQ root would mislabel a bind made from inside a project/worktree.
38
52
  const { code } = runBundledScript({
39
- asset: parsed.asset,
53
+ asset: parsed.entry.asset,
40
54
  args: parsed.args,
41
55
  cwd: process.cwd(),
42
56
  hqRoot,
@@ -1,32 +1,58 @@
1
1
  /**
2
- * Fast-path manifest for relocated scaffold scripts, kept deliberately
2
+ * Fast-path manifest for selected core commands, kept deliberately
3
3
  * dependency-light.
4
4
  *
5
5
  * `main.ts` eagerly imports the CLI's entire (~60-module) command graph, so any
6
6
  * command routed through it pays seconds of startup — fine for a human typing a
7
7
  * command, ruinous for the hot plumbing this hosts: `hq core hq-session` is
8
8
  * called several times per skill/hook, and `hq core checkpoint-stop-gate` runs
9
- * on every turn's Stop. The entrypoint routes those directly to
10
- * `runBundledScript` instead, and this module holds the tiny parser + table that
11
- * routing needs WITHOUT importing anything heavy. `core.ts` names the same
12
- * assets, and a test pins the two lists together so they cannot drift.
9
+ * on every turn's Stop. The entrypoint routes eligible commands before loading
10
+ * the graph, and this module holds the parser + table that routing needs.
13
11
  */
14
12
  export type FastScaffoldScript = {
15
13
  /** Direct subcommand name — `hq core <name>`. */
16
14
  name: string;
15
+ kind: "shell";
17
16
  /** Path under `assets/scaffold/`, e.g. `core/scripts/foo.sh`. */
18
17
  asset: string;
18
+ } | {
19
+ /** Direct subcommand name — `hq core <name>`. */
20
+ name: string;
21
+ kind: "native";
22
+ /** Relative ESM module specifier, resolved from fast-core.js. */
23
+ modulePath: string;
24
+ /** Named export which accepts the native fast-handler contract. */
25
+ exportName: string;
26
+ /** Default-off registry flag for this native route. */
27
+ flagKey: string;
19
28
  };
20
29
  /** The relocated scripts that qualify for the entrypoint fast path. */
21
30
  export declare const FAST_SCAFFOLD_SCRIPTS: readonly FastScaffoldScript[];
22
31
  export type ParsedFastCore = {
23
- /** The bundled asset to run. */
24
- asset: string;
32
+ /** The manifest entry to run. */
33
+ entry: FastScaffoldScript;
25
34
  /** `--hq-root <path>` value, if the caller passed the group option. */
26
35
  hqRootArg?: string;
27
36
  /** Everything after the command name, passed to the script verbatim. */
28
37
  args: string[];
29
38
  };
39
+ export type ParsedSecretsEnvFastRequest = {
40
+ options: {
41
+ only: string[];
42
+ script?: string;
43
+ scriptId?: string;
44
+ };
45
+ scope: {
46
+ company?: string;
47
+ personal?: boolean;
48
+ };
49
+ };
50
+ /**
51
+ * Parse the small, non-ambiguous `hq secrets env` surface for the gated entry
52
+ * fast path. Anything this parser does not understand stays with Commander in
53
+ * the full CLI, which remains the source of truth for help and errors.
54
+ */
55
+ export declare function parseSecretsEnvFastRequest(argv: readonly string[]): ParsedSecretsEnvFastRequest | null;
30
56
  /**
31
57
  * Parse `hq core [--hq-root <path>] <name> [args...]` when `<name>` is a
32
58
  * fast-path script. Returns null for anything else so the caller defers to the