@indigoai-us/hq-cli 5.183.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 (36) hide show
  1. package/CHANGELOG.md +11 -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/doctor/checks/sync-health.js +13 -0
  8. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +26 -0
  9. package/dist/lib/doctor/client-health-reason-codes-flag.js +94 -0
  10. package/dist/lib/doctor/fix/apply.d.ts +5 -0
  11. package/dist/lib/doctor/fix/apply.js +28 -3
  12. package/dist/lib/doctor/fix/json-output.d.ts +20 -0
  13. package/dist/lib/doctor/fix/json-output.js +22 -0
  14. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +12 -0
  15. package/dist/lib/doctor/fix/refresh-sync-outcome.js +25 -0
  16. package/dist/lib/doctor/json-output.d.ts +7 -1
  17. package/dist/lib/doctor/json-output.js +6 -2
  18. package/dist/lib/doctor/types.d.ts +5 -0
  19. package/dist/lib/doctor/types.js +15 -0
  20. package/dist/lib/mesh/live/daemon/index.d.ts +1 -1
  21. package/dist/lib/mesh/live/daemon/index.js +1 -1
  22. package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +7 -0
  23. package/dist/lib/mesh/live/daemon/transcript-watch.js +25 -3
  24. package/dist/lib/mesh/live/emit-client.d.ts +6 -0
  25. package/dist/lib/mesh/live/emit-client.js +8 -0
  26. package/dist/lib/mesh/live/enqueue.d.ts +5 -0
  27. package/dist/lib/mesh/live/enqueue.js +19 -0
  28. package/dist/lib/mesh/live/flush.js +5 -3
  29. package/dist/lib/mesh/live/format-spool-line.d.ts +5 -2
  30. package/dist/lib/mesh/live/format-spool-line.js +10 -0
  31. package/dist/lib/mesh/live/report-summary.d.ts +45 -0
  32. package/dist/lib/mesh/live/report-summary.js +222 -0
  33. package/dist/lib/mesh/live/session-event.schema.json +24 -1
  34. package/dist/lib/mesh/live/validate-session-event.d.ts +1 -1
  35. package/dist/lib/mesh/live/validate-session-event.js +1 -1
  36. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ /** Classify process/HTTP results without retaining their free-text details. */
2
+ export function classifyRefreshSyncOutcome(observation) {
3
+ const details = String(observation.stdout ?? "") + "\n" + String(observation.stderr ?? "");
4
+ const errorCode = observation.errorCode ??
5
+ /\b(ETIMEDOUT|ETIME|EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)\b/.exec(details)?.[1];
6
+ if (errorCode === "ETIMEDOUT" ||
7
+ errorCode === "ETIME" ||
8
+ observation.signal === "SIGTERM") {
9
+ return "pull-timeout";
10
+ }
11
+ if (!errorCode && observation.status === 0) {
12
+ return "pull-succeeded";
13
+ }
14
+ const httpStatus = /(?:HTTP(?:\/\d(?:\.\d)?)?\s+|status(?:\s+code)?\s*[=: ]\s*)([45]\d\d)\b/i.exec(details)?.[1] ?? "";
15
+ if (httpStatus.startsWith("4"))
16
+ return "pull-failed:4xx";
17
+ if (httpStatus.startsWith("5"))
18
+ return "pull-failed:5xx";
19
+ if (errorCode &&
20
+ /^(?:EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNABORTED)$/.test(errorCode)) {
21
+ return "pull-failed:network";
22
+ }
23
+ return "pull-failed:other";
24
+ }
25
+ //# sourceMappingURL=refresh-sync-outcome.js.map
@@ -18,7 +18,7 @@
18
18
  * per-item result shape where both are optional.
19
19
  */
20
20
  import { type DoctorPlatform, type StatusCounts } from "./report.js";
21
- import type { DoctorStatus, FamilyRun } from "./types.js";
21
+ import type { DoctorStatus, FamilyRun, SyncReasonCode } from "./types.js";
22
22
  /**
23
23
  * The `--json` schema version. Bump on any breaking change to the document
24
24
  * shape; the snapshot test in report.test.ts fails until the bump is
@@ -50,6 +50,10 @@ export interface DoctorJsonResult {
50
50
  family: string;
51
51
  /** Stable machine id for the check, e.g. `hooks.settings-present`. */
52
52
  checkId: string;
53
+ /** Stable classification for the sync family; contains no customer data. */
54
+ reasonCode?: SyncReasonCode;
55
+ /** Present only when this result's company opted in through hq-flags. */
56
+ clientHealthReasonCodesEnabled?: true;
53
57
  /** What was inspected. Present only when the result carries one. */
54
58
  target?: string;
55
59
  /** Human-readable explanation. */
@@ -92,6 +96,8 @@ export interface BuildDoctorJsonInput {
92
96
  platform?: DoctorPlatform;
93
97
  /** Families this run was restricted to. Omitted on a full run. */
94
98
  scope?: readonly string[];
99
+ /** Check ids whose own company enabled diagnostic reason codes in hq-flags. */
100
+ clientHealthReasonCodesEnabledCheckIds?: ReadonlySet<string>;
95
101
  }
96
102
  /** Assemble the machine-readable document from a completed run. */
97
103
  export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
@@ -34,7 +34,7 @@ export function buildDoctorJson(input) {
34
34
  const results = [];
35
35
  for (const run of input.families) {
36
36
  for (const result of run.results) {
37
- results.push(toJsonResult(run.family.id, result));
37
+ results.push(toJsonResult(run.family.id, result, input.clientHealthReasonCodesEnabledCheckIds));
38
38
  }
39
39
  }
40
40
  return {
@@ -48,12 +48,16 @@ export function buildDoctorJson(input) {
48
48
  };
49
49
  }
50
50
  /** Convert one {@link CheckResult} into its JSON form, omitting empty optionals. */
51
- function toJsonResult(familyId, result) {
51
+ function toJsonResult(familyId, result, clientHealthReasonCodesEnabledCheckIds) {
52
52
  const remediation = deriveRemediation(result);
53
53
  return {
54
54
  status: result.status,
55
55
  family: familyId,
56
56
  checkId: result.checkId,
57
+ ...(result.reasonCode !== undefined ? { reasonCode: result.reasonCode } : {}),
58
+ ...(clientHealthReasonCodesEnabledCheckIds?.has(result.checkId)
59
+ ? { clientHealthReasonCodesEnabled: true }
60
+ : {}),
57
61
  ...(result.target !== undefined ? { target: result.target } : {}),
58
62
  message: result.message,
59
63
  ...(result.remediation !== undefined
@@ -18,6 +18,9 @@ export declare const DOCTOR_STATUSES: readonly ["PASS", "FAIL", "WARN", "UNTESTE
18
18
  * the type system rejects any other string at every call site.
19
19
  */
20
20
  export type DoctorStatus = (typeof DOCTOR_STATUSES)[number];
21
+ /** Stable, privacy-safe diagnostics emitted by the sync health family. */
22
+ export declare const SYNC_REASON_CODES: readonly ["core-version-unavailable", "core-update-available", "no-local-tree", "never-synced", "invalid-last-sync", "stale-threshold", "manifest-unavailable", "manifest-state-unavailable", "no-manifest-scopes", "never-uploaded", "invalid-upload-time", "unresolved-company-uid"];
23
+ export type SyncReasonCode = (typeof SYNC_REASON_CODES)[number];
21
24
  /** Runtime type guard: true only for a member of the fixed vocabulary. */
22
25
  export declare function isDoctorStatus(value: unknown): value is DoctorStatus;
23
26
  /** A single per-item result produced by a check family. */
@@ -26,6 +29,8 @@ export interface CheckResult {
26
29
  status: DoctorStatus;
27
30
  /** Stable machine id for this item, e.g. `hooks.settings-present`. */
28
31
  checkId: string;
32
+ /** Closed diagnostic enum; never contains paths, company slugs, or prose. */
33
+ reasonCode?: SyncReasonCode;
29
34
  /** What was inspected — a path, hook id, etc. Optional. */
30
35
  target?: string;
31
36
  /** Human-readable one-line explanation of the result. */
@@ -21,6 +21,21 @@ export const DOCTOR_STATUSES = [
21
21
  "UNKNOWN",
22
22
  "KNOWN-DEFECT",
23
23
  ];
24
+ /** Stable, privacy-safe diagnostics emitted by the sync health family. */
25
+ export const SYNC_REASON_CODES = [
26
+ "core-version-unavailable",
27
+ "core-update-available",
28
+ "no-local-tree",
29
+ "never-synced",
30
+ "invalid-last-sync",
31
+ "stale-threshold",
32
+ "manifest-unavailable",
33
+ "manifest-state-unavailable",
34
+ "no-manifest-scopes",
35
+ "never-uploaded",
36
+ "invalid-upload-time",
37
+ "unresolved-company-uid",
38
+ ];
24
39
  /** Runtime type guard: true only for a member of the fixed vocabulary. */
25
40
  export function isDoctorStatus(value) {
26
41
  return (typeof value === "string" &&
@@ -22,6 +22,6 @@ export type { DaemonDoctorDeps, DaemonDoctorReport } from "./doctor.js";
22
22
  export { scopeBundleToAgentIdentity, } from "./credentials.js";
23
23
  export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
24
24
  export type { DaemonHandle, DaemonRunDeps } from "./run.js";
25
- export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
25
+ export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
26
26
  export type { DiscoveredTranscript, DiscoverScanMeta, DiscoverTranscriptsResult, TranscriptDiscoverCache, TranscriptFs, TranscriptKind, TranscriptWatchDeps, TranscriptWatchTickResult, } from "./transcript-watch.js";
27
27
  //# sourceMappingURL=index.d.ts.map
@@ -11,5 +11,5 @@ export { daemonConfigPath, findAncestorHqRoot, isInstallableHqRoot, readDaemonCo
11
11
  export { UNHEALTHY_SPOOL_AGE_MS, collectDaemonDoctor, formatCliIdentityLines, formatCliMismatchLine, formatDaemonDoctor, listHqBinariesOnPath, } from "./doctor.js";
12
12
  export { scopeBundleToAgentIdentity, } from "./credentials.js";
13
13
  export { FLUSH_INTERVAL_MS, DAEMON_VERSION_DRIFT_EXIT_CODE, SPOOL_DEBOUNCE_MS, runMeshDaemon, } from "./run.js";
14
- export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
14
+ export { TRANSCRIPT_ADAPTER_VERSION, TRANSCRIPT_EVENT_SOURCE, TRANSCRIPT_FRESH_MS, TRANSCRIPT_HOOK_COVER_MS, TRANSCRIPT_MARKER, TRANSCRIPT_SESSION_END_MS, TRANSCRIPT_HOOK_QUIET_ADOPT_MS, TRANSCRIPT_TURN_QUIET_MS, TRANSCRIPT_WATCH_BACKOFF_DURATION_MS, TRANSCRIPT_WATCH_BACKOFF_ENTRIES, TRANSCRIPT_WATCH_ENTRY_CAP, TRANSCRIPT_WATCH_INTERVAL_MAX_MS, TRANSCRIPT_WATCH_INTERVAL_MS, TRANSCRIPT_WATCH_REVALIDATE_MS, TranscriptWatcher, createTranscriptDiscoverCache, decodeClaudeProjectDirName, defaultTranscriptFs, discoverTranscripts, discoverTranscriptsWithMeta, isTranscriptHookCovered, nextTranscriptWatchIntervalMs, noteHookSessionsFromSpoolFile, resolveTranscriptRegistration, sessionIdFromClaudeTranscriptPath, sessionIdFromCodexTranscriptPath, sortDiscoveries, } from "./transcript-watch.js";
15
15
  //# sourceMappingURL=index.js.map
@@ -62,6 +62,13 @@ export declare const TRANSCRIPT_START_RETRY_BASE_MS = 30000;
62
62
  export declare const TRANSCRIPT_START_RETRY_MAX_MS = 120000;
63
63
  /** Ignore cold transcripts whose mtime is older than this on first sight. */
64
64
  export declare const TRANSCRIPT_FRESH_MS: number;
65
+ /**
66
+ * Hook-covered sessions skipped as "stale" on first sight never got a quiet
67
+ * session_end (live: skippedHook=411, ended=0). Adopt them for this long after
68
+ * last activity so a 10–120 min quiet window still reports after a daemon
69
+ * restart or a missed track. Older than this stays silent (the 6h reaper).
70
+ */
71
+ export declare const TRANSCRIPT_HOOK_QUIET_ADOPT_MS: number;
65
72
  export declare const TRANSCRIPT_MARKER: "from transcript";
66
73
  export declare const TRANSCRIPT_ADAPTER_VERSION: string;
67
74
  /** Network `source` stamped on every synthesized event (AMEND-001). */
@@ -67,6 +67,13 @@ export const TRANSCRIPT_START_RETRY_BASE_MS = 30_000;
67
67
  export const TRANSCRIPT_START_RETRY_MAX_MS = 120_000;
68
68
  /** Ignore cold transcripts whose mtime is older than this on first sight. */
69
69
  export const TRANSCRIPT_FRESH_MS = TRANSCRIPT_SESSION_END_MS;
70
+ /**
71
+ * Hook-covered sessions skipped as "stale" on first sight never got a quiet
72
+ * session_end (live: skippedHook=411, ended=0). Adopt them for this long after
73
+ * last activity so a 10–120 min quiet window still reports after a daemon
74
+ * restart or a missed track. Older than this stays silent (the 6h reaper).
75
+ */
76
+ export const TRANSCRIPT_HOOK_QUIET_ADOPT_MS = 2 * 60 * 60 * 1000;
70
77
  export const TRANSCRIPT_MARKER = "from transcript";
71
78
  export const TRANSCRIPT_ADAPTER_VERSION = CLI_VERSION;
72
79
  /** Network `source` stamped on every synthesized event (AMEND-001). */
@@ -521,13 +528,24 @@ export function noteHookSessionsFromSpoolFile(spoolPath, lastHookEventAt, atMs,
521
528
  if (!line.trim())
522
529
  continue;
523
530
  try {
524
- const ev = JSON.parse(line);
531
+ const raw = JSON.parse(line);
532
+ const ev = raw.event && typeof raw.event === "object" ? raw.event : raw;
525
533
  if (typeof ev.sessionId !== "string" || !ev.sessionId)
526
534
  continue;
527
535
  // Synthesized transcript events carry source=transcript — not hooks.
528
536
  if (ev.source === TRANSCRIPT_EVENT_SOURCE)
529
537
  continue;
530
- lastHookEventAt.set(ev.sessionId, atMs);
538
+ let eventAt = atMs;
539
+ const atRaw = typeof ev.at === "string" ? ev.at : undefined;
540
+ if (atRaw) {
541
+ const parsed = Date.parse(atRaw);
542
+ if (Number.isFinite(parsed))
543
+ eventAt = parsed;
544
+ }
545
+ const prev = lastHookEventAt.get(ev.sessionId);
546
+ if (prev === undefined || eventAt > prev) {
547
+ lastHookEventAt.set(ev.sessionId, eventAt);
548
+ }
531
549
  noted.push(ev.sessionId);
532
550
  }
533
551
  catch {
@@ -739,7 +757,10 @@ export class TranscriptWatcher {
739
757
  const lastActivity = Math.max(d.mtimeMs, hookAt ?? 0);
740
758
  let track = this.tracked.get(d.path);
741
759
  if (!track) {
742
- if (nowMs - lastActivity > TRANSCRIPT_FRESH_MS)
760
+ // Cold transcripts stay silent. Hook-covered sessions that already sat
761
+ // quiet past TRANSCRIPT_SESSION_END_MS still need one session_end — the
762
+ // live skip path returned here and never tracked them (US-051).
763
+ if (nowMs - lastActivity > TRANSCRIPT_HOOK_QUIET_ADOPT_MS)
743
764
  return;
744
765
  track = {
745
766
  path: d.path,
@@ -857,6 +878,7 @@ export class TranscriptWatcher {
857
878
  root: this.deps.workMeshRoot,
858
879
  env: this.deps.env,
859
880
  now: () => new Date(nowMs),
881
+ ...(kind === "session_end" ? { transcriptPath: track.path } : {}),
860
882
  };
861
883
  this.enqueueFn(opts);
862
884
  }
@@ -74,6 +74,9 @@ export interface MeshEmitEvent {
74
74
  status?: "queued" | "in_progress" | "review" | "done";
75
75
  reason?: string;
76
76
  summary?: string;
77
+ repo?: string;
78
+ gitBranch?: string;
79
+ prUrls?: string[];
77
80
  evidence?: EmitEvidence;
78
81
  }
79
82
  export interface BuildMeshEmitEventInput {
@@ -90,6 +93,9 @@ export interface BuildMeshEmitEventInput {
90
93
  status?: "queued" | "in_progress" | "review" | "done";
91
94
  reason?: string;
92
95
  summary?: string;
96
+ repo?: string;
97
+ gitBranch?: string;
98
+ prUrls?: string[];
93
99
  evidence?: EmitEvidence;
94
100
  }
95
101
  /**
@@ -192,6 +192,14 @@ export function buildMeshEmitEvent(input) {
192
192
  const summary = keep(input.summary);
193
193
  if (summary)
194
194
  event.summary = summary;
195
+ const repo = keep(input.repo);
196
+ if (repo)
197
+ event.repo = repo;
198
+ const gitBranch = keep(input.gitBranch);
199
+ if (gitBranch)
200
+ event.gitBranch = gitBranch;
201
+ if (input.prUrls && input.prUrls.length > 0)
202
+ event.prUrls = input.prUrls;
195
203
  if (input.evidence && Object.keys(input.evidence).length > 0) {
196
204
  event.evidence = input.evidence;
197
205
  }
@@ -23,6 +23,11 @@ export interface EnqueueOptions {
23
23
  status?: "queued" | "in_progress" | "review" | "done";
24
24
  reason?: string;
25
25
  summary?: string;
26
+ repo?: string;
27
+ gitBranch?: string;
28
+ prUrls?: string[];
29
+ /** Local only: used to build a session_end summary; never written to the spool. */
30
+ transcriptPath?: string;
26
31
  cwd?: string;
27
32
  hqRoot?: string;
28
33
  companySlug?: string;
@@ -2,6 +2,7 @@
2
2
  * Local session-event enqueue (US-008): validate + append one spool line.
3
3
  * No network. Shared line format with work-mesh-enqueue.sh.
4
4
  */
5
+ import { enrichSessionEndEvent } from "./report-summary.js";
5
6
  import { validateSessionEvent } from "./validate-session-event.js";
6
7
  import { formatSpoolLine, } from "./format-spool-line.js";
7
8
  import { generateUlid } from "./ulid.js";
@@ -65,6 +66,12 @@ export function enqueueSessionEvent(opts) {
65
66
  event.reason = opts.reason;
66
67
  if (opts.summary)
67
68
  event.summary = opts.summary;
69
+ if (opts.repo)
70
+ event.repo = opts.repo;
71
+ if (opts.gitBranch)
72
+ event.gitBranch = opts.gitBranch;
73
+ if (opts.prUrls && opts.prUrls.length > 0)
74
+ event.prUrls = opts.prUrls;
68
75
  if (opts.cwd)
69
76
  event.cwd = opts.cwd;
70
77
  if (opts.hqRoot)
@@ -77,6 +84,18 @@ export function enqueueSessionEvent(opts) {
77
84
  event.task = opts.task;
78
85
  if (opts.toolWrites !== undefined)
79
86
  event.toolWrites = opts.toolWrites;
87
+ if (event.kind === "session_end") {
88
+ const enriched = enrichSessionEndEvent(event, { transcriptPath: opts.transcriptPath });
89
+ if (typeof enriched.summary === "string")
90
+ event.summary = enriched.summary;
91
+ if (typeof enriched.repo === "string")
92
+ event.repo = enriched.repo;
93
+ if (typeof enriched.gitBranch === "string")
94
+ event.gitBranch = enriched.gitBranch;
95
+ if (Array.isArray(enriched.prUrls) && enriched.prUrls.length > 0) {
96
+ event.prUrls = enriched.prUrls;
97
+ }
98
+ }
80
99
  const candidate = {
81
100
  ...event,
82
101
  v: 1,
@@ -20,6 +20,7 @@ import { repairAckedRegisterStates } from "../../work-context/outbox.js";
20
20
  import { workContextRoot } from "../../work-context/paths.js";
21
21
  import { defaultSleep, FLUSH_MAX_ATTEMPTS, fullJitterDelayMs, } from "./backoff.js";
22
22
  import { LOCAL_ONLY_FIELDS, stripLocalOnlyFields } from "./format-spool-line.js";
23
+ import { enrichSessionEndEvent } from "./report-summary.js";
23
24
  import { SESSION_EVENTS_BATCH_MAX, WRITABLE_CONTEXT_STATUSES, parseSessionEventsBatchAck, } from "./session-events-client.js";
24
25
  import { appendDeadLetterLine, appendDroppedReceipt, appendHeldLine, appendSpoolLine, claimHeld, claimSpool, countJsonlLines, ensureSpoolFile, recoverOrphanClaims, removeClaimFile, } from "./spool.js";
25
26
  import { workMeshHeldPath, workMeshRoot } from "./paths.js";
@@ -316,7 +317,7 @@ export async function flushSessionEvents(deps) {
316
317
  // presence-leave). Untouched events stay in pendingRestore and are put
317
318
  // back on the spool below.
318
319
  const flushItems = remainingItems.slice(0, SESSION_EVENTS_MAX_PER_SESSION_PER_FLUSH);
319
- const stripped = flushItems.map((item) => stripLocalOnlyFields(item.event));
320
+ const stripped = flushItems.map((item) => stripLocalOnlyFields(enrichSessionEndEvent(item.event)));
320
321
  for (let i = 0; i < stripped.length; i += SESSION_EVENTS_BATCH_MAX) {
321
322
  const chunk = stripped.slice(i, i + SESSION_EVENTS_BATCH_MAX);
322
323
  const originalChunk = flushItems.slice(i, i + SESSION_EVENTS_BATCH_MAX);
@@ -608,7 +609,7 @@ function synthesizeSessionEnd(sessionId, sample, seq, at) {
608
609
  const harness = harnessRaw && SESSION_EVENT_HARNESSES.has(harnessRaw)
609
610
  ? harnessRaw
610
611
  : "claude-code";
611
- return stripLocalOnlyFields({
612
+ return stripLocalOnlyFields(enrichSessionEndEvent({
612
613
  v: 1,
613
614
  eventId: generateUlid(),
614
615
  kind: "session_end",
@@ -617,7 +618,8 @@ function synthesizeSessionEnd(sessionId, sample, seq, at) {
617
618
  adapterVersion: eventText(sample, "adapterVersion") || "1.0.0",
618
619
  at,
619
620
  seq,
620
- });
621
+ cwd: eventText(sample, "cwd"),
622
+ }));
621
623
  }
622
624
  async function postPresenceLeaveIfNeeded(input) {
623
625
  const leave = input.state?.presenceLeave;
@@ -5,7 +5,7 @@
5
5
  export declare const LOCAL_ONLY_FIELDS: readonly ["cwd", "hqRoot", "companySlug", "project", "task", "toolWrites"];
6
6
  export type LocalOnlyField = (typeof LOCAL_ONLY_FIELDS)[number];
7
7
  /** Network + local key order matching work-mesh-enqueue.sh. */
8
- export declare const SPOOL_KEY_ORDER: readonly ["v", "eventId", "kind", "sessionId", "harness", "adapterVersion", "runtimeVersion", "source", "at", "seq", "taskId", "status", "reason", "summary", "cwd", "hqRoot", "companySlug", "project", "task", "toolWrites"];
8
+ export declare const SPOOL_KEY_ORDER: readonly ["v", "eventId", "kind", "sessionId", "harness", "adapterVersion", "runtimeVersion", "source", "at", "seq", "taskId", "status", "reason", "summary", "repo", "gitBranch", "prUrls", "cwd", "hqRoot", "companySlug", "project", "task", "toolWrites"];
9
9
  export type SessionEventKind = "session_start" | "turn_start" | "turn_end" | "session_end" | "task_status" | "blocked" | "note";
10
10
  export type SessionEventHarness = "claude-code" | "claude-desktop" | "codex" | "grok" | "hq-sessions" | "agent-box" | "agents-v2";
11
11
  export type SessionEventSource = "hooks" | "transcript";
@@ -24,6 +24,9 @@ export interface SpoolEventInput {
24
24
  status?: "queued" | "in_progress" | "review" | "done";
25
25
  reason?: string;
26
26
  summary?: string;
27
+ repo?: string;
28
+ gitBranch?: string;
29
+ prUrls?: string[];
27
30
  cwd?: string;
28
31
  hqRoot?: string;
29
32
  companySlug?: string;
@@ -35,7 +38,7 @@ export interface SpoolEventInput {
35
38
  * Build a compact JSON object in SPOOL_KEY_ORDER, omitting absent optional
36
39
  * strings. `toolWrites` is included when it is a finite number >= 0 (including 0).
37
40
  */
38
- export declare function buildOrderedSpoolObject(input: SpoolEventInput): Record<string, string | number>;
41
+ export declare function buildOrderedSpoolObject(input: SpoolEventInput): Record<string, string | number | string[]>;
39
42
  /** Compact JSON line body (no trailing newline). */
40
43
  export declare function formatSpoolLine(input: SpoolEventInput): string;
41
44
  export declare function stripLocalOnlyFields(event: Record<string, unknown>): Record<string, unknown>;
@@ -26,6 +26,9 @@ export const SPOOL_KEY_ORDER = [
26
26
  "status",
27
27
  "reason",
28
28
  "summary",
29
+ "repo",
30
+ "gitBranch",
31
+ "prUrls",
29
32
  "cwd",
30
33
  "hqRoot",
31
34
  "companySlug",
@@ -63,6 +66,13 @@ export function buildOrderedSpoolObject(input) {
63
66
  out.reason = input.reason;
64
67
  if (includeString(input.summary))
65
68
  out.summary = input.summary;
69
+ if (includeString(input.repo))
70
+ out.repo = input.repo;
71
+ if (includeString(input.gitBranch))
72
+ out.gitBranch = input.gitBranch;
73
+ if (Array.isArray(input.prUrls) && input.prUrls.length > 0) {
74
+ out.prUrls = input.prUrls.filter(includeString).slice(0, 5);
75
+ }
66
76
  if (includeString(input.cwd))
67
77
  out.cwd = input.cwd;
68
78
  if (includeString(input.hqRoot))
@@ -0,0 +1,45 @@
1
+ /**
2
+ * US-051 — one-sentence quiet-report fields for session_end.
3
+ *
4
+ * Built locally at the quiet trigger / SessionEnd flush. Never includes file
5
+ * contents. Tokens, secret-looking strings, and secrets-file paths are redacted.
6
+ */
7
+ export declare const REPORT_SUMMARY_MAX_CHARS = 140;
8
+ export declare const REPORT_SUMMARY_SCHEMA_MAX = 280;
9
+ export declare const REPORT_TRANSCRIPT_TAIL_BYTES: number;
10
+ export interface SessionEndReportFields {
11
+ summary?: string;
12
+ repo?: string;
13
+ gitBranch?: string;
14
+ prUrls?: string[];
15
+ }
16
+ export interface BuildSessionEndReportFieldsInput {
17
+ sessionId?: string;
18
+ cwd?: string;
19
+ transcriptPath?: string;
20
+ home?: string;
21
+ /** Injectable git runner (tests). */
22
+ git?: (cwd: string, args: string[]) => string | undefined;
23
+ /** Injectable transcript reader (tests). */
24
+ readTranscript?: (p: string) => string | undefined;
25
+ }
26
+ export declare function redactReportSummary(text: string): string;
27
+ export declare function firstSentence(text: string, max?: number): string;
28
+ export declare function extractLastAssistantText(jsonl: string): string | undefined;
29
+ export declare function readTranscriptTail(transcriptPath: string, maxBytes?: number): string | undefined;
30
+ export declare function collectGitContext(cwd: string, git?: (cwd: string, args: string[]) => string | undefined): {
31
+ repo?: string;
32
+ gitBranch?: string;
33
+ };
34
+ export declare function extractPrUrls(text: string, max?: number): string[];
35
+ export declare function claudeTranscriptPathFor(sessionId: string, cwd: string, home?: string): string;
36
+ export declare function resolveTranscriptPath(input: {
37
+ transcriptPath?: string;
38
+ sessionId?: string;
39
+ cwd?: string;
40
+ home?: string;
41
+ }): string | undefined;
42
+ export declare function buildSessionEndReportFields(input: BuildSessionEndReportFieldsInput): SessionEndReportFields;
43
+ /** Merge report fields onto a session_end event object (no-op for other kinds). */
44
+ export declare function enrichSessionEndEvent(event: Record<string, unknown>, extra?: Pick<BuildSessionEndReportFieldsInput, "transcriptPath" | "home" | "git" | "readTranscript">): Record<string, unknown>;
45
+ //# sourceMappingURL=report-summary.d.ts.map