@indigoai-us/hq-cli 5.345.41 → 5.345.43

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.
@@ -1,4 +1,7 @@
1
- import type { RemoteUsageResponse } from "./control-plane.js";
1
+ import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteUsageResponse } from "./control-plane.js";
2
2
  export declare function formatBytes(bytes: number): string;
3
3
  export declare function formatUsage(usage: RemoteUsageResponse): string;
4
+ export declare function formatAppUsage(usage: RemoteAppUsage): string;
5
+ export declare function formatAppUsageResponse(response: RemoteAppUsageResponse): string;
6
+ export declare function formatCompanyApps(apps: RemoteAppUsage[]): string;
4
7
  //# sourceMappingURL=usage-format.d.ts.map
@@ -36,6 +36,72 @@ export function formatUsage(usage) {
36
36
  lines.push(`Over the cap for ${over.join(" and ")}.`);
37
37
  else if (usage.pctDpu >= 80 || usage.pctStorage >= 80)
38
38
  lines.push("Close to a cap (80% or more).");
39
+ if (usage.companyCluster === false) {
40
+ lines.splice(1, 2, "This company has no shared database. Each app has its own, listed below.");
41
+ }
42
+ const apps = formatCompanyApps(usage.apps ?? []);
43
+ if (apps)
44
+ lines.push(apps);
39
45
  return lines.join("\n");
40
46
  }
47
+ function dollars(cents) {
48
+ return `$${(cents / 100).toFixed(2)}`;
49
+ }
50
+ function count(value) {
51
+ return Math.round(value).toLocaleString("en-US");
52
+ }
53
+ function gbMonth(value) {
54
+ return `${value.toFixed(2)} GB-month`;
55
+ }
56
+ const STATE_LINES = {
57
+ ok: "Within the included amounts.",
58
+ warn: "Close to the included amount (80% or more). Use above it is billed as overage.",
59
+ overage: "Above the included amount. The extra use is billed as overage.",
60
+ locked: "At the ceiling. The database is read-only: reads work, writes are refused. Move this app to a larger service, or contact HQ support to raise the limit.",
61
+ };
62
+ export function formatAppUsage(usage) {
63
+ const lines = [
64
+ `Database usage for app ${usage.appId}, ${usage.period}`,
65
+ `Compute: ${count(usage.dpu.used)} of ${count(usage.dpu.included)} included units (${percent(usage.dpu.pct)}), overage ${count(usage.dpu.overage)}, ceiling ${count(usage.dpu.ceiling)}`,
66
+ `Storage: ${formatBytes(usage.storage.usedBytes)} of ${formatBytes(usage.storage.includedBytes)} included (${percent(usage.storage.pct)}), month average ${formatBytes(usage.storage.billedBytes)}, overage ${gbMonth(usage.storage.overageGbMonth)}, ceiling ${formatBytes(usage.storage.ceilingBytes)}`,
67
+ STATE_LINES[usage.locked ? "locked" : usage.state],
68
+ ];
69
+ if (usage.estimate) {
70
+ const e = usage.estimate;
71
+ lines.push(`Estimated charge this month: ${dollars(e.totalCents)} (${dollars(e.baseCents)} base, ${dollars(e.dpuOverageCents)} compute overage, ${dollars(e.storageOverageCents)} storage overage)`);
72
+ }
73
+ else if (usage.exempt) {
74
+ lines.push("Not billed: this company is exempt from database app charges.");
75
+ }
76
+ if (usage.ended)
77
+ lines.push("This app was deleted. Usage up to deletion still counts for the month.");
78
+ lines.push(usage.measuredAt === null
79
+ ? "Not measured yet, so usage shows as 0. Check again in about 15 minutes."
80
+ : `Last measured ${usage.measuredAt}. Numbers can lag by a few minutes.`);
81
+ return lines.join("\n");
82
+ }
83
+ export function formatAppUsageResponse(response) {
84
+ if (response.mode === "company-cluster") {
85
+ return [
86
+ `App ${response.appId} uses the shared company database, so these are the company numbers.`,
87
+ formatUsage(response),
88
+ ].join("\n");
89
+ }
90
+ return formatAppUsage(response);
91
+ }
92
+ export function formatCompanyApps(apps) {
93
+ if (apps.length === 0)
94
+ return "";
95
+ const rows = apps.map((app) => {
96
+ const note = app.locked
97
+ ? ", read-only"
98
+ : app.state === "warn"
99
+ ? ", close to the included amount"
100
+ : app.state === "overage"
101
+ ? ", over the included amount"
102
+ : "";
103
+ return ` ${app.appId}: ${count(app.dpu.used)} of ${count(app.dpu.included)} compute (${percent(app.dpu.pct)}), ${formatBytes(app.storage.usedBytes)} of ${formatBytes(app.storage.includedBytes)} storage (${percent(app.storage.pct)})${note}${app.ended ? ", deleted" : ""}`;
104
+ });
105
+ return ["Apps with their own database:", ...rows, "Run hq db usage --app <name> for one app."].join("\n");
106
+ }
41
107
  //# sourceMappingURL=usage-format.js.map
@@ -13,6 +13,8 @@ import type { CheckFamily } from "../types.js";
13
13
  export declare const REMEDIATION: {
14
14
  readonly installClaude: "hq install --global --runtime claude";
15
15
  readonly installCodex: "hq install --global --runtime codex";
16
+ readonly repairClaudeSkills: "hq uninstall --global --runtime claude && hq install --global --runtime claude";
17
+ readonly repairCodexSkills: "hq uninstall --global --runtime codex && hq install --global --runtime codex";
16
18
  readonly daemonInstall: "hq daemon install";
17
19
  readonly daemonRestart: "hq daemon restart --daemon";
18
20
  readonly link: "hq link <repo-dir> <company>";
@@ -22,6 +24,13 @@ interface DaemonServiceState {
22
24
  loaded: boolean;
23
25
  detail: string;
24
26
  }
27
+ export interface DaemonServiceProbeOptions {
28
+ platform?: NodeJS.Platform;
29
+ uid?: number;
30
+ launchdProbe?: (target: string) => boolean;
31
+ /** Test seam for the desktop app's live managed daemon state. */
32
+ hostedDaemonProbe?: () => boolean;
33
+ }
25
34
  export interface AnywhereDeps {
26
35
  home?: string;
27
36
  env?: NodeJS.ProcessEnv;
@@ -40,5 +49,6 @@ export interface AnywhereDeps {
40
49
  }
41
50
  export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
42
51
  export declare const anywhereFamily: CheckFamily;
52
+ export declare function defaultServiceState(home: string, options?: DaemonServiceProbeOptions): DaemonServiceState;
43
53
  export {};
44
54
  //# sourceMappingURL=anywhere.d.ts.map
@@ -21,6 +21,8 @@ import { GROK_MODE } from "../../install/grok-targets.js";
21
21
  import { HqdClientError, hqdRequest } from "../../daemon/protocol.js";
22
22
  import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/heartbeat.js";
23
23
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
24
+ import { hostedDaemonRunning } from "../../daemon/handoff.js";
25
+ import { daemonPaths } from "../../daemon/paths.js";
24
26
  import { hqdSocketPath } from "../../daemon/protocol.js";
25
27
  import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
26
28
  import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
@@ -29,6 +31,8 @@ const ANYWHERE_FAMILY_ID = "anywhere";
29
31
  export const REMEDIATION = {
30
32
  installClaude: "hq install --global --runtime claude",
31
33
  installCodex: "hq install --global --runtime codex",
34
+ repairClaudeSkills: "hq uninstall --global --runtime claude && hq install --global --runtime claude",
35
+ repairCodexSkills: "hq uninstall --global --runtime codex && hq install --global --runtime codex",
32
36
  daemonInstall: "hq daemon install",
33
37
  daemonRestart: "hq daemon restart --daemon",
34
38
  link: "hq link <repo-dir> <company>",
@@ -137,13 +141,14 @@ async function runAnywhereChecks(context, deps = {}) {
137
141
  else
138
142
  out.push(result("PASS", "daemon-installed", `daemon service is installed and loaded (${service.detail})`));
139
143
  const running = health.reachable ? health.version : null;
144
+ const daemonFix = service.installed ? REMEDIATION.daemonRestart : REMEDIATION.daemonInstall;
140
145
  if (health.reachable)
141
146
  out.push(result("PASS", "socket", `hqd answered on ${socketPath}`, undefined, socketPath));
142
147
  else
143
- out.push(result("FAIL", "socket", `hqd is not reachable on ${socketPath}`, REMEDIATION.daemonInstall, socketPath));
148
+ out.push(result("FAIL", "socket", `hqd is not reachable on ${socketPath}`, daemonFix, socketPath));
144
149
  const cliVersion = deps.cliVersion ?? CLI_VERSION;
145
150
  if (running === null)
146
- out.push(result("FAIL", "version", `cannot compare versions: hqd did not answer (hq-cli is ${cliVersion})`, REMEDIATION.daemonInstall));
151
+ out.push(result("FAIL", "version", `cannot compare versions: hqd did not answer (hq-cli is ${cliVersion})`, daemonFix));
147
152
  else if (running !== cliVersion)
148
153
  out.push(result("FAIL", "version", `hqd runs ${running} but hq-cli ${cliVersion} is installed`, REMEDIATION.daemonRestart));
149
154
  else
@@ -159,9 +164,9 @@ async function runAnywhereChecks(context, deps = {}) {
159
164
  if (pluginInstall)
160
165
  out.push(result("PASS", "claude-skills", `HQ Claude skills are provided by plugin ${pluginInstall.pluginId}`));
161
166
  else
162
- out.push(checkSkillLinks("claude", claude.skillsDir, REMEDIATION.installClaude));
167
+ out.push(checkSkillLinks("claude", claude.skillsDir, REMEDIATION.installClaude, REMEDIATION.repairClaudeSkills));
163
168
  if (mcp.runtimes.codex.installed)
164
- out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex));
169
+ out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex, REMEDIATION.repairCodexSkills));
165
170
  return out;
166
171
  }
167
172
  function unreachableHealth(socketPath) {
@@ -321,7 +326,7 @@ function checkRegistration(runtimeName, status, runtimeState, fix) {
321
326
  return result("FAIL", id, `${runtimeName} hq MCP entry does not use absolute paths that exist (${status.target ?? ""})`, fix);
322
327
  return result("PASS", id, `hq MCP server registered in ${runtimeName} with absolute paths`);
323
328
  }
324
- function checkSkillLinks(runtime, dir, fix) {
329
+ function checkSkillLinks(runtime, dir, fix, repair = fix) {
325
330
  const id = `${runtime}-skills`;
326
331
  let names;
327
332
  try {
@@ -335,19 +340,42 @@ function checkSkillLinks(runtime, dir, fix) {
335
340
  return result("WARN", id, `no hq-* skill links in ${dir}`, fix, dir);
336
341
  const broken = links.filter((n) => !fs.existsSync(path.join(dir, n)));
337
342
  if (broken.length > 0)
338
- return result("FAIL", id, `${broken.length} broken skill links in ${dir}: ${broken.slice(0, 5).join(", ")}`, fix, dir);
343
+ return result("FAIL", id, `${broken.length} broken skill links in ${dir}: ${broken.slice(0, 5).join(", ")}`, repair, dir);
339
344
  return result("PASS", id, `${links.length} hq-* skill links in ${dir} resolve`, undefined, dir);
340
345
  }
341
- function defaultServiceState(home) {
342
- if (process.platform === "darwin") {
343
- const plist = path.join(home, "Library", "LaunchAgents", `${DAEMON_LAUNCHD_LABEL}.plist`);
344
- if (!fs.existsSync(plist))
345
- return { installed: false, loaded: false, detail: `no ${plist}` };
346
- const uid = process.getuid?.() ?? 0;
347
- const probe = spawnSync("launchctl", ["print", `gui/${uid}/${DAEMON_LAUNCHD_LABEL}`], { encoding: "utf8", timeout: 5_000 });
348
- return { installed: true, loaded: probe.status === 0, detail: `launchd ${DAEMON_LAUNCHD_LABEL}` };
346
+ export function defaultServiceState(home, options = {}) {
347
+ const platform = options.platform ?? process.platform;
348
+ if (platform === "darwin") {
349
+ const launchAgents = path.join(home, "Library", "LaunchAgents");
350
+ const nativePlist = path.join(launchAgents, `${DAEMON_LAUNCHD_LABEL}.plist`);
351
+ const desktopLabel = "ai.indigo.hq-sync-menubar";
352
+ const desktopPlist = path.join(launchAgents, `${desktopLabel}.plist`);
353
+ const uid = options.uid ?? process.getuid?.() ?? 0;
354
+ const probe = options.launchdProbe ?? ((target) => spawnSync("launchctl", ["print", target], { encoding: "utf8", timeout: 5_000 }).status === 0);
355
+ const hasNative = fs.existsSync(nativePlist);
356
+ const hasDesktop = fs.existsSync(desktopPlist);
357
+ const nativeTarget = `gui/${uid}/${DAEMON_LAUNCHD_LABEL}`;
358
+ if (hasNative && probe(nativeTarget)) {
359
+ return { installed: true, loaded: true, detail: `launchd ${DAEMON_LAUNCHD_LABEL}` };
360
+ }
361
+ const desktopTarget = `gui/${uid}/${desktopLabel}`;
362
+ const desktopHostedDaemon = options.hostedDaemonProbe
363
+ ? options.hostedDaemonProbe()
364
+ : hostedDaemonRunning(daemonPaths(home));
365
+ if (desktopHostedDaemon) {
366
+ return { installed: true, loaded: true, detail: "HQ Desktop-hosted hqd is running (host=desktop)" };
367
+ }
368
+ if (hasNative)
369
+ return { installed: true, loaded: false, detail: `launchd ${DAEMON_LAUNCHD_LABEL}` };
370
+ if (!hasDesktop)
371
+ return { installed: false, loaded: false, detail: `no ${nativePlist} or live desktop-hosted hqd` };
372
+ const desktopAgentLoaded = hasDesktop && probe(desktopTarget);
373
+ const desktopDetail = hasDesktop
374
+ ? `HQ Desktop LaunchAgent ${desktopLabel}${desktopAgentLoaded ? " is loaded" : " is not loaded"}, but no live desktop-hosted hqd was found`
375
+ : "no live desktop-hosted hqd was found";
376
+ return { installed: false, loaded: false, detail: `no ${nativePlist}; ${desktopDetail}` };
349
377
  }
350
- if (process.platform === "linux") {
378
+ if (platform === "linux") {
351
379
  const unit = path.join(home, ".config", "systemd", "user", DAEMON_SYSTEMD_UNIT);
352
380
  const systemUnit = path.join("/etc/systemd/system", DAEMON_SYSTEMD_UNIT);
353
381
  if (!fs.existsSync(unit) && !fs.existsSync(systemUnit))
@@ -356,13 +384,13 @@ function defaultServiceState(home) {
356
384
  const probe = spawnSync("systemctl", [...(systemScope ? [] : ["--user"]), "is-active", DAEMON_SYSTEMD_UNIT], { encoding: "utf8", timeout: 5_000 });
357
385
  return { installed: true, loaded: probe.status === 0, detail: `systemd ${systemScope ? "system" : "user"} ${DAEMON_SYSTEMD_UNIT}` };
358
386
  }
359
- if (process.platform === "win32") {
387
+ if (platform === "win32") {
360
388
  const probe = spawnSync("schtasks.exe", ["/Query", "/TN", DAEMON_WINDOWS_TASK, "/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 5_000 });
361
389
  if (probe.status !== 0)
362
390
  return { installed: false, loaded: false, detail: `no Scheduled Task ${DAEMON_WINDOWS_TASK}` };
363
391
  const loaded = /,\s*"?Running"?\s*$/i.test(probe.stdout.trim());
364
392
  return { installed: true, loaded, detail: `Scheduled Task ${DAEMON_WINDOWS_TASK}${loaded ? " is running" : " is not running"}` };
365
393
  }
366
- return { installed: false, loaded: false, detail: `${process.platform}: service probe not available` };
394
+ return { installed: false, loaded: false, detail: `${platform}: service probe not available` };
367
395
  }
368
396
  //# sourceMappingURL=anywhere.js.map
@@ -31,6 +31,8 @@ export interface FlagRegistryDependencies {
31
31
  sentry?: FlagRegistrySentry;
32
32
  /** Test seam for exercising the no-source fail-open path. */
33
33
  workerSource?: string;
34
+ /** Start another bounded one-shot worker after an earlier process refresh. */
35
+ forceRefresh?: boolean;
34
36
  captureWorkerFailure?: (error: Error, tags: Record<string, string>) => void;
35
37
  }
36
38
  export interface CompanyFlagGateDependencies {
@@ -54,6 +56,8 @@ export declare function createProcessSnapshotReader(companyIdentifiers: string[]
54
56
  * null and the caller's exact legacy fallback decides the gate.
55
57
  */
56
58
  export declare function kickFlagRegistryReadiness(dependencies?: FlagRegistryDependencies): FlagReader | null;
59
+ /** Refresh the registry snapshot for a long-running process and wait briefly for its result. */
60
+ export declare function refreshFlagRegistrySnapshot(timeoutMs?: number): Promise<void>;
57
61
  /**
58
62
  * Evaluate a registry-backed gate without changing its outage default.
59
63
  * `isEnabled` itself is synchronous and snapshot-only; any reader failure
@@ -11,7 +11,7 @@ import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
11
11
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
12
12
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
13
13
  import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
14
- import { flagSnapshotWriteFailureTags, readFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
14
+ import { flagSnapshotWriteFailureTags, readFlagSnapshotFile, writePersonFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
15
15
  function captureFlagRegistryException(error, tags, sentry) {
16
16
  if (!sentry)
17
17
  return;
@@ -50,6 +50,7 @@ let refreshCompanyUid;
50
50
  let refreshHasCompanyContext = false;
51
51
  let refreshPendingSnapshot = null;
52
52
  let refreshSentry;
53
+ let refreshDependencies = {};
53
54
  /** Wait briefly for the current company refresh to persist or report completion. */
54
55
  export async function waitForFlagRegistryRefresh(timeoutMs = 500, companyUid) {
55
56
  const resolvedCompanyUid = normalizedCompanyUid(companyUid);
@@ -70,6 +71,21 @@ export async function waitForFlagRegistryRefresh(timeoutMs = 500, companyUid) {
70
71
  completeRefresh?.();
71
72
  completeRefresh = null;
72
73
  }
74
+ else if (refreshPendingSnapshot && !refreshHasCompanyContext) {
75
+ try {
76
+ writePersonFlagSnapshotFile(refreshPendingSnapshot.flags);
77
+ }
78
+ catch (error) {
79
+ captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
80
+ component: "flag-snapshot-cache-write",
81
+ company_uid_present: "false",
82
+ ...flagSnapshotWriteFailureTags(error),
83
+ }, refreshSentry);
84
+ }
85
+ refreshPendingSnapshot = null;
86
+ completeRefresh?.();
87
+ completeRefresh = null;
88
+ }
73
89
  if (!completeRefresh)
74
90
  return;
75
91
  const worker = refreshWorker;
@@ -123,8 +139,10 @@ function processSnapshotReader(companyIdentifiers) {
123
139
  * null and the caller's exact legacy fallback decides the gate.
124
140
  */
125
141
  export function kickFlagRegistryReadiness(dependencies = {}) {
126
- if (processReader)
142
+ if (processReader && !dependencies.forceRefresh)
127
143
  return processReader;
144
+ if (!dependencies.forceRefresh)
145
+ refreshDependencies = { ...dependencies };
128
146
  const endpoint = resolveFlagRegistryEndpoint(dependencies.endpoint, process.env);
129
147
  const companyScope = dependencies.companyScope ?? explicitCompanyScopeFromArgv(dependencies.argv ?? []);
130
148
  const companyEnv = companyScope
@@ -233,6 +251,16 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
233
251
  finishRefresh();
234
252
  }
235
253
  else if (!refreshHasCompanyContext) {
254
+ try {
255
+ writePersonFlagSnapshotFile(value.flags);
256
+ }
257
+ catch (error) {
258
+ captureFlagRegistryException(error instanceof Error ? error : new Error(String(error)), {
259
+ component: "flag-snapshot-cache-write",
260
+ company_uid_present: "false",
261
+ ...flagSnapshotWriteFailureTags(error),
262
+ }, sentry);
263
+ }
236
264
  refreshPendingSnapshot = null;
237
265
  finishRefresh();
238
266
  }
@@ -258,6 +286,18 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
258
286
  }
259
287
  return processReader;
260
288
  }
289
+ /** Refresh the registry snapshot for a long-running process and wait briefly for its result. */
290
+ export async function refreshFlagRegistrySnapshot(timeoutMs = 5_000) {
291
+ const refreshIsPending = () => completeRefresh !== null;
292
+ if (refreshIsPending()) {
293
+ await waitForFlagRegistryRefresh(timeoutMs);
294
+ if (refreshIsPending())
295
+ return;
296
+ }
297
+ processReader = null;
298
+ kickFlagRegistryReadiness({ ...refreshDependencies, forceRefresh: true });
299
+ await waitForFlagRegistryRefresh(timeoutMs);
300
+ }
261
301
  /**
262
302
  * Evaluate a registry-backed gate without changing its outage default.
263
303
  * `isEnabled` itself is synchronous and snapshot-only; any reader failure
@@ -348,5 +388,6 @@ export function _resetFlagRegistryForTests() {
348
388
  refreshHasCompanyContext = false;
349
389
  refreshPendingSnapshot = null;
350
390
  refreshSentry = undefined;
391
+ refreshDependencies = {};
351
392
  }
352
393
  //# sourceMappingURL=flag-registry.js.map
@@ -7,8 +7,25 @@ export interface PersistedFlagSnapshot {
7
7
  fetched_at: string;
8
8
  flags: Record<string, boolean>;
9
9
  }
10
+ export interface PersistedPersonFlagSnapshot {
11
+ scope: "person";
12
+ fetched_at: string;
13
+ flags: Record<string, boolean>;
14
+ }
10
15
  export declare function _resetFlagSnapshotCacheForTests(): void;
11
16
  export declare function flagSnapshotPath(companyUid: string, env?: NodeJS.ProcessEnv): string | undefined;
17
+ export declare function personFlagSnapshotPath(env?: NodeJS.ProcessEnv): string;
18
+ /** Persist the no-company flag resolution without storing identity or token data. */
19
+ export declare function writePersonFlagSnapshotFile(flags: Readonly<Record<string, unknown>>, options?: {
20
+ env?: NodeJS.ProcessEnv;
21
+ fetchedAt?: string;
22
+ }): string;
23
+ /** Read only a fresh, correctly scoped person snapshot. Every invalid state is a cache miss. */
24
+ export declare function readPersonFlagSnapshotFile(options?: {
25
+ env?: NodeJS.ProcessEnv;
26
+ now?: number;
27
+ maxAgeMs?: number;
28
+ }): PersistedPersonFlagSnapshot | null;
12
29
  /** Write just the validated flag decisions and company scope, never auth data. */
13
30
  export declare function writeFlagSnapshotFile(companyUid: string, flags: Readonly<Record<string, unknown>>, options?: {
14
31
  env?: NodeJS.ProcessEnv;
@@ -102,19 +102,169 @@ function fallbackSnapshotPath(companyUid, create) {
102
102
  }
103
103
  return path.join(directories[2], `snapshot-${companyUid}.json`);
104
104
  }
105
+ function fallbackPersonSnapshotPath(create) {
106
+ if (typeof process.getuid !== "function")
107
+ return undefined;
108
+ const uid = process.getuid();
109
+ const root = path.join(os.tmpdir(), `hq-cli-flag-snapshots-${uid}`);
110
+ const directories = [root, path.join(root, "hq"), path.join(root, "hq", "flags")];
111
+ for (const directory of directories) {
112
+ try {
113
+ if (create)
114
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
115
+ const stat = fs.lstatSync(directory);
116
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid)
117
+ return undefined;
118
+ if (create)
119
+ fs.chmodSync(directory, 0o700);
120
+ else if ((stat.mode & 0o077) !== 0)
121
+ return undefined;
122
+ }
123
+ catch {
124
+ return undefined;
125
+ }
126
+ }
127
+ return path.join(directories[2], "snapshot-person.json");
128
+ }
105
129
  export function flagSnapshotPath(companyUid, env = process.env) {
106
130
  if (!COMPANY_UID_RE.test(companyUid))
107
131
  return undefined;
108
132
  const cacheHome = env.XDG_CACHE_HOME?.trim() || path.join(os.homedir(), ".cache");
109
133
  return path.join(cacheHome, "hq", "flags", `snapshot-${companyUid}.json`);
110
134
  }
135
+ export function personFlagSnapshotPath(env = process.env) {
136
+ const cacheHome = env.XDG_CACHE_HOME?.trim() || path.join(os.homedir(), ".cache");
137
+ return path.join(cacheHome, "hq", "flags", "snapshot-person.json");
138
+ }
139
+ function normalizedFlagValues(flags) {
140
+ return Object.fromEntries(Object.entries(flags).filter((entry) => Boolean(entry[0].trim()) && typeof entry[1] === "boolean"));
141
+ }
142
+ function writeSnapshotAt(destination, snapshot, cacheRootKind) {
143
+ const directory = path.dirname(destination);
144
+ const context = (step) => ({
145
+ step,
146
+ cacheRootKind,
147
+ directory,
148
+ });
149
+ atWriteStep(context("mkdir"), () => fs.mkdirSync(directory, { recursive: true, mode: 0o700 }));
150
+ const temporary = `${destination}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
151
+ let fd;
152
+ try {
153
+ fd = atWriteStep(context("open_tmp"), () => fs.openSync(temporary, "wx", 0o600));
154
+ atWriteStep(context("write_tmp"), () => {
155
+ fs.writeFileSync(fd, `${JSON.stringify(snapshot)}\n`, "utf8");
156
+ fs.fsyncSync(fd);
157
+ fs.closeSync(fd);
158
+ fd = undefined;
159
+ });
160
+ atWriteStep(context("rename"), () => fs.renameSync(temporary, destination));
161
+ }
162
+ catch (error) {
163
+ if (fd !== undefined)
164
+ fs.closeSync(fd);
165
+ try {
166
+ fs.unlinkSync(temporary);
167
+ }
168
+ catch (cleanupError) {
169
+ if (cleanupError.code !== "ENOENT")
170
+ throw cleanupError;
171
+ }
172
+ throw error;
173
+ }
174
+ return destination;
175
+ }
176
+ function writePersonFallback(snapshot, originalError) {
177
+ const fallbackFile = fallbackPersonSnapshotPath(true);
178
+ if (!fallbackFile) {
179
+ if (originalError) {
180
+ markFallbackFailed(originalError);
181
+ throw originalError;
182
+ }
183
+ const error = Object.assign(new Error("Flag snapshot fallback directory is unavailable"), { code: "EACCES" });
184
+ markFallbackFailed(error);
185
+ throw error;
186
+ }
187
+ try {
188
+ const written = writeSnapshotAt(fallbackFile, snapshot, "home");
189
+ fallbackCacheUsed = true;
190
+ return written;
191
+ }
192
+ catch (fallbackError) {
193
+ if (originalError) {
194
+ markFallbackFailed(originalError);
195
+ throw originalError;
196
+ }
197
+ markFallbackFailed(fallbackError);
198
+ throw fallbackError;
199
+ }
200
+ }
201
+ /** Persist the no-company flag resolution without storing identity or token data. */
202
+ export function writePersonFlagSnapshotFile(flags, options = {}) {
203
+ const env = options.env ?? process.env;
204
+ const snapshot = {
205
+ scope: "person",
206
+ fetched_at: options.fetchedAt ?? new Date().toISOString(),
207
+ flags: normalizedFlagValues(flags),
208
+ };
209
+ if (!Number.isFinite(Date.parse(snapshot.fetched_at))) {
210
+ throw new Error("Cannot persist a flag snapshot with an invalid fetched_at timestamp");
211
+ }
212
+ if (fallbackCacheUsed)
213
+ return writePersonFallback(snapshot);
214
+ try {
215
+ return writeSnapshotAt(personFlagSnapshotPath(env), snapshot, env.XDG_CACHE_HOME?.trim() ? "xdg" : "home");
216
+ }
217
+ catch (error) {
218
+ if (!isPermissionFailure(error))
219
+ throw error;
220
+ return writePersonFallback(snapshot, error);
221
+ }
222
+ }
223
+ /** Read only a fresh, correctly scoped person snapshot. Every invalid state is a cache miss. */
224
+ export function readPersonFlagSnapshotFile(options = {}) {
225
+ const env = options.env ?? process.env;
226
+ const files = [personFlagSnapshotPath(env)];
227
+ const fallback = fallbackPersonSnapshotPath(false);
228
+ if (fallbackCacheUsed && fallback)
229
+ files.unshift(fallback);
230
+ else if (fallback)
231
+ files.push(fallback);
232
+ for (const file of files) {
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
236
+ }
237
+ catch {
238
+ continue;
239
+ }
240
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
241
+ continue;
242
+ const value = parsed;
243
+ if (Object.keys(value).sort().join(",") !== "fetched_at,flags,scope" ||
244
+ value.scope !== "person" ||
245
+ typeof value.fetched_at !== "string" ||
246
+ !value.flags || typeof value.flags !== "object" || Array.isArray(value.flags))
247
+ continue;
248
+ const fetchedAt = Date.parse(value.fetched_at);
249
+ const age = (options.now ?? Date.now()) - fetchedAt;
250
+ if (!Number.isFinite(fetchedAt) || age < 0 || age > (options.maxAgeMs ?? FLAG_SNAPSHOT_MAX_AGE_MS))
251
+ continue;
252
+ const flags = value.flags;
253
+ if (!Object.values(flags).every((enabled) => typeof enabled === "boolean"))
254
+ continue;
255
+ if (file === fallback && fallback)
256
+ fallbackCacheUsed = true;
257
+ return { scope: "person", fetched_at: value.fetched_at, flags: flags };
258
+ }
259
+ return null;
260
+ }
111
261
  /** Write just the validated flag decisions and company scope, never auth data. */
112
262
  export function writeFlagSnapshotFile(companyUid, flags, options = {}) {
113
263
  const env = options.env ?? process.env;
114
264
  const file = flagSnapshotPath(companyUid, env);
115
265
  if (!file)
116
266
  throw new Error("Cannot persist a flag snapshot without a valid company uid");
117
- const normalizedFlags = Object.fromEntries(Object.entries(flags).filter((entry) => Boolean(entry[0].trim()) && typeof entry[1] === "boolean"));
267
+ const normalizedFlags = normalizedFlagValues(flags);
118
268
  const snapshot = {
119
269
  company_uid: companyUid,
120
270
  fetched_at: options.fetchedAt ?? new Date().toISOString(),
@@ -124,40 +274,6 @@ export function writeFlagSnapshotFile(companyUid, flags, options = {}) {
124
274
  throw new Error("Cannot persist a flag snapshot with an invalid fetched_at timestamp");
125
275
  }
126
276
  const cacheRootKind = env.XDG_CACHE_HOME?.trim() ? "xdg" : "home";
127
- const writeAt = (destination) => {
128
- const directory = path.dirname(destination);
129
- const context = (step) => ({
130
- step,
131
- cacheRootKind,
132
- directory,
133
- });
134
- atWriteStep(context("mkdir"), () => fs.mkdirSync(directory, { recursive: true, mode: 0o700 }));
135
- const temporary = `${destination}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
136
- let fd;
137
- try {
138
- fd = atWriteStep(context("open_tmp"), () => fs.openSync(temporary, "wx", 0o600));
139
- atWriteStep(context("write_tmp"), () => {
140
- fs.writeFileSync(fd, `${JSON.stringify(snapshot)}\n`, "utf8");
141
- fs.fsyncSync(fd);
142
- fs.closeSync(fd);
143
- fd = undefined;
144
- });
145
- atWriteStep(context("rename"), () => fs.renameSync(temporary, destination));
146
- }
147
- catch (error) {
148
- if (fd !== undefined)
149
- fs.closeSync(fd);
150
- try {
151
- fs.unlinkSync(temporary);
152
- }
153
- catch (cleanupError) {
154
- if (cleanupError.code !== "ENOENT")
155
- throw cleanupError;
156
- }
157
- throw error;
158
- }
159
- return destination;
160
- };
161
277
  const writeFallback = (originalError) => {
162
278
  const fallbackFile = fallbackSnapshotPath(companyUid, true);
163
279
  if (!fallbackFile) {
@@ -170,7 +286,7 @@ export function writeFlagSnapshotFile(companyUid, flags, options = {}) {
170
286
  throw error;
171
287
  }
172
288
  try {
173
- const written = writeAt(fallbackFile);
289
+ const written = writeSnapshotAt(fallbackFile, snapshot, cacheRootKind);
174
290
  fallbackCacheUsed = true;
175
291
  return written;
176
292
  }
@@ -186,7 +302,7 @@ export function writeFlagSnapshotFile(companyUid, flags, options = {}) {
186
302
  if (fallbackCacheUsed)
187
303
  return writeFallback();
188
304
  try {
189
- return writeAt(file);
305
+ return writeSnapshotAt(file, snapshot, cacheRootKind);
190
306
  }
191
307
  catch (error) {
192
308
  if (!isPermissionFailure(error))
@@ -24,4 +24,6 @@ export declare const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-h
24
24
  export declare const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
25
25
  /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
26
26
  export declare const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
27
+ /** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
28
+ export declare const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
27
29
  //# sourceMappingURL=flag-keys.d.ts.map
@@ -24,4 +24,6 @@ export const LEAD_AUTOPILOT_HEADLINE_STALENESS_FLAG = "lanes.autopilot-headline-
24
24
  export const LEAD_AUTOPILOT_OUTPOST_NATIVE_LEADS_FLAG = "lanes.autopilot-outpost-native-leads";
25
25
  /** Allows applying an operator-reviewed repair of messages stranded in bare-ULID dropboxes. */
26
26
  export const LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG = "lanes.orphaned-bare-id-dropbox-repair";
27
+ /** Allows a Claude lane to leave an exhausted profile account pin for an eligible account. */
28
+ export const LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG = "lanes.profile-pin-exhausted-fallback";
27
29
  //# sourceMappingURL=flag-keys.js.map
@@ -785,7 +785,7 @@ export interface AccountHistoryEntry {
785
785
  from: string | null;
786
786
  to: string;
787
787
  at: string;
788
- reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "keep_alive_resume" | "operator_move";
788
+ reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "load_balance" | "keep_alive_resume" | "operator_move" | "profile_pin_exhausted";
789
789
  }
790
790
  export interface FailedResumeHistoryEntry {
791
791
  at: string;
@@ -45,7 +45,10 @@ export const RESIDENT_PACKAGE_MODULES = {
45
45
  coreJobRun: ["../lib/flag-registry.js"],
46
46
  "daemon-call": ["../lib/daemon/protocol.js"],
47
47
  daemon: [
48
- "../sentry.js", "../lib/doctor/client-health-reason-codes-flag.js", "@indigoai-us/hq-flags-client",
48
+ "../sentry.js",
49
+ "../lib/doctor/client-health-reason-codes-flag.js",
50
+ "@indigoai-us/hq-flags-client",
51
+ "../lib/flag-registry.js",
49
52
  "../utils/cognito-session.js",
50
53
  "../lib/plan-limit-nag.js",
51
54
  "../utils/plan-gate-error.js",
@@ -207,6 +210,7 @@ const LOADERS = {
207
210
  await import("@indigoai-us/hq-flags-client");
208
211
  preloadSentrySdk();
209
212
  },
213
+ () => import("../lib/flag-registry.js"),
210
214
  () => import("../utils/cognito-session.js"),
211
215
  () => import("../lib/plan-limit-nag.js"),
212
216
  () => import("../utils/plan-gate-error.js"),