@indigoai-us/hq-cli 5.345.156 → 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 (188) hide show
  1. package/CHANGELOG.md +255 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. package/package.json +8 -8
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.91","bundleSha256":"ea41f1765d5015a22304787abedc68a531393d601a349bb8bff3c5428413f3a3","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.98","bundleSha256":"70fe5497fffdbb678370515f8b7df63438d7a59a95467102307459e78f9f4715","formatVersion":1}
@@ -15,6 +15,7 @@
15
15
  * item until the bot acks (`POST /v1/agents/{uid}/inbox/{id}/ack`), and the
16
16
  * poller dedupes by id so the same item is never mirrored twice.
17
17
  */
18
+ import { createWakeSecretResolver } from "../wake-secrets.js";
18
19
  import * as fs from "node:fs";
19
20
  import * as path from "node:path";
20
21
  import { vaultApiFetch } from "../../../utils/vault-api.js";
@@ -247,7 +248,7 @@ async function maybeWake(deps, freshCount) {
247
248
  return;
248
249
  const notice = buildWakeNotice(deps.agentUid, reason, pending.map((e) => e.id), now);
249
250
  try {
250
- await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now });
251
+ await (deps.fireWake ?? fireWake)(config, notice, { paths: deps.paths, agentUid: deps.agentUid, log: deps.log, now: deps.now, resolveHeaderSecrets: createWakeSecretResolver(deps.companySlug, deps.getToken, deps.apiBaseUrl) });
251
252
  }
252
253
  catch (err) {
253
254
  deps.log("warn", `wake failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -0,0 +1,7 @@
1
+ /** Wake-only vault reads: use the kit machine token, never cache plaintext. */
2
+ import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
3
+ export declare function createWakeSecretResolver(companySlug: string | undefined, getToken: () => Promise<string>, baseUrl: string, deps?: {
4
+ getCompanyUid: typeof getCompanyUid;
5
+ vaultApiFetch: typeof vaultApiFetch;
6
+ }): (names: string[]) => Promise<Map<string, string>>;
7
+ //# sourceMappingURL=wake-secrets.d.ts.map
@@ -0,0 +1,32 @@
1
+ /** Wake-only vault reads: use the kit machine token, never cache plaintext. */
2
+ import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
3
+ export function createWakeSecretResolver(companySlug, getToken, baseUrl, deps = { getCompanyUid, vaultApiFetch }) {
4
+ return async (names) => {
5
+ if (!companySlug)
6
+ throw new Error("Wake secrets require an enrolled company");
7
+ const token = await getToken();
8
+ const companyUid = await deps.getCompanyUid(token, companySlug, baseUrl);
9
+ const response = await deps.vaultApiFetch({
10
+ token, baseUrl, path: `/secrets/${encodeURIComponent(companyUid)}/load`, method: "POST",
11
+ body: { names, usage: { channel: "exec", reason: "agent kit webhook wake" } },
12
+ signal: AbortSignal.timeout(15_000),
13
+ });
14
+ if (!response.ok)
15
+ throw new Error("Wake secret authorization failed");
16
+ const data = await response.json();
17
+ if (!Array.isArray(data.secrets) || !Array.isArray(data.errors) || data.errors.length) {
18
+ throw new Error("Wake secret lookup failed");
19
+ }
20
+ const values = new Map();
21
+ for (const row of data.secrets) {
22
+ if (!row || typeof row.name !== "string" || !names.includes(row.name) || values.has(row.name) ||
23
+ typeof row.value !== "string" || row.revokedAt != null)
24
+ throw new Error("Invalid wake secret response");
25
+ values.set(row.name, row.value);
26
+ }
27
+ if (values.size !== new Set(names).size)
28
+ throw new Error("Wake secret missing");
29
+ return values;
30
+ };
31
+ }
32
+ //# sourceMappingURL=wake-secrets.js.map
@@ -30,12 +30,15 @@ export type WakeConfig = {
30
30
  kind: "url";
31
31
  url: string;
32
32
  retryMs: number;
33
+ headerSecrets?: Record<string, string>;
33
34
  } | {
34
35
  kind: "command";
35
36
  command: string[];
36
37
  timeoutMs: number;
37
38
  retryMs: number;
38
39
  };
40
+ /** Store references only. Reject ambiguous and transport-controlled headers. */
41
+ export declare function parseHeaderSecrets(specs: string[]): Record<string, string>;
39
42
  export interface WakeNotice {
40
43
  event: "hq.inbox.pending";
41
44
  agentUid: string;
@@ -59,6 +62,7 @@ export interface WakeDeps {
59
62
  agentUid: string;
60
63
  log: KitLogger;
61
64
  now?: () => Date;
65
+ resolveHeaderSecrets?: (names: string[]) => Promise<Map<string, string>>;
62
66
  fetchImpl?: typeof fetch;
63
67
  spawnImpl?: typeof spawn;
64
68
  pidAlive?: (pid: number | undefined) => boolean;
@@ -18,6 +18,7 @@
18
18
  * The inbox poller fires the wake when new items are mirrored, and re-fires
19
19
  * every `retryMs` while items stay pending, so a missed webhook heals.
20
20
  */
21
+ import { SECRET_NAME_PATTERN } from "../../commands/_patterns.js";
21
22
  import { spawn } from "node:child_process";
22
23
  import * as fs from "node:fs";
23
24
  import * as path from "node:path";
@@ -27,6 +28,33 @@ export const WAKE_STATE_NAME = "wake-state.json";
27
28
  export const DEFAULT_WAKE_RETRY_MS = 10 * 60_000;
28
29
  export const WAKE_URL_TIMEOUT_MS = 15_000;
29
30
  export const DEFAULT_WAKE_COMMAND_TIMEOUT_MS = 10 * 60_000;
31
+ /** Store references only. Reject ambiguous and transport-controlled headers. */
32
+ export function parseHeaderSecrets(specs) {
33
+ const result = {};
34
+ const seen = new Set();
35
+ for (const spec of specs) {
36
+ const at = spec.indexOf("=");
37
+ const header = spec.slice(0, at);
38
+ const name = spec.slice(at + 1);
39
+ const lower = header.toLowerCase();
40
+ if (at < 1 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(header) || !SECRET_NAME_PATTERN.test(name) ||
41
+ ["host", "content-length", "transfer-encoding", "connection", "content-type", "user-agent", "__proto__"].includes(lower) || seen.has(lower)) {
42
+ throw new Error("Invalid --header-secret: use a unique Header-Name=VAULT_SECRET_NAME reference");
43
+ }
44
+ seen.add(lower);
45
+ result[header] = name;
46
+ }
47
+ return result;
48
+ }
49
+ function validateHeaderSecrets(raw) {
50
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
51
+ throw new Error("Invalid header secret references");
52
+ return parseHeaderSecrets(Object.entries(raw).map(([header, name]) => {
53
+ if (typeof name !== "string")
54
+ throw new Error("Invalid header secret reference");
55
+ return `${header}=${name}`;
56
+ }));
57
+ }
30
58
  export const WAKE_INSTRUCTIONS = "New HQ messages are waiting. Run `hq agent inbox` (or the hq_inbox_read tool). Reply where each message " +
31
59
  "arrived: if it has a channelId, post in that channel with `hq dm <channelId> \"…\"`; otherwise reply to the " +
32
60
  "sender with `hq dm <sender-uid> \"…\"` (or hq_dm_send). Do not reply to another bot's greeting, thanks or " +
@@ -82,7 +110,7 @@ export function readWakeConfig(paths) {
82
110
  const retryMs = typeof raw.retryMs === "number" && raw.retryMs >= 60_000 ? raw.retryMs : DEFAULT_WAKE_RETRY_MS;
83
111
  if (raw.kind === "url" && typeof raw.url === "string") {
84
112
  try {
85
- return { kind: "url", url: validateWakeUrl(raw.url), retryMs };
113
+ return { kind: "url", url: validateWakeUrl(raw.url), retryMs, ...(raw.headerSecrets === undefined ? {} : { headerSecrets: validateHeaderSecrets(raw.headerSecrets) }) };
86
114
  }
87
115
  catch {
88
116
  return null;
@@ -167,9 +195,27 @@ export async function fireWake(config, notice, deps) {
167
195
  const timer = setTimeout(() => controller.abort(), WAKE_URL_TIMEOUT_MS);
168
196
  const target = redactWakeUrl(config.url);
169
197
  try {
198
+ const headers = new Headers({ "content-type": "application/json", "user-agent": "hq-agent-kit" });
199
+ if (config.headerSecrets !== undefined) {
200
+ const refs = validateHeaderSecrets(config.headerSecrets);
201
+ const names = [...new Set(Object.values(refs))];
202
+ if (names.length) {
203
+ if (!deps.resolveHeaderSecrets)
204
+ throw new Error("Secret resolution unavailable");
205
+ const values = await deps.resolveHeaderSecrets(names);
206
+ for (const [header, name] of Object.entries(refs)) {
207
+ const value = values.get(name);
208
+ if (!value || /[\r\n\0]/.test(value))
209
+ throw new Error("Invalid secret header value");
210
+ headers.set(header, value);
211
+ }
212
+ }
213
+ }
170
214
  const res = await doFetch(config.url, {
171
215
  method: "POST",
172
- headers: { "content-type": "application/json", "user-agent": "hq-agent-kit" },
216
+ headers,
217
+ // Custom authentication headers must never follow a redirect.
218
+ redirect: "error",
173
219
  body: JSON.stringify(notice),
174
220
  signal: controller.signal,
175
221
  });
@@ -180,7 +226,7 @@ export async function fireWake(config, notice, deps) {
180
226
  return { fired: true, ok, detail };
181
227
  }
182
228
  catch (err) {
183
- const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : err.message : String(err)}`;
229
+ const detail = `wake url ${target} failed: ${err instanceof Error ? err.name === "AbortError" ? "timeout" : ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(err.message) ? err.message : "request or secret resolution failed" : "request or secret resolution failed"}`;
184
230
  deps.log("warn", detail);
185
231
  return { fired: true, ok: false, detail };
186
232
  }
@@ -18,6 +18,12 @@
18
18
  * stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
19
19
  * that event; the `error`-message parsing stays for runners older than
20
20
  * 6.18.17.
21
+ *
22
+ * US-024: the server's sentence wins. A `plan-limit` event carries the 402
23
+ * body's `message` (for a usage pause, the `usagePause` message hq-pro writes
24
+ * for the person who hit it), and a runner that forwards `usagePause` gives
25
+ * its message and `actionUrl` directly. The CLI adds no plan name and labels
26
+ * the link `Billing:`, never "Upgrade".
21
27
  */
22
28
  import { type SyncProgressEvent } from "@indigoai-us/hq-cloud";
23
29
  /** What one push leg was refused. `null` from the tracker means nothing was. */
@@ -29,10 +35,12 @@ export interface PlanLimitRefusalSummary {
29
35
  count: number;
30
36
  /** Vault-relative paths the runner named, de-duplicated, in report order. */
31
37
  paths: string[];
32
- /** Server sentence from a refused file's error, when one was carried. */
38
+ /** Server sentence (usage pause message, event message, or a refused file's error). */
33
39
  message?: string;
34
- /** Server-selected (or company billing fallback) upgrade link. */
40
+ /** Server-selected (or company billing fallback) billing link. */
35
41
  upgradeUrl?: string;
42
+ /** True when the server said this is a usage pause (`usagePause`). */
43
+ usagePause?: boolean;
36
44
  }
37
45
  export interface PlanLimitRefusalTracker {
38
46
  observe(event: SyncProgressEvent): void;
@@ -57,9 +65,11 @@ export declare function createPlanLimitRefusalTracker(opts?: {
57
65
  export declare function trackingProgressRecorder(tracker: PlanLimitRefusalTracker, company: string | null): (event: SyncProgressEvent) => void;
58
66
  /** Add a later leg's refusals to an earlier total. */
59
67
  export declare function mergePlanLimitRefusals(a: PlanLimitRefusalSummary | null | undefined, b: PlanLimitRefusalSummary | null | undefined): PlanLimitRefusalSummary | null;
68
+ /** The sentence when the server sent none. Names no plan. */
69
+ export declare const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
60
70
  /**
61
71
  * Plain lines describing a refusal: what was not uploaded, why, and the
62
- * upgrade link. Colour and the leading status line are the caller's.
72
+ * billing link. Colour and the leading status line are the caller's.
63
73
  */
64
74
  export declare function renderPlanLimitRefusalLines(summary: PlanLimitRefusalSummary, indent?: string): string[];
65
75
  /** "1 new file" / "3 new files", or "new files" when only a notice said so. */
@@ -18,9 +18,16 @@
18
18
  * stops re-sending paused keys. The `path`/`paths`/`count` reads below consume
19
19
  * that event; the `error`-message parsing stays for runners older than
20
20
  * 6.18.17.
21
+ *
22
+ * US-024: the server's sentence wins. A `plan-limit` event carries the 402
23
+ * body's `message` (for a usage pause, the `usagePause` message hq-pro writes
24
+ * for the person who hit it), and a runner that forwards `usagePause` gives
25
+ * its message and `actionUrl` directly. The CLI adds no plan name and labels
26
+ * the link `Billing:`, never "Upgrade".
21
27
  */
22
28
  import { createSyncProgressRecorder, } from "@indigoai-us/hq-cloud";
23
29
  import { planLimitUpgradeFallbackUrl } from "./plan-limit-upgrade-url.js";
30
+ import { usagePauseFromBody } from "./usage-pause.js";
24
31
  /** Upgrade destination when neither the event nor the error named one. */
25
32
  const FALLBACK_UPGRADE_URL = "https://hq.computer/billing";
26
33
  const PLAN_LIMIT_CODE_TOKEN = /\bcode=(?:PLAN_LIMIT_EXCEEDED|PLAN_LIMIT_REACHED|plan_limit_reached)\b/;
@@ -72,6 +79,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
72
79
  let reportedCount = 0;
73
80
  let message;
74
81
  let upgradeUrl;
82
+ let usagePaused = false;
75
83
  const addPath = (value) => {
76
84
  if (typeof value === "string" && value && !paths.includes(value)) {
77
85
  paths.push(value);
@@ -82,7 +90,17 @@ export function createPlanLimitRefusalTracker(opts = {}) {
82
90
  const record = event;
83
91
  if (event.type === "plan-limit") {
84
92
  noticeSeen = true;
93
+ const pause = usagePauseFromBody(record);
94
+ if (pause) {
95
+ usagePaused = true;
96
+ message = pause.message;
97
+ if (pause.actionUrl)
98
+ upgradeUrl = pause.actionUrl;
99
+ }
85
100
  upgradeUrl ??= safeHttpsUrl(record.upgradeUrl);
101
+ if (typeof record.message === "string" && record.message.trim()) {
102
+ message ??= record.message.trim();
103
+ }
86
104
  addPath(record.path);
87
105
  if (Array.isArray(record.paths))
88
106
  record.paths.forEach(addPath);
@@ -111,6 +129,7 @@ export function createPlanLimitRefusalTracker(opts = {}) {
111
129
  paths: [...paths],
112
130
  ...(message ? { message } : {}),
113
131
  ...(url ? { upgradeUrl: url } : {}),
132
+ ...(usagePaused ? { usagePause: true } : {}),
114
133
  };
115
134
  },
116
135
  };
@@ -141,22 +160,26 @@ export function mergePlanLimitRefusals(a, b) {
141
160
  paths.push(p);
142
161
  const message = a.message ?? b.message;
143
162
  const upgradeUrl = a.upgradeUrl ?? b.upgradeUrl;
163
+ const usagePause = a.usagePause === true || b.usagePause === true;
144
164
  return {
145
165
  count: a.count + b.count,
146
166
  paths,
147
167
  ...(message ? { message } : {}),
148
168
  ...(upgradeUrl ? { upgradeUrl } : {}),
169
+ ...(usagePause ? { usagePause } : {}),
149
170
  };
150
171
  }
151
172
  /** How many named paths to print before summarising the rest. */
152
173
  const MAX_LISTED_PATHS = 20;
174
+ /** The sentence when the server sent none. Names no plan. */
175
+ export const REFUSAL_FALLBACK_SENTENCE = "New files are paused for this account. Open its Billing page to see why.";
153
176
  /**
154
177
  * Plain lines describing a refusal: what was not uploaded, why, and the
155
- * upgrade link. Colour and the leading status line are the caller's.
178
+ * billing link. Colour and the leading status line are the caller's.
156
179
  */
157
180
  export function renderPlanLimitRefusalLines(summary, indent = " ") {
158
181
  const lines = [];
159
- lines.push(`${indent}${summary.message ?? "New files are paused by your plan limit."}`);
182
+ lines.push(`${indent}${summary.message ?? REFUSAL_FALLBACK_SENTENCE}`);
160
183
  if (summary.paths.length > 0) {
161
184
  lines.push(`${indent}Not uploaded:`);
162
185
  for (const p of summary.paths.slice(0, MAX_LISTED_PATHS)) {
@@ -170,7 +193,7 @@ export function renderPlanLimitRefusalLines(summary, indent = " ") {
170
193
  lines.push(`${indent}New files stay on this machine and were not uploaded.`);
171
194
  }
172
195
  if (summary.upgradeUrl)
173
- lines.push(`${indent}Upgrade: ${summary.upgradeUrl}`);
196
+ lines.push(`${indent}Billing: ${summary.upgradeUrl}`);
174
197
  return lines;
175
198
  }
176
199
  /** "1 new file" / "3 new files", or "new files" when only a notice said so. */
@@ -3,6 +3,14 @@ export interface PlanLimitUpgradeUrlChoices {
3
3
  nestedUrl?: string | null;
4
4
  legacyUrl: string;
5
5
  }
6
+ /**
7
+ * The console's self-serve switch to usage pricing for one company. While HQ
8
+ * Workforce is closed to companies not already on it (hq-pro
9
+ * `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
10
+ * upgrade leads; the Workforce checkout refuses. Null when no console origin
11
+ * can be derived; without a slug the generic billing page.
12
+ */
13
+ export declare function usageSwitchPageUrl(companySlug?: string): string | null;
6
14
  /** Resolve and attribute the final selected company billing destination. */
7
15
  export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
8
16
  export declare function withCliLimitEntrySurface(value: string): string;
@@ -11,6 +11,24 @@ function companyBillingUrl(companySlug) {
11
11
  : "/billing";
12
12
  return `${origin}${route}`;
13
13
  }
14
+ /**
15
+ * The console's self-serve switch to usage pricing for one company. While HQ
16
+ * Workforce is closed to companies not already on it (hq-pro
17
+ * `billing.legacy-plans-closed`, owner decision 2026-10-19) this is where an
18
+ * upgrade leads; the Workforce checkout refuses. Null when no console origin
19
+ * can be derived; without a slug the generic billing page.
20
+ */
21
+ export function usageSwitchPageUrl(companySlug) {
22
+ const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
23
+ const origin = consoleOrigin(controlPlaneUrl);
24
+ if (!origin)
25
+ return null;
26
+ const slug = companySlug?.trim();
27
+ const route = slug
28
+ ? `/companies/${encodeURIComponent(slug)}/billing/usage-switch?entrySurface=cli_limit`
29
+ : "/billing";
30
+ return `${origin}${route}`;
31
+ }
14
32
  /** Resolve and attribute the final selected company billing destination. */
15
33
  export function selectPlanLimitUpgradeUrl(companySlug, choices) {
16
34
  const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
@@ -17,6 +17,13 @@
17
17
  * Copy rule (US-036): no string a customer reads may contain "locked" or
18
18
  * "read-only". `plan-lock.test.ts` scans every rendered string for both.
19
19
  *
20
+ * Copy rule (US-024): the flat plans are closed, so nothing here offers the
21
+ * Workforce plan, its price or the free month, and nothing says "Upgrade".
22
+ * The plan is named (Starter) only when hq-pro said the company is on an
23
+ * earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
24
+ * gets copy that names no plan, and the way out besides trimming is the
25
+ * switch to usage pricing or the company's Billing page.
26
+ *
20
27
  * An absent field is UNKNOWN, so nothing here ever infers over-limit from
21
28
  * missing data (hq-absent-field-never-means-constraining-value).
22
29
  */
@@ -58,8 +65,18 @@ export interface PlanLimitStatus {
58
65
  * which keeps the legacy copy.
59
66
  */
60
67
  pricingPlan?: PricingPlan;
68
+ /**
69
+ * True when hq-pro says HQ Workforce is closed to this company (hq-flags
70
+ * `billing.legacy-plans-closed`, owner decision 2026-10-19). `upgradeUrl`
71
+ * is then the console's usage-pricing switch page, the free month never
72
+ * stands, and the renderers offer the switch instead of a Workforce
73
+ * upgrade. Absent keeps the legacy copy (older servers).
74
+ */
75
+ legacyPlansClosed?: boolean;
61
76
  checkedAt: string;
62
77
  }
78
+ /** The way out while HQ Workforce is closed. Copy lives in ONE place. */
79
+ export declare const USAGE_SWITCH_LABEL = "Switch to usage pricing";
63
80
  /** Starter member cap quoted when the server did not send a users.limit. */
64
81
  export declare const STARTER_MEMBER_TARGET = 5;
65
82
  /**
@@ -71,10 +88,8 @@ export declare const STARTER_INTEGRATION_TARGET = 3;
71
88
  export declare const STARTER_SECRET_TARGET = 10;
72
89
  /** Starter agent cap quoted when the server did not send an agents.limit. */
73
90
  export declare const STARTER_AGENT_TARGET = 0;
74
- /** Upgrade destination quoted when the server did not send one. */
91
+ /** Billing destination quoted when the server did not send one. */
75
92
  export declare const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
76
- /** The paid plan the nag sends owners to. Copy lives in ONE place. */
77
- export declare const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
78
93
  /**
79
94
  * Pick the membership row for `companySlug` (or `cmp_…` uid) out of a decoded
80
95
  * `/membership/me` body. No matching row is UNKNOWN. Does not read `planLock`.
@@ -133,9 +148,10 @@ export declare function renderPlanLimitNotice(status: PlanLimitStatus, opts?: {
133
148
  */
134
149
  export declare function renderPlanLimitLine(status: PlanLimitStatus): string;
135
150
  /**
136
- * The `Plan: …` orientation line. Starter only: a paid or enterprise
137
- * workspace — and an UNKNOWN plan — gets no line at all. A usage-pricing
138
- * company gets the usage line instead.
151
+ * The `Plan: …` orientation line. Starter only, and only when hq-pro said the
152
+ * company is on an earlier plan: a paid or enterprise workspace gets no line,
153
+ * and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
154
+ * usage line instead.
139
155
  */
140
156
  export declare function renderPlanLine(status: PlanLimitStatus): string | null;
141
157
  /**
@@ -17,6 +17,13 @@
17
17
  * Copy rule (US-036): no string a customer reads may contain "locked" or
18
18
  * "read-only". `plan-lock.test.ts` scans every rendered string for both.
19
19
  *
20
+ * Copy rule (US-024): the flat plans are closed, so nothing here offers the
21
+ * Workforce plan, its price or the free month, and nothing says "Upgrade".
22
+ * The plan is named (Starter) only when hq-pro said the company is on an
23
+ * earlier plan (`billingState` or `pricingPlan: "current"`); an unknown plan
24
+ * gets copy that names no plan, and the way out besides trimming is the
25
+ * switch to usage pricing or the company's Billing page.
26
+ *
20
27
  * An absent field is UNKNOWN, so nothing here ever infers over-limit from
21
28
  * missing data (hq-absent-field-never-means-constraining-value).
22
29
  */
@@ -24,7 +31,7 @@ import chalk from "chalk";
24
31
  import { diagnosticsEnabled } from "../diagnostics.js";
25
32
  import { vaultApiFetch } from "../../utils/vault-api.js";
26
33
  import { parseFreeMonth } from "./free-month.js";
27
- import { selectPlanLimitUpgradeUrl } from "./plan-limit-upgrade-url.js";
34
+ import { selectPlanLimitUpgradeUrl, usageSwitchPageUrl, } from "./plan-limit-upgrade-url.js";
28
35
  import { fetchCompanyPricingPlan, isUsageOnly, pricingPlanFromBody, pricingPlanFromMembership, usageOnlyPlanLine, usageOnlyUpgradeLines, } from "./usage-only.js";
29
36
  // The free-month parser lives in a leaf module so the plan-gate 402 decoder can
30
37
  // share it without importing the HTTP layer; re-exported so existing callers
@@ -41,6 +48,8 @@ export const PLAN_LIMIT_REASONS = [
41
48
  "secrets",
42
49
  "agents",
43
50
  ];
51
+ /** The way out while HQ Workforce is closed. Copy lives in ONE place. */
52
+ export const USAGE_SWITCH_LABEL = "Switch to usage pricing";
44
53
  /** Starter member cap quoted when the server did not send a users.limit. */
45
54
  export const STARTER_MEMBER_TARGET = 5;
46
55
  /**
@@ -52,10 +61,8 @@ export const STARTER_INTEGRATION_TARGET = 3;
52
61
  export const STARTER_SECRET_TARGET = 10;
53
62
  /** Starter agent cap quoted when the server did not send an agents.limit. */
54
63
  export const STARTER_AGENT_TARGET = 0;
55
- /** Upgrade destination quoted when the server did not send one. */
64
+ /** Billing destination quoted when the server did not send one. */
56
65
  export const DEFAULT_UPGRADE_URL = "https://hq.computer/billing";
57
- /** The paid plan the nag sends owners to. Copy lives in ONE place. */
58
- export const WORKFORCE_PLAN_LABEL = "HQ Workforce ($500/mo)";
59
66
  function finiteNumber(value) {
60
67
  return typeof value === "number" && Number.isFinite(value) ? value : null;
61
68
  }
@@ -157,6 +164,17 @@ function selectUpgradeUrl(body, companySlug) {
157
164
  const nestedUrl = typeof nested?.upgradeUrl === "string" && nested.upgradeUrl.trim().length > 0
158
165
  ? nested.upgradeUrl.trim()
159
166
  : undefined;
167
+ // While HQ Workforce is closed the way out is the usage-pricing switch, not
168
+ // the company billing page the generated URL would land on. Build it from
169
+ // the slug when known; otherwise keep the server's own switch URL.
170
+ if (rec?.legacyPlansClosed === true) {
171
+ const slug = companySlug?.trim();
172
+ return (slug ? usageSwitchPageUrl(slug) : undefined) ??
173
+ topLevelUrl ??
174
+ nestedUrl ??
175
+ usageSwitchPageUrl(undefined) ??
176
+ DEFAULT_UPGRADE_URL;
177
+ }
160
178
  return selectPlanLimitUpgradeUrl(companySlug, {
161
179
  topLevelUrl,
162
180
  nestedUrl,
@@ -210,6 +228,7 @@ function statusFromUsage(companySlug, companyUid, body, upgradeCompanySlug) {
210
228
  upgradeUrl: selectUpgradeUrl(body, upgradeCompanySlug),
211
229
  freeMonth: parseFreeMonth(rec?.freeMonth),
212
230
  plan: selectPlan(body) ?? undefined,
231
+ ...(rec?.legacyPlansClosed === true ? { legacyPlansClosed: true } : {}),
213
232
  checkedAt: new Date().toISOString(),
214
233
  }, pricingPlan);
215
234
  }
@@ -295,6 +314,10 @@ export async function fetchPlanLimitStatus(token, companyRef, opts = {}) {
295
314
  return null;
296
315
  }
297
316
  }
317
+ /** True when hq-pro said the company is on an earlier (flat) plan. */
318
+ function knownEarlierPlan(status) {
319
+ return status.pricingPlan === "current";
320
+ }
298
321
  /** Why the workspace is over, one clause per reason. */
299
322
  export function reasonLabel(reason) {
300
323
  switch (reason) {
@@ -305,7 +328,7 @@ export function reasonLabel(reason) {
305
328
  case "secrets":
306
329
  return "too many secrets";
307
330
  case "agents":
308
- return "agents are not included on Starter";
331
+ return "agents are not included";
309
332
  }
310
333
  }
311
334
  function usageTarget(reason, status) {
@@ -357,7 +380,7 @@ function reasonDetail(reason, status) {
357
380
  ? `${row.used} of ${target} secrets`
358
381
  : `over its ${target}-secret limit`;
359
382
  case "agents":
360
- return "agents are not included on Starter";
383
+ return "agents are not included";
361
384
  }
362
385
  }
363
386
  function usageLine(reason, status) {
@@ -368,20 +391,20 @@ function usageLine(reason, status) {
368
391
  const members = row ?? status.members;
369
392
  return members
370
393
  ? ` Members: ${members.used} of ${target}.`
371
- : ` Members allowed on Starter: ${target}.`;
394
+ : ` Members allowed: ${target}.`;
372
395
  }
373
396
  case "integrations":
374
397
  return row
375
398
  ? ` Integrations: ${row.used} of ${target}.`
376
- : ` Integrations allowed on Starter: ${target}.`;
399
+ : ` Integrations allowed: ${target}.`;
377
400
  case "secrets":
378
401
  return row
379
402
  ? ` Secrets: ${row.used} of ${target}.`
380
- : ` Secrets allowed on Starter: ${target}.`;
403
+ : ` Secrets allowed: ${target}.`;
381
404
  case "agents":
382
405
  return row
383
406
  ? ` Agents: ${row.used} of ${target}.`
384
- : ` Agents allowed on Starter: ${target}.`;
407
+ : ` Agents allowed: ${target}.`;
385
408
  }
386
409
  }
387
410
  /**
@@ -409,15 +432,17 @@ function capitalize(text) {
409
432
  * about a dimension nobody measured.
410
433
  */
411
434
  export function renderPlanLimitNotice(status, opts = {}) {
412
- const { companySlug, freeMonth } = status;
435
+ const { companySlug } = status;
413
436
  const lines = [];
414
437
  if (isUsageOnly(status.pricingPlan)) {
415
438
  return [`${companySlug}:`, ...usageOnlyUpgradeLines().map((l) => ` ${l}`)].join("\n");
416
439
  }
417
- lines.push(`HQ Starter — ${companySlug} is over its plan limits.`);
440
+ lines.push(knownEarlierPlan(status)
441
+ ? `HQ Starter — ${companySlug} is over its Starter limits.`
442
+ : `${companySlug} is over its limits.`);
418
443
  const reasons = status.reasons.length
419
444
  ? status.reasons.map(reasonLabel).join("; ")
420
- : "over the Starter plan limits";
445
+ : "over its limits";
421
446
  lines.push(` Why: ${reasons}.`);
422
447
  for (const reason of status.reasons) {
423
448
  lines.push(usageLine(reason, status));
@@ -426,21 +451,20 @@ export function renderPlanLimitNotice(status, opts = {}) {
426
451
  lines.push(HARD_STOP_SENTENCE_LINES[1]);
427
452
  const remedy = status.reasons.length
428
453
  ? status.reasons.map((reason) => reasonRemedy(reason, status)).join(", and ")
429
- : "come back under the Starter plan limits";
430
- const freeMonthUrl = freeMonth?.url;
431
- const hasFreeMonth = Boolean(freeMonth?.eligible && freeMonthUrl);
432
- if (hasFreeMonth) {
433
- lines.push(" Three ways to sort it:");
434
- lines.push(` 1. Start your free month — add a card, first month free: ${freeMonthUrl}`);
435
- lines.push(` 2. ${capitalize(remedy)}.`);
436
- lines.push(` 3. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
454
+ : "come back under its limits";
455
+ // The flat plans are closed: no Workforce offer and no free month (it led
456
+ // to the closed checkout). The second way out is the switch to usage
457
+ // pricing when the server said the company is on an earlier plan or that
458
+ // the flat plans are closed to it; otherwise the company's Billing page.
459
+ lines.push(" Two ways to sort it:");
460
+ lines.push(` 1. ${capitalize(remedy)}.`);
461
+ if (status.legacyPlansClosed || knownEarlierPlan(status)) {
462
+ lines.push(` 2. ${USAGE_SWITCH_LABEL}: no plan fee, and these limits stop applying.`);
437
463
  }
438
464
  else {
439
- lines.push(" Two ways to sort it:");
440
- lines.push(` 1. ${capitalize(remedy)}.`);
441
- lines.push(` 2. Upgrade to ${WORKFORCE_PLAN_LABEL}.`);
465
+ lines.push(" 2. Open the company's Billing page.");
442
466
  }
443
- lines.push(` Upgrade: ${status.upgradeUrl}`);
467
+ lines.push(` ${status.legacyPlansClosed ? "Switch" : "Billing"}: ${status.upgradeUrl}`);
444
468
  if (opts.inviteTeammateEnabled === true &&
445
469
  status.plan === "free" &&
446
470
  status.over &&
@@ -459,18 +483,22 @@ export function renderPlanLimitLine(status) {
459
483
  }
460
484
  const detail = status.reasons.length
461
485
  ? status.reasons.map((reason) => reasonDetail(reason, status)).join("; ")
462
- : "over the Starter plan limits";
463
- return (`Company ${status.companySlug} is over its Starter limits (${detail}). ` +
486
+ : "over its limits";
487
+ const limits = knownEarlierPlan(status) ? "its Starter limits" : "its limits";
488
+ return (`Company ${status.companySlug} is over ${limits} (${detail}). ` +
464
489
  `New files and new secrets are paused until this is sorted: ${status.upgradeUrl}`);
465
490
  }
466
491
  /**
467
- * The `Plan: …` orientation line. Starter only: a paid or enterprise
468
- * workspace — and an UNKNOWN plan — gets no line at all. A usage-pricing
469
- * company gets the usage line instead.
492
+ * The `Plan: …` orientation line. Starter only, and only when hq-pro said the
493
+ * company is on an earlier plan: a paid or enterprise workspace gets no line,
494
+ * and an UNKNOWN pricing plan names no plan. A usage-pricing company gets the
495
+ * usage line instead.
470
496
  */
471
497
  export function renderPlanLine(status) {
472
498
  if (isUsageOnly(status.pricingPlan))
473
499
  return usageOnlyPlanLine();
500
+ if (!knownEarlierPlan(status))
501
+ return status.over ? "Plan: over its limits" : null;
474
502
  if (status.over)
475
503
  return "Plan: Starter — over its limits";
476
504
  if (status.plan !== "free")