peaks-loop 4.0.4 → 4.0.5

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 (64) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README-en.md +26 -0
  3. package/README.md +26 -0
  4. package/dist/cli/commands/_register.js +3 -0
  5. package/dist/cli/commands/core/skill-command.js +2 -0
  6. package/dist/cli/commands/dispatch-commands.js +41 -2
  7. package/dist/cli/commands/e2e-verify.d.ts +47 -0
  8. package/dist/cli/commands/e2e-verify.js +211 -0
  9. package/dist/cli/commands/gate-commands.js +17 -4
  10. package/dist/cli/commands/statusline-commands.d.ts +6 -0
  11. package/dist/cli/commands/statusline-commands.js +48 -11
  12. package/dist/cli/commands/sub-agent-shutdown-commands.d.ts +31 -0
  13. package/dist/cli/commands/sub-agent-shutdown-commands.js +110 -0
  14. package/dist/cli/commands/worktree-auth-commands.js +27 -0
  15. package/dist/services/code/auto-compact-orchestrator.d.ts +39 -0
  16. package/dist/services/code/auto-compact-orchestrator.js +358 -41
  17. package/dist/services/code/post-compact-detector.js +12 -4
  18. package/dist/services/code/step-08-gate.js +6 -2
  19. package/dist/services/compact-statusline/compact-lifecycle-store.d.ts +68 -0
  20. package/dist/services/compact-statusline/compact-lifecycle-store.js +222 -0
  21. package/dist/services/compact-statusline/compact-statusline-service.d.ts +25 -5
  22. package/dist/services/compact-statusline/compact-statusline-service.js +244 -54
  23. package/dist/services/context/auto-compact-reader.js +12 -4
  24. package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
  25. package/dist/services/context/build-dispatch-system-prompt.js +33 -2
  26. package/dist/services/dispatch/conflict-replay.d.ts +32 -0
  27. package/dist/services/dispatch/conflict-replay.js +44 -0
  28. package/dist/services/dispatch/dispatch-record-writer.d.ts +31 -1
  29. package/dist/services/dispatch/dispatch-record-writer.js +27 -7
  30. package/dist/services/dispatch/e2e-fixtures.d.ts +18 -0
  31. package/dist/services/dispatch/e2e-fixtures.js +72 -0
  32. package/dist/services/dispatch/merge-back-runner.d.ts +18 -0
  33. package/dist/services/dispatch/merge-back-runner.js +83 -0
  34. package/dist/services/dispatch/post-merge.d.ts +21 -0
  35. package/dist/services/dispatch/post-merge.js +13 -0
  36. package/dist/services/dispatch/service-shutdown.d.ts +22 -0
  37. package/dist/services/dispatch/service-shutdown.js +51 -0
  38. package/dist/services/hooks/presence-marker-detector.js +12 -4
  39. package/dist/services/hooks/worktree-authorization-gate.d.ts +4 -0
  40. package/dist/services/hooks/worktree-authorization-gate.js +22 -0
  41. package/dist/services/skills/skill-statusline-renderer.d.ts +86 -3
  42. package/dist/services/skills/skill-statusline-renderer.js +374 -29
  43. package/dist/services/skills/skill-statusline-service.d.ts +2 -0
  44. package/dist/services/skills/skill-statusline-service.js +34 -5
  45. package/dist/services/skills/sync-service.d.ts +3 -0
  46. package/dist/services/skills/sync-service.js +1 -0
  47. package/dist/services/standards/project-context.d.ts +2 -1
  48. package/dist/services/standards/project-context.js +17 -2
  49. package/dist/services/standards/project-standards-service.d.ts +9 -0
  50. package/dist/services/standards/project-standards-service.js +26 -0
  51. package/dist/services/worktree/dispatch-provenance.d.ts +25 -0
  52. package/dist/services/worktree/dispatch-provenance.js +47 -0
  53. package/dist/services/worktree/git-worktree-parser.d.ts +8 -0
  54. package/dist/services/worktree/git-worktree-parser.js +45 -0
  55. package/dist/services/worktree/host-worktree-reconciler.d.ts +15 -0
  56. package/dist/services/worktree/host-worktree-reconciler.js +39 -0
  57. package/dist/services/worktree/long-path-cleanup.d.ts +10 -0
  58. package/dist/services/worktree/long-path-cleanup.js +38 -0
  59. package/dist/services/worktree/playwright-profile.d.ts +8 -0
  60. package/dist/services/worktree/playwright-profile.js +23 -0
  61. package/dist/services/worktree/worktree-gc-guard.d.ts +8 -0
  62. package/dist/services/worktree/worktree-gc-guard.js +38 -0
  63. package/package.json +4 -4
  64. package/scripts/install-skills.mjs +11 -2
@@ -2,7 +2,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
2
2
  import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
3
3
  import { findProjectRoot } from '../../services/config/config-safety.js';
4
4
  import { buildStatusLineModel, parseStatusLineStdin } from '../../services/skills/skill-statusline-service.js';
5
- import { renderStatusLine } from '../../services/skills/skill-statusline-renderer.js';
5
+ import { renderStatusLine, resolveStatusLineCapability, } from '../../services/skills/skill-statusline-renderer.js';
6
6
  import { applyStatusLineInstall, planStatusLineInstall, removeStatusLineInstall } from '../../services/skills/statusline-settings-service.js';
7
7
  import { readHookStatus as readSettingsStatus } from '../../services/skills/hooks-settings-service.js';
8
8
  import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
@@ -61,6 +61,12 @@ function resolveIdeForCommand(options, projectRoot) {
61
61
  * action (Bug-02 dispatch) and the explicit `render` subcommand. Exported
62
62
  * so a unit test can exercise the JSON / text output paths without going
63
63
  * through commander.
64
+ *
65
+ * Capability resolution is delegated to {@link resolveStatusLineCapability}
66
+ * with only `env` and `isTTY` — the env-driven path is the single first-
67
+ * version source of truth. No CLI flag widens the surface. The model
68
+ * itself is read-only: `buildStatusLineModel` is the sole I/O boundary,
69
+ * and it now also resolves compact state.
64
70
  */
65
71
  export async function runDefaultStatuslineRender(options, io) {
66
72
  const raw = await readStdin();
@@ -69,7 +75,11 @@ export async function runDefaultStatuslineRender(options, io) {
69
75
  ? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
70
76
  : stdin;
71
77
  const model = buildStatusLineModel(seeded, Date.now());
72
- const text = renderStatusLine(model);
78
+ const capability = resolveStatusLineCapability({
79
+ env: process.env,
80
+ isTTY: Boolean(process.stdout.isTTY),
81
+ });
82
+ const text = renderStatusLine(model, { capability });
73
83
  if (options.json === true) {
74
84
  io.stdout(JSON.stringify({ ok: true, command: 'statusline.render', data: { text } }, null, 2));
75
85
  return;
@@ -193,19 +203,46 @@ export function registerStatusLineCommands(program, io) {
193
203
  }
194
204
  });
195
205
  // peaks statusline compact (slice 2026-07-30-compact-visibility)
206
+ //
207
+ // --session-id contract: this flag is registered on the compact
208
+ // subcommand but NOT on the default `peaks statusline` render path.
209
+ // The flag is the internal-supported surface for the QA / LLM layer
210
+ // (the integration test, the QA criteria, and any future
211
+ // peaks-loop-internal tool that wants to read a non-canonical session's
212
+ // compact state). The IDE consumer (Claude Code) does NOT need this
213
+ // flag — its primary-line path binds to the canonical session via
214
+ // `getSessionIdCanonical(projectRoot)`. Documenting both paths here
215
+ // keeps the surface small and consistent: the compact subcommand is
216
+ // the "I have a specific session id in mind" surface; the default
217
+ // render is the "use the canonical binding" surface.
196
218
  addJsonOption(statusline
197
219
  .command('compact')
198
220
  .description('Render the single-line auto-compact indicator the LLM embeds ' +
199
221
  "in Claude Code's statusline. Default output is plain text; " +
200
- 'pass --json for the structured envelope.')
222
+ 'pass --json for the structured envelope. ' +
223
+ 'Reads the lifecycle record for the canonical session ' +
224
+ '(`.peaks/_runtime/<sid>/compact-lifecycle.json`); pass ' +
225
+ '--session-id to target a non-canonical session explicitly.')
201
226
  .option('--project <path>', 'project root (auto-detected from cwd when omitted)')
202
- .option('--session-id <sid>', 'override the active session id (defaults to the canonical binding)')
203
- .action((options) => {
227
+ .option('--session-id <sid>', 'override the active session id (defaults to the canonical binding; supported for QA / internal tooling)')
228
+ .action((options, command) => {
229
+ // Re-resolve options via `command.optsWithGlobals()` (Commander 12.x
230
+ // compatibility): when the parent `statusline` command has its own
231
+ // `--json` registered AND the user types `peaks statusline compact
232
+ // --json`, commander's parent-vs-child option parser can land
233
+ // `--json` on the parent scope only. `optsWithGlobals()` merges
234
+ // parent + child options so the JSON branch fires regardless of
235
+ // which scope commander assigned the flag to. Verified by the
236
+ // `compact --json emits the documented envelope` integration test.
237
+ const merged = {
238
+ ...options,
239
+ ...command.optsWithGlobals?.(),
240
+ };
204
241
  try {
205
- const projectRoot = options.project !== undefined
206
- ? options.project
242
+ const projectRoot = merged.project !== undefined
243
+ ? merged.project
207
244
  : (findProjectRoot(process.cwd()) ?? process.cwd());
208
- const sid = options.sessionId ?? getSessionIdCanonical(projectRoot) ?? null;
245
+ const sid = merged.sessionId ?? getSessionIdCanonical(projectRoot) ?? null;
209
246
  const state = decideCompactStatusline({
210
247
  projectRoot,
211
248
  sessionId: sid,
@@ -215,8 +252,8 @@ export function registerStatusLineCommands(program, io) {
215
252
  // For non-JSON mode we print the LABEL DIRECTLY to stdout
216
253
  // (no envelope wrapping) because the consumer is the IDE
217
254
  // statusline, not a peaks-Loop caller.
218
- if (options.json === true) {
219
- printResult(io, ok('statusline.compact', { label, state }), options.json);
255
+ if (merged.json === true) {
256
+ printResult(io, ok('statusline.compact', { label, state }), merged.json);
220
257
  }
221
258
  else {
222
259
  process.stdout.write(`${label}\n`);
@@ -224,7 +261,7 @@ export function registerStatusLineCommands(program, io) {
224
261
  }
225
262
  catch (error) {
226
263
  const message = getErrorMessage(error);
227
- printResult(io, fail('statusline.compact', 'STATUSLINE_COMPACT_FAILED', message, {}, [message]), options.json);
264
+ printResult(io, fail('statusline.compact', 'STATUSLINE_COMPACT_FAILED', message, {}, [message]), merged.json);
228
265
  process.exitCode = 1;
229
266
  }
230
267
  }));
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `peaks sub-agent shutdown register|unregister|list` CLI verbs.
3
+ *
4
+ * Slice 2026-08-01-subagent-merge-and-e2e (Task 6). The sub-agent
5
+ * dispatches a long-lived local process (vite dev, mock API, docker
6
+ * proxy, etc.). Before it exits, it MUST register the process with
7
+ * `peaks sub-agent shutdown register --pid <pid> --name <label>`. The
8
+ * parent session reads the resulting `service-registrations.json`
9
+ * before the merge-back step and best-effort-kills each entry via
10
+ * killRegisteredServices (src/services/dispatch/service-shutdown.ts).
11
+ *
12
+ * The CLI is intentionally tiny: it appends / removes entries from a
13
+ * JSON file at `.peaks/_runtime/<sid>/dispatch/<dispatchId>/service-registrations.json`.
14
+ * The file is the contract between the sub-agent (writer) and the
15
+ * merge-back-runner (reader). No locking is needed — the sub-agent
16
+ * writes at most once before exit, and the parent reads after the
17
+ * sub-agent's process is gone.
18
+ *
19
+ * Skill-first / CLI-auxiliary red line: the user does not invoke
20
+ * these commands directly. They are invoked by the sub-agent (or its
21
+ * own scripts) at teardown.
22
+ */
23
+ import type { Command } from 'commander';
24
+ import { type ProgramIO } from '../cli-helpers.js';
25
+ /** Re-exported shape — kept here so the CLI owns the on-disk contract. */
26
+ export type ServiceRegistration = {
27
+ readonly pid: number;
28
+ readonly name: string;
29
+ readonly url?: string;
30
+ };
31
+ export declare function registerSubAgentShutdownCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
4
+ import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
5
+ import { printResult } from '../cli-helpers.js';
6
+ const REGISTRATIONS_FILE = 'service-registrations.json';
7
+ function registrationsPath(input) {
8
+ return join(input.projectRoot, '.peaks', '_runtime', input.sessionId, 'dispatch', input.dispatchId, REGISTRATIONS_FILE);
9
+ }
10
+ function readAll(file) {
11
+ if (!existsSync(file))
12
+ return [];
13
+ try {
14
+ return JSON.parse(readFileSync(file, 'utf8'));
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ }
20
+ function writeAll(file, regs) {
21
+ mkdirSync(join(file, '..'), { recursive: true });
22
+ writeFileSync(file, JSON.stringify(regs, null, 2), 'utf8');
23
+ }
24
+ function resolveDispatchId(input) {
25
+ if (typeof input.dispatchId === 'string' && input.dispatchId.length > 0)
26
+ return input.dispatchId;
27
+ return process.env.PEAKS_DISPATCH_ID ?? 'current';
28
+ }
29
+ export function registerSubAgentShutdownCommands(program, io) {
30
+ // The existing `sub-agent-commands.ts` already creates the `sub-agent`
31
+ // parent; this file attaches the `shutdown` subcommand to the same
32
+ // parent via `findCommand` so we do not double-register. Fall back
33
+ // to a standalone `peaks sub-agent-shutdown` parent if the parent
34
+ // is not yet registered (e.g. when this file is loaded in isolation).
35
+ const existing = program.commands.find((c) => c.name() === 'sub-agent');
36
+ const root = existing ?? program.command('sub-agent-shutdown')
37
+ .description('Sub-agent shutdown registration (forensic hook)');
38
+ const shutdown = root
39
+ .command('shutdown')
40
+ .description('Register local services for the parent to kill before merge-back');
41
+ const sessionRoot = process.cwd();
42
+ shutdown
43
+ .command('register')
44
+ .requiredOption('--pid <pid>', 'process id')
45
+ .requiredOption('--name <label>', 'human-readable label (vite / mock-api / etc.)')
46
+ .option('--url <url>', 'optional URL the service exposes')
47
+ .option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
48
+ .action((options) => {
49
+ try {
50
+ const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
51
+ const file = registrationsPath({
52
+ projectRoot: sessionRoot,
53
+ sessionId: sid,
54
+ dispatchId: resolveDispatchId(options),
55
+ });
56
+ const all = readAll(file);
57
+ const reg = {
58
+ pid: Number(options.pid),
59
+ name: options.name,
60
+ ...(options.url !== undefined ? { url: options.url } : {}),
61
+ };
62
+ writeAll(file, [...all, reg]);
63
+ printResult(io, ok('sub-agent.shutdown.register', { file, reg }), options.json);
64
+ }
65
+ catch (error) {
66
+ printResult(io, fail('sub-agent.shutdown.register', 'REGISTER_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
67
+ process.exitCode = 1;
68
+ }
69
+ });
70
+ shutdown
71
+ .command('unregister')
72
+ .requiredOption('--pid <pid>', 'process id to remove from the registration list')
73
+ .option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
74
+ .action((options) => {
75
+ try {
76
+ const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
77
+ const file = registrationsPath({
78
+ projectRoot: sessionRoot,
79
+ sessionId: sid,
80
+ dispatchId: resolveDispatchId(options),
81
+ });
82
+ const filtered = readAll(file).filter((r) => r.pid !== Number(options.pid));
83
+ writeAll(file, filtered);
84
+ printResult(io, ok('sub-agent.shutdown.unregister', { file, removed: options.pid }), options.json);
85
+ }
86
+ catch (error) {
87
+ printResult(io, fail('sub-agent.shutdown.unregister', 'UNREGISTER_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
88
+ process.exitCode = 1;
89
+ }
90
+ });
91
+ shutdown
92
+ .command('list')
93
+ .option('--dispatch-id <id>', 'dispatch id; default = PEAKS_DISPATCH_ID env or "current"')
94
+ .option('--json', 'emit JSON envelope')
95
+ .action((options) => {
96
+ try {
97
+ const sid = getCurrentSessionId(sessionRoot) ?? 'unknown-sid';
98
+ const file = registrationsPath({
99
+ projectRoot: sessionRoot,
100
+ sessionId: sid,
101
+ dispatchId: resolveDispatchId(options),
102
+ });
103
+ printResult(io, ok('sub-agent.shutdown.list', { file, registrations: readAll(file) }), options.json);
104
+ }
105
+ catch (error) {
106
+ printResult(io, fail('sub-agent.shutdown.list', 'LIST_FAILED', getErrorMessage(error), {}, [getErrorMessage(error)]), options.json);
107
+ process.exitCode = 1;
108
+ }
109
+ });
110
+ }
@@ -33,6 +33,7 @@ import { atomicWriteJson } from '../../services/ide/shared/atomic-json.js';
33
33
  import { emitLeaseEvent } from '../../services/observability/observability-service.js';
34
34
  import { deserializeLease, finalizeLease, generateLeaseId, isLeaseActive, isLeaseGcEligible, leaseFilePath, leaseStoreDir, listLeasesSync, markExpired, markGc, markReleased, renewLease, ttlForRole, worktreePath, } from '../../services/worktree/worktree-lease.js';
35
35
  import { execSync } from 'node:child_process';
36
+ import { reconcileHostWorktrees } from '../../services/worktree/host-worktree-reconciler.js';
36
37
  import { existsSync, readdirSync, readFileSync as readFileSyncNode, statSync } from 'node:fs';
37
38
  const DEFAULT_TTL_MS = 5 * 60 * 1_000;
38
39
  const ALLOWED_OPERATIONS = [
@@ -65,6 +66,32 @@ export function registerWorktreeAuthCommand(program, io) {
65
66
  'The grant is current-task scoped: the LLM must invoke grant after the user has explicitly ' +
66
67
  'asked for the operation. The PreToolUse gate fail-closes on missing or expired grants.');
67
68
  const auth_ = auth.command('auth').description('Manage worktree authorization grants (granted by the LLM after explicit user opt-in).');
69
+ addJsonOption(auth
70
+ .command('reconcile-host')
71
+ .description('Read-only reconciliation of host-created .claude/worktrees/agent-* against the canonical Peaks lease store.')
72
+ .option('--session <sid>', 'override session id')
73
+ .option('--project <path>', 'project root (default: findProjectRoot(cwd))')
74
+ .option('--host-root <path>', 'override host worktree root')).action((options) => {
75
+ const projectRoot = resolveProjectRoot(options);
76
+ const sessionId = resolveSessionId(options, projectRoot);
77
+ try {
78
+ const result = reconcileHostWorktrees({
79
+ projectRoot,
80
+ sessionId,
81
+ ...(options.hostRoot !== undefined ? { hostRoot: options.hostRoot } : {}),
82
+ });
83
+ const warnings = result.unmanaged.length > 0
84
+ ? [`${result.unmanaged.length} host worktree(s) are outside Peaks lease governance.`]
85
+ : [];
86
+ printResult(io, ok('worktree.reconcile-host', { ...result, sessionId, projectRoot }, warnings), options.json);
87
+ if (result.unmanaged.length > 0)
88
+ process.exitCode = 1;
89
+ }
90
+ catch (error) {
91
+ printResult(io, fail('worktree.reconcile-host', 'HOST_WORKTREE_RECONCILE_FAILED', getErrorMessage(error), { sessionId, projectRoot }, []), options.json);
92
+ process.exitCode = 1;
93
+ }
94
+ });
68
95
  addJsonOption(auth_
69
96
  .command('grant')
70
97
  .description('Append a single grant to the current session\'s worktree authorization file.')
@@ -1,6 +1,7 @@
1
1
  import { type CompactTrigger, type ConvergencePlan, type InFlightBatchProbe, type AutoCompactResult } from '../context/auto-compact-types.js';
2
2
  import type { CompactTarget } from '../context/auto-compact-dispatcher.js';
3
3
  import { type AutoCompactMode } from './auto-compact-modes.js';
4
+ import { type CompactLifecycleRecord, type CompactLifecycleStage } from '../compact-statusline/compact-lifecycle-store.js';
4
5
  export interface AutoCompactInput {
5
6
  /** Project root for context (default cwd). */
6
7
  readonly projectRoot: string;
@@ -35,6 +36,37 @@ export interface AutoCompactInput {
35
36
  * `--mode <mode>` overrides the 24h-mode auto-detection.
36
37
  */
37
38
  readonly mode?: AutoCompactMode | undefined;
39
+ /**
40
+ * Slice 2026-08-01-compact-lifecycle (Task 5): observer fired on
41
+ * every lifecycle stage this process actually proved. Telemetry
42
+ * only — it can neither change the threshold decision nor the
43
+ * dispatch outcome, and a throwing observer is swallowed.
44
+ */
45
+ readonly onLifecycleStage?: ((stage: CompactLifecycleStage, record: CompactLifecycleRecord) => void) | undefined;
46
+ /**
47
+ * Test-only injection seams for lifecycle failure paths. NEVER set in
48
+ * production — these exist so the unit suite can drive the `failed`
49
+ * transition and the store-outage branch without mocking the SUT.
50
+ * Each flag toggles a single, well-scoped throw at a documented point:
51
+ * - `failPreparing`: throw inside the checkpoint/plan/recovery phase
52
+ * (so the record rests at `failedAt: 'preparing'`)
53
+ * - `failCompacting`: throw inside the IDE-dispatch phase
54
+ * (so the record rests at `failedAt: 'compacting'`)
55
+ * - `failLifecycleWrite`: make every lifecycle-store write throw
56
+ * (so the compact envelope is proven independent of telemetry)
57
+ */
58
+ readonly testHooks?: AutoCompactTestHooks | undefined;
59
+ }
60
+ /**
61
+ * Test-only injection seams. The `boolean` shape (vs. an injected error)
62
+ * is deliberate: tests assert the *transition*, not the thrown value —
63
+ * the value would just be a brittle fingerprint. The real error path
64
+ * is still exercised by `summarizeLifecycleError`'s tests.
65
+ */
66
+ export interface AutoCompactTestHooks {
67
+ readonly failPreparing?: boolean | undefined;
68
+ readonly failCompacting?: boolean | undefined;
69
+ readonly failLifecycleWrite?: boolean | undefined;
38
70
  }
39
71
  /**
40
72
  * Map a context ratio to a `CompactTrigger` action. Pure; the side
@@ -68,6 +100,13 @@ export declare function evaluateAutoCompactDecision(input: {
68
100
  force?: boolean | undefined;
69
101
  bypassRedLine?: boolean | undefined;
70
102
  mode?: AutoCompactMode | undefined;
103
+ /**
104
+ * Source tag from `readContextPercent.source`. Optional — when absent, the
105
+ * function behaves exactly as pre-rid (ratio-only). Slice 2026-07-31-rid-
106
+ * mac-transcript-estimate-trigger uses this to carry forward an explicit
107
+ * carve-out for the Mac-only `transcript-estimate` signal.
108
+ */
109
+ source?: string | undefined;
71
110
  }): {
72
111
  shouldCompact: boolean;
73
112
  reason: 'below-threshold' | 'in-flight-batch' | 'pre-compact' | 'red-line';