@indigoai-us/hq-cli 5.345.202 → 5.345.204

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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/command-catalog.generated.d.ts +24 -0
  3. package/dist/command-catalog.generated.js +26 -0
  4. package/dist/commands/bot.js +7 -2
  5. package/dist/commands/capacity.js +10 -2
  6. package/dist/commands/cloud.d.ts +14 -2
  7. package/dist/commands/cloud.js +25 -16
  8. package/dist/commands/daemon.d.ts +2 -1
  9. package/dist/commands/daemon.js +225 -4
  10. package/dist/commands/integrations-core.d.ts +3 -0
  11. package/dist/commands/integrations-core.js +11 -0
  12. package/dist/commands/integrations-manage.js +2 -2
  13. package/dist/commands/integrations.js +5 -5
  14. package/dist/commands/sync-core.js +4 -1
  15. package/dist/lib/bot/claude-login.d.ts +7 -1
  16. package/dist/lib/bot/claude-login.js +56 -16
  17. package/dist/lib/bot/executive-settings.d.ts +1 -1
  18. package/dist/lib/bot/executive-settings.js +8 -6
  19. package/dist/lib/bot/jobs/meeting-detect.js +2 -2
  20. package/dist/lib/bot/jobs/meeting-rider.js +2 -2
  21. package/dist/lib/bot/run.js +3 -3
  22. package/dist/lib/capacity/codex-availability.d.ts +6 -0
  23. package/dist/lib/capacity/codex-availability.js +33 -0
  24. package/dist/lib/capacity/collectors/claude-oauth.d.ts +53 -5
  25. package/dist/lib/capacity/collectors/claude-oauth.js +251 -71
  26. package/dist/lib/capacity/ledger.d.ts +0 -11
  27. package/dist/lib/capacity/ledger.js +30 -3
  28. package/dist/lib/capacity/route.js +11 -5
  29. package/dist/lib/daemon/catalog.d.ts +3 -0
  30. package/dist/lib/daemon/catalog.js +17 -4
  31. package/dist/lib/daemon/handoff.d.ts +2 -0
  32. package/dist/lib/daemon/handoff.js +9 -2
  33. package/dist/lib/daemon/install.d.ts +99 -0
  34. package/dist/lib/daemon/install.js +316 -7
  35. package/dist/lib/daemon/migrate.d.ts +3 -1
  36. package/dist/lib/daemon/migrate.js +14 -2
  37. package/dist/lib/daemon/plist-refresh.d.ts +34 -0
  38. package/dist/lib/daemon/plist-refresh.js +62 -0
  39. package/dist/lib/daemon/run.js +27 -2
  40. package/dist/lib/daemon/selfcheck.d.ts +71 -0
  41. package/dist/lib/daemon/selfcheck.js +150 -0
  42. package/dist/lib/daemon/supervisor.d.ts +31 -0
  43. package/dist/lib/daemon/supervisor.js +130 -6
  44. package/dist/lib/daemon/unit-holds.js +2 -1
  45. package/dist/lib/lanes/config.js +13 -0
  46. package/dist/lib/lanes/throttle.d.ts +12 -1
  47. package/dist/lib/lanes/throttle.js +27 -8
  48. package/dist/lib/lanes/types.d.ts +5 -0
  49. package/dist/lib/service-manager/launchd.d.ts +25 -0
  50. package/dist/lib/service-manager/launchd.js +51 -3
  51. package/dist/lib/service-manager/migration.d.ts +14 -0
  52. package/dist/lib/service-manager/migration.js +34 -0
  53. package/dist/lib/service-manager/systemd.d.ts +13 -0
  54. package/dist/lib/service-manager/systemd.js +19 -0
  55. package/dist/lib/service-manager/types.d.ts +8 -0
  56. package/dist/lib/signals/steps/extract.d.ts +110 -4
  57. package/dist/lib/signals/steps/extract.js +520 -39
  58. package/dist/lib/sync-core/engine-home.d.ts +2 -1
  59. package/dist/lib/sync-core/engine-home.js +25 -8
  60. package/dist/lib/sync-core/reconcile.d.ts +7 -2
  61. package/dist/lib/sync-core/reconcile.js +32 -9
  62. package/dist/lib/sync-core/release.d.ts +35 -3
  63. package/dist/lib/sync-core/release.js +95 -12
  64. package/package.json +7 -7
@@ -34,6 +34,23 @@ export const MEMORY_PRESSURE_SYSCTLS = [
34
34
  "kern.memorystatus_vm_pressure_level",
35
35
  "hw.memsize",
36
36
  ];
37
+ /**
38
+ * Default for throttle.memory_warn_min_free_percent. On darwin a warn pressure
39
+ * level holds admission only while the free percentage is at or below this.
40
+ */
41
+ export const DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT = 20;
42
+ /**
43
+ * Whether a darwin memory_pressure level holds admission on its own. Critical
44
+ * always holds. Warn holds only at or below warnMinFreePercent free, or when
45
+ * the free percentage is unknown.
46
+ */
47
+ export function pressureLevelHolds(signal, warnMinFreePercent = DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT) {
48
+ if (signal.pressure_level === "critical")
49
+ return true;
50
+ if (signal.pressure_level !== "warn")
51
+ return false;
52
+ return signal.free_percent === undefined || signal.free_percent <= warnMinFreePercent;
53
+ }
37
54
  export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
38
55
  export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
39
56
  function defaultSleep(ms) {
@@ -340,11 +357,11 @@ function formatMb(mb) {
340
357
  return `${Math.round(mb)} MB`;
341
358
  }
342
359
  /** Plain explanation of a memory hold: which signal, its value, and the threshold. */
343
- export function formatMemoryHoldMessage(signal, thresholdMb) {
360
+ export function formatMemoryHoldMessage(signal, thresholdMb, warnMinFreePercent = DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT) {
344
361
  if (signal.source === "memory_pressure") {
345
362
  const reading = `memory_pressure reports ${signal.free_percent}% free (about ${formatMb(signal.available_mb)}), pressure level ${signal.pressure_level}`;
346
- if (signal.pressure_level !== "normal") {
347
- return `Holding new lanes: ${reading}. Lanes wait while the level is warn or critical (throttle.min_available_mem_mb is ${thresholdMb}).`;
363
+ if (pressureLevelHolds(signal, warnMinFreePercent)) {
364
+ return `Holding new lanes: ${reading}. Lanes wait while the level is critical, or warn with ${warnMinFreePercent}% free or less (throttle.memory_warn_min_free_percent; throttle.min_available_mem_mb is ${thresholdMb}).`;
348
365
  }
349
366
  return `Holding new lanes: ${reading}, below throttle.min_available_mem_mb ${thresholdMb}.`;
350
367
  }
@@ -379,6 +396,8 @@ export function evaluateThrottle(input) {
379
396
  const pause = input.throttle.cpu_busy_pause;
380
397
  const resume = input.throttle.cpu_busy_resume;
381
398
  const minMem = input.throttle.min_available_mem_mb;
399
+ const warnMinFree = input.throttle.memory_warn_min_free_percent ??
400
+ DEFAULT_MEMORY_WARN_MIN_FREE_PERCENT;
382
401
  // Each gate is opt-in: an unset threshold never blocks admission.
383
402
  let cpuBlocks = false;
384
403
  if (pause !== undefined && input.cpuCheckEnabled && input.busy !== null) {
@@ -389,8 +408,9 @@ export function evaluateThrottle(input) {
389
408
  cpuBlocks = input.busy >= pause;
390
409
  }
391
410
  }
392
- // On darwin the signal is memory_pressure's: a warn or critical level holds
393
- // even above the threshold, and a normal level holds only below it.
411
+ // On darwin the signal is memory_pressure's: a critical level holds even
412
+ // above the threshold, a warn level holds only at or below
413
+ // memory_warn_min_free_percent free, and otherwise only below the threshold.
394
414
  let memBlocks = false;
395
415
  let memorySignal;
396
416
  if (minMem !== undefined && input.availableMemMb !== null) {
@@ -398,8 +418,7 @@ export function evaluateThrottle(input) {
398
418
  ? input.memory
399
419
  : { source: "meminfo", available_mb: input.availableMemMb };
400
420
  memBlocks = input.availableMemMb < minMem ||
401
- memorySignal.pressure_level === "warn" ||
402
- memorySignal.pressure_level === "critical";
421
+ pressureLevelHolds(memorySignal, warnMinFree);
403
422
  }
404
423
  if (!cpuBlocks && !memBlocks) {
405
424
  return {
@@ -420,7 +439,7 @@ export function evaluateThrottle(input) {
420
439
  memory_hold: {
421
440
  ...memorySignal,
422
441
  threshold_mb: minMem,
423
- message: formatMemoryHoldMessage(memorySignal, minMem),
442
+ message: formatMemoryHoldMessage(memorySignal, minMem, warnMinFree),
424
443
  },
425
444
  };
426
445
  }
@@ -1226,6 +1226,11 @@ export interface ThrottleConfig {
1226
1226
  cpu_busy_pause?: number;
1227
1227
  cpu_busy_resume?: number;
1228
1228
  min_available_mem_mb?: number;
1229
+ /**
1230
+ * darwin only: a warn memory_pressure level holds admission only at or
1231
+ * below this free percentage (0-100). Unset means 20. Critical always holds.
1232
+ */
1233
+ memory_warn_min_free_percent?: number;
1229
1234
  }
1230
1235
  /** Session liveness windows from spec §9. Missing keys keep these documented defaults. */
1231
1236
  export interface LivenessWindows {
@@ -114,6 +114,31 @@ export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?
114
114
  sleep?: SleepFn;
115
115
  unloadDeadlineMs?: number;
116
116
  }): void;
117
+ export interface LaunchdBootstrapResult {
118
+ ok: boolean;
119
+ attempts: number;
120
+ error?: string;
121
+ }
122
+ /**
123
+ * Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
124
+ * backoff. A label that is already loaded counts as success. Unlike
125
+ * reloadLaunchd this never boots anything out, so it is safe to call on a
126
+ * running job.
127
+ */
128
+ export declare function bootstrapLaunchd(plistPath: string, run: RunCommand, uid?: number, opts?: {
129
+ sleep?: SleepFn;
130
+ }): LaunchdBootstrapResult;
131
+ export interface LaunchdJobInfo {
132
+ loaded: boolean;
133
+ /** launchd's `state = ...` line (running, not running, spawn scheduled, ...). */
134
+ state?: string;
135
+ pid?: number;
136
+ /** The loaded job's program arguments (`arguments = { ... }`), when printed. */
137
+ arguments?: string[];
138
+ }
139
+ /** What `launchctl print gui/<uid>/<label>` says about one job. */
140
+ export declare function launchdJobInfo(label: string, run: RunCommand, uid?: number): LaunchdJobInfo;
141
+ export declare function parseLaunchctlPrintJob(stdout: string): LaunchdJobInfo;
117
142
  export declare function deactivateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
118
143
  export declare function launchdIsLoaded(label: string, run: RunCommand, uid?: number): boolean;
119
144
  //# sourceMappingURL=launchd.d.ts.map
@@ -58,9 +58,11 @@ export function launchdPlistPath(home, spec) {
58
58
  return path.join(launchAgentsDir(home), `${spec.label}.plist`);
59
59
  }
60
60
  export function renderLaunchdPlist(spec, host) {
61
- const discovered = discoverLaunchdIdentity(host.home);
62
- const associatedBundleIdentifier = host.associatedBundleIdentifier ?? discovered.associatedBundleIdentifier;
63
- const launcherBinary = host.launchdLauncherBinary ?? discovered.launcherBinary;
61
+ const discovered = spec.independentOfApp ? {} : discoverLaunchdIdentity(host.home);
62
+ const associatedBundleIdentifier = spec.independentOfApp
63
+ ? undefined
64
+ : host.associatedBundleIdentifier ?? discovered.associatedBundleIdentifier;
65
+ const launcherBinary = spec.independentOfApp ? undefined : host.launchdLauncherBinary ?? discovered.launcherBinary;
64
66
  const args = launchdProgramArguments({ ...host, launchdLauncherBinary: launcherBinary }, spec.args)
65
67
  .map((a) => ` <string>${escapeXml(a)}</string>`)
66
68
  .join("\n");
@@ -303,6 +305,52 @@ export function activateLaunchd(plistPath, run, uid, opts) {
303
305
  }
304
306
  throw new Error(`${detail}. ${target} is NOT loaded and will not run until it is. Load it with: launchctl bootstrap ${domain} ${plistPath}`);
305
307
  }
308
+ /**
309
+ * Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
310
+ * backoff. A label that is already loaded counts as success. Unlike
311
+ * reloadLaunchd this never boots anything out, so it is safe to call on a
312
+ * running job.
313
+ */
314
+ export function bootstrapLaunchd(plistPath, run, uid, opts) {
315
+ const domain = guiDomain(uid);
316
+ const target = launchdServiceTarget(plistPath, uid);
317
+ const sleep = opts?.sleep ?? blockingSleep;
318
+ if (run("launchctl", ["print", target]).status === 0)
319
+ return { ok: true, attempts: 0 };
320
+ let last = run("launchctl", ["bootstrap", domain, plistPath]);
321
+ let attempts = 1;
322
+ for (const backoff of LAUNCHD_BOOTSTRAP_BACKOFF_MS) {
323
+ if (last.status === 0 || !isRetryableLaunchdBootstrap(last))
324
+ break;
325
+ sleep(backoff);
326
+ last = run("launchctl", ["bootstrap", domain, plistPath]);
327
+ attempts++;
328
+ }
329
+ // "already bootstrapped" races (another caller loaded it) are success.
330
+ if (last.status === 0 || run("launchctl", ["print", target]).status === 0)
331
+ return { ok: true, attempts };
332
+ return { ok: false, attempts, error: last.stderr.trim() || last.stdout.trim() || `exit ${last.status}` };
333
+ }
334
+ /** What `launchctl print gui/<uid>/<label>` says about one job. */
335
+ export function launchdJobInfo(label, run, uid) {
336
+ const r = run("launchctl", ["print", `${guiDomain(uid)}/${label}`]);
337
+ if (r.status !== 0)
338
+ return { loaded: false };
339
+ return parseLaunchctlPrintJob(r.stdout);
340
+ }
341
+ export function parseLaunchctlPrintJob(stdout) {
342
+ const state = /^\s*state\s*=\s*(.+?)\s*$/m.exec(stdout)?.[1];
343
+ const pidText = /^\s*pid\s*=\s*(\d+)\s*$/m.exec(stdout)?.[1];
344
+ const pid = pidText ? Number(pidText) : undefined;
345
+ const argsBlock = /^\s*arguments\s*=\s*\{\s*\n([\s\S]*?)\n\s*\}/m.exec(stdout)?.[1];
346
+ const args = argsBlock?.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
347
+ return {
348
+ loaded: true,
349
+ ...(state ? { state } : {}),
350
+ ...(pid && pid > 0 ? { pid } : {}),
351
+ ...(args && args.length > 0 ? { arguments: args } : {}),
352
+ };
353
+ }
306
354
  export function deactivateLaunchd(plistPath, run, uid) {
307
355
  run("launchctl", ["bootout", guiDomain(uid), plistPath]);
308
356
  }
@@ -1,5 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import { type RunCommand, type SleepFn } from "./launchd.js";
3
+ /**
4
+ * Agents that must load at login without the app, so they are never wrapped
5
+ * in the HQ.app launcher or attributed to its bundle. Kept in step with
6
+ * DAEMON_LAUNCHD_LABEL in daemon/install.ts.
7
+ */
8
+ export declare const APP_INDEPENDENT_LAUNCHD_LABELS: ReadonlySet<string>;
3
9
  export interface LaunchdMigrationOptions {
4
10
  home: string;
5
11
  run: RunCommand;
@@ -19,4 +25,12 @@ export interface LaunchdMigrationResult {
19
25
  }
20
26
  /** First-CLI-run cleanup for HQ's old Node-first LaunchAgents and backup plists. */
21
27
  export declare function migrateLegacyNodeLaunchAgents(options: LaunchdMigrationOptions): LaunchdMigrationResult;
28
+ /**
29
+ * The app-independent form of an HQ LaunchAgent plist written by an older
30
+ * install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
31
+ * dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
32
+ * removed. Everything else (environment, logs, KeepAlive) is kept as written.
33
+ * Returns null when the plist is already app-independent.
34
+ */
35
+ export declare function appIndependentLaunchdPlist(source: string): string | null;
22
36
  //# sourceMappingURL=migration.d.ts.map
@@ -5,6 +5,12 @@ import { isVersionedNodePath, preferStableNodeBinary } from "../node-binary.js";
5
5
  import { preferStableHqBinary } from "../mesh/live/daemon/install.js";
6
6
  import { launchdMigrationMarkerPath, shouldRunLaunchdMigration, writeLaunchdMigrationMarker } from "./migration-state.js";
7
7
  const HQ_LABEL_PREFIXES = ["ai.indigo.hq-", "ai.getindigo.hq-"];
8
+ /**
9
+ * Agents that must load at login without the app, so they are never wrapped
10
+ * in the HQ.app launcher or attributed to its bundle. Kept in step with
11
+ * DAEMON_LAUNCHD_LABEL in daemon/install.ts.
12
+ */
13
+ export const APP_INDEPENDENT_LAUNCHD_LABELS = new Set(["ai.getindigo.hq-daemon"]);
8
14
  function xml(value) {
9
15
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
10
16
  }
@@ -36,6 +42,8 @@ export function migrateLegacyNodeLaunchAgents(options) {
36
42
  }
37
43
  if (!name.endsWith(".plist") || !HQ_LABEL_PREFIXES.some((prefix) => name.startsWith(prefix)))
38
44
  continue;
45
+ if (APP_INDEPENDENT_LAUNCHD_LABELS.has(name.slice(0, -".plist".length)))
46
+ continue;
39
47
  const source = files.readFileSync(fullPath, "utf8");
40
48
  const args = launchdPlistProgramArguments(source);
41
49
  if (args.length < 2 || !isNodeProgram(args[0]))
@@ -82,4 +90,30 @@ export function migrateLegacyNodeLaunchAgents(options) {
82
90
  writeLaunchdMigrationMarker(options.home, deferred);
83
91
  return { migrated, backupsRemoved, deferred, alreadyDone: false };
84
92
  }
93
+ /**
94
+ * The app-independent form of an HQ LaunchAgent plist written by an older
95
+ * install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
96
+ * dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
97
+ * removed. Everything else (environment, logs, KeepAlive) is kept as written.
98
+ * Returns null when the plist is already app-independent.
99
+ */
100
+ export function appIndependentLaunchdPlist(source) {
101
+ const args = launchdPlistProgramArguments(source);
102
+ const launcherIndex = args.indexOf("--hq-launchd-task");
103
+ const wrapped = launcherIndex === 1 && args.length > 3;
104
+ const associated = /<key>AssociatedBundleIdentifiers<\/key>/.test(source);
105
+ if (!wrapped && !associated)
106
+ return null;
107
+ let updated = source;
108
+ if (wrapped) {
109
+ updated = updated.replace(/<key>ProgramArguments<\/key>\s*<array>[\s\S]*?<\/array>/, plistArgs(args.slice(2)).trimStart());
110
+ }
111
+ if (associated) {
112
+ updated = updated.replace(/[ \t]*<key>AssociatedBundleIdentifiers<\/key>\s*(?:<array>[\s\S]*?<\/array>|<string>[^<]*<\/string>)[ \t]*\n?/, "");
113
+ }
114
+ if (updated === source || /<key>AssociatedBundleIdentifiers<\/key>/.test(updated) || launchdPlistProgramArguments(updated).includes("--hq-launchd-task")) {
115
+ throw new Error("Could not safely rewrite the LaunchAgent to load without HQ.app");
116
+ }
117
+ return updated;
118
+ }
85
119
  //# sourceMappingURL=migration.js.map
@@ -16,4 +16,17 @@ export declare function renderSystemdUserUnit(spec: ServiceSpec, host: ServiceHo
16
16
  export declare function activateSystemd(unitName: string, run: RunCommand): void;
17
17
  export declare function deactivateSystemd(unitName: string, run: RunCommand): void;
18
18
  export declare function systemdIsActive(unitName: string, run: RunCommand): boolean;
19
+ export interface SystemdUnitInfo {
20
+ /** `systemctl is-enabled` printed enabled (or enabled-runtime / linked / alias). */
21
+ enabled: boolean;
22
+ /** `systemctl is-active` printed active (or activating / reloading). */
23
+ active: boolean;
24
+ /** The raw `is-enabled` / `is-active` answers, for status output. */
25
+ enabledState: string;
26
+ activeState: string;
27
+ /** MainPID when systemd reports one. */
28
+ pid?: number;
29
+ }
30
+ /** Ask systemd about one unit; `scope: "user"` adds `--user`. */
31
+ export declare function systemdUnitInfo(unitName: string, run: RunCommand, scope?: "user" | "system"): SystemdUnitInfo;
19
32
  //# sourceMappingURL=systemd.d.ts.map
@@ -69,4 +69,23 @@ export function systemdIsActive(unitName, run) {
69
69
  const r = run("systemctl", ["--user", "is-active", "--quiet", unitName]);
70
70
  return r.status === 0;
71
71
  }
72
+ /** Ask systemd about one unit; `scope: "user"` adds `--user`. */
73
+ export function systemdUnitInfo(unitName, run, scope = "user") {
74
+ const prefix = scope === "user" ? ["--user"] : [];
75
+ const word = (args) => {
76
+ const r = run("systemctl", [...prefix, ...args, unitName]);
77
+ return (r.stdout.trim().split("\n")[0] ?? "").trim() || "unknown";
78
+ };
79
+ const enabledState = word(["is-enabled"]);
80
+ const activeState = word(["is-active"]);
81
+ const show = run("systemctl", [...prefix, "show", "-p", "MainPID", "--value", unitName]);
82
+ const pid = Number(show.stdout.trim());
83
+ return {
84
+ enabled: /^(enabled|enabled-runtime|linked|linked-runtime|alias|static|indirect|generated)$/.test(enabledState),
85
+ active: /^(active|activating|reloading)$/.test(activeState),
86
+ enabledState,
87
+ activeState,
88
+ ...(show.status === 0 && Number.isInteger(pid) && pid > 0 ? { pid } : {}),
89
+ };
90
+ }
72
91
  //# sourceMappingURL=systemd.js.map
@@ -21,6 +21,14 @@ export interface ServiceSpec {
21
21
  restartSec: number;
22
22
  /** systemd unit file name; defaults to `hq-agent-<name>.service`. */
23
23
  unitName?: string;
24
+ /**
25
+ * macOS: run the job directly under node, without the HQ.app launcher and
26
+ * without AssociatedBundleIdentifiers, so launchd loads it at login whether
27
+ * or not the app is installed, enabled or allowed. The daemon sets this:
28
+ * on 2026-11-03 launchd skipped both app-associated agents at login and the
29
+ * daemon stayed down for 16 hours.
30
+ */
31
+ independentOfApp?: boolean;
24
32
  }
25
33
  export interface ServiceHostPaths {
26
34
  home: string;
@@ -1,6 +1,6 @@
1
1
  import { type StoredPriorSignal } from "../prior-signals.js";
2
2
  import { type BotRuntimeId } from "../../bot/config.js";
3
- import type { SignalsStep, StepContext, StepResult } from "./types.js";
3
+ import type { SignalsStep, SpawnSyncFn, StepContext, StepResult } from "./types.js";
4
4
  export declare const EXTRACT_SIGNAL_TYPES: readonly ["action_item", "commitment", "decision", "risk", "question", "key_point", "participant_contribution", "summary"];
5
5
  export declare const DEFAULT_WORKLIST_LIMIT = 20;
6
6
  export declare const DEFAULT_TURN_TIMEOUT_MS: number;
@@ -13,6 +13,20 @@ export declare const WORKLIST_ITEM_TIMEOUT_MS = 10000;
13
13
  export declare function worklistTimeoutMs(limit: number): number;
14
14
  export declare const CODEX_PERMISSION_PROFILE = "hq_signals_extract";
15
15
  export declare const CANDIDATE_WRAPPER_NAME = "write-candidate.sh";
16
+ /** The sandbox profile the step writes to <turn dir>/.grok/sandbox.toml for a grok turn. */
17
+ export declare const GROK_SANDBOX_PROFILE = "hq_signals_extract";
18
+ export declare const GROK_SANDBOX_FILE: string;
19
+ /** The oldest grok whose `--help` lists every flag in GROK_EXTRACT_FLAGS. */
20
+ export declare const GROK_MIN_VERSION = "1.0.70";
21
+ /** Flags a grok turn needs; one missing from `--help` means the installed grok cannot be locked down. */
22
+ export declare const GROK_EXTRACT_FLAGS: readonly ["--permission-mode", "--tools", "--disallowed-tools", "--allow", "--deny", "--sandbox", "--disable-web-search", "--no-subagents", "--max-turns", "--cwd", "--output-format"];
23
+ /**
24
+ * grok's built-in read-only shell list (its permissions guide, "Read-Only
25
+ * Shell Commands"): auto-approved even under dontAsk, so each is denied by
26
+ * name. A Bash rule without `*` is a prefix match, so `Bash(cat)` also covers
27
+ * `cat` with any arguments.
28
+ */
29
+ export declare const GROK_READ_ONLY_COMMANDS: readonly ["ls", "cat", "pwd", "date", "whoami", "hostname", "uptime", "ps", "head", "tail", "wc", "sort", "uniq", "tr", "cut", "mkdir", "touch", "git", "grep", "rg", "kubectl"];
16
30
  /** The only tools a claude turn may use: Read inside the channel folder and the pinned wrapper. */
17
31
  export declare function claudeAllowedTools(channelDir: string, wrapperPath: string): string[];
18
32
  export interface PriorSignal {
@@ -36,10 +50,13 @@ export interface ExtractStepOptions {
36
50
  resolveBotRuntime?: () => BotRuntimeId | null;
37
51
  claudeBin?: string;
38
52
  codexBin?: string;
53
+ grokBin?: string;
39
54
  worklistLimit?: number;
40
55
  turnTimeoutMs?: number;
41
56
  /** Run channels whose source.yaml says run: cloud too (passes --force). */
42
57
  force?: boolean;
58
+ /** OS used to decide whether grok's child-process network block exists. Default: this process. */
59
+ platform?: NodeJS.Platform;
43
60
  }
44
61
  interface SourceChannel {
45
62
  channel: string;
@@ -77,8 +94,11 @@ export declare function buildExtractPrompt(input: {
77
94
  item: WorkItem;
78
95
  itemPath: string;
79
96
  wrapperPath: string;
80
- /** codex has no Read tool; it reads with shell commands inside its sandbox. */
81
- runtime?: "claude" | "codex";
97
+ /**
98
+ * codex has no Read tool; it reads with shell commands inside its sandbox.
99
+ * grok reads with read_file and returns candidate commands as JSON data.
100
+ */
101
+ runtime?: "claude" | "codex" | "grok";
82
102
  }): string;
83
103
  /**
84
104
  * The per-item candidate command. It pins --company, --audience and
@@ -91,7 +111,16 @@ export declare function buildCandidateWrapper(input: {
91
111
  audience: string;
92
112
  sourceRef: string;
93
113
  }): string;
94
- /** Parse a stream-json transcript (or a codex --json event stream). */
114
+ /**
115
+ * Parse a stream-json transcript (claude), a codex --json event stream, or a
116
+ * grok streaming-json event stream.
117
+ *
118
+ * grok writes `{"type":"text","data":…}` deltas and one terminal
119
+ * `{"type":"end","stopReason":…,"num_turns":n,"total_cost_usd":x}`; a refused
120
+ * tool call ends the turn with stopReason "cancelled", and an API failure is
121
+ * `{"type":"error","message":…}`. Both are mapped to an error result so the
122
+ * item fails and is retried; only "end_turn" is success.
123
+ */
95
124
  export declare function parseTurnOutput(stdout: string): {
96
125
  result: Record<string, unknown> | null;
97
126
  turns: number;
@@ -134,9 +163,86 @@ export interface TurnScope {
134
163
  wrapperPath: string;
135
164
  /** companies/{co}/{signals,ontology}/_candidates, realpath. */
136
165
  candidateDirs: string[];
166
+ /**
167
+ * Text of the pinned writer, when this turn generated one. A grok turn
168
+ * compares the file to this immediately before executing it.
169
+ */
170
+ wrapperBody?: string;
137
171
  }
138
172
  /** TOML inline table for the codex permission profile's filesystem entries. */
139
173
  export declare function codexFilesystemProfile(scope: TurnScope, codexBin: string, envPath: string | undefined): string;
174
+ /**
175
+ * The sandbox profile for a grok turn, written to <turn dir>/.grok/sandbox.toml.
176
+ * grok's `strict` already covers the turn dir (cwd), system paths and ~/.grok;
177
+ * the grants add bash's install prefix (Homebrew bash loads from it) and the
178
+ * read-only HQ paths of hqTurnGrants. Candidate folders are not granted:
179
+ * the host writes them after the turn, outside this sandbox.
180
+ */
181
+ export declare function grokSandboxProfile(scope: TurnScope, envPath: string | undefined): string;
182
+ /** Grok can read the source; candidate writing belongs to the host. */
183
+ export declare function grokAllowRules(channelDir: string): string[];
184
+ /** Parse literal arguments only. No shell ever interprets model output. */
185
+ export declare function grokCandidateArgv(command: string, wrapperPath: string): string[] | null;
186
+ /**
187
+ * Validate the whole batch before writing. Only the host executes the pinned
188
+ * writer. When `expectedWrapper` is set, the file is compared to that text
189
+ * immediately before each exec. Strict can write the turn directory, so a
190
+ * writer that no longer matches is not run.
191
+ */
192
+ export declare function executeGrokCandidates(text: string, wrapperPath: string, spawn: SpawnSyncFn, opts: Parameters<SpawnSyncFn>[2], expectedWrapper?: string): string | undefined;
193
+ /**
194
+ * Read denies for the grok login. Grok matches a `~` tool path literally and
195
+ * expands `~` only after that check, so the absolute temp home is not enough.
196
+ * `realHomes` is every home the original login might live under (the process
197
+ * HOME we copied from, and os.homedir() when that differs).
198
+ */
199
+ export declare function grokAuthReadDenies(tempHome: string, realHomes: readonly string[]): string[];
200
+ /** Deny rules of a grok turn: every other tool class, the login file, and the read-only shell list. */
201
+ export declare function grokDenyRules(tempHome: string, realHomes: readonly string[]): string[];
202
+ export declare function grokTurnArgs(prompt: string, scope: TurnScope, maxTurns: number, tempHome: string, realHomes: readonly string[]): string[];
203
+ /** Parent env keys copied into a grok turn. Everything else, including credentials, stays out. */
204
+ export declare const GROK_ENV_ALLOW: readonly ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "LC_CTYPE", "USER", "LOGNAME", "SHELL", "SystemRoot", "COMSPEC", "PATHEXT", "WINDIR", "TERM", "TZ", "HQ_ROOT"];
205
+ /** The turn's environment for grok: an allowlist, no sandbox or config overrides, no Claude or Cursor configuration scans. */
206
+ export declare function grokTurnEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
207
+ export type GrokExtractSupport = {
208
+ ok: true;
209
+ } | {
210
+ ok: false;
211
+ missing: string[];
212
+ };
213
+ /** Whether `grok --help` lists every flag a locked-down turn needs, and streaming-json output. */
214
+ export declare function grokExtractSupportFromHelp(helpText: string): GrokExtractSupport;
215
+ export declare function resetGrokExtractProbeCache(): void;
216
+ /** Probe `grok --help` once per grok binary path (empty or failed output counts as every flag missing). */
217
+ export declare function probeGrokExtractSupport(grokBin: string, readHelp: () => string): GrokExtractSupport;
218
+ export type GrokAvailability = {
219
+ installed: false;
220
+ } | {
221
+ installed: true;
222
+ support: GrokExtractSupport;
223
+ };
224
+ export interface ToolAvailability {
225
+ installed: boolean;
226
+ /** The tool's own answer; null when it did not say (then it is not used as a fallback). */
227
+ signedIn: boolean | null;
228
+ }
229
+ export type ExtractRuntimeChoice = {
230
+ runtime: BotRuntimeId;
231
+ fallback?: string;
232
+ } | {
233
+ error: string;
234
+ };
235
+ /**
236
+ * Which runtime runs the turns. The main bot's runtime, except that a grok
237
+ * that is missing or cannot be locked down is never run: the step uses
238
+ * claude, then codex, when that tool is installed and signed in, and
239
+ * otherwise fails. The claude and codex checks run only when needed.
240
+ */
241
+ export declare function chooseExtractRuntime(main: BotRuntimeId, avail: {
242
+ grok: () => GrokAvailability;
243
+ claude: () => ToolAvailability;
244
+ codex: () => ToolAvailability;
245
+ }, platform?: NodeJS.Platform): ExtractRuntimeChoice;
140
246
  export declare function claudeTurnArgs(prompt: string, scope: TurnScope, maxTurns: number, budgetUsd: number): string[];
141
247
  export declare function codexTurnArgs(prompt: string, scope: TurnScope, codexBin: string, envPath: string | undefined): string[];
142
248
  export declare function extractPartialDocument(ctx: StepContext, doc: PartialDocument, prior: readonly PriorSignal[]): Promise<StepResult>;