@indigoai-us/hq-cli 5.345.146 → 5.345.148

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 (138) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +14 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +326 -4
  5. package/dist/command-catalog.generated.js +403 -4
  6. package/dist/commands/agent-inbox.js +2 -1
  7. package/dist/commands/auth.js +4 -2
  8. package/dist/commands/bot.js +1 -1
  9. package/dist/commands/cloud.js +31 -1
  10. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  11. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  12. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  13. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  14. package/dist/commands/core.js +6 -0
  15. package/dist/commands/daemon.d.ts +7 -0
  16. package/dist/commands/daemon.js +37 -3
  17. package/dist/commands/dm.js +9 -2
  18. package/dist/commands/doctor.d.ts +11 -1
  19. package/dist/commands/doctor.js +56 -1
  20. package/dist/commands/files.js +10 -6
  21. package/dist/commands/install-global.d.ts +7 -0
  22. package/dist/commands/install-global.js +129 -35
  23. package/dist/commands/lanes-shared.d.ts +3 -1
  24. package/dist/commands/lanes.d.ts +4 -4
  25. package/dist/commands/lanes.js +380 -25
  26. package/dist/commands/login.js +4 -3
  27. package/dist/commands/pack-install.d.ts +2 -2
  28. package/dist/commands/pack-install.js +133 -35
  29. package/dist/commands/sync-manifest.d.ts +2 -9
  30. package/dist/commands/sync-manifest.js +4 -15
  31. package/dist/hook-engine-path.d.ts +4 -0
  32. package/dist/hook-engine-path.js +36 -0
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.js +6 -0
  36. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  37. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  38. package/dist/lib/agent-kit/inbox-state.js +11 -2
  39. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  40. package/dist/lib/agent-kit/run/inbox.js +36 -7
  41. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  42. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  43. package/dist/lib/bot/daemon.js +3 -0
  44. package/dist/lib/bot/runtime/index.js +3 -1
  45. package/dist/lib/company-skill-wrapper.js +6 -4
  46. package/dist/lib/daemon/install.d.ts +47 -1
  47. package/dist/lib/daemon/install.js +181 -15
  48. package/dist/lib/daemon/lock.d.ts +32 -0
  49. package/dist/lib/daemon/lock.js +55 -2
  50. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  51. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  52. package/dist/lib/doctor/checks/anywhere.js +23 -8
  53. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  54. package/dist/lib/doctor/checks/cli-path.js +45 -0
  55. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  56. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  57. package/dist/lib/doctor/checks/release-links.d.ts +23 -0
  58. package/dist/lib/doctor/checks/release-links.js +152 -0
  59. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  60. package/dist/lib/doctor/checks/sync-health.js +38 -2
  61. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  62. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  63. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  64. package/dist/lib/doctor/fix/apply.js +176 -10
  65. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  66. package/dist/lib/doctor/fix/json-output.js +6 -0
  67. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  68. package/dist/lib/doctor/fix/remediation.js +23 -1
  69. package/dist/lib/doctor/registry.js +13 -0
  70. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  71. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/flag-registry.js +9 -1
  75. package/dist/lib/flag-snapshot-file.js +8 -3
  76. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  77. package/dist/lib/hook-engine/resolve.js +134 -0
  78. package/dist/lib/lanes/admission-host.d.ts +1 -0
  79. package/dist/lib/lanes/admission-host.js +8 -1
  80. package/dist/lib/lanes/admission.d.ts +4 -0
  81. package/dist/lib/lanes/admission.js +6 -2
  82. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  83. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  84. package/dist/lib/lanes/index.d.ts +2 -2
  85. package/dist/lib/lanes/index.js +1 -1
  86. package/dist/lib/lanes/keep-alive.js +4 -1
  87. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  88. package/dist/lib/lanes/local-host-identity.js +314 -0
  89. package/dist/lib/lanes/reaper.d.ts +1 -1
  90. package/dist/lib/lanes/reaper.js +7 -0
  91. package/dist/lib/lanes/reconcile.d.ts +4 -0
  92. package/dist/lib/lanes/reconcile.js +16 -8
  93. package/dist/lib/lanes/spawn.d.ts +20 -0
  94. package/dist/lib/lanes/spawn.js +48 -2
  95. package/dist/lib/lanes/status-fields.js +6 -1
  96. package/dist/lib/lanes/store.js +8 -1
  97. package/dist/lib/lanes/throttle.d.ts +49 -6
  98. package/dist/lib/lanes/throttle.js +154 -17
  99. package/dist/lib/lanes/types.d.ts +22 -1
  100. package/dist/lib/lanes/types.js +27 -3
  101. package/dist/lib/lanes/watcher.js +27 -2
  102. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  103. package/dist/lib/lanes/worker-continuity.js +18 -11
  104. package/dist/lib/manifest-disabled.d.ts +13 -0
  105. package/dist/lib/manifest-disabled.js +19 -0
  106. package/dist/lib/service-manager/launchd.d.ts +1 -0
  107. package/dist/lib/service-manager/launchd.js +1 -1
  108. package/dist/lib/windows/user-path.d.ts +48 -0
  109. package/dist/lib/windows/user-path.js +114 -0
  110. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  111. package/dist/lib/work-context/auto-bind.js +22 -3
  112. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  113. package/dist/lib/work-context/lane-bind.js +101 -0
  114. package/dist/lib/work-context/organize.js +1 -0
  115. package/dist/lib/work-context/prd-sync.js +7 -0
  116. package/dist/lib/work-context/project.d.ts +36 -10
  117. package/dist/lib/work-context/project.js +130 -41
  118. package/dist/lib/work-context/reconcile.d.ts +6 -0
  119. package/dist/lib/work-context/reconcile.js +25 -5
  120. package/dist/lib/work-context/state.d.ts +6 -0
  121. package/dist/lib/work-context/state.js +29 -1
  122. package/dist/lib/workers-registry/index.d.ts +6 -0
  123. package/dist/lib/workers-registry/index.js +8 -0
  124. package/dist/utils/copy-tree.d.ts +61 -0
  125. package/dist/utils/copy-tree.js +249 -0
  126. package/dist/utils/environmental-error.js +7 -3
  127. package/dist/utils/headless.d.ts +2 -0
  128. package/dist/utils/headless.js +8 -0
  129. package/dist/utils/login-browser.d.ts +4 -0
  130. package/dist/utils/login-browser.js +122 -0
  131. package/dist/utils/npm-invocation.d.ts +27 -0
  132. package/dist/utils/npm-invocation.js +54 -0
  133. package/dist/utils/safe-link.d.ts +50 -0
  134. package/dist/utils/safe-link.js +300 -0
  135. package/dist/utils/team-upgrade.js +1 -7
  136. package/package.json +17 -5
  137. package/scripts/hook-engine-manifest.mjs +22 -0
  138. package/scripts/install-hook-engine.mjs +10 -0
@@ -17,6 +17,14 @@ export declare const STARVATION_MAX_DEFERRALS = 10;
17
17
  export declare const STARVATION_MAX_MS: number;
18
18
  export declare const PROC_MEMINFO = "/proc/meminfo";
19
19
  export declare const VM_STAT_BIN = "/usr/bin/vm_stat";
20
+ export declare const SYSCTL_BIN = "/usr/sbin/sysctl";
21
+ /**
22
+ * The sysctls behind macOS `memory_pressure`: its "System-wide memory free
23
+ * percentage" is kern.memorystatus_level, and its pressure level is
24
+ * kern.memorystatus_vm_pressure_level (1 normal, 2 warn, 4 critical).
25
+ * hw.memsize turns the percentage into MiB for min_available_mem_mb.
26
+ */
27
+ export declare const MEMORY_PRESSURE_SYSCTLS: readonly ["kern.memorystatus_level", "kern.memorystatus_vm_pressure_level", "hw.memsize"];
20
28
  export declare const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
21
29
  export declare const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
22
30
  export interface CpuTimes {
@@ -38,7 +46,8 @@ export interface ThrottleDeps {
38
46
  freemem: () => number;
39
47
  platform: () => NodeJS.Platform;
40
48
  /**
41
- * Darwin only: read vm_stat when /proc/meminfo is absent. Unused on Linux.
49
+ * Darwin only: read the memory_pressure sysctls (vm_stat as a fallback) when
50
+ * /proc/meminfo is absent. Unused on Linux.
42
51
  */
43
52
  execFileSync: (file: string, args: readonly string[], options: {
44
53
  encoding: "utf8";
@@ -82,16 +91,45 @@ export declare function recordCpuBusyObservation(state: ThrottleState, observati
82
91
  export declare function parseMemAvailableKb(contents: string): number | null;
83
92
  /**
84
93
  * Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
85
- * pages. That is the reclaimable set os.freemem() omits on macOS.
94
+ * pages. Only a fallback when the memory_pressure sysctls cannot be read: it
95
+ * leaves out purgeable and compressible memory, so it reads lower than
96
+ * `memory_pressure` on a Mac that is not under pressure.
86
97
  */
87
98
  export declare function parseVmStatAvailableMb(contents: string): number | null;
99
+ export type MemoryPressureLevel = "normal" | "warn" | "critical";
100
+ export type MemorySignalSource = "memory_pressure" | "vm_stat" | "meminfo" | "freemem";
101
+ /** One memory reading and where it came from. */
102
+ export interface MemorySignal {
103
+ source: MemorySignalSource;
104
+ available_mb: number;
105
+ /** memory_pressure only: "System-wide memory free percentage". */
106
+ free_percent?: number;
107
+ /** memory_pressure only: kern.memorystatus_vm_pressure_level. */
108
+ pressure_level?: MemoryPressureLevel;
109
+ }
110
+ /** The memory gate's reason for holding admission, shown to the user. */
111
+ export interface MemoryHold extends MemorySignal {
112
+ threshold_mb: number;
113
+ message: string;
114
+ }
115
+ /**
116
+ * Parse `sysctl -n kern.memorystatus_level kern.memorystatus_vm_pressure_level
117
+ * hw.memsize`: one integer per line, in that order.
118
+ */
119
+ export declare function parseMemoryPressureSysctl(contents: string): MemorySignal | null;
88
120
  /**
89
- * Available memory in MiB. Prefers kernel MemAvailable. On darwin without
90
- * /proc/meminfo, uses vm_stat (free+inactive+speculative) and never os.freemem().
91
- * Elsewhere, falls back to os.freemem() only where MemAvailable is absent.
92
- * Returning null skips the memory gate instead of holding every lane.
121
+ * Current memory reading for the admission gate. Prefers kernel MemAvailable.
122
+ * On darwin without /proc/meminfo, reads the same values `memory_pressure`
123
+ * prints (free percentage and pressure level), falls back to vm_stat, and never
124
+ * uses os.freemem(). Elsewhere, falls back to os.freemem() only where
125
+ * MemAvailable is absent. Returning null skips the memory gate instead of
126
+ * holding every lane.
93
127
  */
128
+ export declare function readMemorySignal(deps?: ThrottleDeps): MemorySignal | null;
129
+ /** Available memory in MiB from readMemorySignal; null skips the memory gate. */
94
130
  export declare function readAvailableMemMb(deps?: ThrottleDeps): number | null;
131
+ /** Plain explanation of a memory hold: which signal, its value, and the threshold. */
132
+ export declare function formatMemoryHoldMessage(signal: MemorySignal, thresholdMb: number): string;
95
133
  export interface ThrottleState {
96
134
  consecutiveDeferrals: number;
97
135
  lastAdmissionAt: string | null;
@@ -100,6 +138,8 @@ export interface ThrottleState {
100
138
  last_busy?: number | null;
101
139
  busy_source?: CpuBusySource;
102
140
  sampled_at?: string;
141
+ /** Set while the memory gate holds admission; cleared on the next admit or CPU hold. */
142
+ memory_hold?: MemoryHold;
103
143
  }
104
144
  export type ThrottleAdmitReason = "ok" | "starvation_bound" | "unreadable" | "cpu_check_disabled";
105
145
  export type ThrottleDeferReason = "cpu" | "memory";
@@ -111,12 +151,15 @@ export type ThrottleDecision = {
111
151
  reason: ThrottleDeferReason;
112
152
  busy?: number;
113
153
  availableMemMb?: number;
154
+ memory_hold?: MemoryHold;
114
155
  };
115
156
  export declare function emptyThrottleState(): ThrottleState;
116
157
  export declare function starvationBoundReached(state: ThrottleState, nowMs: number): boolean;
117
158
  export declare function evaluateThrottle(input: {
118
159
  busy: number | null;
119
160
  availableMemMb: number | null;
161
+ /** Where availableMemMb came from. Without it the reading is reported as MemAvailable. */
162
+ memory?: MemorySignal | null;
120
163
  cpuCheckEnabled: boolean;
121
164
  state: ThrottleState | "unreadable";
122
165
  throttle: ThrottleConfig;
@@ -22,6 +22,18 @@ export const STARVATION_MAX_MS = 30 * 60 * 1000;
22
22
  export const PROC_MEMINFO = "/proc/meminfo";
23
23
  export const VM_STAT_BIN = "/usr/bin/vm_stat";
24
24
  const VM_STAT_TIMEOUT_MS = 2_000;
25
+ export const SYSCTL_BIN = "/usr/sbin/sysctl";
26
+ /**
27
+ * The sysctls behind macOS `memory_pressure`: its "System-wide memory free
28
+ * percentage" is kern.memorystatus_level, and its pressure level is
29
+ * kern.memorystatus_vm_pressure_level (1 normal, 2 warn, 4 critical).
30
+ * hw.memsize turns the percentage into MiB for min_available_mem_mb.
31
+ */
32
+ export const MEMORY_PRESSURE_SYSCTLS = [
33
+ "kern.memorystatus_level",
34
+ "kern.memorystatus_vm_pressure_level",
35
+ "hw.memsize",
36
+ ];
25
37
  export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
26
38
  export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
27
39
  function defaultSleep(ms) {
@@ -200,7 +212,9 @@ function parseVmStatPages(contents, label) {
200
212
  }
201
213
  /**
202
214
  * Available memory in MiB from darwin `vm_stat`: free + inactive + speculative
203
- * pages. That is the reclaimable set os.freemem() omits on macOS.
215
+ * pages. Only a fallback when the memory_pressure sysctls cannot be read: it
216
+ * leaves out purgeable and compressible memory, so it reads lower than
217
+ * `memory_pressure` on a Mac that is not under pressure.
204
218
  */
205
219
  export function parseVmStatAvailableMb(contents) {
206
220
  const pageSizeMatch = /page size of (\d+) bytes/i.exec(contents);
@@ -214,7 +228,53 @@ export function parseVmStatAvailableMb(contents) {
214
228
  return null;
215
229
  return ((free + inactive + speculative) * pageSize) / (1024 * 1024);
216
230
  }
217
- function readDarwinAvailableMemMb(deps) {
231
+ const PRESSURE_LEVELS = {
232
+ 1: "normal",
233
+ 2: "warn",
234
+ 4: "critical",
235
+ };
236
+ /**
237
+ * Parse `sysctl -n kern.memorystatus_level kern.memorystatus_vm_pressure_level
238
+ * hw.memsize`: one integer per line, in that order.
239
+ */
240
+ export function parseMemoryPressureSysctl(contents) {
241
+ const lines = contents.split("\n").map((line) => line.trim()).filter((line) => line !== "");
242
+ if (lines.length !== MEMORY_PRESSURE_SYSCTLS.length)
243
+ return null;
244
+ if (!lines.every((line) => /^\d+$/.test(line)))
245
+ return null;
246
+ const [freePercent, level, memsize] = lines.map(Number);
247
+ const pressureLevel = PRESSURE_LEVELS[level];
248
+ if (!Number.isSafeInteger(freePercent) || freePercent < 0 || freePercent > 100 ||
249
+ pressureLevel === undefined ||
250
+ !Number.isSafeInteger(memsize) || memsize <= 0) {
251
+ return null;
252
+ }
253
+ return {
254
+ source: "memory_pressure",
255
+ available_mb: (memsize * (freePercent / 100)) / (1024 * 1024),
256
+ free_percent: freePercent,
257
+ pressure_level: pressureLevel,
258
+ };
259
+ }
260
+ function readDarwinMemoryPressure(deps) {
261
+ try {
262
+ const contents = deps.execFileSync(SYSCTL_BIN, ["-n", ...MEMORY_PRESSURE_SYSCTLS], {
263
+ encoding: "utf8",
264
+ timeout: VM_STAT_TIMEOUT_MS,
265
+ });
266
+ const signal = parseMemoryPressureSysctl(contents);
267
+ if (signal !== null)
268
+ return signal;
269
+ process.stderr.write("lanes throttle: darwin memory_pressure sysctls were unreadable; falling back to vm_stat\n");
270
+ return null;
271
+ }
272
+ catch (err) {
273
+ process.stderr.write(`lanes throttle: darwin memory_pressure sysctls failed: ${err instanceof Error ? err.message : String(err)}; falling back to vm_stat\n`);
274
+ return null;
275
+ }
276
+ }
277
+ function readDarwinVmStat(deps) {
218
278
  try {
219
279
  const contents = deps.execFileSync(VM_STAT_BIN, [], {
220
280
  encoding: "utf8",
@@ -222,7 +282,7 @@ function readDarwinAvailableMemMb(deps) {
222
282
  });
223
283
  const mb = parseVmStatAvailableMb(contents);
224
284
  if (mb !== null)
225
- return mb;
285
+ return { source: "vm_stat", available_mb: mb };
226
286
  process.stderr.write("lanes throttle: darwin vm_stat output was unreadable; skipping the memory gate\n");
227
287
  return null;
228
288
  }
@@ -236,7 +296,7 @@ function readFreememMb(deps) {
236
296
  const bytes = deps.freemem();
237
297
  if (!Number.isFinite(bytes) || bytes < 0)
238
298
  return null;
239
- return bytes / (1024 * 1024);
299
+ return { source: "freemem", available_mb: bytes / (1024 * 1024) };
240
300
  }
241
301
  catch (err) {
242
302
  process.stderr.write(`lanes throttle: freemem fallback failed: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -244,17 +304,19 @@ function readFreememMb(deps) {
244
304
  }
245
305
  }
246
306
  /**
247
- * Available memory in MiB. Prefers kernel MemAvailable. On darwin without
248
- * /proc/meminfo, uses vm_stat (free+inactive+speculative) and never os.freemem().
249
- * Elsewhere, falls back to os.freemem() only where MemAvailable is absent.
250
- * Returning null skips the memory gate instead of holding every lane.
307
+ * Current memory reading for the admission gate. Prefers kernel MemAvailable.
308
+ * On darwin without /proc/meminfo, reads the same values `memory_pressure`
309
+ * prints (free percentage and pressure level), falls back to vm_stat, and never
310
+ * uses os.freemem(). Elsewhere, falls back to os.freemem() only where
311
+ * MemAvailable is absent. Returning null skips the memory gate instead of
312
+ * holding every lane.
251
313
  */
252
- export function readAvailableMemMb(deps = defaultThrottleDeps()) {
314
+ export function readMemorySignal(deps = defaultThrottleDeps()) {
253
315
  try {
254
316
  const contents = deps.readFileSync(PROC_MEMINFO, "utf8");
255
317
  const kb = parseMemAvailableKb(contents);
256
318
  if (kb !== null)
257
- return kb / 1024;
319
+ return { source: "meminfo", available_mb: kb / 1024 };
258
320
  // /proc/meminfo exists but has no MemAvailable (unusual). Fall back.
259
321
  process.stderr.write("lanes throttle: MemAvailable missing from /proc/meminfo; falling back to os.freemem()\n");
260
322
  }
@@ -264,11 +326,35 @@ export function readAvailableMemMb(deps = defaultThrottleDeps()) {
264
326
  process.stderr.write(`lanes throttle: failed to read ${PROC_MEMINFO}: ${err instanceof Error ? err.message : String(err)}\n`);
265
327
  return null;
266
328
  }
267
- if (deps.platform() === "darwin")
268
- return readDarwinAvailableMemMb(deps);
329
+ if (deps.platform() === "darwin") {
330
+ return readDarwinMemoryPressure(deps) ?? readDarwinVmStat(deps);
331
+ }
269
332
  }
270
333
  return readFreememMb(deps);
271
334
  }
335
+ /** Available memory in MiB from readMemorySignal; null skips the memory gate. */
336
+ export function readAvailableMemMb(deps = defaultThrottleDeps()) {
337
+ return readMemorySignal(deps)?.available_mb ?? null;
338
+ }
339
+ function formatMb(mb) {
340
+ return `${Math.round(mb)} MB`;
341
+ }
342
+ /** Plain explanation of a memory hold: which signal, its value, and the threshold. */
343
+ export function formatMemoryHoldMessage(signal, thresholdMb) {
344
+ if (signal.source === "memory_pressure") {
345
+ 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}).`;
348
+ }
349
+ return `Holding new lanes: ${reading}, below throttle.min_available_mem_mb ${thresholdMb}.`;
350
+ }
351
+ const label = signal.source === "vm_stat"
352
+ ? "vm_stat free+inactive+speculative"
353
+ : signal.source === "meminfo"
354
+ ? "/proc/meminfo MemAvailable"
355
+ : "os.freemem()";
356
+ return `Holding new lanes: ${label} is ${formatMb(signal.available_mb)}, below throttle.min_available_mem_mb ${thresholdMb}.`;
357
+ }
272
358
  export function emptyThrottleState() {
273
359
  return {
274
360
  consecutiveDeferrals: 0,
@@ -303,9 +389,17 @@ export function evaluateThrottle(input) {
303
389
  cpuBlocks = input.busy >= pause;
304
390
  }
305
391
  }
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.
306
394
  let memBlocks = false;
395
+ let memorySignal;
307
396
  if (minMem !== undefined && input.availableMemMb !== null) {
308
- memBlocks = input.availableMemMb < minMem;
397
+ memorySignal = input.memory && input.memory.available_mb === input.availableMemMb
398
+ ? input.memory
399
+ : { source: "meminfo", available_mb: input.availableMemMb };
400
+ memBlocks = input.availableMemMb < minMem ||
401
+ memorySignal.pressure_level === "warn" ||
402
+ memorySignal.pressure_level === "critical";
309
403
  }
310
404
  if (!cpuBlocks && !memBlocks) {
311
405
  return {
@@ -323,27 +417,67 @@ export function evaluateThrottle(input) {
323
417
  admit: false,
324
418
  reason: "memory",
325
419
  availableMemMb: input.availableMemMb ?? undefined,
420
+ memory_hold: {
421
+ ...memorySignal,
422
+ threshold_mb: minMem,
423
+ message: formatMemoryHoldMessage(memorySignal, minMem),
424
+ },
326
425
  };
327
426
  }
328
427
  export function applyThrottleDecision(state, decision, nowMs) {
428
+ const { memory_hold: _previousHold, ...rest } = state;
329
429
  if (decision.admit) {
330
430
  return {
331
- ...state,
431
+ ...rest,
332
432
  consecutiveDeferrals: 0,
333
433
  lastAdmissionAt: new Date(nowMs).toISOString(),
334
434
  cpuPaused: false,
335
435
  };
336
436
  }
337
437
  return {
338
- ...state,
438
+ ...rest,
339
439
  consecutiveDeferrals: state.consecutiveDeferrals + 1,
340
440
  lastAdmissionAt: state.lastAdmissionAt,
341
441
  cpuPaused: decision.reason === "cpu" ? true : state.cpuPaused,
442
+ ...(decision.memory_hold !== undefined ? { memory_hold: decision.memory_hold } : {}),
342
443
  };
343
444
  }
344
445
  export function throttleStatePath(hqRoot) {
345
446
  return path.join(hqRoot, "workspace", "lanes-runs", "throttle-state.json");
346
447
  }
448
+ const MEMORY_SIGNAL_SOURCES = [
449
+ "memory_pressure",
450
+ "vm_stat",
451
+ "meminfo",
452
+ "freemem",
453
+ ];
454
+ /** A stored memory_hold is informational; a malformed one is dropped, not fatal. */
455
+ function parseMemoryHold(value) {
456
+ if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
457
+ return undefined;
458
+ }
459
+ const hold = value;
460
+ const finiteNonNegative = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
461
+ if (!MEMORY_SIGNAL_SOURCES.includes(hold.source) ||
462
+ !finiteNonNegative(hold.available_mb) ||
463
+ !finiteNonNegative(hold.threshold_mb) ||
464
+ typeof hold.message !== "string" || hold.message.trim() === "" ||
465
+ (hold.free_percent !== undefined && !finiteNonNegative(hold.free_percent)) ||
466
+ (hold.pressure_level !== undefined &&
467
+ !["normal", "warn", "critical"].includes(String(hold.pressure_level)))) {
468
+ return undefined;
469
+ }
470
+ return {
471
+ source: hold.source,
472
+ available_mb: hold.available_mb,
473
+ ...(hold.free_percent !== undefined ? { free_percent: hold.free_percent } : {}),
474
+ ...(hold.pressure_level !== undefined
475
+ ? { pressure_level: hold.pressure_level }
476
+ : {}),
477
+ threshold_mb: hold.threshold_mb,
478
+ message: hold.message,
479
+ };
480
+ }
347
481
  export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
348
482
  if (!deps.existsSync(filePath))
349
483
  return "missing";
@@ -410,10 +544,12 @@ export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
410
544
  process.stderr.write(`lanes throttle: state ${filePath} has invalid sampled_at\n`);
411
545
  return "unreadable";
412
546
  }
547
+ const memoryHold = parseMemoryHold(obj.memory_hold);
413
548
  return {
414
549
  consecutiveDeferrals: obj.consecutiveDeferrals,
415
550
  lastAdmissionAt: obj.lastAdmissionAt,
416
551
  cpuPaused: obj.cpuPaused,
552
+ ...(memoryHold !== undefined ? { memory_hold: memoryHold } : {}),
417
553
  ...(cpuSnapshot !== undefined ? { cpu_snapshot: cpuSnapshot } : {}),
418
554
  ...(obj.last_busy !== undefined ? { last_busy: obj.last_busy } : {}),
419
555
  ...(obj.busy_source !== undefined ? { busy_source: obj.busy_source } : {}),
@@ -465,11 +601,12 @@ export async function decideAdmission(hqRoot, throttle = {}, deps = defaultThrot
465
601
  const busy = cpuBusyFraction === undefined
466
602
  ? observation?.busy ?? null
467
603
  : cpuBusyFraction;
468
- const availableMemMb = readAvailableMemMb(deps);
604
+ const memory = readMemorySignal(deps);
469
605
  const nowMs = deps.now();
470
606
  const decision = evaluateThrottle({
471
607
  busy,
472
- availableMemMb,
608
+ availableMemMb: memory?.available_mb ?? null,
609
+ memory,
473
610
  cpuCheckEnabled,
474
611
  state,
475
612
  throttle,
@@ -181,6 +181,17 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
181
181
  export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
182
182
  export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
183
183
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
184
+ /**
185
+ * Who runs a lane's work. A process lane (the default, recorded by absence)
186
+ * is a provider process HQ spawned and watches. A subagent lane is a record
187
+ * only: an in-session Claude subagent does the work and reports through
188
+ * `hq lanes progress`, so admission, watchers, reapers and keep-alive skip it.
189
+ */
190
+ export declare const LANE_RUNNERS: readonly ["process", "subagent"];
191
+ export type LaneRunner = (typeof LANE_RUNNERS)[number];
192
+ export declare function parseLaneRunner(value: unknown): LaneRunner;
193
+ /** True when an in-session subagent, not an HQ-spawned process, runs this lane. */
194
+ export declare function isSubagentLane(lane: Pick<LaneRecord, "runner">): boolean;
184
195
  export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "lead_reserve", "worker_profile_changed", "low_disk", "admit_failed"];
185
196
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
186
197
  type QueuedDetails = {
@@ -616,6 +627,8 @@ export interface LaneRecord {
616
627
  /** How policy files were delivered on the most recent profile spawn. */
617
628
  worker_policy_delivery?: "hook" | "prompt";
618
629
  provider: LaneProvider;
630
+ /** `subagent` when an in-session subagent runs the work; absent for HQ-spawned process lanes. */
631
+ runner?: "subagent";
619
632
  /** Open each Claude turn as a named Remote Control session using account a's login. */
620
633
  remote_control?: boolean;
621
634
  /** Human-readable Remote Control name, fixed when the lane is created. Older lanes may lack it. */
@@ -1121,6 +1134,14 @@ export interface LanesConfig {
1121
1134
  }
1122
1135
  export declare function isIsoDateTime(value: string): boolean;
1123
1136
  export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
1124
- export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
1137
+ export interface ParseLaneEnvelopeOptions {
1138
+ /**
1139
+ * False for a lane started with --no-story. Such a lane has no board story
1140
+ * for story_status to describe, so a story_complete / story_status mismatch
1141
+ * is repaired from story_complete instead of failing the envelope.
1142
+ */
1143
+ storyBound?: boolean;
1144
+ }
1145
+ export declare function parseLaneEnvelope(raw: unknown, options?: ParseLaneEnvelopeOptions): LaneEnvelope;
1125
1146
  export {};
1126
1147
  //# sourceMappingURL=types.d.ts.map
@@ -106,6 +106,23 @@ export const TERMINAL_LANE_STATES = [
106
106
  "killed",
107
107
  ];
108
108
  export const LANE_PROVIDERS = ["claude", "codex", "grok"];
109
+ /**
110
+ * Who runs a lane's work. A process lane (the default, recorded by absence)
111
+ * is a provider process HQ spawned and watches. A subagent lane is a record
112
+ * only: an in-session Claude subagent does the work and reports through
113
+ * `hq lanes progress`, so admission, watchers, reapers and keep-alive skip it.
114
+ */
115
+ export const LANE_RUNNERS = ["process", "subagent"];
116
+ export function parseLaneRunner(value) {
117
+ if (typeof value === "string" && LANE_RUNNERS.includes(value)) {
118
+ return value;
119
+ }
120
+ throw new LanesError("unknown_lane_runner", `Unknown lane runner ${JSON.stringify(value)}. Accepted: ${LANE_RUNNERS.join(", ")}`, { accepted: [...LANE_RUNNERS], value });
121
+ }
122
+ /** True when an in-session subagent, not an HQ-spawned process, runs this lane. */
123
+ export function isSubagentLane(lane) {
124
+ return lane.runner === "subagent";
125
+ }
109
126
  export const QUEUED_REASONS = [
110
127
  "cap",
111
128
  "provider_cap",
@@ -240,7 +257,7 @@ export const LANE_FIELD_WRITERS = {
240
257
  pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
241
258
  priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
242
259
  keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile", "spawn"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile", "spawn"], keep_alive_next_eligible_at: ["command", "reconcile", "spawn"], keep_alive_exhausted_at: ["command", "reconcile", "spawn"], loop_mode: ["creator"],
243
- provider: ["creator", "command"], remote_control: ["creator"], remote_control_title: ["creator"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], account_pick: ["creator"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
260
+ provider: ["creator", "command"], runner: ["creator"], remote_control: ["creator"], remote_control_title: ["creator"], provider_source: ["creator", "command"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_source: ["creator", "command"], profile_settings_pinned: ["command"], account_selection: ["creator", "command"], account_pick: ["creator"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
244
261
  version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["command", "watcher", "reconcile"],
245
262
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
246
263
  last_message_at: ["creator", "command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
@@ -288,7 +305,8 @@ export function parseMajorSchema(schemaVersion, expectedPrefix, expectedMajor) {
288
305
  throw new LaneSchemaError(`Unsupported ${expectedPrefix} major version ${major} (found ${schemaVersion})`, { schema_version: schemaVersion, major });
289
306
  }
290
307
  }
291
- export function parseLaneEnvelope(raw) {
308
+ export function parseLaneEnvelope(raw, options = {}) {
309
+ const storyBound = options.storyBound !== false;
292
310
  if (!isPlainObject(raw)) {
293
311
  throw new LaneSchemaError("Lane envelope must be a JSON object");
294
312
  }
@@ -345,7 +363,7 @@ export function parseLaneEnvelope(raw) {
345
363
  else if (raw.blocked_reason !== undefined) {
346
364
  throw new LaneSchemaError('blocked_reason is present exactly when decision is "blocked"');
347
365
  }
348
- const storyStatus = raw.story_status;
366
+ let storyStatus = raw.story_status;
349
367
  let storyComplete;
350
368
  let storyCompleteReason;
351
369
  if (raw.story_complete !== undefined) {
@@ -358,6 +376,12 @@ export function parseLaneEnvelope(raw) {
358
376
  raw.story_complete_reason.trim().length === 0) {
359
377
  throw new LaneSchemaError("story_complete_reason is required when story_complete is present", { field: "story_complete_reason" });
360
378
  }
379
+ if (!storyBound && raw.story_complete === true && storyStatus !== "done") {
380
+ storyStatus = "done";
381
+ }
382
+ else if (!storyBound && raw.story_complete === false && storyStatus === "done") {
383
+ storyStatus = "in_progress";
384
+ }
361
385
  if (raw.story_complete === true && storyStatus !== "done") {
362
386
  throw new LaneSchemaError("story_complete true requires story_status done", {
363
387
  error: "story_complete_status_mismatch",
@@ -33,7 +33,7 @@
33
33
  */
34
34
  import * as fs from "node:fs";
35
35
  import * as path from "node:path";
36
- import { LanesError } from "./errors.js";
36
+ import { LaneSchemaError, LanesError } from "./errors.js";
37
37
  import { compareEnvelopeNames } from "./envelope-sequence.js";
38
38
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
39
39
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
@@ -738,7 +738,7 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
738
738
  ref,
739
739
  path: filePath,
740
740
  text,
741
- envelope: parseLaneEnvelope(JSON.parse(text)),
741
+ envelope: parseLaneEnvelopeForLane(hqRoot, laneId, JSON.parse(text)),
742
742
  error: null,
743
743
  };
744
744
  }
@@ -752,6 +752,31 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
752
752
  };
753
753
  }
754
754
  }
755
+ /**
756
+ * Parse strictly first. Only a story_complete / story_status mismatch reads the
757
+ * lane record: a --no-story lane (no_story_reason set) has no board story, so
758
+ * that mismatch is repaired instead of failing the lane. An unreadable lane
759
+ * record keeps the strict error.
760
+ */
761
+ function parseLaneEnvelopeForLane(hqRoot, laneId, raw) {
762
+ try {
763
+ return parseLaneEnvelope(raw);
764
+ }
765
+ catch (err) {
766
+ if (!(err instanceof LaneSchemaError) || err.details.error !== "story_complete_status_mismatch")
767
+ throw err;
768
+ let noStory = false;
769
+ try {
770
+ noStory = readLane(hqRoot, laneId).no_story_reason !== undefined;
771
+ }
772
+ catch {
773
+ throw err;
774
+ }
775
+ if (!noStory)
776
+ throw err;
777
+ return parseLaneEnvelope(raw, { storyBound: false });
778
+ }
779
+ }
755
780
  /** A descriptor for this run's manifest slot when no envelope file was written. */
756
781
  export function missingExpectedEnvelopeFile(hqRoot, laneId, envelopePath) {
757
782
  if (!envelopePath || !path.isAbsolute(envelopePath))
@@ -5,6 +5,8 @@ export interface WorkerContinuityLookup {
5
5
  provider: LaneProvider;
6
6
  company: LaneRecord["company"];
7
7
  freshThread?: boolean;
8
+ /** When set, a live lane on another machine names that machine and how to free the worker. */
9
+ runsHere?: (lane: LaneRecord) => boolean;
8
10
  }
9
11
  export interface WorkerContinuityResult {
10
12
  previous?: LaneRecord;
@@ -14,6 +14,22 @@ function sameCompany(left, right) {
14
14
  function compareNewest(left, right) {
15
15
  return right.created_at.localeCompare(left.created_at) || right.lane_id.localeCompare(left.lane_id);
16
16
  }
17
+ function workerBusyError(lookup, lane, lead, extra = {}) {
18
+ const remote = lookup.runsHere !== undefined && lookup.runsHere(lane) === false;
19
+ const fix = `hq lanes stop ${lane.lane_id}`;
20
+ const message = remote
21
+ ? `${lead} on ${lane.host.kind}:${lane.host.id} (state ${lane.state}). That lane is on another machine. Free the worker on that machine with \`${fix}\`.`
22
+ : `${lead} (state ${lane.state}). Free the worker with \`${fix}\`.`;
23
+ return new LanesError("worker_lane_busy", message, {
24
+ worker: lookup.worker,
25
+ lane_id: lane.lane_id,
26
+ state: lane.state,
27
+ host: lane.host,
28
+ remote,
29
+ fix_command: fix,
30
+ ...extra,
31
+ });
32
+ }
17
33
  export function sameLaneHost(left, right) {
18
34
  return left.kind === right.kind && left.id === right.id;
19
35
  }
@@ -25,11 +41,7 @@ export function findWorkerContinuityLane(lanes, lookup) {
25
41
  .sort(compareNewest);
26
42
  const live = scoped.find((lane) => !isTerminalLaneState(lane.state));
27
43
  if (live) {
28
- throw new LanesError("worker_lane_busy", `Worker ${lookup.worker} already has live lane ${live.lane_id}`, {
29
- worker: lookup.worker,
30
- lane_id: live.lane_id,
31
- state: live.state,
32
- });
44
+ throw workerBusyError(lookup, live, `Worker ${lookup.worker} already has live lane ${live.lane_id}`);
33
45
  }
34
46
  const previous = scoped[0];
35
47
  if (!previous)
@@ -45,12 +57,7 @@ export function findWorkerContinuityLane(lanes, lookup) {
45
57
  (lane.state === "queued" || lane.state === "running"))
46
58
  .sort(compareNewest)[0];
47
59
  if (liveDescendant) {
48
- throw new LanesError("worker_lane_busy", `Provider thread lineage is active in lane ${liveDescendant.lane_id}`, {
49
- worker: lookup.worker,
50
- lane_id: liveDescendant.lane_id,
51
- state: liveDescendant.state,
52
- provider_session_id: previousSessionId,
53
- });
60
+ throw workerBusyError(lookup, liveDescendant, `Provider thread lineage is active in lane ${liveDescendant.lane_id}`, { provider_session_id: previousSessionId });
54
61
  }
55
62
  }
56
63
  // Only clean, resumable terminal outcomes are safe for automatic reuse.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The kill switch hq-cloud's manifest pass honours. Restated locally (rather
3
+ * than imported) for two reasons: it lives in the same unreleased module as
4
+ * the manifest exports, and — more importantly — we must be able to read it
5
+ * BEFORE deciding to do any work at all.
6
+ *
7
+ * Kept in its own leaf module so `hq doctor` can read the switch without
8
+ * loading `hq sync manifest` and its imports.
9
+ */
10
+ export declare const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
11
+ /** True when the kill switch is set to anything other than empty/0/false. */
12
+ export declare function manifestUploadsDisabled(env?: NodeJS.ProcessEnv): boolean;
13
+ //# sourceMappingURL=manifest-disabled.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The kill switch hq-cloud's manifest pass honours. Restated locally (rather
3
+ * than imported) for two reasons: it lives in the same unreleased module as
4
+ * the manifest exports, and — more importantly — we must be able to read it
5
+ * BEFORE deciding to do any work at all.
6
+ *
7
+ * Kept in its own leaf module so `hq doctor` can read the switch without
8
+ * loading `hq sync manifest` and its imports.
9
+ */
10
+ export const MANIFEST_DISABLED_ENV = "HQ_SYNC_MANIFEST_DISABLED";
11
+ /** True when the kill switch is set to anything other than empty/0/false. */
12
+ export function manifestUploadsDisabled(env = process.env) {
13
+ const raw = env[MANIFEST_DISABLED_ENV];
14
+ if (raw === undefined)
15
+ return false;
16
+ const value = raw.trim().toLowerCase();
17
+ return value !== "" && value !== "0" && value !== "false";
18
+ }
19
+ //# sourceMappingURL=manifest-disabled.js.map
@@ -62,6 +62,7 @@ export declare const LAUNCHD_BOOTSTRAP_BACKOFF_MS: readonly number[];
62
62
  /** Bootstrap attempts after bootout (first try + EIO retries). */
63
63
  export declare const LAUNCHD_BOOTSTRAP_ATTEMPTS: number;
64
64
  export declare const defaultRunCommand: RunCommand;
65
+ export declare function blockingSleep(ms: number): void;
65
66
  export declare function launchdServiceTarget(plistPath: string, uid?: number): string;
66
67
  /** ProgramArguments from a LaunchAgent plist. */
67
68
  export declare function launchdPlistProgramArguments(plist: string): string[];
@@ -177,7 +177,7 @@ export const defaultRunCommand = (cmd, args) => {
177
177
  stderr: r.error ? r.error.message : (typeof r.stderr === "string" ? r.stderr : ""),
178
178
  };
179
179
  };
180
- function blockingSleep(ms) {
180
+ export function blockingSleep(ms) {
181
181
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
182
182
  }
183
183
  function guiDomain(uid = os.userInfo().uid) {