@indigoai-us/hq-cli 5.158.0 → 5.159.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.159.0] — 2026-09-24
6
+
7
+ ### Fixed
8
+
9
+ - Hook timeout reports now identify the slow child hook and group concurrent
10
+ session-wide slowness together. Update checks have bounded network calls, and
11
+ the core-write guard avoids repeated subprocess work.
12
+ - Machine-wide hook warnings and exceeded timeouts now keep separate Sentry
13
+ groups, and per-script rate limits no longer suppress a later machine-wide
14
+ report from the same session.
15
+
5
16
  ## [5.158.0] — 2026-09-24
6
17
 
7
18
  ### Added
@@ -27,7 +27,7 @@ export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
27
27
  * is owner-approved telemetry for the exact hook script and may include a
28
28
  * company directory; it is never a substitute for hook input or a transcript.
29
29
  */
30
- export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence"];
30
+ export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence"];
31
31
  export declare const MAX_SENTRY_REPORT_METADATA_KEYS: number;
32
32
  export type HookSequenceEntry = {
33
33
  script: string;
@@ -45,11 +45,26 @@ const HOOK_TIMEOUT_WARNING_METRIC = "hq.hook_timeout_warning";
45
45
  const HOOK_TIMEOUT_EXCEEDED_METRIC = "hq.hook_timeout_exceeded";
46
46
  const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
47
47
  const HOOK_TIMEOUT_WARNING_FINGERPRINT = "hook-timeout";
48
+ const HOOK_TIMEOUT_MACHINE_WIDE_FINGERPRINT = "hq-hook-slow";
48
49
  const MAX_HOOK_SEQUENCE_ENTRIES = 20;
49
50
  const HOOK_TIMEOUT_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1_000;
50
51
  const RATE_LIMIT_LOCK_WAIT_MS = 500;
51
52
  const RATE_LIMIT_LOCK_POLL_MS = 5;
52
53
  const RATE_LIMIT_LOCK_STALE_MS = 5_000;
54
+ const KNOWN_HOOK_SCRIPTS = new Set([
55
+ "master-hook.sh",
56
+ "hook-gate.sh",
57
+ "hook-timeout-watchdog.sh",
58
+ "check-hq-update.sh",
59
+ "block-core-writes-bash.sh",
60
+ "block-core-writes.sh",
61
+ "block-hq-worktree-session.sh",
62
+ "inject-policy-on-trigger.sh",
63
+ "inject-local-context.sh",
64
+ "session-title.sh",
65
+ "45-lanes-senior-monitor.sh",
66
+ "lanes-senior-monitor-stop-gate.sh",
67
+ ]);
53
68
  /**
54
69
  * The only hook diagnostics that can be attached as metadata. The list omits
55
70
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -71,6 +86,9 @@ export const ALLOWED_SENTRY_REPORT_METADATA_KEYS = [
71
86
  "hq_version",
72
87
  "platform",
73
88
  "load_average",
89
+ "spawn_ms",
90
+ "slow_child",
91
+ "slow_child_ms",
74
92
  "exit_code",
75
93
  "final_elapsed_ms",
76
94
  "bash_env_set",
@@ -223,6 +241,9 @@ export function parseSentryReportEvent(stdin) {
223
241
  ? boundedHookSequence(rawMetadata[key])
224
242
  : rawMetadata[key];
225
243
  }
244
+ if (typeof metadata.slow_child === "string") {
245
+ metadata.slow_child = safeSlowChild(metadata);
246
+ }
226
247
  return {
227
248
  code: 0,
228
249
  event: {
@@ -311,6 +332,63 @@ function loadBucket(loadAverage) {
311
332
  return "10-20";
312
333
  return ">=20";
313
334
  }
335
+ function parsedLoadAverage(metadata) {
336
+ const value = metadata.load_average;
337
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0)
338
+ return value;
339
+ if (typeof value !== "string")
340
+ return undefined;
341
+ if (value === "unavailable")
342
+ return "unavailable";
343
+ const match = /^\s*\{?\s*([0-9]+(?:\.[0-9]+)?)/.exec(value);
344
+ if (!match)
345
+ return undefined;
346
+ const parsed = Number(match[1]);
347
+ return Number.isFinite(parsed) ? parsed : undefined;
348
+ }
349
+ function operatingSystemName(metadata) {
350
+ const platform = stringMetadata(metadata, "platform", "").toLowerCase();
351
+ if (/windows|win32|mingw|msys|cygwin/.test(platform))
352
+ return "windows";
353
+ if (/darwin|macos|mac os/.test(platform))
354
+ return "macos";
355
+ if (/linux/.test(platform))
356
+ return "linux";
357
+ return "other";
358
+ }
359
+ function spawnMsBucket(metadata) {
360
+ const value = metadata.spawn_ms;
361
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
362
+ return "unavailable";
363
+ if (value < 100)
364
+ return "<100ms";
365
+ if (value < 500)
366
+ return "100-499ms";
367
+ if (value < 1_000)
368
+ return "500-999ms";
369
+ return ">=1000ms";
370
+ }
371
+ function safeSlowChild(metadata) {
372
+ const value = metadata.slow_child;
373
+ if (typeof value !== "string" || value.length === 0)
374
+ return "other";
375
+ const script = basenameFromPath(value);
376
+ return KNOWN_HOOK_SCRIPTS.has(script) ? script : "other";
377
+ }
378
+ function isMachineWideHookSlowness(metadata) {
379
+ const spawnMs = metadata.spawn_ms;
380
+ if (typeof spawnMs === "number" && Number.isFinite(spawnMs) && spawnMs >= 500)
381
+ return true;
382
+ if (!Array.isArray(metadata.hook_sequence))
383
+ return false;
384
+ const slowScripts = new Set();
385
+ for (const entry of metadata.hook_sequence) {
386
+ if (entry.ms < 20_000 || !SAFE_HOOK_SCRIPT.test(entry.script))
387
+ continue;
388
+ slowScripts.add(entry.script);
389
+ }
390
+ return slowScripts.size >= 3;
391
+ }
314
392
  function bashEnvTag(metadata) {
315
393
  const value = metadata.bash_env_set;
316
394
  if (value === true || value === "set")
@@ -330,7 +408,8 @@ function hookTimeoutTags(event) {
330
408
  : undefined;
331
409
  const shell = stringMetadata(metadata, "shell");
332
410
  const nproc = metadata.nproc;
333
- const loadAverage = metadata.load_average;
411
+ const loadAverage = parsedLoadAverage(metadata);
412
+ const slowChildMs = metadata.slow_child_ms;
334
413
  return {
335
414
  hook_event: hookEvent(metadata),
336
415
  hook_script: hookScript(metadata),
@@ -346,7 +425,14 @@ function hookTimeoutTags(event) {
346
425
  shell: safeTagValue(shell),
347
426
  bash_env: bashEnvTag(metadata),
348
427
  cwd_kind: safeMetadataTag(metadata, "cwd_kind"),
349
- load_bucket: typeof loadAverage === "number" ? loadBucket(loadAverage) : "unknown",
428
+ os_name: operatingSystemName(metadata),
429
+ load_bucket: loadAverage === "unavailable" ? "unavailable" : loadBucket(loadAverage),
430
+ spawn_ms_bucket: spawnMsBucket(metadata),
431
+ slow_child: safeSlowChild(metadata),
432
+ slow_child_ms_bucket: typeof slowChildMs === "number" && Number.isFinite(slowChildMs)
433
+ ? elapsedBucket(slowChildMs)
434
+ : "unknown",
435
+ machine_wide: isMachineWideHookSlowness(metadata) ? "true" : "false",
350
436
  nproc: typeof nproc === "number" || typeof nproc === "string"
351
437
  ? safeTagValue(String(nproc), SAFE_NPROC)
352
438
  : "unknown",
@@ -393,7 +479,20 @@ function hookTimeoutExceededOverrides(event) {
393
479
  const eventName = hookEvent(event.metadata);
394
480
  if (!isHookTimeoutEvent(event))
395
481
  return undefined;
396
- if (hookTimeoutKind(event) === "warning") {
482
+ const kind = hookTimeoutKind(event);
483
+ if (isMachineWideHookSlowness(event.metadata)) {
484
+ return {
485
+ level: "warning",
486
+ fingerprint: [
487
+ HOOK_TIMEOUT_MACHINE_WIDE_FINGERPRINT,
488
+ "machine-wide",
489
+ operatingSystemName(event.metadata),
490
+ eventName,
491
+ kind,
492
+ ],
493
+ };
494
+ }
495
+ if (kind === "warning") {
397
496
  return {
398
497
  level: "warning",
399
498
  fingerprint: [HOOK_TIMEOUT_WARNING_FINGERPRINT, eventName, hookScript(event.metadata)],
@@ -536,7 +635,11 @@ function pruneExpiredRateLimitState(directory, now, protectedStatePath) {
536
635
  }
537
636
  function timeoutRateLimitLease(event, kind, options, dependencies) {
538
637
  const sessionId = stringMetadata(event.metadata, "session_id");
539
- const key = `${sessionId}\0${hookEvent(event.metadata)}\0${hookScript(event.metadata)}\0${kind}`;
638
+ const machineWide = isMachineWideHookSlowness(event.metadata);
639
+ const classification = machineWide
640
+ ? `machine-wide\0${operatingSystemName(event.metadata)}`
641
+ : `per-script\0${hookScript(event.metadata)}`;
642
+ const key = `${sessionId}\0${hookEvent(event.metadata)}\0${kind}\0${classification}`;
540
643
  const directory = rateLimitDirectory(options, dependencies);
541
644
  const statePath = rateLimitStatePath(directory, key);
542
645
  const releaseLock = acquireRateLimitLock(directory);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.158.0",
3
+ "version": "5.159.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {