@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.
- package/README.md +20 -2
- package/demo/pi-stardeck.tape +6 -3
- package/docs/architecture.md +2 -1
- package/package.json +2 -3
- package/packages/pi-stardeck/index.ts +0 -10
- package/packages/pi-stardeck/src/deck.ts +30 -52
- package/packages/pi-stardeck/src/title-state.ts +63 -136
- package/packages/status/README.md +20 -34
- package/packages/status/index.ts +28 -143
- package/packages/status/src/active-work.ts +16 -109
- package/packages/status/src/config.ts +1 -3
- package/packages/status/src/ledger.ts +1 -7
- package/packages/status/src/openspec.ts +25 -17
- package/packages/status/src/publisher.ts +30 -91
- package/packages/status/src/reducer.ts +2 -36
- package/packages/status/src/room-aim.ts +151 -0
- package/packages/status/src/title-state.ts +43 -135
- package/packages/status/src/types.ts +7 -8
- package/src/index.ts +0 -2
- package/packages/pi-stardeck/src/activity-age.ts +0 -184
- package/packages/work-state/README.md +0 -33
- package/packages/work-state/index.ts +0 -160
- package/packages/work-state/package.json +0 -33
- package/packages/work-state/src/focus.ts +0 -100
|
@@ -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
|
|
61
|
-
|
|
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
|
-
|
|
63
|
+
active OpenSpec footer by themselves.
|
|
66
64
|
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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.
|
|
322
|
-
session-scoped task focus.
|
|
308
|
+
prevent Pi from starting.
|
|
323
309
|
|
|
324
310
|
## Commands
|
|
325
311
|
|
package/packages/status/index.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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:
|
|
548
|
+
{ ...snapshot, openspec: null },
|
|
602
549
|
{
|
|
603
|
-
|
|
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:
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
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
|
|
922
|
-
snapshot.openspec,
|
|
923
|
-
this.focus.mode === 'task' ? this.focus.taskId : null,
|
|
924
|
-
)}`
|
|
830
|
+
? `OpenSpec: ${formatOpenSpec(snapshot.openspec)}`
|
|
925
831
|
: this.openSpecFeedback
|
|
926
|
-
? `OpenSpec
|
|
927
|
-
: 'OpenSpec
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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'
|
|
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 {
|
|
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 }
|