peaks-loop 4.0.11 → 4.0.13

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 +47 -0
  2. package/dist/cli/commands/code-commands.js +2 -0
  3. package/dist/cli/commands/code-orchestrator-can-do.d.ts +17 -0
  4. package/dist/cli/commands/code-orchestrator-can-do.js +75 -0
  5. package/dist/cli/commands/core/doctor-command.js +26 -3
  6. package/dist/cli/commands/workspace/reconcile-command.js +5 -2
  7. package/dist/services/code/orchestrator-can-do.d.ts +114 -0
  8. package/dist/services/code/orchestrator-can-do.js +205 -0
  9. package/dist/services/doctor/doctor-service/checks/multi-binary-drift.d.ts +61 -0
  10. package/dist/services/doctor/doctor-service/checks/multi-binary-drift.js +268 -0
  11. package/dist/services/doctor/doctor-service/checks/skill-presence.js +1 -1
  12. package/dist/services/doctor/doctor-service/checks/workspace-layout.d.ts +5 -2
  13. package/dist/services/doctor/doctor-service/checks/workspace-layout.js +70 -9
  14. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  15. package/dist/services/doctor/doctor-service/report/final-summary.d.ts +13 -5
  16. package/dist/services/doctor/doctor-service/report/final-summary.js +19 -9
  17. package/dist/services/doctor/doctor-service/types.d.ts +64 -0
  18. package/dist/services/doctor/doctor-service.d.ts +2 -1
  19. package/dist/services/doctor/doctor-service.js +1 -0
  20. package/dist/services/doctor/index.d.ts +1 -1
  21. package/dist/services/doctor/index.js +1 -1
  22. package/dist/services/hooks/presence-marker-detector.d.ts +7 -5
  23. package/dist/services/hooks/presence-marker-detector.js +45 -85
  24. package/dist/services/migration/v2-10-to-v2-11-service.js +1 -3
  25. package/dist/services/sc/sc-service.d.ts +11 -7
  26. package/dist/services/sc/sc-service.js +29 -26
  27. package/dist/services/session/binding-store.d.ts +6 -6
  28. package/dist/services/skills/hooks-settings-service.d.ts +1 -1
  29. package/dist/services/skills/hooks-settings-service.js +1 -1
  30. package/dist/services/skills/skill-presence-service.d.ts +0 -1
  31. package/dist/services/skills/skill-presence-service.js +112 -102
  32. package/dist/services/skills/skill-statusline-renderer.d.ts +3 -1
  33. package/dist/services/skills/skill-statusline-renderer.js +37 -1
  34. package/dist/services/skills/skill-statusline-service.d.ts +14 -0
  35. package/dist/services/skills/skill-statusline-service.js +109 -53
  36. package/dist/services/skills/skill-statusline-sid-suffix.d.ts +55 -0
  37. package/dist/services/skills/skill-statusline-sid-suffix.js +65 -0
  38. package/package.json +4 -4
  39. package/skills/peaks-code/SKILL.md +6 -0
  40. package/skills/peaks-code/references/session-overload-signal-index.md +60 -0
@@ -2,7 +2,15 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { findProjectRoot } from '../config/config-safety.js';
4
4
  import { ensureMemoryBootstrap } from '../memory/project-memory-service.js';
5
- import { getSessionMeta } from '../session/session-manager.js';
5
+ import { getSessionId, getSessionMeta } from '../session/session-manager.js';
6
+ // Slice 4.0.8 compat shim: the canonical write path lives in
7
+ // `presence-lease-service.ts`. The legacy shim dynamically imports
8
+ // it inside `setSkillPresence` so the cold path of the legacy
9
+ // `getSkillPresence` / `checkStalePresence` / `clearStalePresenceOnRotation`
10
+ // read paths stays cheap. The static imports below are reserved for
11
+ // the migration window and are referenced through the dynamic
12
+ // `leaseMod.*` accessor at runtime.
13
+ import { listPresenceLeases } from './presence-lease-service.js';
6
14
  // Re-export the 4.0.8 compat surface so legacy callers
7
15
  // (`presence-service` consumers in `code-mode-gate-commands.ts`,
8
16
  // `mode-enforcement.ts`, `code-job-shape-commands.ts`, etc.) keep
@@ -37,14 +45,13 @@ function getCurrentOuterSessionId() {
37
45
  return claude;
38
46
  return undefined;
39
47
  }
40
- // As of slice 2026-06-05-peaks-runtime-layer the orchestrator's
41
- // active-skill marker lives under `.peaks/_runtime/active-skill.json`.
42
- // The legacy `.peaks/.active-skill.json` path is preserved as a
43
- // read-only fallback for one minor release so older CLI versions (or
44
- // trees that have not been migrated by `peaks workspace reconcile`)
45
- // keep working without a forced re-init.
46
- const PRESENCE_FILE = join('.peaks', '_runtime', 'active-skill.json');
47
- const PRESENCE_FILE_LEGACY = '.peaks/.active-skill.json';
48
+ // Slice 4.0.11 statusline-sid-scoped-lease C: the deprecated
49
+ // project-level single-slot presence files
50
+ // `.peaks/_runtime/active-skill.json` and `.peaks/.active-skill.json`
51
+ // are REMOVED from this module. The canonical sid-scoped lease
52
+ // projection (`.peaks/_runtime/<sid>/leases/presence-*.json`) is the
53
+ // only source of truth. The legacy constants `PRESENCE_FILE` and
54
+ // `PRESENCE_FILE_LEGACY` are deleted; readers use `listPresenceLeases`.
48
55
  const SESSION_FILE = join('.peaks', '_runtime', 'session.json');
49
56
  const SESSION_FILE_LEGACY = '.peaks/.session.json';
50
57
  function resolveProjectRoot(override) {
@@ -52,36 +59,6 @@ function resolveProjectRoot(override) {
52
59
  return resolve(override);
53
60
  return findProjectRoot(process.cwd()) ?? process.cwd();
54
61
  }
55
- function resolvePresencePath(projectRootOverride) {
56
- return resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE);
57
- }
58
- /**
59
- * Back-compat read for the active-skill marker. Prefers the new
60
- * canonical `.peaks/_runtime/active-skill.json`; falls back to the
61
- * legacy `.peaks/.active-skill.json` for one minor release.
62
- *
63
- * Returns the parsed SkillPresence object, or null when neither
64
- * file is present / valid. The legacy file is never written by
65
- * current code — only the new path receives writes.
66
- */
67
- function readSkillPresenceBackCompat(projectRootOverride) {
68
- const presencePath = resolvePresencePath(projectRootOverride);
69
- const legacyPath = resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE_LEGACY);
70
- const pathToRead = existsSync(presencePath) ? presencePath : legacyPath;
71
- if (!existsSync(pathToRead))
72
- return null;
73
- try {
74
- const raw = readFileSync(pathToRead, 'utf8');
75
- const parsed = JSON.parse(raw);
76
- if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
77
- return null;
78
- }
79
- return { presence: parsed, path: pathToRead };
80
- }
81
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
82
- return null;
83
- }
84
- }
85
62
  /**
86
63
  * Resolve the active peaks session id from
87
64
  * `.peaks/_runtime/session.json` (legacy: `.peaks/.session.json`).
@@ -112,6 +89,43 @@ export function getCurrentSessionId(projectRootOverride) {
112
89
  return null;
113
90
  }
114
91
  }
92
+ /**
93
+ * Slice 4.0.11 statusline-sid-scoped-lease C: read the canonical
94
+ * sid-scoped lease projection and project the freshest in-flight
95
+ * lease into the legacy `SkillPresence` shape that the public
96
+ * `getSkillPresence` / `checkStalePresence` /
97
+ * `clearStalePresenceOnRotation` consumers expect. The projection
98
+ * is lossy (the lease does not carry `outerSessionId`; `setAt`
99
+ * maps to `startedAt`; `lastHeartbeat` is reused as `lastHeartbeat`).
100
+ *
101
+ * Returns `null` when the bound session has no in-flight leases
102
+ * (`running` or `preparing`); terminalized leases are ignored so
103
+ * the read does not resurrect a stale skill on a fresh session.
104
+ */
105
+ function readSkillPresenceFromLease(projectRootOverride) {
106
+ const projectRoot = resolveProjectRoot(projectRootOverride);
107
+ const sessionId = getSessionId(projectRoot);
108
+ if (sessionId === null)
109
+ return null;
110
+ const leases = listPresenceLeases(projectRoot, sessionId);
111
+ const inFlight = leases.filter((l) => l.status === 'running' || l.status === 'preparing');
112
+ if (inFlight.length === 0)
113
+ return null;
114
+ inFlight.sort((a, b) => b.lastHeartbeat.localeCompare(a.lastHeartbeat));
115
+ const lease = inFlight[0];
116
+ if (lease === undefined)
117
+ return null;
118
+ // The lease carries `callerId` (the adapter / harness session id),
119
+ // not the legacy `outerSessionId` field. Project it for back-compat
120
+ // with `clearStalePresenceOnRotation`'s outer-mismatch check.
121
+ return {
122
+ skill: lease.skill,
123
+ sessionId,
124
+ outerSessionId: lease.callerId,
125
+ setAt: lease.startedAt,
126
+ lastHeartbeat: lease.lastHeartbeat
127
+ };
128
+ }
115
129
  /**
116
130
  * Look up the outer-session-id that was bound to the *current* peaks
117
131
  * session, i.e. the one written to the per-session
@@ -160,23 +174,14 @@ function getBoundOuterSessionId(projectRootOverride) {
160
174
  * legacy `.peaks/.active-skill.json` for one minor release.
161
175
  */
162
176
  function getPreviousOuterSessionId(projectRootOverride) {
163
- const result = readSkillPresenceBackCompat(projectRootOverride);
177
+ const result = readSkillPresenceFromLease(projectRootOverride);
164
178
  if (result === null)
165
179
  return undefined;
166
- const parsed = result.presence;
167
- if (typeof parsed.outerSessionId === 'string' && parsed.outerSessionId.length > 0) {
168
- return parsed.outerSessionId;
169
- }
170
- // Legacy field name. Honour it on the read side so v1.2.x
171
- // presence files do not show as a false mismatch.
172
- if (typeof parsed.claudeSessionId === 'string' && parsed.claudeSessionId.length > 0) {
173
- return parsed.claudeSessionId;
180
+ if (typeof result.outerSessionId === 'string' && result.outerSessionId.length > 0) {
181
+ return result.outerSessionId;
174
182
  }
175
183
  return undefined;
176
184
  }
177
- export function exportSkillPresence(projectRootOverride) {
178
- return resolvePresencePath(projectRootOverride);
179
- }
180
185
  // ============================================================================
181
186
  // Slice 020 — caller-keyed active-skill marker (D6).
182
187
  // ============================================================================
@@ -336,12 +341,12 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
336
341
  catch { /* fall through to legacy write */ }
337
342
  })();
338
343
  }
339
- const presencePath = resolvePresencePath(projectRootOverride);
340
- const presenceDir = dirname(presencePath);
341
- if (!existsSync(presenceDir)) {
342
- mkdirSync(presenceDir, { recursive: true });
343
- }
344
- writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
344
+ // Slice 4.0.11 statusline-sid-scoped-lease A: the canonical write
345
+ // path is now exclusively `presence-lease-service.setPresenceLease`.
346
+ // The legacy single-slot marker file write was removed in this
347
+ // slice: every `setSkillPresence` call flows through the sid-scoped
348
+ // lease + presence-index machinery above. The legacy
349
+ // `active-skill.json` file is NOT recreated here.
345
350
  // Skill-activation side effect: ensure `.peaks/memory/` and a full-shape
346
351
  // empty `index.json` exist for the project. This is the user-facing fix
347
352
  // for "stock projects never get a memory directory or index". Every peaks
@@ -354,26 +359,17 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
354
359
  return presence;
355
360
  }
356
361
  export function getSkillPresence(projectRootOverride) {
357
- const result = readSkillPresenceBackCompat(projectRootOverride);
358
- if (result === null)
359
- return null;
360
- const { presence, path: presencePath } = result;
361
- if (typeof presence.sessionId === 'string' && presence.sessionId.length > 0) {
362
- const currentSessionId = getCurrentSessionId(projectRootOverride);
363
- if (currentSessionId && presence.sessionId !== currentSessionId) {
364
- try {
365
- unlinkSync(presencePath);
366
- }
367
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
368
- // best effort
369
- }
370
- return null;
371
- }
372
- }
373
- return presence;
362
+ // Slice 4.0.11 statusline-sid-scoped-lease C: read from the
363
+ // canonical sid-scoped lease projection. The legacy
364
+ // `unlinkSync(presencePath)` purge on session-mismatch is
365
+ // removed: the lease projection is keyed off the bound session
366
+ // id, so a mismatch cannot happen — `listPresenceLeases`
367
+ // already filters by the session-id resolved from the workspace
368
+ // binding.
369
+ return readSkillPresenceFromLease(projectRootOverride);
374
370
  }
375
371
  export function checkStalePresence(opts) {
376
- const result = readSkillPresenceBackCompat(opts?.projectRootOverride);
372
+ const result = readSkillPresenceFromLease(opts?.projectRootOverride);
377
373
  // `opts?.currentOuter === undefined` is the omitted-key case. A
378
374
  // falsy string `''` is an explicit "no signal" (used by tests to
379
375
  // simulate a CLI run with no harness env vars).
@@ -389,7 +385,7 @@ export function checkStalePresence(opts) {
389
385
  recordedOuterSessionId: undefined
390
386
  };
391
387
  }
392
- const recorded = result.presence.outerSessionId;
388
+ const recorded = result.outerSessionId;
393
389
  // Suppress false-positives when NEITHER side recorded an outer
394
390
  // session id (legacy project, no harness signal). Two unknowns
395
391
  // are not a swap — they are "no signal available yet".
@@ -399,7 +395,7 @@ export function checkStalePresence(opts) {
399
395
  return {
400
396
  stale: false,
401
397
  reason: null,
402
- presence: result.presence,
398
+ presence: result,
403
399
  currentOuterSessionId: current,
404
400
  recordedOuterSessionId: recorded
405
401
  };
@@ -408,7 +404,7 @@ export function checkStalePresence(opts) {
408
404
  return {
409
405
  stale: false,
410
406
  reason: null,
411
- presence: result.presence,
407
+ presence: result,
412
408
  currentOuterSessionId: current,
413
409
  recordedOuterSessionId: recorded
414
410
  };
@@ -416,7 +412,7 @@ export function checkStalePresence(opts) {
416
412
  return {
417
413
  stale: true,
418
414
  reason: 'outer-session-mismatch',
419
- presence: result.presence,
415
+ presence: result,
420
416
  currentOuterSessionId: current,
421
417
  recordedOuterSessionId: recorded
422
418
  };
@@ -444,11 +440,11 @@ export function checkStalePresence(opts) {
444
440
  * decide what the "rotated-out" id was.
445
441
  */
446
442
  export function clearStalePresenceOnRotation(opts) {
447
- const result = readSkillPresenceBackCompat(opts.projectRootOverride);
443
+ const result = readSkillPresenceFromLease(opts.projectRootOverride);
448
444
  if (result === null) {
449
445
  return { cleared: false, reason: 'no-presence' };
450
446
  }
451
- const recorded = result.presence.outerSessionId;
447
+ const recorded = result.outerSessionId;
452
448
  const current = opts.currentOuterSessionId;
453
449
  // If the recorded outer id matches the new (current) outer id,
454
450
  // this presence is NOT stale — the user just reconnected from
@@ -483,24 +479,25 @@ export function clearStalePresenceOnRotation(opts) {
483
479
  };
484
480
  }
485
481
  export function touchSkillHeartbeat(projectRootOverride) {
486
- const result = readSkillPresenceBackCompat(projectRootOverride);
487
- if (result === null)
482
+ // Slice 4.0.11 statusline-sid-scoped-lease C: read from the
483
+ // canonical sid-scoped lease projection. The legacy
484
+ // unlink-on-session-mismatch purge is removed: the lease is
485
+ // keyed off the bound session id, so the mismatch path cannot
486
+ // fire (a lease under session A is never returned when the bound
487
+ // session is B).
488
+ const presence = readSkillPresenceFromLease(projectRootOverride);
489
+ if (presence === null)
488
490
  return null;
489
- const { presence, path: presencePath } = result;
490
- if (typeof presence.sessionId === 'string' && presence.sessionId.length > 0) {
491
- const currentSessionId = getCurrentSessionId(projectRootOverride);
492
- if (currentSessionId && presence.sessionId !== currentSessionId) {
493
- try {
494
- unlinkSync(presencePath);
495
- }
496
- catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
497
- // best effort
498
- }
499
- return null;
500
- }
501
- }
491
+ // Slice 4.0.11 statusline-sid-scoped-lease A: the legacy write to
492
+ // the single-slot marker file was removed. Heartbeat refresh now
493
+ // lives exclusively in `presence-lease-service.setPresenceLease`
494
+ // (the canonical sid-scoped lease). This function remains as a
495
+ // read-only back-compat shim for the 4-B / 4-C sub-slices; callers
496
+ // still receive the in-memory `presence` (with the new
497
+ // `lastHeartbeat` stamped) but the legacy `active-skill.json` file
498
+ // is no longer touched. The canonical lease / presence-index
499
+ // entries are the source of truth for heartbeat freshness.
502
500
  presence.lastHeartbeat = new Date().toISOString();
503
- writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
504
501
  return presence;
505
502
  }
506
503
  export function clearSkillPresence(projectRootOverride) {
@@ -518,12 +515,25 @@ export function clearSkillPresence(projectRootOverride) {
518
515
  // `.peaks/.active-skill.json` so a stale marker from a prior CLI
519
516
  // version cannot resurrect after a fresh `clear`. The canonical
520
517
  // lease + index entries are NOT touched by this shim — they
521
- // remain under the workflow's terminalize lock until
522
- // `terminalizeWorkflow` or the next workflow init reclaims them.
523
- const presencePath = resolvePresencePath(projectRootOverride);
524
- const legacyPath = resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE_LEGACY);
518
+ // Slice 4.0.11 statusline-sid-scoped-lease C: the deprecated
519
+ // single-slot presence files (`.peaks/_runtime/active-skill.json`
520
+ // and `.peaks/.active-skill.json`) are removed. The canonical
521
+ // sid-scoped lease projection (under
522
+ // `.peaks/_runtime/<sid>/leases/`) is untouched here — workflow
523
+ // leases route through `terminalizeWorkflow`, NOT this shim.
524
+ //
525
+ // This shim remains as a best-effort stale-file cleanup for
526
+ // projects that still carry the deprecated single-slot file from
527
+ // a pre-4.0.11 CLI version. After one minor release the legacy
528
+ // paths are gone and the loop is a no-op. Returns true when a
529
+ // stale file was actually removed.
530
+ const projectRoot = resolveProjectRoot(projectRootOverride);
531
+ const candidates = [
532
+ resolve(projectRoot, '.peaks', '_runtime', 'active-skill.json'),
533
+ resolve(projectRoot, '.peaks', '.active-skill.json')
534
+ ];
525
535
  let cleared = false;
526
- for (const p of [presencePath, legacyPath]) {
536
+ for (const p of candidates) {
527
537
  if (!existsSync(p))
528
538
  continue;
529
539
  try {
@@ -1,4 +1,7 @@
1
1
  import type { StatusLineModel } from './skill-statusline-service.js';
2
+ import { computeRootSuffix as computeRootSuffixImpl } from './skill-statusline-sid-suffix.js';
3
+ export { formatShortSid } from './skill-statusline-sid-suffix.js';
4
+ export declare const computeRootSuffix: typeof computeRootSuffixImpl;
2
5
  /**
3
6
  * Pure formatting layer for the Peaks statusLine. Takes the read-only status
4
7
  * model and produces the single line Claude Code paints at the bottom of the
@@ -76,4 +79,3 @@ export declare function applyMarquee(s: string, nowMs: number, capability: Statu
76
79
  * states all keep the marquee.
77
80
  */
78
81
  export declare function renderStatusLine(model: StatusLineModel, options?: StatusLineRenderOptions, env?: NodeJS.ProcessEnv): string;
79
- export {};
@@ -1,4 +1,12 @@
1
1
  import { basename } from 'node:path';
2
+ import { computeRootSuffix as computeRootSuffixImpl, } from './skill-statusline-sid-suffix.js';
3
+ // Re-export so existing test imports
4
+ // (`import { formatShortSid, computeRootSuffix } from '.../skill-statusline-renderer'`)
5
+ // keep working byte-identically after the helper extraction in slice
6
+ // 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
7
+ // repair cycle.
8
+ export { formatShortSid } from './skill-statusline-sid-suffix.js';
9
+ export const computeRootSuffix = computeRootSuffixImpl;
2
10
  /**
3
11
  * Internal: the brand prefix is plain ASCII so a tiny terminal without
4
12
  * monospaced UTF-8 still sees a recognizable string. The mountain glyphs
@@ -186,6 +194,14 @@ function rootLabel(projectRoot) {
186
194
  return '';
187
195
  return basename(projectRoot);
188
196
  }
197
+ /**
198
+ * `formatShortSid` + `computeRootSuffix` were extracted to
199
+ * `./skill-statusline-sid-suffix.ts` in the slice
200
+ * 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
201
+ * repair cycle so the renderer file stays under the Karpathy
202
+ * 800-line cap. Both helpers are re-exported above for back-compat
203
+ * with existing test imports.
204
+ */
189
205
  /**
190
206
  * Build the "middle" of the status line — everything between the brand
191
207
  * prefix and the project root label. Kept separate so the token layout
@@ -568,7 +584,27 @@ export function renderStatusLine(model, options, env) {
568
584
  const noColor = env !== undefined ? isNoColor(env) : false;
569
585
  const palette = paletteFor(capability, noColor);
570
586
  const root = rootLabel(model.projectRoot);
571
- const rootSuffix = root ? `${palette.trailSeparator}${root}` : '';
587
+ // short-sid suffix (slices
588
+ // - 2026-08-05-statusline-empty-render-and-short-sid-suffix (active only)
589
+ // - 2026-08-05-statusline-sid-only-marker (idle + stale also)
590
+ // ):
591
+ // the project root cell carries ` [shortSid]` after `peaks-loop` whenever
592
+ // a canonical session id resolves. Per-state matrix:
593
+ //
594
+ // active → append sid when sessionId !== null (AC5/PB1)
595
+ // idle → append sid when sessionId !== null (AC1)
596
+ // stale → append sid when sessionId !== null (AC2)
597
+ // invalid-presence → NEVER append sid (G2 — read-error signal must
598
+ // stay loud; showing sid would mask the warning)
599
+ // (AC4)
600
+ //
601
+ // Compact state with presence is still rendered with the active sid logic
602
+ // (the active branch already covered this in the prior slice). Compact state
603
+ // without presence is the same as the underlying idle / invalid-presence /
604
+ // stale branch; the helper inherits that. Pure ASCII (no narrow space /
605
+ // smart quotes), so Windows PowerShell + Git Bash + zsh render byte-
606
+ // identical (AC7 of the prior slice + AC10 of this slice).
607
+ const rootSuffix = computeRootSuffix(model, root, palette);
572
608
  const brand = brandText(capability, noColor);
573
609
  // Breathing pulse: key off a single wall-clock read at the start of
574
610
  // render so the output is deterministic for a given invocation. The
@@ -27,6 +27,20 @@ export type StatusLineModel = {
27
27
  ageMs: number | null;
28
28
  compact: CompactStatuslineState;
29
29
  activeLeaf: StatusLineActiveLeaf | null;
30
+ /**
31
+ * Slice 2026-08-05-statusline-sid-only-marker: the canonical session
32
+ * id resolved for the project root (`getSessionIdCanonical`), or
33
+ * `null` when no project root is bound or no `.peaks/_runtime/<sid>/`
34
+ * session is on disk. The renderer reads this to append ` [shortSid]`
35
+ * to the project name cell for idle / stale states (G1); invalid-
36
+ * presence still suppresses the suffix (G2).
37
+ *
38
+ * Carry-forward for AC1/AC2/AC3: the value is `null` whenever
39
+ * `projectRoot === null` — the renderer relies on this to skip the
40
+ * suffix without re-running `getSessionIdCanonical` against a non-
41
+ * existent root.
42
+ */
43
+ sessionId: string | null;
30
44
  };
31
45
  export declare function parseStatusLineStdin(raw: string): StatusLineStdin | null;
32
46
  export declare function buildStatusLineModel(stdin: StatusLineStdin | null, nowMs: number): StatusLineModel;