@indigoai-us/hq-cli 5.182.0 → 5.183.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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.183.0] — 2026-09-25
6
+
7
+ ### Added
8
+
9
+ - `hq core sentry report` can attach bounded hook-timeout timing context to Sentry events when the company-scoped `hook-timeout-debug-context` flag is enabled. The flag defaults off.
10
+
5
11
  ## [5.182.0] — 2026-09-24
6
12
 
7
13
  ### Fixed
@@ -0,0 +1,20 @@
1
+ import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
2
+ export declare const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG = "hook-timeout-debug-context";
3
+ export declare const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK = false;
4
+ export declare const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS = 150;
5
+ type DebugContextFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
6
+ export interface HookTimeoutDebugContextFlagDependencies {
7
+ env?: NodeJS.ProcessEnv;
8
+ createClient?: (options: FlagClientOptions) => DebugContextFlagClient;
9
+ getToken?: FlagClientOptions["getToken"];
10
+ fetch?: typeof fetch;
11
+ reportFailure?: (error: unknown) => void;
12
+ }
13
+ /**
14
+ * Read the optional diagnostic gate with a short, cancellable hq-flags request.
15
+ * Missing identity/configuration and every client failure use the default-off
16
+ * result so a flag outage cannot delay or suppress the underlying timeout.
17
+ */
18
+ export declare function hookTimeoutDebugContextEnabled(dependencies?: HookTimeoutDebugContextFlagDependencies, callerSignal?: AbortSignal): Promise<boolean>;
19
+ export {};
20
+ //# sourceMappingURL=hook-timeout-debug-context-flag.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
+ import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
+ export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG = "hook-timeout-debug-context";
4
+ export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK = false;
5
+ export const HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS = 150;
6
+ function safeErrorClass(error) {
7
+ const name = error instanceof Error ? error.name : "UnknownError";
8
+ return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
9
+ }
10
+ function reportFlagFailure(error) {
11
+ process.stderr.write(`HQ hook timeout debug flag lookup failed (${safeErrorClass(error)}); using default off.\n`);
12
+ }
13
+ function combineSignals(first, second) {
14
+ return first ? AbortSignal.any([first, second]) : second;
15
+ }
16
+ /**
17
+ * Read the optional diagnostic gate with a short, cancellable hq-flags request.
18
+ * Missing identity/configuration and every client failure use the default-off
19
+ * result so a flag outage cannot delay or suppress the underlying timeout.
20
+ */
21
+ export async function hookTimeoutDebugContextEnabled(dependencies = {}, callerSignal) {
22
+ const env = dependencies.env ?? process.env;
23
+ const endpoint = env.HQ_FLAGS_API_URL?.trim() || "";
24
+ const companyUid = env.HQ_COMPANY_UID?.trim() || "";
25
+ if (!endpoint || !/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
26
+ return HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK;
27
+ }
28
+ const companySlug = env.HQ_COMPANY_SLUG?.trim() || "";
29
+ const deadline = AbortSignal.timeout(HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS);
30
+ const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
31
+ const doFetch = dependencies.fetch ?? globalThis.fetch;
32
+ let client;
33
+ let failed = false;
34
+ let failureReported = false;
35
+ let enabled = HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK;
36
+ const reportFailureOnce = (error) => {
37
+ failed = true;
38
+ if (failureReported)
39
+ return;
40
+ failureReported = true;
41
+ (dependencies.reportFailure ?? reportFlagFailure)(error);
42
+ };
43
+ try {
44
+ client = (dependencies.createClient ?? createFlagClient)({
45
+ endpoint,
46
+ companyUid,
47
+ ...(companySlug && /^[A-Za-z0-9][A-Za-z0-9-]{0,63}$/.test(companySlug)
48
+ ? { companyIdentifiers: [companySlug] }
49
+ : {}),
50
+ env: {},
51
+ getToken: dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? ""),
52
+ fetch: (input, init) => doFetch(input, {
53
+ ...init,
54
+ signal: combineSignals(init?.signal ?? undefined, signal),
55
+ }),
56
+ refreshIntervalMs: 0,
57
+ requestTimeoutMs: HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_TIMEOUT_MS,
58
+ onError: reportFailureOnce,
59
+ });
60
+ await client.ready();
61
+ if (!failed && !signal.aborted) {
62
+ enabled = client.isEnabled(HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG, {
63
+ fallback: HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK,
64
+ });
65
+ }
66
+ }
67
+ catch (error) {
68
+ reportFailureOnce(error);
69
+ }
70
+ finally {
71
+ try {
72
+ client?.close();
73
+ }
74
+ catch (error) {
75
+ reportFailureOnce(error);
76
+ }
77
+ }
78
+ return failed || signal.aborted ? HOOK_TIMEOUT_DEBUG_CONTEXT_FLAG_FALLBACK : enabled;
79
+ }
80
+ //# sourceMappingURL=hook-timeout-debug-context-flag.js.map
@@ -27,14 +27,32 @@ 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", "spawn_ms", "slow_child", "slow_child_ms", "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", "hook_timeout_debug_context"];
31
31
  export declare const MAX_SENTRY_REPORT_METADATA_KEYS: number;
32
32
  export type HookSequenceEntry = {
33
33
  script: string;
34
34
  event: string;
35
35
  ms: number;
36
36
  };
37
- export type SentryReportMetadataValue = string | number | boolean | HookSequenceEntry[];
37
+ export type HookTimeoutDebugPhaseTiming = {
38
+ phase: string;
39
+ elapsed_ms: number;
40
+ };
41
+ export type HookTimeoutDebugContext = {
42
+ hook_name: string;
43
+ hook_event: string;
44
+ budget_ms: number;
45
+ elapsed_ms: number;
46
+ remaining_ms: number;
47
+ phase_timings: HookTimeoutDebugPhaseTiming[];
48
+ wait_point: string;
49
+ waiting_child_basename: string;
50
+ waiting_child_elapsed_ms: number;
51
+ load_average?: number | "unavailable";
52
+ spawn_ms?: number | "unavailable";
53
+ process_count?: number | "unavailable";
54
+ };
55
+ export type SentryReportMetadataValue = string | number | boolean | HookSequenceEntry[] | HookTimeoutDebugContext;
38
56
  export type SentryReportMetadata = Record<string, SentryReportMetadataValue>;
39
57
  export type SentryReportEvent = {
40
58
  type: string;
@@ -53,6 +71,8 @@ export type SentryReportOptions = {
53
71
  rateLimitDirectory?: string;
54
72
  /** Test seam for deterministic rate-limit windows. */
55
73
  rateLimitNow?: () => number;
74
+ /** Optional caller cancellation forwarded to the bounded flag lookup. */
75
+ signal?: AbortSignal;
56
76
  };
57
77
  export type SentryReportDependencies = {
58
78
  sentry: {
@@ -67,6 +87,8 @@ export type SentryReportDependencies = {
67
87
  rateLimitDirectory?: string;
68
88
  /** Test seam; production uses Date.now. */
69
89
  rateLimitNow?: () => number;
90
+ /** Test seam for the company-scoped, bounded debug-context gate. */
91
+ timeoutDebugContextEnabled?: (signal?: AbortSignal) => Promise<boolean>;
70
92
  };
71
93
  export type SentryReportResult = {
72
94
  code: 0 | 1;
@@ -23,6 +23,7 @@ import * as Sentry from "@sentry/node";
23
23
  import { CLI_VERSION } from "../../cli-version.js";
24
24
  import { beforeSend } from "../../sentry-before-send.js";
25
25
  import { resolveLiveRoot } from "../../utils/hq-roots.js";
26
+ import { hookTimeoutDebugContextEnabled } from "./hook-timeout-debug-context-flag.js";
26
27
  export const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
27
28
  export const MAX_SENTRY_REPORT_TIMEOUT_MS = 5_000;
28
29
  export const MAX_SENTRY_REPORT_PAYLOAD_BYTES = 2_048;
@@ -47,6 +48,8 @@ const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
47
48
  const HOOK_TIMEOUT_WARNING_FINGERPRINT = "hook-timeout";
48
49
  const HOOK_TIMEOUT_MACHINE_WIDE_FINGERPRINT = "hq-hook-slow";
49
50
  const MAX_HOOK_SEQUENCE_ENTRIES = 20;
51
+ const MAX_HOOK_TIMEOUT_DEBUG_PHASES = 24;
52
+ const MAX_HOOK_TIMEOUT_DEBUG_MS = 86_400_000;
50
53
  const HOOK_TIMEOUT_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1_000;
51
54
  const RATE_LIMIT_LOCK_WAIT_MS = 500;
52
55
  const RATE_LIMIT_LOCK_POLL_MS = 5;
@@ -65,6 +68,72 @@ const KNOWN_HOOK_SCRIPTS = new Set([
65
68
  "45-lanes-senior-monitor.sh",
66
69
  "lanes-senior-monitor-stop-gate.sh",
67
70
  ]);
71
+ const DEBUG_CONTEXT_HOOK_NAMES = new Set([
72
+ ...KNOWN_HOOK_SCRIPTS,
73
+ "reindex.sh",
74
+ ]);
75
+ const DEBUG_CONTEXT_CHILD_NAMES = new Set([
76
+ ...DEBUG_CONTEXT_HOOK_NAMES,
77
+ "bash",
78
+ "bash.exe",
79
+ "sh",
80
+ "cmd",
81
+ "cmd.exe",
82
+ "powershell",
83
+ "powershell.exe",
84
+ "wmic.exe",
85
+ "jq",
86
+ "jq.exe",
87
+ "node",
88
+ "node.exe",
89
+ "git",
90
+ "git.exe",
91
+ "python",
92
+ "python.exe",
93
+ "python3",
94
+ "python3.exe",
95
+ "true",
96
+ "sleep",
97
+ "cat",
98
+ "grep",
99
+ "sed",
100
+ "awk",
101
+ "find",
102
+ "timeout",
103
+ "curl",
104
+ "gh",
105
+ "hq",
106
+ "npm",
107
+ "pnpm",
108
+ "npx",
109
+ ]);
110
+ const DEBUG_CONTEXT_HOOK_EVENTS = new Set([
111
+ "PreToolUse",
112
+ "PostToolUse",
113
+ "UserPromptSubmit",
114
+ "SessionStart",
115
+ "SessionEnd",
116
+ "Stop",
117
+ "PreCompact",
118
+ "Notification",
119
+ "PermissionRequest",
120
+ "SubagentStart",
121
+ "SubagentStop",
122
+ "TaskCompleted",
123
+ "TeammateIdle",
124
+ ]);
125
+ const DEBUG_CONTEXT_PHASES = new Set([
126
+ "startup",
127
+ "source",
128
+ "config_load",
129
+ "policy_load",
130
+ "external_command",
131
+ "output_write",
132
+ "wait",
133
+ "child_wait",
134
+ "probe",
135
+ "parse",
136
+ ]);
68
137
  /**
69
138
  * The only hook diagnostics that can be attached as metadata. The list omits
70
139
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -96,6 +165,7 @@ export const ALLOWED_SENTRY_REPORT_METADATA_KEYS = [
96
165
  "shell",
97
166
  "nproc",
98
167
  "hook_sequence",
168
+ "hook_timeout_debug_context",
99
169
  ];
100
170
  const ALLOWED_METADATA_KEYS = new Set(ALLOWED_SENTRY_REPORT_METADATA_KEYS);
101
171
  const SENTRY_REPORT_METADATA_KEY_HEADROOM = 2;
@@ -107,6 +177,7 @@ const defaultDependencies = {
107
177
  sentry: Sentry,
108
178
  stdout: process.stdout,
109
179
  stderr: process.stderr,
180
+ timeoutDebugContextEnabled: (signal) => hookTimeoutDebugContextEnabled({}, signal),
110
181
  };
111
182
  /**
112
183
  * Read no more than the accepted event size. Returning early keeps a malformed
@@ -153,6 +224,90 @@ function isHookSequenceEntry(value) {
153
224
  Number.isFinite(value.ms) &&
154
225
  value.ms >= 0);
155
226
  }
227
+ function boundedDebugName(value, known) {
228
+ if (typeof value !== "string" || value.length === 0)
229
+ return "other";
230
+ return known.has(basenameFromPath(value)) ? basenameFromPath(value) : "other";
231
+ }
232
+ function boundedDebugPhase(value) {
233
+ return typeof value === "string" && DEBUG_CONTEXT_PHASES.has(value) ? value : "other";
234
+ }
235
+ function boundedDebugHookEvent(value) {
236
+ return typeof value === "string" && DEBUG_CONTEXT_HOOK_EVENTS.has(value) ? value : "other";
237
+ }
238
+ function boundedDebugMilliseconds(value, allowNegative = false) {
239
+ if (typeof value !== "number" || !Number.isFinite(value))
240
+ return null;
241
+ const bounded = allowNegative
242
+ ? Math.abs(value) <= MAX_HOOK_TIMEOUT_DEBUG_MS
243
+ : value >= 0 && value <= MAX_HOOK_TIMEOUT_DEBUG_MS;
244
+ return bounded ? value : null;
245
+ }
246
+ function boundedDebugProbe(value) {
247
+ if (value === "unavailable")
248
+ return value;
249
+ return boundedDebugMilliseconds(value) ?? undefined;
250
+ }
251
+ function boundedDebugPhaseTimings(value) {
252
+ if (!Array.isArray(value) || value.length > MAX_HOOK_TIMEOUT_DEBUG_PHASES)
253
+ return null;
254
+ const timings = [];
255
+ for (const entry of value) {
256
+ if (!isRecord(entry))
257
+ return null;
258
+ const elapsed = boundedDebugMilliseconds(entry.elapsed_ms);
259
+ if (elapsed === null)
260
+ return null;
261
+ const timing = {
262
+ phase: boundedDebugPhase(entry.phase),
263
+ elapsed_ms: elapsed,
264
+ };
265
+ timings.push(timing);
266
+ }
267
+ return timings;
268
+ }
269
+ /** Return only the closed, bounded shell-to-CLI context shape. */
270
+ function boundedHookTimeoutDebugContext(value) {
271
+ if (!isRecord(value))
272
+ return null;
273
+ const budget = boundedDebugMilliseconds(value.budget_ms);
274
+ const elapsed = boundedDebugMilliseconds(value.elapsed_ms);
275
+ const remaining = boundedDebugMilliseconds(value.remaining_ms, true);
276
+ const childElapsed = boundedDebugMilliseconds(value.waiting_child_elapsed_ms);
277
+ const phaseTimings = boundedDebugPhaseTimings(value.phase_timings);
278
+ if (budget === null ||
279
+ elapsed === null ||
280
+ remaining === null ||
281
+ childElapsed === null ||
282
+ phaseTimings === null) {
283
+ return null;
284
+ }
285
+ const loadAverage = boundedDebugProbe(value.load_average);
286
+ const spawnMs = boundedDebugProbe(value.spawn_ms);
287
+ const processCount = boundedDebugProbe(value.process_count);
288
+ const hasLoadAverage = typeof loadAverage === "number";
289
+ const hasWindowsProbe = spawnMs !== undefined && processCount !== undefined;
290
+ if (!hasLoadAverage && !hasWindowsProbe)
291
+ return null;
292
+ if (typeof processCount === "number" &&
293
+ (!Number.isInteger(processCount) || processCount > 1_000_000)) {
294
+ return null;
295
+ }
296
+ return {
297
+ hook_name: boundedDebugName(value.hook_name, DEBUG_CONTEXT_HOOK_NAMES),
298
+ hook_event: boundedDebugHookEvent(value.hook_event),
299
+ budget_ms: budget,
300
+ elapsed_ms: elapsed,
301
+ remaining_ms: remaining,
302
+ phase_timings: phaseTimings,
303
+ wait_point: boundedDebugPhase(value.wait_point),
304
+ waiting_child_basename: boundedDebugName(value.waiting_child_basename, DEBUG_CONTEXT_CHILD_NAMES),
305
+ waiting_child_elapsed_ms: childElapsed,
306
+ ...(loadAverage !== undefined ? { load_average: loadAverage } : {}),
307
+ ...(spawnMs !== undefined ? { spawn_ms: spawnMs } : {}),
308
+ ...(processCount !== undefined ? { process_count: processCount } : {}),
309
+ };
310
+ }
156
311
  function boundedHookSequence(value) {
157
312
  if (!Array.isArray(value))
158
313
  return null;
@@ -168,6 +323,8 @@ function boundedHookSequence(value) {
168
323
  function isAllowedMetadataValue(key, value) {
169
324
  if (key === "hook_sequence")
170
325
  return boundedHookSequence(value) !== null;
326
+ if (key === "hook_timeout_debug_context")
327
+ return boundedHookTimeoutDebugContext(value) !== null;
171
328
  if (!isScalarMetadataValue(value))
172
329
  return false;
173
330
  if (key === "tool_name" || key === "session_id" || key === "hook_path") {
@@ -236,9 +393,10 @@ export function parseSentryReportEvent(stdin) {
236
393
  }
237
394
  if (!isAllowedMetadataValue(key, rawMetadata[key]))
238
395
  return invalidInput();
239
- metadata[key] =
240
- key === "hook_sequence"
241
- ? boundedHookSequence(rawMetadata[key])
396
+ metadata[key] = key === "hook_sequence"
397
+ ? boundedHookSequence(rawMetadata[key])
398
+ : key === "hook_timeout_debug_context"
399
+ ? boundedHookTimeoutDebugContext(rawMetadata[key])
242
400
  : rawMetadata[key];
243
401
  }
244
402
  if (typeof metadata.slow_child === "string") {
@@ -319,6 +477,11 @@ function elapsedBucket(elapsedMs) {
319
477
  return "20-60s";
320
478
  return ">=60s";
321
479
  }
480
+ function remainingBucket(remainingMs) {
481
+ return remainingMs < 0
482
+ ? `overdue-${elapsedBucket(Math.abs(remainingMs))}`
483
+ : elapsedBucket(remainingMs);
484
+ }
322
485
  function loadBucket(loadAverage) {
323
486
  if (loadAverage === undefined)
324
487
  return "unknown";
@@ -368,6 +531,53 @@ function spawnMsBucket(metadata) {
368
531
  return "500-999ms";
369
532
  return ">=1000ms";
370
533
  }
534
+ function debugSpawnMsBucket(value) {
535
+ if (typeof value !== "number")
536
+ return "unavailable";
537
+ if (value < 100)
538
+ return "<100ms";
539
+ if (value < 500)
540
+ return "100-499ms";
541
+ if (value < 1_000)
542
+ return "500-999ms";
543
+ return ">=1000ms";
544
+ }
545
+ function processCountBucket(value) {
546
+ if (typeof value !== "number")
547
+ return "unavailable";
548
+ if (value < 5)
549
+ return "0-4";
550
+ if (value < 17)
551
+ return "5-16";
552
+ if (value < 65)
553
+ return "17-64";
554
+ return "65+";
555
+ }
556
+ function hookTimeoutDebugContextTags(context) {
557
+ const tags = {
558
+ timeout_debug_hook_name: context.hook_name,
559
+ timeout_debug_hook_event: context.hook_event,
560
+ timeout_debug_budget_bucket: elapsedBucket(context.budget_ms),
561
+ timeout_debug_elapsed_bucket: elapsedBucket(context.elapsed_ms),
562
+ timeout_debug_remaining_bucket: remainingBucket(context.remaining_ms),
563
+ timeout_debug_wait_point: context.wait_point,
564
+ timeout_debug_waiting_child: context.waiting_child_basename,
565
+ timeout_debug_waiting_child_elapsed_bucket: elapsedBucket(context.waiting_child_elapsed_ms),
566
+ timeout_debug_load_bucket: context.load_average === "unavailable"
567
+ ? "unavailable"
568
+ : loadBucket(context.load_average),
569
+ timeout_debug_spawn_ms_bucket: debugSpawnMsBucket(context.spawn_ms),
570
+ timeout_debug_process_count_bucket: processCountBucket(context.process_count),
571
+ };
572
+ const phaseElapsed = new Map();
573
+ for (const timing of context.phase_timings) {
574
+ phaseElapsed.set(timing.phase, Math.max(phaseElapsed.get(timing.phase) ?? 0, timing.elapsed_ms));
575
+ }
576
+ for (const [phase, elapsedMs] of phaseElapsed) {
577
+ tags[`timeout_debug_phase_${phase}_bucket`] = elapsedBucket(elapsedMs);
578
+ }
579
+ return tags;
580
+ }
371
581
  function safeSlowChild(metadata) {
372
582
  const value = metadata.slow_child;
373
583
  if (typeof value !== "string" || value.length === 0)
@@ -464,10 +674,10 @@ function sentryEnvelope(event, overrides, additionalTags = {}) {
464
674
  };
465
675
  }
466
676
  /** Build the same scrubbed event projection that the live SDK path uses. */
467
- function scrubbedSentryEnvelope(event, overrides) {
677
+ function scrubbedSentryEnvelope(event, overrides, additionalTags = {}) {
468
678
  return beforeSend({
469
679
  message: event.message,
470
- ...sentryEnvelope(event, overrides),
680
+ ...sentryEnvelope(event, overrides, additionalTags),
471
681
  }, {});
472
682
  }
473
683
  /**
@@ -740,19 +950,47 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
740
950
  const noun = result.droppedMetadataKeys === 1 ? "key" : "keys";
741
951
  dependencies.stderr.write(`Dropped ${result.droppedMetadataKeys} unsupported Sentry report metadata ${noun}.\n`);
742
952
  }
743
- const overrides = hookTimeoutExceededOverrides(result.event);
953
+ let event = result.event;
954
+ let debugContextTags = {};
955
+ const debugContext = event.metadata.hook_timeout_debug_context;
956
+ if (debugContext && isHookTimeoutEvent(event)) {
957
+ let enabled;
958
+ try {
959
+ enabled =
960
+ (await dependencies.timeoutDebugContextEnabled?.(options.signal)) ?? false;
961
+ }
962
+ catch {
963
+ // An optional diagnostic flag read must never suppress the base report.
964
+ enabled = false;
965
+ }
966
+ if (enabled) {
967
+ debugContextTags = hookTimeoutDebugContextTags(debugContext);
968
+ }
969
+ else {
970
+ const metadata = { ...event.metadata };
971
+ delete metadata.hook_timeout_debug_context;
972
+ event = { ...event, metadata };
973
+ }
974
+ }
975
+ else if (debugContext) {
976
+ const metadata = { ...event.metadata };
977
+ delete metadata.hook_timeout_debug_context;
978
+ event = { ...event, metadata };
979
+ }
980
+ result.event = event;
981
+ const overrides = hookTimeoutExceededOverrides(event);
744
982
  if (options.dryRun) {
745
- const scrubbed = scrubbedSentryEnvelope(result.event, overrides);
983
+ const scrubbed = scrubbedSentryEnvelope(event, overrides, debugContextTags);
746
984
  if (scrubbed)
747
985
  dependencies.stdout.write(`${JSON.stringify(scrubbed)}\n`);
748
986
  return result;
749
987
  }
750
- if (isHookTimeoutEvent(result.event)) {
751
- addHookSequenceBreadcrumbs(result.event, dependencies);
752
- const kind = hookTimeoutKind(result.event);
988
+ if (isHookTimeoutEvent(event)) {
989
+ addHookSequenceBreadcrumbs(event, dependencies);
990
+ const kind = hookTimeoutKind(event);
753
991
  let rateLimit;
754
992
  try {
755
- rateLimit = timeoutRateLimitLease(result.event, kind, options, dependencies);
993
+ rateLimit = timeoutRateLimitLease(event, kind, options, dependencies);
756
994
  }
757
995
  catch {
758
996
  dependencies.stderr.write("Sentry timeout rate limit state unavailable; sending without rate limiting.\n");
@@ -764,14 +1002,14 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
764
1002
  };
765
1003
  }
766
1004
  if (!rateLimit.capture) {
767
- return reportSuppressedTimeout(result, result.event, timeoutMs, dependencies, kind);
1005
+ return reportSuppressedTimeout(result, event, timeoutMs, dependencies, kind);
768
1006
  }
769
1007
  const tags = rateLimit.suppressedSinceLast > 0
770
1008
  ? { suppressed_since_last: String(rateLimit.suppressedSinceLast) }
771
1009
  : {};
772
- const envelope = sentryEnvelope(result.event, overrides, tags);
1010
+ const envelope = sentryEnvelope(event, overrides, { ...debugContextTags, ...tags });
773
1011
  try {
774
- dependencies.sentry.captureMessage(result.event.message, envelope);
1012
+ dependencies.sentry.captureMessage(event.message, envelope);
775
1013
  rateLimit.commitCapture();
776
1014
  if (await flushWithin(dependencies.sentry, timeoutMs))
777
1015
  return result;
@@ -783,9 +1021,9 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
783
1021
  dependencies.stderr.write("Sentry report failed.\n");
784
1022
  return invalidInput();
785
1023
  }
786
- const envelope = sentryEnvelope(result.event, overrides);
1024
+ const envelope = sentryEnvelope(event, overrides, debugContextTags);
787
1025
  try {
788
- dependencies.sentry.captureMessage(result.event.message, envelope);
1026
+ dependencies.sentry.captureMessage(event.message, envelope);
789
1027
  if (await flushWithin(dependencies.sentry, timeoutMs))
790
1028
  return result;
791
1029
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.182.0",
3
+ "version": "5.183.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {