@indigoai-us/hq-cli 5.345.159 → 5.345.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/command-catalog.generated.d.ts +4 -4
  3. package/dist/command-catalog.generated.js +4 -4
  4. package/dist/commands/access.d.ts +6 -0
  5. package/dist/commands/access.js +18 -1
  6. package/dist/commands/acl-lock.d.ts +41 -0
  7. package/dist/commands/acl-lock.js +28 -0
  8. package/dist/commands/agent-enroll.d.ts +2 -0
  9. package/dist/commands/agent-enroll.js +9 -4
  10. package/dist/commands/agents.js +5 -11
  11. package/dist/commands/billing-sponsor.d.ts +4 -4
  12. package/dist/commands/billing-sponsor.js +11 -11
  13. package/dist/commands/billing-usage.d.ts +22 -8
  14. package/dist/commands/billing-usage.js +57 -22
  15. package/dist/commands/billing.d.ts +7 -7
  16. package/dist/commands/billing.js +13 -13
  17. package/dist/commands/cloud-provision.js +2 -2
  18. package/dist/commands/cloud.js +7 -7
  19. package/dist/commands/db-remote-deps.js +12 -1
  20. package/dist/commands/files.js +19 -7
  21. package/dist/commands/meetings.d.ts +4 -4
  22. package/dist/commands/meetings.js +17 -8
  23. package/dist/commands/skill.js +6 -6
  24. package/dist/commands/whoami.js +1 -1
  25. package/dist/hook-engine-path.js +13 -2
  26. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +982 -426
  27. package/dist/hq-cloud-sync-runner/package.json +1 -1
  28. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  29. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  30. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  31. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  32. package/dist/lib/billing/plan-lock.d.ts +22 -6
  33. package/dist/lib/billing/plan-lock.js +58 -30
  34. package/dist/lib/billing/usage-cost.d.ts +24 -2
  35. package/dist/lib/billing/usage-cost.js +103 -3
  36. package/dist/lib/billing/usage-only.d.ts +37 -1
  37. package/dist/lib/billing/usage-only.js +89 -4
  38. package/dist/lib/billing/usage-pause.d.ts +27 -0
  39. package/dist/lib/billing/usage-pause.js +50 -0
  40. package/dist/lib/db/control-plane.d.ts +5 -0
  41. package/dist/lib/db/control-plane.js +10 -0
  42. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  43. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  44. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  45. package/dist/lib/hook-engine/resolve.js +55 -1
  46. package/dist/lib/plan-limit-nag.d.ts +0 -16
  47. package/dist/lib/plan-limit-nag.js +17 -18
  48. package/dist/main.d.ts +0 -2
  49. package/dist/main.js +19 -57
  50. package/dist/utils/plan-gate-error.d.ts +30 -14
  51. package/dist/utils/plan-gate-error.js +60 -46
  52. package/dist/utils/team-upgrade.d.ts +19 -6
  53. package/dist/utils/team-upgrade.js +37 -39
  54. package/package.json +8 -8
@@ -13,16 +13,16 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import { Command } from "commander";
28
28
  interface DatabaseBilling {
@@ -13,22 +13,22 @@
13
13
  * hq billing checkout [--company <slug>] [--personal] — mint a card-capture link
14
14
  * hq billing usage [--company <slug>] [--personal] — usage, projected bill,
15
15
  * drivers (read-only)
16
- * hq billing upgrade [--company <slug>] — Workforce checkout
17
- * (legacy plans only)
16
+ * hq billing upgrade [--company <slug>] — the switch to usage
17
+ * pricing (earlier plans)
18
18
  * hq billing sponsor … — ask a company to cover
19
- * your $50 plan, or
19
+ * your personal plan, or
20
20
  * answer such a request
21
21
  *
22
- * Usage pricing (US-012): a company whose billing summary says
23
- * `pricingPlan: "usage-only"` has no plan to upgrade to. `hq billing upgrade`
24
- * tells it so and points at `hq billing usage` instead of opening Workforce
25
- * checkout. Legacy companies keep the Workforce flow unchanged.
22
+ * Usage pricing (US-012, US-024): a company on usage pricing has no plan to
23
+ * upgrade to; `hq billing upgrade` tells it so and points at
24
+ * `hq billing usage`. Every other company is pointed at the switch to usage
25
+ * pricing: the flat-plan checkout is closed, so the CLI never opens it.
26
26
  */
27
27
  import chalk from "chalk";
28
28
  import { ensureCognitoToken } from "../utils/cognito-session.js";
29
29
  import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
30
30
  import { mintPaymentLink } from "../utils/billing-gate.js";
31
- import { upgradeToTeam } from "../utils/team-upgrade.js";
31
+ import { openUsageSwitch } from "../utils/team-upgrade.js";
32
32
  import { registerBillingSponsorCommands } from "./billing-sponsor.js";
33
33
  import { fetchCompanyPricingPlan, isUsageOnly, usageOnlyUpgradeLines, } from "../lib/billing/usage-only.js";
34
34
  import { fetchUsageCard } from "../lib/billing/usage-card.js";
@@ -157,10 +157,10 @@ export function registerBillingCommand(program) {
157
157
  });
158
158
  billing
159
159
  .command("upgrade")
160
- .description("Open Stripe Checkout to upgrade a company on an earlier plan (usage-pricing companies have no plan to upgrade)")
160
+ .description("Open the switch to usage pricing for a company on an earlier plan (usage-pricing companies have nothing to change)")
161
161
  .option("--company <slug>", "Company slug (resolves to companyUid)")
162
- .option("--no-browser", "Print the Stripe Checkout URL instead of opening it")
163
- .option("--json", "Emit the Checkout URL as JSON")
162
+ .option("--no-browser", "Print the URL instead of opening it")
163
+ .option("--json", "Emit the URL as JSON")
164
164
  .action(async function (opts) {
165
165
  try {
166
166
  const token = await ensureCognitoToken();
@@ -177,8 +177,8 @@ export function registerBillingCommand(program) {
177
177
  console.log(line);
178
178
  return;
179
179
  }
180
- await upgradeToTeam({
181
- companyUid,
180
+ await openUsageSwitch({
181
+ companySlug: companyOf(this),
182
182
  noBrowser: opts.browser === false,
183
183
  json: opts.json,
184
184
  });
@@ -484,8 +484,8 @@ async function defaultRunInitialSync(args) {
484
484
  if (event.type === "plan-limit") {
485
485
  failureCount += Math.max(1, event.count ?? 0, event.paths?.length ?? 0);
486
486
  firstFailure ??= {
487
- path: event.paths?.[0] ?? "plan limit",
488
- message: event.message ?? "plan limit refused upload",
487
+ path: event.paths?.[0] ?? "new files",
488
+ message: event.message ?? "new files are paused for this account",
489
489
  };
490
490
  return;
491
491
  }
@@ -1119,7 +1119,7 @@ export function registerCloudCommands(program) {
1119
1119
  // and keep their exit status.
1120
1120
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1121
1121
  `${result.filesSkipped} skipped), but ${describeRefusedFiles(planLimitRefused)} ` +
1122
- "refused by your plan limit."));
1122
+ "refused: new files are paused for this account."));
1123
1123
  for (const line of renderPlanLimitRefusalLines(planLimitRefused)) {
1124
1124
  log(chalk.yellow(line));
1125
1125
  }
@@ -1811,7 +1811,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1811
1811
  `${r.conflictPaths.length} conflict(s)` +
1812
1812
  (r.aborted ? " — aborted" : "") +
1813
1813
  (legRefused
1814
- ? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
1814
+ ? ` — ${describeRefusedCount(legRefused)} refused (new files paused)`
1815
1815
  : ""));
1816
1816
  if (legRefused) {
1817
1817
  printPlanLimitRefusal(legRefused);
@@ -1828,7 +1828,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1828
1828
  `${result.filesRefusedStale ?? 0} stale-refused; ` +
1829
1829
  `${result.filesUploadFailed ?? 0} failed` +
1830
1830
  (refusedTargets > 0
1831
- ? `; new files refused by a plan limit on ${refusedTargets} target(s)`
1831
+ ? `; new files paused on ${refusedTargets} target(s)`
1832
1832
  : "");
1833
1833
  console.log(errored > 0 ||
1834
1834
  result.filesExcludedByScope > 0 ||
@@ -1973,7 +1973,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1973
1973
  `${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
1974
1974
  (pushResult.aborted ? " — aborted" : "") +
1975
1975
  (pushRefused
1976
- ? ` — ${describeRefusedCount(pushRefused)} refused by plan limit`
1976
+ ? ` — ${describeRefusedCount(pushRefused)} refused (new files paused)`
1977
1977
  : ""));
1978
1978
  if (pushRefused)
1979
1979
  printPlanLimitRefusal(pushRefused, " ");
@@ -2105,7 +2105,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2105
2105
  if (pushRefused) {
2106
2106
  // The pull ran, but the push did not upload everything: no success line.
2107
2107
  console.log(chalk.yellow(`\n⚠ Sync now finished, but ${describeRefusedFiles(pushRefused)} ` +
2108
- "refused by your plan limit (listed above)."));
2108
+ "refused: new files are paused for this account (listed above)."));
2109
2109
  throw new SyncExitError(1, { health: "succeeded" });
2110
2110
  }
2111
2111
  if (pushResult.filesExcludedByScope > 0 ||
@@ -2159,9 +2159,9 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
2159
2159
  // like a clean sync. Its per-target details were printed above.
2160
2160
  if (refused.planLimitRefused) {
2161
2161
  console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused.planLimitRefused)} refused ` +
2162
- "by your plan limit (listed under push --all above)."));
2162
+ "because new files are paused for this account (listed under push --all above)."));
2163
2163
  if (refused.planLimitRefused.upgradeUrl) {
2164
- console.log(chalk.yellow(` Upgrade: ${refused.planLimitRefused.upgradeUrl}`));
2164
+ console.log(chalk.yellow(` Billing: ${refused.planLimitRefused.upgradeUrl}`));
2165
2165
  }
2166
2166
  }
2167
2167
  console.log(chalk.yellow("\n⚠ Sync finished, but one or more requested uploads were refused."));
@@ -74,7 +74,18 @@ export function describeRemoteError(error) {
74
74
  return "Remote databases are not available yet.";
75
75
  }
76
76
  if (status === 402 || code === "PLAN_REQUIRED") {
77
- return "Remote databases require the Team plan.";
77
+ // US-024: the server's usage pause message, else its own sentence. The
78
+ // CLI names no plan: the flat plans are closed.
79
+ const pause = error.usagePause;
80
+ if (pause && typeof pause.message === "string" && pause.message) {
81
+ return typeof pause.actionUrl === "string" && pause.actionUrl
82
+ ? `${pause.message}\nBilling: ${pause.actionUrl}`
83
+ : pause.message;
84
+ }
85
+ const serverMessage = error.serverMessage;
86
+ if (typeof serverMessage === "string" && serverMessage)
87
+ return serverMessage;
88
+ return "Remote databases are not available on this company's current billing. Run `hq billing status` to check it.";
78
89
  }
79
90
  if (status === 403)
80
91
  return "You are not a member of this company, or this app belongs to another company.";
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { aclLockedBy, describeAclLock } from "./acl-lock.js";
2
3
  import open from "open";
3
4
  import * as readline from "node:readline";
4
5
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
@@ -489,17 +490,21 @@ export function registerFilesCommand(program) {
489
490
  throw new Error('Invalid ACL tree response');
490
491
  }
491
492
  const row = tree.directRow;
493
+ // The most-specific row has no grants, so broader grants do not apply.
494
+ const lockedBy = aclLockedBy(tree);
492
495
  if (opts.json) {
493
- console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null }, null, 2));
496
+ console.log(JSON.stringify({ ...tree, schemaVersion: 1, companyUid, exists: row !== null, lockedBy }, null, 2));
494
497
  return;
495
498
  }
496
499
  // No own row AND nothing inherited or granted below — preserve the
497
500
  // original "no ACL record" exit path.
498
- if (!row && tree.inherited.length === 0 && tree.children.length === 0) {
501
+ if (!row && !lockedBy && tree.inherited.length === 0 && tree.children.length === 0) {
499
502
  console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
500
503
  process.exit(1);
501
504
  }
502
- const aclStatus = row?.open ? "open" : "restricted";
505
+ const aclStatus = lockedBy
506
+ ? lockedBy.readUnknown ? "locked unless open" : "locked"
507
+ : row?.open ? "open" : "restricted";
503
508
  console.log(chalk.green(`ACL for ${displayAclPattern(tree.prefix)} (${aclStatus})`));
504
509
  if (isPrivateFolderPattern(tree.prefix)) {
505
510
  console.log(chalk.gray(privateFolderExplanation(tree.prefix)));
@@ -507,9 +512,12 @@ export function registerFilesCommand(program) {
507
512
  if (row) {
508
513
  console.log(`Creator: ${row.creatorUid}`);
509
514
  }
510
- else {
515
+ else if (!lockedBy) {
511
516
  console.log(chalk.gray("No direct ACL row — access flows from the inherited/descendant grants below."));
512
517
  }
518
+ if (lockedBy) {
519
+ console.log(chalk.yellow(describeAclLock(lockedBy, displayAclPattern(lockedBy.path))));
520
+ }
513
521
  if (tree.effectivePermission) {
514
522
  console.log(`Your effective permission: ${tree.effectivePermission}`);
515
523
  }
@@ -560,9 +568,9 @@ export function registerFilesCommand(program) {
560
568
  const directEntries = tree.direct;
561
569
  if (directEntries.length === 0) {
562
570
  if (row?.open) {
563
- console.log(chalk.gray("Open ACL — all active members have read access."));
571
+ console.log(chalk.gray("No grants on this row (open: broader grants still apply). All active members have at least read access."));
564
572
  }
565
- else if (row) {
573
+ else if (row && !lockedBy) {
566
574
  console.log(chalk.gray("No explicit grants on this prefix — only creator has access."));
567
575
  }
568
576
  }
@@ -571,7 +579,11 @@ export function registerFilesCommand(program) {
571
579
  printEntryTable(directEntries, false);
572
580
  }
573
581
  if (tree.inherited.length > 0) {
574
- console.log("\nInherited (granted on an ancestor prefix):");
582
+ console.log(lockedBy
583
+ ? lockedBy.readUnknown
584
+ ? `\nGranted on ancestor prefixes (apply only if ${displayAclPattern(lockedBy.path)} is open):`
585
+ : `\nGranted on ancestor prefixes (do not apply: locked by ${displayAclPattern(lockedBy.path)}):`
586
+ : "\nInherited (granted on an ancestor prefix):");
575
587
  printEntryTable(tree.inherited, true);
576
588
  }
577
589
  if (tree.children.length > 0) {
@@ -3,10 +3,10 @@ import { type PricingPlan } from "../lib/billing/usage-only.js";
3
3
  /** Upper bound on list pages one short-id lookup reads (per cursor chain). */
4
4
  export declare const SHORT_ID_MAX_PAGES = 50;
5
5
  /**
6
- * The 402 copy for meeting bots. A usage-only company (US-012: the body or the
7
- * company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
8
- * plan to buy, so it is told to add a card. Every other company keeps the
9
- * Workforce copy.
6
+ * The 402 copy for meeting bots. A usage pause (US-024) prints the server's
7
+ * message and Billing link. A usage-only company (US-012) is told to add a
8
+ * card. Every other company gets the server's sentence under a line that
9
+ * names no plan: the flat plans are closed.
10
10
  */
11
11
  export declare function meetingPaymentRequiredLines(body: Record<string, unknown>, resolvePlan?: () => Promise<PricingPlan | null>): Promise<string[]>;
12
12
  export declare function registerMeetingsCommand(program: Command): void;
@@ -4,7 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { vaultApiFetch, getCompanyUid, resolveCallerPersonUid, } from "../utils/vault-api.js";
5
5
  import { isPlanGateError, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
6
6
  import { mapWithConcurrency } from "./sync-manifest.js";
7
- import { isUsageOnly, pricingPlanFromBody, resolveCompanyPricingPlan, usageOnlyCardRequiredLines, } from "../lib/billing/usage-only.js";
7
+ import { isUsageOnly, pricingPlanFromBody, resolveCompanyPricingPlan, usageOnlyCardRequiredLines, usagePauseFromBody, } from "../lib/billing/usage-only.js";
8
8
  const LIVE_SNAPSHOT_STATUSES = new Set([
9
9
  "joining",
10
10
  "recording",
@@ -318,23 +318,32 @@ async function resolveShortId(token, prefix, query) {
318
318
  process.exit(1);
319
319
  }
320
320
  /**
321
- * The 402 copy for meeting bots. A usage-only company (US-012: the body or the
322
- * company's billing summary says `pricingPlan: "usage-only"`) has no Workforce
323
- * plan to buy, so it is told to add a card. Every other company keeps the
324
- * Workforce copy.
321
+ * The 402 copy for meeting bots. A usage pause (US-024) prints the server's
322
+ * message and Billing link. A usage-only company (US-012) is told to add a
323
+ * card. Every other company gets the server's sentence under a line that
324
+ * names no plan: the flat plans are closed.
325
325
  */
326
326
  export async function meetingPaymentRequiredLines(body, resolvePlan = () => resolveCompanyPricingPlan()) {
327
327
  const plan = pricingPlanFromBody(body) ?? (await resolvePlan().catch(() => null));
328
328
  if (isUsageOnly(plan)) {
329
329
  return usageOnlyCardRequiredLines("A meeting notetaker bot").map((line, i) => i === 0 ? chalk.red(line) : chalk.dim(line));
330
330
  }
331
+ // US-024: a usage pause is the server's own answer: its message and link.
332
+ const pause = usagePauseFromBody(body);
333
+ if (pause) {
334
+ return [
335
+ chalk.red(pause.message),
336
+ ...(pause.actionUrl ? [chalk.dim(`Billing: ${pause.actionUrl}`)] : []),
337
+ ];
338
+ }
339
+ // The server's sentence when it sent one; otherwise copy that names no plan.
331
340
  const detail = typeof body.message === "string"
332
341
  ? body.message
333
342
  : typeof body.error === "string"
334
343
  ? body.error
335
- : "Upgrade your company to HQ Workforce to record meetings.";
344
+ : "Run `hq billing status` or open the company's Billing page to check it.";
336
345
  return [
337
- chalk.red("Meeting notetaker bots require the $500/mo HQ Workforce plan."),
346
+ chalk.red("Meeting notetaker bots are not available on this company's current billing."),
338
347
  chalk.dim(detail),
339
348
  ];
340
349
  }
@@ -660,7 +669,7 @@ export function registerMeetingsCommand(program) {
660
669
  const companySlug = meetings.opts().company;
661
670
  // Always name a company. With no companyId the backend files the bot
662
671
  // under Personal, which can never record, so an entitled user would
663
- // get the HQ Workforce upgrade prompt. Without --company this uses the
672
+ // get the payment-required refusal. Without --company this uses the
664
673
  // caller's only active company, or asks them to pick one.
665
674
  const query = {
666
675
  companyId: await getCompanyUid(token, companySlug),
@@ -28,7 +28,7 @@ import { AuthError } from "../utils/auth-error.js";
28
28
  import { redactErrorText } from "../utils/redact-error-text.js";
29
29
  import { stampVaultAccessDenied, vaultAccessDeniedDiagnostics, } from "../utils/vault-access-denied-error.js";
30
30
  import { PlanGateError } from "../utils/plan-gate-error.js";
31
- import { createPlanLimitRefusalTracker, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
31
+ import { createPlanLimitRefusalTracker, REFUSAL_FALLBACK_SENTENCE, trackingProgressRecorder, } from "../lib/billing/plan-limit-refusals.js";
32
32
  export const SKILL_UID_PATTERN = /^skl_[A-Za-z0-9]+$/;
33
33
  export const SKILL_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
34
34
  const COMPANY_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -54,16 +54,16 @@ async function defaultSyncFile(input) {
54
54
  };
55
55
  }
56
56
  /**
57
- * The upload of a stamped skill file was refused by a plan limit. Registration
57
+ * The upload of a stamped skill file was refused (new files paused). Registration
58
58
  * and the local stamp already succeeded, so say that on stderr, then return a
59
- * typed plan gate for main.ts to render (reason and upgrade link) and exit
59
+ * typed plan gate for main.ts to render (reason and billing link) and exit
60
60
  * non-zero. Never "Skill ready … Sync: complete".
61
61
  */
62
62
  function skillUploadRefused(skillUid, filePath, companySlug, refused) {
63
- console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused by your plan limit.`));
63
+ console.warn(chalk.yellow(`⚠ Skill ${skillUid} is stamped locally at '${filePath}', but the upload was refused: new files are paused for this account.`));
64
64
  return new PlanGateError("PLAN_LIMIT_EXCEEDED", {
65
- reason: `${refused.message ?? "New files are paused by your plan limit."} ` +
66
- "The skill file was not uploaded. Once the limit is sorted, upload it with: " +
65
+ reason: `${refused.message ?? REFUSAL_FALLBACK_SENTENCE} ` +
66
+ "The skill file was not uploaded. Once uploads resume, upload it with: " +
67
67
  `hq sync push --company ${companySlug} '${filePath}'`,
68
68
  ...(refused.upgradeUrl ? { upgradeUrl: refused.upgradeUrl } : {}),
69
69
  });
@@ -123,7 +123,7 @@ export function registerWhoamiCommand(program) {
123
123
  .command('whoami')
124
124
  .description('Show the currently authenticated user')
125
125
  .option('--json', 'Output identity metadata as JSON (never tokens)')
126
- .option('--company <slug>', 'Also report this company\'s workspace plan state (Starter nag)')
126
+ .option('--company <slug>', 'Also report this company\'s billing and usage notice')
127
127
  .action(async (opts) => {
128
128
  try {
129
129
  // Effective source (same as auth status / ensureCognitoToken): env
@@ -18,7 +18,7 @@ export async function runHookEnginePath(args) {
18
18
  console.log('Usage: hq core hook-engine-path [--repair] [--for-registration]\nResolve and pin the installed native hook engine offline.\n--for-registration prints a path only when the launcher flag is enabled.');
19
19
  return;
20
20
  }
21
- const { installHookEngine, hookEngineRequired, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
21
+ const { installHookEngine, hookEngineRequired, removeHookEnginePointer, verifyHookEngine, HOOK_ENGINE_REPAIR } = await import('./lib/hook-engine/resolve.js');
22
22
  if (args.includes('--for-registration')) {
23
23
  const { launcherEnabled } = await import('./lib/hook-engine/registration.js');
24
24
  if (!launcherEnabled()) {
@@ -27,8 +27,19 @@ export async function runHookEnginePath(args) {
27
27
  }
28
28
  }
29
29
  const binary = installHookEngine({ repair: args.includes('--repair') });
30
- if (binary)
30
+ if (binary) {
31
+ // The printed path is one that runs and reports as an hq-hook engine.
32
+ // A binary that cannot answer its self-check must not be bridged, so the
33
+ // shell pointer is removed and the command exits non-zero.
34
+ const problem = verifyHookEngine(binary);
35
+ if (problem) {
36
+ removeHookEnginePointer();
37
+ console.error(`${problem}. Existing bash hooks remain active. Repair: ${HOOK_ENGINE_REPAIR}`);
38
+ process.exitCode = 1;
39
+ return;
40
+ }
31
41
  console.log(binary);
42
+ }
32
43
  else {
33
44
  console.error(`Native hook engine missing; ${hookEngineRequired() ? 'repair before running hooks' : 'existing bash hooks remain active'}. Repair: ${HOOK_ENGINE_REPAIR}`);
34
45
  process.exitCode = 1;