@indigoai-us/hq-cli 5.182.0 → 5.184.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/command-catalog.generated.d.ts +3 -0
  3. package/dist/command-catalog.generated.js +4 -0
  4. package/dist/commands/doctor.d.ts +2 -0
  5. package/dist/commands/doctor.js +100 -6
  6. package/dist/commands/mesh.js +18 -1
  7. package/dist/lib/core-utils/hook-timeout-debug-context-flag.d.ts +20 -0
  8. package/dist/lib/core-utils/hook-timeout-debug-context-flag.js +80 -0
  9. package/dist/lib/core-utils/sentry-report.d.ts +24 -2
  10. package/dist/lib/core-utils/sentry-report.js +254 -16
  11. package/dist/lib/doctor/checks/sync-health.js +13 -0
  12. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +26 -0
  13. package/dist/lib/doctor/client-health-reason-codes-flag.js +94 -0
  14. package/dist/lib/doctor/fix/apply.d.ts +5 -0
  15. package/dist/lib/doctor/fix/apply.js +28 -3
  16. package/dist/lib/doctor/fix/json-output.d.ts +20 -0
  17. package/dist/lib/doctor/fix/json-output.js +22 -0
  18. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +12 -0
  19. package/dist/lib/doctor/fix/refresh-sync-outcome.js +25 -0
  20. package/dist/lib/doctor/json-output.d.ts +7 -1
  21. package/dist/lib/doctor/json-output.js +6 -2
  22. package/dist/lib/doctor/types.d.ts +5 -0
  23. package/dist/lib/doctor/types.js +15 -0
  24. package/dist/lib/mesh/live/daemon/index.d.ts +1 -1
  25. package/dist/lib/mesh/live/daemon/index.js +1 -1
  26. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +7 -0
  27. package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -3
  28. package/dist/lib/mesh/live/emit-client.d.ts +6 -0
  29. package/dist/lib/mesh/live/emit-client.js +8 -0
  30. package/dist/lib/mesh/live/enqueue.d.ts +5 -0
  31. package/dist/lib/mesh/live/enqueue.js +19 -0
  32. package/dist/lib/mesh/live/flush.js +5 -3
  33. package/dist/lib/mesh/live/format-spool-line.d.ts +5 -2
  34. package/dist/lib/mesh/live/format-spool-line.js +10 -0
  35. package/dist/lib/mesh/live/report-summary.d.ts +45 -0
  36. package/dist/lib/mesh/live/report-summary.js +222 -0
  37. package/dist/lib/mesh/live/session-event.schema.json +24 -1
  38. package/dist/lib/mesh/live/validate-session-event.d.ts +1 -1
  39. package/dist/lib/mesh/live/validate-session-event.js +1 -1
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.184.0] — 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - Work Mesh transcript-watch now emits `session_end` for hook-covered sessions that have already been quiet for 10 minutes when first seen (the live skip path that left `skippedHook=411 ended=0`). Held/spool rescans stamp last hook activity from each event's `at`, not the scan clock, so leftover `held.jsonl` lines cannot reset the quiet timer.
10
+ - Quiet `session_end` (and SessionEnd flush) send an optional one-sentence summary of the last assistant reply (~140 chars, tokens and secrets-file paths redacted), plus git repo, branch, and PR URLs when cheap.
11
+
12
+ ### Added
13
+
14
+ - `hq doctor --json` includes stable sync `reasonCode` enums. Client-health reports add `checkId: reasonCode` pairs only when the company-scoped `client-health-sync-reason-codes` hq-flags gate is enabled. `hq doctor --fix --json` reports refresh-sync outcomes without raw error details.
15
+
16
+ ## [5.183.0] — 2026-09-25
17
+
18
+ ### Added
19
+
20
+ - `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.
21
+
5
22
  ## [5.182.0] — 2026-09-24
6
23
 
7
24
  ### Fixed
@@ -5495,6 +5495,9 @@ export declare const COMMAND_CATALOG: readonly [{
5495
5495
  }, {
5496
5496
  readonly flags: "--fix";
5497
5497
  readonly description: "Apply the allowlisted safe repairs (backs up first; read-only without this flag).";
5498
+ }, {
5499
+ readonly flags: "--client-health-report";
5500
+ readonly description: "Resolve the opt-in hq-flags gate used by the client-health report hook.";
5498
5501
  }, {
5499
5502
  readonly flags: "--yes";
5500
5503
  readonly description: "Skip the interactive --fix confirmation (non-interactive use).";
@@ -7089,6 +7089,10 @@ export const COMMAND_CATALOG = [
7089
7089
  "flags": "--fix",
7090
7090
  "description": "Apply the allowlisted safe repairs (backs up first; read-only without this flag)."
7091
7091
  },
7092
+ {
7093
+ "flags": "--client-health-report",
7094
+ "description": "Resolve the opt-in hq-flags gate used by the client-health report hook."
7095
+ },
7092
7096
  {
7093
7097
  "flags": "--yes",
7094
7098
  "description": "Skip the interactive --fix confirmation (non-interactive use)."
@@ -55,6 +55,8 @@ export interface RunDoctorOptions {
55
55
  stderr?: (chunk: string) => void;
56
56
  /** Emit the machine-readable JSON document instead of text. Default: false. */
57
57
  json?: boolean;
58
+ /** Resolve hq-flags independently for each company represented in sync results. */
59
+ resolveClientHealthReasonCodesForCompany?: (companySlug: string | null) => Promise<boolean>;
58
60
  /** In text mode, also print every PASS result. Default: false. */
59
61
  verbose?: boolean;
60
62
  /** In text mode, emit ANSI colour. Default: false (no escape sequences). */
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import * as fs from "node:fs";
25
25
  import * as path from "node:path";
26
+ import { Option } from "commander";
26
27
  import { createDefaultRegistry, } from "../lib/doctor/registry.js";
27
28
  import { computeExitCode, renderText, UNKNOWN_PLATFORM, } from "../lib/doctor/report.js";
28
29
  import { buildDoctorJson, renderJson } from "../lib/doctor/json-output.js";
@@ -30,6 +31,10 @@ import { detectPlatform } from "../lib/doctor/platform.js";
30
31
  import { DEEP_FAMILY_ID, DEEP_FAMILY_TITLE, runDeepGuardTests, } from "../lib/doctor/deep/executor.js";
31
32
  import { PARITY_FAMILY_ID, PARITY_FAMILY_TITLE, runParityReplay, } from "../lib/doctor/deep/parity.js";
32
33
  import { applyFixes } from "../lib/doctor/fix/apply.js";
34
+ import { renderDoctorFixJson } from "../lib/doctor/fix/json-output.js";
35
+ import { clientHealthReasonCodesEnabled as isActiveCompanyClientHealthReasonCodesEnabled, clientHealthReasonCodesEnabledForCompany, } from "../lib/doctor/client-health-reason-codes-flag.js";
36
+ import { isCompanyJournalSlug } from "../lib/doctor/checks/sync-health.js";
37
+ import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
33
38
  import * as readline from "node:readline";
34
39
  import { runSideEffectTests } from "../lib/doctor/deep/sandbox.js";
35
40
  /**
@@ -157,7 +162,16 @@ export async function runDoctor(options = {}) {
157
162
  });
158
163
  }
159
164
  if (options.json) {
160
- write(renderJson(buildDoctorJson({ hqRoot, families, platform, scope })));
165
+ const clientHealthReasonCodesEnabledCheckIds = options.resolveClientHealthReasonCodesForCompany
166
+ ? await resolveClientHealthReasonCodesCheckIds(families, options.resolveClientHealthReasonCodesForCompany)
167
+ : undefined;
168
+ write(renderJson(buildDoctorJson({
169
+ hqRoot,
170
+ families,
171
+ platform,
172
+ scope,
173
+ clientHealthReasonCodesEnabledCheckIds,
174
+ })));
161
175
  }
162
176
  else {
163
177
  write(renderText({
@@ -172,6 +186,61 @@ export async function runDoctor(options = {}) {
172
186
  // UNTESTED, NA, and KNOWN-DEFECT are reported but never fail the command.
173
187
  return { exitCode: computeExitCode(families), hqRoot, families };
174
188
  }
189
+ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompany) {
190
+ const checksByCompany = new Map();
191
+ for (const run of families) {
192
+ if (run.family.id !== "sync")
193
+ continue;
194
+ for (const result of run.results) {
195
+ if (result.reasonCode === undefined ||
196
+ (result.status !== "FAIL" && result.status !== "WARN")) {
197
+ continue;
198
+ }
199
+ const scoped = /^sync\.(?:journal|manifest)\.(.+)$/.exec(result.checkId);
200
+ const companySlug = scoped && isCompanyJournalSlug(scoped[1]) ? scoped[1] : null;
201
+ const checkIds = checksByCompany.get(companySlug) ?? [];
202
+ checkIds.push(result.checkId);
203
+ checksByCompany.set(companySlug, checkIds);
204
+ }
205
+ }
206
+ const enabledChecks = new Set();
207
+ const decisions = await Promise.all([...checksByCompany.entries()].map(async ([companySlug, checkIds]) => ({
208
+ checkIds,
209
+ enabled: await resolveForCompany(companySlug),
210
+ })));
211
+ for (const decision of decisions) {
212
+ if (decision.enabled) {
213
+ for (const checkId of decision.checkIds)
214
+ enabledChecks.add(checkId);
215
+ }
216
+ }
217
+ return enabledChecks;
218
+ }
219
+ function createClientHealthReasonCodeCompanyResolver(hqRoot) {
220
+ const companies = readCompaniesManifestMap(hqRoot);
221
+ return async (companySlug) => {
222
+ if (companySlug === null) {
223
+ return isActiveCompanyClientHealthReasonCodesEnabled();
224
+ }
225
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug)) {
226
+ return false;
227
+ }
228
+ const manifestUid = companies?.[companySlug]?.cloud_uid;
229
+ const activeSlug = process.env.HQ_COMPANY_SLUG?.trim() || "";
230
+ const activeUid = process.env.HQ_COMPANY_UID?.trim() || "";
231
+ const companyUid = typeof manifestUid === "string" && manifestUid.trim()
232
+ ? manifestUid.trim()
233
+ : companySlug === activeSlug
234
+ ? activeUid
235
+ : "";
236
+ if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid))
237
+ return false;
238
+ return clientHealthReasonCodesEnabledForCompany({
239
+ companyUid,
240
+ companySlug,
241
+ });
242
+ };
243
+ }
175
244
  /** Register the top-level `hq doctor` command so it appears in `hq --help`. */
176
245
  export function registerDoctorCommand(program) {
177
246
  program
@@ -186,6 +255,7 @@ export function registerDoctorCommand(program) {
186
255
  .option("--company <slug>", "Company slug for integrations checks (otherwise resolves your single active company).")
187
256
  .option("--only <families>", "Run only these comma-separated check families (e.g. hooks). Unknown names are rejected; --json then records the scope.")
188
257
  .option("--fix", "Apply the allowlisted safe repairs (backs up first; read-only without this flag).")
258
+ .addOption(new Option("--client-health-report", "Resolve the opt-in hq-flags gate used by the client-health report hook.").hideHelp())
189
259
  .option("--yes", "Skip the interactive --fix confirmation (non-interactive use).")
190
260
  .option("--force", "Let --fix run despite uncommitted changes under .claude/, .codex/, or .grok/.")
191
261
  .action(async (opts) => {
@@ -208,8 +278,17 @@ export function registerDoctorCommand(program) {
208
278
  if (opts.fix === true) {
209
279
  const hqRoot = resolveHqRoot();
210
280
  if (!hqRoot) {
211
- process.stderr.write(`hq doctor --fix: not inside an HQ tree.\n` +
212
- ` Looking for a directory containing all of: ${HQ_ROOT_MARKERS.join(", ")}\n`);
281
+ if (opts.json === true) {
282
+ process.stdout.write(renderDoctorFixJson({
283
+ exitCode: 1,
284
+ refused: "outside-hq-tree",
285
+ applied: [],
286
+ }));
287
+ }
288
+ else {
289
+ process.stderr.write(`hq doctor --fix: not inside an HQ tree.\n` +
290
+ ` Looking for a directory containing all of: ${HQ_ROOT_MARKERS.join(", ")}\n`);
291
+ }
213
292
  process.exitCode = 1;
214
293
  return;
215
294
  }
@@ -217,8 +296,14 @@ export function registerDoctorCommand(program) {
217
296
  hqRoot,
218
297
  yes: opts.yes === true,
219
298
  force: opts.force === true,
220
- confirm: promptYesNo,
299
+ confirm: () => promptYesNo(opts.json === true ? process.stderr : process.stdout),
300
+ ...(opts.json === true
301
+ ? { stdout: () => { }, stderr: () => { } }
302
+ : {}),
221
303
  });
304
+ if (opts.json === true) {
305
+ process.stdout.write(renderDoctorFixJson(fixResult));
306
+ }
222
307
  process.exitCode = fixResult.exitCode;
223
308
  return;
224
309
  }
@@ -237,6 +322,14 @@ export function registerDoctorCommand(program) {
237
322
  id: detection.platform,
238
323
  evidence: detection.evidence,
239
324
  };
325
+ const clientHealthReasonCodesResolver = json && opts.clientHealthReport === true
326
+ ? (() => {
327
+ const hqRoot = resolveHqRoot();
328
+ return hqRoot
329
+ ? createClientHealthReasonCodeCompanyResolver(hqRoot)
330
+ : undefined;
331
+ })()
332
+ : undefined;
240
333
  const result = await runDoctor({
241
334
  json,
242
335
  verbose: opts.verbose === true,
@@ -247,6 +340,7 @@ export function registerDoctorCommand(program) {
247
340
  liveRuntimes: opts.liveRuntimes === true,
248
341
  company: opts.company,
249
342
  only: parseOnly(opts.only),
343
+ resolveClientHealthReasonCodesForCompany: clientHealthReasonCodesResolver,
250
344
  });
251
345
  // Set the exit code rather than calling process.exit, so the CLI's
252
346
  // normal shutdown (telemetry flush) still runs. Non-zero means either an
@@ -272,12 +366,12 @@ function parseOnly(value) {
272
366
  * (so a piped run without `--yes` writes nothing) and on anything other than an
273
367
  * explicit yes.
274
368
  */
275
- function promptYesNo() {
369
+ function promptYesNo(output = process.stdout) {
276
370
  if (!process.stdin.isTTY)
277
371
  return Promise.resolve(false);
278
372
  const rl = readline.createInterface({
279
373
  input: process.stdin,
280
- output: process.stdout,
374
+ output,
281
375
  });
282
376
  return new Promise((resolve) => {
283
377
  rl.question("Apply these repairs? [y/N] ", (answer) => {
@@ -29,6 +29,7 @@ const MEMBERSHIP_VALIDATION_TIMEOUT_MS = 5_000;
29
29
  import { buildEmitEvidence, machineEmitDisposition } from "../lib/mesh/live/emit-evidence.js";
30
30
  import { appendHeldLine } from "../lib/mesh/live/spool.js";
31
31
  import { createEmitPoster, buildMeshEmitEvent } from "../lib/mesh/live/emit-client.js";
32
+ import { enrichSessionEndEvent } from "../lib/mesh/live/report-summary.js";
32
33
  import { emitEvents, formatEmitRejectionLines } from "../lib/mesh/live/emit.js";
33
34
  import { readEmitRetry, writeEmitRetry } from "../lib/mesh/live/emit-retry.js";
34
35
  import { generateUlid } from "../lib/mesh/live/ulid.js";
@@ -1533,6 +1534,14 @@ async function runSessionEnqueue(cliKind, opts) {
1533
1534
  task: opts.task,
1534
1535
  env: process.env,
1535
1536
  });
1537
+ const reportFields = kind === "session_end"
1538
+ ? enrichSessionEndEvent({
1539
+ kind,
1540
+ sessionId: sessionId,
1541
+ cwd: opts.cwd,
1542
+ summary: opts.summary,
1543
+ })
1544
+ : {};
1536
1545
  const event = buildMeshEmitEvent({
1537
1546
  eventId: opts.eventId?.trim() || generateUlid(Date.now()),
1538
1547
  kind: kind,
@@ -1546,7 +1555,15 @@ async function runSessionEnqueue(cliKind, opts) {
1546
1555
  taskId: opts.taskId,
1547
1556
  status: opts.status,
1548
1557
  reason: opts.reason,
1549
- summary: opts.summary,
1558
+ summary: opts.summary ||
1559
+ (typeof reportFields.summary === "string" ? reportFields.summary : undefined),
1560
+ repo: typeof reportFields.repo === "string" ? reportFields.repo : undefined,
1561
+ gitBranch: typeof reportFields.gitBranch === "string"
1562
+ ? reportFields.gitBranch
1563
+ : undefined,
1564
+ prUrls: Array.isArray(reportFields.prUrls)
1565
+ ? reportFields.prUrls
1566
+ : undefined,
1550
1567
  evidence,
1551
1568
  });
1552
1569
  const meshRoot = workMeshHomeRoot();
@@ -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;