@aemonge-dev/pi-stardeck 0.13.0 → 0.15.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.
@@ -57,49 +57,36 @@ The provider uses the installed OpenSpec CLI and reads only each listed change's
57
57
  5. The task artifact is read once per refresh to recover labels such as `3.8` and kinds
58
58
  such as Story, Spike, Lab, or Hack.
59
59
 
60
- The session focus takes precedence over configured `openspec.change`, which takes
61
- precedence over `PI_OPENSPEC_CHANGE`. Configuration and environment values can select a
62
- change for collection/debugging, but they do not create session task focus or expose an
63
- active OpenSpec footer by themselves.
60
+ The focused change no longer lives in session state: the cockpit reads the
61
+ room aim (`openspec/.focus`) directly and prints its fields verbatim.
64
62
 
65
- ## Session focus and terminal title
63
+ active OpenSpec footer by themselves.
66
64
 
67
- A Pi session has zero or one explicit OpenSpec task focus and an independent
68
- ephemeral Session Work state. Display priority is OpenSpec
69
- Task → Session Work → safe direct fallback. OpenSpec focus clears active Session
70
- Work when acquired, preventing hidden or stale work from resurfacing later.
65
+ ## Room aim and terminal title
71
66
 
72
- Session Work is a legacy surface: its writer tool and `/work` command are retired, and
73
- only old sessions' versioned custom entries remain, restored fail-soft on the active
74
- Pi session branch (never a repository status file). OpenSpec focus is stored the same
75
- way and supports this lifecycle:
67
+ The focused change is the Human's room aim — `pios-gate <slug>` writes
68
+ `openspec/.focus`, the cockpit reads it on every refresh and renders its five
69
+ fields verbatim on the right work tile:
76
70
 
77
71
  ```text
78
- /openspec-focus set <change> <task>
79
- /openspec-focus validate
80
- /openspec-focus replace <change> <task>
81
- /openspec-focus clear
72
+ {change} ⟩ {artifact_name} {artifact-idx} › {task} ⟩ {next-step}
82
73
  ```
83
74
 
84
- When no OpenSpec portfolio is available, the matching row-one footer fallback is the
85
- compact `▶ Focus` or `◇ Validate`; an entirely empty
86
- project uses ` no OpenSpec`. The detailed Session Work intent remains in the header
87
- instead of being duplicated in the footer. Active and validation states also replace the
88
- fallback terminal title. Reload, resume, and tree navigation restore the newest valid
89
- branch entry. The agent-callable `openspec_focus` tool shares the command's `status`,
90
- safeguarded `set`, deliberate `replace`, validating `validate`, and persistent `clear`
91
- transitions; the `awaitingValidation` flag marks a focused task as awaiting Human
92
- validation and auto-clears on the next `set`, `replace`, or `clear`. The
93
- retired `work_focus`, `stage`, and `title` instruments are gone: stages
94
- derive from events and Taskflow phases, the focus row falls back to the
95
- native session name (named by `pi-title`) and then to legacy `session-subject`
96
- entries from old sessions, read defensively and fail-soft.
75
+ Read as-is, printed as-is: no prefixes, no relabeling. A missing field drops
76
+ its segment; absence never becomes zero; an aim without a change slug is no
77
+ aim at all. The agent never writes the file — it is the Human's word, written
78
+ by their terminal verbs.
79
+
80
+ Session Work and the `openspec_focus` tool are retired instruments: old
81
+ sessions' versioned custom entries restore fail-soft and carry no writers.
82
+ The focus row falls back to the native session name (named by `pi-title`)
83
+ and then to legacy `session-subject` entries from old sessions, read
84
+ defensively and fail-soft.
97
85
 
98
86
  Global routing belongs to native Ramona policy rather than this status package. This
99
87
  package does not inject parent routing prompts, block parent tools, create Session Work
100
88
  for a global route, or promote a raw request into OpenSpec focus. It continues to own
101
- branch-local OpenSpec and Session Work state, commands, restoration, titles, and footer
102
- publication.
89
+ branch-local Session Work restoration, commands, titles, and footer publication.
103
90
 
104
91
  The header projection uses the rich lifecycle `understanding`, `working`, `waiting`,
105
92
  `assuring`, `learning`, `answering`, `blocked`, `listening`, or `aborted`. It separately
@@ -318,8 +305,7 @@ configuration directory in this installation). This repository keeps the source
318
305
  ```
319
306
 
320
307
  Unknown or invalid fields are ignored and reported by the debug command; they do not
321
- prevent Pi from starting. `openspec.change` is an explicit collection target, not a
322
- session-scoped task focus.
308
+ prevent Pi from starting.
323
309
 
324
310
  ## Commands
325
311
 
@@ -7,17 +7,14 @@ import { promises as fs, watch } from 'node:fs';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import {
9
9
  formatActiveWorkTitle,
10
- type OpenSpecFocus,
11
- parseOpenSpecFocus,
12
- restoreOpenSpecFocus,
13
10
  restoreSessionSubject,
14
- sameOpenSpecFocus,
15
11
  sameSessionSubject,
16
12
  type SessionSubject,
17
13
  } from './src/active-work.ts';
18
14
  import { collectDiagnostics } from './src/diagnostics.ts';
19
15
  import { loadConfig } from './src/config.ts';
20
- import { collectOpenSpec, collectOpenSpecOverview } from './src/openspec.ts';
16
+ import { readRoomAim } from './src/room-aim.ts';
17
+ import { collectOpenSpecOverview } from './src/openspec.ts';
21
18
  import { collectOrchestration } from './src/orchestration.ts';
22
19
  import {
23
20
  buildHeaderStatusEvent,
@@ -170,11 +167,6 @@ class GalacticaStatusRuntime {
170
167
  private openSpecFeedback: OpenSpecFeedback | undefined;
171
168
  private openSpecOverview: OpenSpecOverview | undefined;
172
169
  private openSpecProjectDetected: boolean | undefined;
173
- private focus: OpenSpecFocus;
174
- /** Publisher-owned timestamp of the current focus fact — seeded at
175
- * construction, refreshed on every accepted focus change; staleness
176
- * compares it against the change directory's file truth. */
177
- private focusAt: number | undefined;
178
170
  private subject: SessionSubject;
179
171
  private sessionName: string | undefined;
180
172
  private diagnosticsPath: string | undefined;
@@ -203,30 +195,21 @@ class GalacticaStatusRuntime {
203
195
  pi: ExtensionAPI,
204
196
  ctx: SessionContext,
205
197
  publisher: FancyFooterPublisher,
206
- focus: OpenSpecFocus,
207
198
  subject: SessionSubject,
208
199
  sessionName: string | undefined,
209
200
  ) {
210
201
  this.pi = pi;
211
202
  this.ctx = ctx;
212
203
  this.publisher = publisher;
213
- this.focus = focus;
214
204
  this.subject = subject;
215
205
  this.sessionName = sessionName;
216
- this.focusAt = Date.now();
217
- const restoredAwaitingValidation =
218
- focus.mode === 'task' && focus.awaitingValidation;
219
- this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
220
- this.liveActivity = restoredAwaitingValidation
221
- ? { kind: 'awaiting-validation' }
222
- : undefined;
206
+ this.liveLifecycle = 'listening';
223
207
  this.store = new AtomicStatusStore({
224
208
  cwd: ctx.cwd,
225
209
  projectRoot: ctx.cwd,
226
210
  configPath: '',
227
211
  });
228
212
  const seededAt = Date.now();
229
- this.recordEvent({ type: 'focus/openspec', at: seededAt, focus });
230
213
  this.recordEvent({ type: 'focus/subject', at: seededAt, subject });
231
214
  }
232
215
 
@@ -379,7 +362,6 @@ class GalacticaStatusRuntime {
379
362
  const generation = ++this.generation;
380
363
  if (!this.loadedConfig || this.stopped) return;
381
364
  const config = this.loadedConfig.config;
382
- const focus = this.focus;
383
365
  const at = Date.now();
384
366
  let projectRoot = await detectProjectRoot(this.ctx.cwd, this.runner);
385
367
 
@@ -388,6 +370,9 @@ class GalacticaStatusRuntime {
388
370
  openSpecEnabled && (await pathIsDirectory(join(projectRoot, 'openspec')));
389
371
  let openSpecOverview: RefreshOutcome<OpenSpecOverview> = { kind: 'absent' };
390
372
  let openspec: RefreshOutcome<OpenSpecState> = { kind: 'absent' };
373
+ // The per-change collector retired with the focus store: the room aim
374
+ // carries the tile fields verbatim; the overview feeds the rest.
375
+ void openspec;
391
376
 
392
377
  if (hasOpenSpecProject) {
393
378
  openSpecOverview = await this.outcome('openspec', () =>
@@ -398,31 +383,6 @@ class GalacticaStatusRuntime {
398
383
  }
399
384
  }
400
385
 
401
- if (focus.mode === 'none') {
402
- const passiveChange = config.openspec.change ?? process.env.PI_OPENSPEC_CHANGE;
403
- if (passiveChange) {
404
- openspec = await this.outcome('openspec', () =>
405
- collectOpenSpec({
406
- cwd: projectRoot,
407
- runner: this.runner,
408
- configuredChange: passiveChange,
409
- now: at,
410
- }),
411
- );
412
- if (openspec.kind === 'success') projectRoot = openspec.value.projectRoot;
413
- }
414
- } else {
415
- openspec = await this.outcome('openspec', () =>
416
- collectOpenSpec({
417
- cwd: projectRoot,
418
- runner: this.runner,
419
- configuredChange: focus.change,
420
- now: at,
421
- }),
422
- );
423
- if (openspec.kind === 'success') projectRoot = openspec.value.projectRoot;
424
- }
425
-
426
386
  const [diagnostics, orchestration] = await Promise.all([
427
387
  !config.enabled || !config.diagnostics.enabled
428
388
  ? Promise.resolve<RefreshOutcome<DiagnosticsState>>({ kind: 'absent' })
@@ -452,11 +412,7 @@ class GalacticaStatusRuntime {
452
412
  }),
453
413
  ]);
454
414
 
455
- if (
456
- this.stopped ||
457
- generation !== this.generation ||
458
- !sameOpenSpecFocus(focus, this.focus)
459
- ) {
415
+ if (this.stopped || generation !== this.generation) {
460
416
  return;
461
417
  }
462
418
  const previous = this.store.get();
@@ -481,19 +437,18 @@ class GalacticaStatusRuntime {
481
437
  } else if (openSpecOverview.kind === 'absent') {
482
438
  this.openSpecOverview = undefined;
483
439
  }
484
- this.openSpecFeedback =
485
- !hasOpenSpecProject || focus.mode === 'task'
486
- ? undefined
487
- : openSpecOverview.kind === 'error'
488
- ? 'unavailable'
489
- : openSpecOverview.kind === 'success' &&
490
- openSpecOverview.value.actionableChanges > 0
491
- ? 'no-focus'
492
- : 'clear';
440
+ this.openSpecFeedback = !hasOpenSpecProject
441
+ ? undefined
442
+ : openSpecOverview.kind === 'error'
443
+ ? 'unavailable'
444
+ : openSpecOverview.kind === 'success' &&
445
+ openSpecOverview.value.actionableChanges > 0
446
+ ? 'no-focus'
447
+ : 'clear';
493
448
  this.store.applyRefresh({
494
449
  projectRoot,
495
450
  at,
496
- openspec,
451
+ openspec: { kind: 'absent' },
497
452
  diagnostics,
498
453
  orchestration,
499
454
  });
@@ -587,21 +542,12 @@ class GalacticaStatusRuntime {
587
542
  private publish(): void {
588
543
  if (this.stopped) return;
589
544
  const snapshot = this.store.get();
590
- // Builders read focus facts through the snapshot layer (recordEvent stores
591
- // the same object references), so output stays byte-identical while the
592
- // ledger becomes the feeding seam.
593
- const focus = this.cockpitFacts.openSpec;
594
545
  const subject = this.cockpitFacts.subject;
595
546
  const title = this.currentTitleState(snapshot);
596
- const focusedOpenSpec =
597
- focus.mode === 'task' && snapshot.openspec?.changeId === focus.change
598
- ? snapshot.openspec
599
- : null;
600
547
  const widgets = buildWidgetSnapshots(
601
- { ...snapshot, openspec: focusedOpenSpec },
548
+ { ...snapshot, openspec: null },
602
549
  {
603
- focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
604
- openSpecFeedback: focus.mode === 'none' ? this.openSpecFeedback : undefined,
550
+ openSpecFeedback: this.openSpecFeedback,
605
551
  openSpecOverview: this.openSpecOverview,
606
552
  openSpecProjectDetected: this.openSpecProjectDetected,
607
553
  },
@@ -610,8 +556,7 @@ class GalacticaStatusRuntime {
610
556
  this.pi.events.emit(
611
557
  GALACTICA_HEADER_CHANNEL,
612
558
  buildHeaderStatusEvent(widgets, {
613
- openSpec: focusedOpenSpec,
614
- focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
559
+ openSpec: null,
615
560
  subject,
616
561
  orchestration: snapshot.orchestration,
617
562
  lifecycle: this.liveLifecycle,
@@ -633,33 +578,20 @@ class GalacticaStatusRuntime {
633
578
 
634
579
  private currentTitleState(snapshot = this.store.get()): TitleState {
635
580
  const facts = this.cockpitFacts;
636
- const focusedTaskId =
637
- facts.openSpec.mode === 'task' ? facts.openSpec.taskId : undefined;
638
- const taskTitle =
639
- facts.openSpec.mode === 'task' &&
640
- snapshot.openspec?.changeId === facts.openSpec.change &&
641
- !snapshot.openspec.meta.stale
642
- ? (snapshot.openspec.allTasks ?? snapshot.openspec.pendingTasks).find(
643
- (task) => task.id === focusedTaskId,
644
- )?.title
645
- : undefined;
646
- // The band chip renders the fallback chain — task focus, native session
647
- // name, legacy subject, directory. The terminal titlebar is pi-title's
648
- // alone; this package never paints it (pure-renderer-split Slice 2).
581
+ // The left title is Human-owned — session name, subject, directory —
582
+ // never openspec text (truthful-rendered-state law 2026-11-10); the
583
+ // right tile prints the room aim verbatim (observe-room-aim law).
584
+ const aim = readRoomAim(snapshot.projectRoot);
649
585
  const subjectTitle = formatActiveWorkTitle({
650
- focus: facts.openSpec,
651
586
  subject: facts.subject,
652
- ...(taskTitle ? { taskTitle } : {}),
653
587
  ...(this.sessionName ? { sessionName: this.sessionName } : {}),
654
588
  projectRoot: snapshot.projectRoot,
655
589
  cwd: snapshot.cwd,
656
590
  });
657
591
  return buildTitleState({
658
592
  subject: { state: 'set', title: subjectTitle },
659
- focus: facts.openSpec,
660
- openspec: snapshot.openspec,
593
+ aim,
661
594
  activity: this.titleActivity.current(),
662
- focusAt: this.focusAt,
663
595
  });
664
596
  }
665
597
 
@@ -673,10 +605,6 @@ class GalacticaStatusRuntime {
673
605
  return snapshotFromFacts(this.cockpitFacts, this.cockpitVersion);
674
606
  }
675
607
 
676
- currentFocus(): OpenSpecFocus {
677
- return this.focus;
678
- }
679
-
680
608
  currentSubject(): SessionSubject {
681
609
  return this.subject;
682
610
  }
@@ -693,21 +621,6 @@ class GalacticaStatusRuntime {
693
621
  this.setLiveProjection('understanding', { kind: 'understanding' });
694
622
  }
695
623
 
696
- setFocus(focus: OpenSpecFocus): void {
697
- if (sameOpenSpecFocus(this.focus, focus)) return;
698
- this.focus = focus;
699
- this.focusAt = Date.now();
700
- this.recordEvent({ type: 'focus/openspec', at: Date.now(), focus });
701
- this.store.clearOpenSpec();
702
- if (focus.mode === 'task' && focus.awaitingValidation) {
703
- this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
704
- } else if (focus.mode === 'none') {
705
- this.setLiveProjection('listening');
706
- }
707
- this.publish();
708
- this.launch(this.refresh(false), 'openspec');
709
- }
710
-
711
624
  setSubject(subject: SessionSubject): void {
712
625
  if (sameSessionSubject(this.subject, subject)) return;
713
626
  this.subject = subject;
@@ -890,10 +803,6 @@ class GalacticaStatusRuntime {
890
803
  this.publish();
891
804
  return;
892
805
  }
893
- if (this.focus.mode === 'task') {
894
- this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
895
- return;
896
- }
897
806
  this.setLiveProjection('listening');
898
807
  }
899
808
 
@@ -918,13 +827,10 @@ class GalacticaStatusRuntime {
918
827
  );
919
828
  lines.push(
920
829
  snapshot.openspec
921
- ? `OpenSpec focus: ${formatOpenSpec(
922
- snapshot.openspec,
923
- this.focus.mode === 'task' ? this.focus.taskId : null,
924
- )}`
830
+ ? `OpenSpec: ${formatOpenSpec(snapshot.openspec)}`
925
831
  : this.openSpecFeedback
926
- ? `OpenSpec focus: ${this.openSpecFeedback}`
927
- : 'OpenSpec focus: none',
832
+ ? `OpenSpec: ${this.openSpecFeedback}`
833
+ : 'OpenSpec: no aim',
928
834
  );
929
835
  lines.push(
930
836
  snapshot.diagnostics
@@ -946,7 +852,6 @@ class GalacticaStatusRuntime {
946
852
  projectRoot: snapshot.projectRoot,
947
853
  configPath: snapshot.configPath,
948
854
  configWarnings: snapshot.configWarnings,
949
- focus: this.focus,
950
855
  openSpecFeedback: this.openSpecFeedback ?? null,
951
856
  openSpecOverview: this.openSpecOverview ?? null,
952
857
  detected: {
@@ -997,30 +902,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
997
902
  }
998
903
  });
999
904
 
1000
- // work-state owns focus writes and emits `focus-changed` after each
1001
- // successful one (pi.events); the renderer subscribes for immediate
1002
- // repaint (pure-renderer-split Slice 2).
1003
- const stopFocusRepaint = pi.events.on('focus-changed', (data: unknown) => {
1004
- const focus = parseOpenSpecFocus(data);
1005
- if (!focus) return;
1006
- runtime?.setFocus(focus);
1007
- runtime?.settle();
1008
- });
1009
-
1010
905
  pi.on('session_start', (_event, ctx) => {
1011
906
  runtime?.stop();
1012
907
  const branch = ctx.sessionManager.getBranch();
1013
- const focus = restoreOpenSpecFocus(branch);
1014
908
  const subject = restoreSessionSubject(branch);
1015
909
  const sessionName = pi.getSessionName() ?? ctx.sessionManager.getSessionName();
1016
- runtime = new GalacticaStatusRuntime(
1017
- pi,
1018
- ctx,
1019
- publisher,
1020
- focus,
1021
- subject,
1022
- sessionName,
1023
- );
910
+ runtime = new GalacticaStatusRuntime(pi, ctx, publisher, subject, sessionName);
1024
911
  runtime.start();
1025
912
  });
1026
913
 
@@ -1030,7 +917,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1030
917
 
1031
918
  pi.on('session_tree', (_event, ctx) => {
1032
919
  const branch = ctx.sessionManager.getBranch();
1033
- runtime?.setFocus(restoreOpenSpecFocus(branch));
1034
920
  runtime?.setSubject(restoreSessionSubject(branch));
1035
921
  runtime?.settle();
1036
922
  });
@@ -1117,7 +1003,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
1117
1003
  runtime = undefined;
1118
1004
  publisher.removeAll();
1119
1005
  stopReady();
1120
- stopFocusRepaint();
1121
1006
  });
1122
1007
 
1123
1008
  pi.registerCommand('galactica-status', {
@@ -1,26 +1,13 @@
1
1
  import { basename } from 'node:path';
2
2
 
3
- export const OPEN_SPEC_FOCUS_ENTRY_TYPE = 'galactica-status.openspec-focus.v1';
4
3
  export const SESSION_SUBJECT_ENTRY_TYPE = 'galactica-status.session-subject.v1';
5
4
 
6
- export type TaskOpenSpecFocus = {
7
- mode: 'task';
8
- change: string;
9
- taskId: string;
10
- awaitingValidation: boolean;
11
- };
12
-
13
- export type OpenSpecFocus = { mode: 'none' } | TaskOpenSpecFocus;
14
-
15
- export const NO_OPEN_SPEC_FOCUS: OpenSpecFocus = { mode: 'none' };
16
-
17
5
  export type SessionSubject = { state: 'clear' } | { state: 'set'; title: string };
18
6
 
19
7
  export const NO_SESSION_SUBJECT: SessionSubject = { state: 'clear' };
20
8
 
21
9
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
22
10
  const MAX_TITLE_POINTS = 180;
23
- const MAX_TASK_TITLE_POINTS = 72;
24
11
  const MAX_SUBJECT_POINTS = 120;
25
12
 
26
13
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -43,29 +30,6 @@ function explicitId(value: unknown): string | undefined {
43
30
  return normalized || undefined;
44
31
  }
45
32
 
46
- export function taskFocus(
47
- change: unknown,
48
- taskId: unknown,
49
- awaitingValidation?: unknown,
50
- ): TaskOpenSpecFocus | undefined {
51
- const normalizedChange = explicitId(change);
52
- const normalizedTaskId = explicitId(taskId);
53
- if (!normalizedChange || !normalizedTaskId) return undefined;
54
- return {
55
- mode: 'task',
56
- change: normalizedChange,
57
- taskId: normalizedTaskId,
58
- awaitingValidation: awaitingValidation === true,
59
- };
60
- }
61
-
62
- export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
63
- if (!isRecord(value)) return undefined;
64
- if (value.mode === 'none') return NO_OPEN_SPEC_FOCUS;
65
- if (value.mode !== 'task') return undefined;
66
- return taskFocus(value.change, value.taskId, value.awaitingValidation);
67
- }
68
-
69
33
  export function sessionSubject(
70
34
  state: unknown,
71
35
  title?: unknown,
@@ -85,22 +49,6 @@ export function parseSessionSubject(value: unknown): SessionSubject | undefined
85
49
  return sessionSubject(value.state, value.title);
86
50
  }
87
51
 
88
- export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus {
89
- for (let index = entries.length - 1; index >= 0; index -= 1) {
90
- const entry = entries[index];
91
- if (
92
- !isRecord(entry) ||
93
- entry.type !== 'custom' ||
94
- entry.customType !== OPEN_SPEC_FOCUS_ENTRY_TYPE
95
- ) {
96
- continue;
97
- }
98
- const focus = parseOpenSpecFocus(entry.data);
99
- if (focus) return focus;
100
- }
101
- return NO_OPEN_SPEC_FOCUS;
102
- }
103
-
104
52
  export function restoreSessionSubject(entries: readonly unknown[]): SessionSubject {
105
53
  for (let index = entries.length - 1; index >= 0; index -= 1) {
106
54
  const entry = entries[index];
@@ -117,39 +65,6 @@ export function restoreSessionSubject(entries: readonly unknown[]): SessionSubje
117
65
  return NO_SESSION_SUBJECT;
118
66
  }
119
67
 
120
- export function taskflowRunFocus(input: unknown): TaskOpenSpecFocus | undefined {
121
- if (!isRecord(input) || input.action !== 'run' || !isRecord(input.args)) {
122
- return undefined;
123
- }
124
- const focus = taskFocus(input.args.change, input.args.task);
125
- return focus && focus.change !== 'none' && focus.taskId !== 'none'
126
- ? focus
127
- : undefined;
128
- }
129
-
130
- /** Task identity (mode · change · taskId) — the replace-safeguard's
131
- * question; the validation flag is state, not identity. */
132
- export function sameTaskFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
133
- if (left.mode !== right.mode) return false;
134
- return (
135
- left.mode === 'none' ||
136
- (right.mode === 'task' &&
137
- left.change === right.change &&
138
- left.taskId === right.taskId)
139
- );
140
- }
141
-
142
- export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
143
- if (left.mode !== right.mode) return false;
144
- return (
145
- left.mode === 'none' ||
146
- (right.mode === 'task' &&
147
- left.change === right.change &&
148
- left.taskId === right.taskId &&
149
- left.awaitingValidation === right.awaitingValidation)
150
- );
151
- }
152
-
153
68
  export function sameSessionSubject(
154
69
  left: SessionSubject,
155
70
  right: SessionSubject,
@@ -160,34 +75,26 @@ export function sameSessionSubject(
160
75
  );
161
76
  }
162
77
 
78
+ /**
79
+ * The Human-owned left title: session name, subject, then the directory
80
+ * fallbacks. Layout law 2026-11-10 (truthful-rendered-state): openspec
81
+ * identity renders exclusively on the right work tile — this chain never
82
+ * carries change, task, or gate text.
83
+ */
163
84
  export function formatActiveWorkTitle(options: {
164
- focus: OpenSpecFocus;
165
85
  subject?: SessionSubject;
166
- taskTitle?: string;
167
86
  sessionName?: string;
168
87
  projectRoot?: string;
169
88
  cwd: string;
170
89
  }): string {
171
- let detail: string;
172
- if (options.focus.mode === 'task') {
173
- const taskTitle = options.taskTitle
174
- ? shorten(normalizeTitlePart(options.taskTitle), MAX_TASK_TITLE_POINTS)
175
- : '';
176
- detail = `${options.focus.change} › ${options.focus.taskId}${taskTitle ? ` ${taskTitle}` : ''}`;
177
- } else if (normalizeTitlePart(options.sessionName ?? '')) {
178
- detail = normalizeTitlePart(options.sessionName ?? '');
179
- } else if (options.subject && options.subject.state === 'set') {
180
- detail = options.subject.title;
181
- } else {
182
- detail =
183
- normalizeTitlePart(options.sessionName ?? '') ||
184
- normalizeTitlePart(basename(options.projectRoot ?? '')) ||
185
- normalizeTitlePart(basename(options.cwd)) ||
186
- 'pi';
187
- }
188
-
189
- // Bare text — line 2 carries no chrome (frozen amendment): the session
190
- // name, change, and task stand naked, and pi-title's naming is quoted,
191
- // never re-decorated.
192
- return shorten(normalizeTitlePart(detail), MAX_TITLE_POINTS);
90
+ const detail =
91
+ normalizeTitlePart(options.sessionName ?? '') ||
92
+ (options.subject && options.subject.state === 'set' ? options.subject.title : '') ||
93
+ normalizeTitlePart(basename(options.projectRoot ?? '')) ||
94
+ normalizeTitlePart(basename(options.cwd)) ||
95
+ 'pi';
96
+
97
+ // Bare text — line 2 carries no chrome (frozen amendment): the title
98
+ // stands naked, and pi-title's naming is quoted, never re-decorated.
99
+ return shorten(detail, MAX_TITLE_POINTS);
193
100
  }
@@ -13,7 +13,6 @@ export const DEFAULT_CONFIG: GalacticaStatusConfig = {
13
13
  enabled: true,
14
14
  openspec: {
15
15
  enabled: true,
16
- change: null,
17
16
  },
18
17
  diagnostics: {
19
18
  enabled: true,
@@ -126,7 +125,7 @@ export function parseConfig(value: unknown): {
126
125
  const diagnostics = isRecord(value.diagnostics) ? value.diagnostics : {};
127
126
  const orchestration = isRecord(value.orchestration) ? value.orchestration : {};
128
127
 
129
- unknownKeys(openspec, ['enabled', 'change'], 'openspec', warnings);
128
+ unknownKeys(openspec, ['enabled'], 'openspec', warnings);
130
129
  unknownKeys(
131
130
  diagnostics,
132
131
  [
@@ -148,7 +147,6 @@ export function parseConfig(value: unknown): {
148
147
  enabled: booleanValue(value.enabled, DEFAULT_CONFIG.enabled),
149
148
  openspec: {
150
149
  enabled: booleanValue(openspec.enabled, DEFAULT_CONFIG.openspec.enabled),
151
- change: nullableString(openspec.change, DEFAULT_CONFIG.openspec.change),
152
150
  },
153
151
  diagnostics: {
154
152
  enabled: booleanValue(diagnostics.enabled, DEFAULT_CONFIG.diagnostics.enabled),
@@ -5,7 +5,7 @@ import type {
5
5
  RunSpan,
6
6
  StageValue,
7
7
  } from './types.ts';
8
- import type { OpenSpecFocus, SessionSubject } from './active-work.ts';
8
+ import type { SessionSubject } from './active-work.ts';
9
9
 
10
10
  /**
11
11
  * Append-only event ledger for the cockpit status core.
@@ -17,7 +17,6 @@ import type { OpenSpecFocus, SessionSubject } from './active-work.ts';
17
17
  */
18
18
 
19
19
  export const LEDGER_EVENT_TYPES = [
20
- 'focus/openspec',
21
20
  'focus/subject',
22
21
  'run/start',
23
22
  'run/stage',
@@ -31,11 +30,6 @@ export const LEDGER_EVENT_TYPES = [
31
30
  export type LedgerEventType = (typeof LEDGER_EVENT_TYPES)[number];
32
31
 
33
32
  export type LedgerEvent =
34
- | {
35
- type: 'focus/openspec';
36
- at: number;
37
- focus: OpenSpecFocus;
38
- }
39
33
  | { type: 'focus/subject'; at: number; subject: SessionSubject }
40
34
  | { type: 'run/start'; at: number; span: RunSpan }
41
35
  | { type: 'run/stage'; at: number; id: string; stage: StageValue; declared: boolean }