@aemonge-dev/pi-stardeck 0.14.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 +26 -42
- package/packages/pi-stardeck/src/title-state.ts +64 -162
- package/packages/status/README.md +20 -34
- package/packages/status/index.ts +26 -155
- package/packages/status/src/active-work.ts +0 -84
- package/packages/status/src/config.ts +1 -3
- package/packages/status/src/ledger.ts +1 -7
- package/packages/status/src/openspec.ts +2 -2
- package/packages/status/src/publisher.ts +8 -78
- package/packages/status/src/reducer.ts +2 -36
- package/packages/status/src/room-aim.ts +151 -0
- package/packages/status/src/title-state.ts +42 -148
- package/packages/status/src/types.ts +1 -5
- package/src/index.ts +0 -2
- package/packages/pi-stardeck/src/activity-age.ts +0 -184
- package/packages/status/src/gate-act.ts +0 -85
- 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,18 +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 {
|
|
21
|
-
import {
|
|
16
|
+
import { readRoomAim } from './src/room-aim.ts';
|
|
17
|
+
import { collectOpenSpecOverview } from './src/openspec.ts';
|
|
22
18
|
import { collectOrchestration } from './src/orchestration.ts';
|
|
23
19
|
import {
|
|
24
20
|
buildHeaderStatusEvent,
|
|
@@ -171,15 +167,6 @@ class GalacticaStatusRuntime {
|
|
|
171
167
|
private openSpecFeedback: OpenSpecFeedback | undefined;
|
|
172
168
|
private openSpecOverview: OpenSpecOverview | undefined;
|
|
173
169
|
private openSpecProjectDetected: boolean | undefined;
|
|
174
|
-
private focus: OpenSpecFocus;
|
|
175
|
-
/** Publisher-owned timestamp of the current focus fact — seeded at
|
|
176
|
-
* construction, refreshed on every accepted focus change; staleness
|
|
177
|
-
* compares it against the change directory's file truth. */
|
|
178
|
-
private focusAt: number | undefined;
|
|
179
|
-
/** The gate's relayed act line for the focused change — commentary
|
|
180
|
-
* beside the declared focus, never a replacement; cleared whenever
|
|
181
|
-
* the focus moves so stale gate truth never survives a switch. */
|
|
182
|
-
private gateAct: { change: string; line?: string } | undefined;
|
|
183
170
|
private subject: SessionSubject;
|
|
184
171
|
private sessionName: string | undefined;
|
|
185
172
|
private diagnosticsPath: string | undefined;
|
|
@@ -208,30 +195,21 @@ class GalacticaStatusRuntime {
|
|
|
208
195
|
pi: ExtensionAPI,
|
|
209
196
|
ctx: SessionContext,
|
|
210
197
|
publisher: FancyFooterPublisher,
|
|
211
|
-
focus: OpenSpecFocus,
|
|
212
198
|
subject: SessionSubject,
|
|
213
199
|
sessionName: string | undefined,
|
|
214
200
|
) {
|
|
215
201
|
this.pi = pi;
|
|
216
202
|
this.ctx = ctx;
|
|
217
203
|
this.publisher = publisher;
|
|
218
|
-
this.focus = focus;
|
|
219
204
|
this.subject = subject;
|
|
220
205
|
this.sessionName = sessionName;
|
|
221
|
-
this.
|
|
222
|
-
const restoredAwaitingValidation =
|
|
223
|
-
focus.mode === 'task' && focus.awaitingValidation;
|
|
224
|
-
this.liveLifecycle = restoredAwaitingValidation ? 'waiting' : 'listening';
|
|
225
|
-
this.liveActivity = restoredAwaitingValidation
|
|
226
|
-
? { kind: 'awaiting-validation' }
|
|
227
|
-
: undefined;
|
|
206
|
+
this.liveLifecycle = 'listening';
|
|
228
207
|
this.store = new AtomicStatusStore({
|
|
229
208
|
cwd: ctx.cwd,
|
|
230
209
|
projectRoot: ctx.cwd,
|
|
231
210
|
configPath: '',
|
|
232
211
|
});
|
|
233
212
|
const seededAt = Date.now();
|
|
234
|
-
this.recordEvent({ type: 'focus/openspec', at: seededAt, focus });
|
|
235
213
|
this.recordEvent({ type: 'focus/subject', at: seededAt, subject });
|
|
236
214
|
}
|
|
237
215
|
|
|
@@ -384,7 +362,6 @@ class GalacticaStatusRuntime {
|
|
|
384
362
|
const generation = ++this.generation;
|
|
385
363
|
if (!this.loadedConfig || this.stopped) return;
|
|
386
364
|
const config = this.loadedConfig.config;
|
|
387
|
-
const focus = this.focus;
|
|
388
365
|
const at = Date.now();
|
|
389
366
|
let projectRoot = await detectProjectRoot(this.ctx.cwd, this.runner);
|
|
390
367
|
|
|
@@ -393,6 +370,9 @@ class GalacticaStatusRuntime {
|
|
|
393
370
|
openSpecEnabled && (await pathIsDirectory(join(projectRoot, 'openspec')));
|
|
394
371
|
let openSpecOverview: RefreshOutcome<OpenSpecOverview> = { kind: 'absent' };
|
|
395
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;
|
|
396
376
|
|
|
397
377
|
if (hasOpenSpecProject) {
|
|
398
378
|
openSpecOverview = await this.outcome('openspec', () =>
|
|
@@ -403,31 +383,6 @@ class GalacticaStatusRuntime {
|
|
|
403
383
|
}
|
|
404
384
|
}
|
|
405
385
|
|
|
406
|
-
if (focus.mode === 'none') {
|
|
407
|
-
const passiveChange = config.openspec.change ?? process.env.PI_OPENSPEC_CHANGE;
|
|
408
|
-
if (passiveChange) {
|
|
409
|
-
openspec = await this.outcome('openspec', () =>
|
|
410
|
-
collectOpenSpec({
|
|
411
|
-
cwd: projectRoot,
|
|
412
|
-
runner: this.runner,
|
|
413
|
-
configuredChange: passiveChange,
|
|
414
|
-
now: at,
|
|
415
|
-
}),
|
|
416
|
-
);
|
|
417
|
-
if (openspec.kind === 'success') projectRoot = openspec.value.projectRoot;
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
openspec = await this.outcome('openspec', () =>
|
|
421
|
-
collectOpenSpec({
|
|
422
|
-
cwd: projectRoot,
|
|
423
|
-
runner: this.runner,
|
|
424
|
-
configuredChange: focus.change,
|
|
425
|
-
now: at,
|
|
426
|
-
}),
|
|
427
|
-
);
|
|
428
|
-
if (openspec.kind === 'success') projectRoot = openspec.value.projectRoot;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
386
|
const [diagnostics, orchestration] = await Promise.all([
|
|
432
387
|
!config.enabled || !config.diagnostics.enabled
|
|
433
388
|
? Promise.resolve<RefreshOutcome<DiagnosticsState>>({ kind: 'absent' })
|
|
@@ -457,26 +412,9 @@ class GalacticaStatusRuntime {
|
|
|
457
412
|
}),
|
|
458
413
|
]);
|
|
459
414
|
|
|
460
|
-
if (
|
|
461
|
-
this.stopped ||
|
|
462
|
-
generation !== this.generation ||
|
|
463
|
-
!sameOpenSpecFocus(focus, this.focus)
|
|
464
|
-
) {
|
|
415
|
+
if (this.stopped || generation !== this.generation) {
|
|
465
416
|
return;
|
|
466
417
|
}
|
|
467
|
-
// The gate act is focus-owned commentary: resolved only for a task
|
|
468
|
-
// focus inside an OpenSpec project, after the commit guard, so a
|
|
469
|
-
// superseded refresh never spends a probe on stale truth.
|
|
470
|
-
if (focus.mode === 'task' && hasOpenSpecProject) {
|
|
471
|
-
this.gateAct = {
|
|
472
|
-
change: focus.change,
|
|
473
|
-
line: await resolveGateAct({
|
|
474
|
-
change: focus.change,
|
|
475
|
-
projectRoot,
|
|
476
|
-
runner: this.runner,
|
|
477
|
-
}),
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
418
|
const previous = this.store.get();
|
|
481
419
|
this.updateRefreshHealth(
|
|
482
420
|
'openspec',
|
|
@@ -499,19 +437,18 @@ class GalacticaStatusRuntime {
|
|
|
499
437
|
} else if (openSpecOverview.kind === 'absent') {
|
|
500
438
|
this.openSpecOverview = undefined;
|
|
501
439
|
}
|
|
502
|
-
this.openSpecFeedback =
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
: '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';
|
|
511
448
|
this.store.applyRefresh({
|
|
512
449
|
projectRoot,
|
|
513
450
|
at,
|
|
514
|
-
openspec,
|
|
451
|
+
openspec: { kind: 'absent' },
|
|
515
452
|
diagnostics,
|
|
516
453
|
orchestration,
|
|
517
454
|
});
|
|
@@ -605,21 +542,12 @@ class GalacticaStatusRuntime {
|
|
|
605
542
|
private publish(): void {
|
|
606
543
|
if (this.stopped) return;
|
|
607
544
|
const snapshot = this.store.get();
|
|
608
|
-
// Builders read focus facts through the snapshot layer (recordEvent stores
|
|
609
|
-
// the same object references), so output stays byte-identical while the
|
|
610
|
-
// ledger becomes the feeding seam.
|
|
611
|
-
const focus = this.cockpitFacts.openSpec;
|
|
612
545
|
const subject = this.cockpitFacts.subject;
|
|
613
546
|
const title = this.currentTitleState(snapshot);
|
|
614
|
-
const focusedOpenSpec =
|
|
615
|
-
focus.mode === 'task' && snapshot.openspec?.changeId === focus.change
|
|
616
|
-
? snapshot.openspec
|
|
617
|
-
: null;
|
|
618
547
|
const widgets = buildWidgetSnapshots(
|
|
619
|
-
{ ...snapshot, openspec:
|
|
548
|
+
{ ...snapshot, openspec: null },
|
|
620
549
|
{
|
|
621
|
-
|
|
622
|
-
openSpecFeedback: focus.mode === 'none' ? this.openSpecFeedback : undefined,
|
|
550
|
+
openSpecFeedback: this.openSpecFeedback,
|
|
623
551
|
openSpecOverview: this.openSpecOverview,
|
|
624
552
|
openSpecProjectDetected: this.openSpecProjectDetected,
|
|
625
553
|
},
|
|
@@ -628,8 +556,7 @@ class GalacticaStatusRuntime {
|
|
|
628
556
|
this.pi.events.emit(
|
|
629
557
|
GALACTICA_HEADER_CHANNEL,
|
|
630
558
|
buildHeaderStatusEvent(widgets, {
|
|
631
|
-
openSpec:
|
|
632
|
-
focusedTaskId: focus.mode === 'task' ? focus.taskId : undefined,
|
|
559
|
+
openSpec: null,
|
|
633
560
|
subject,
|
|
634
561
|
orchestration: snapshot.orchestration,
|
|
635
562
|
lifecycle: this.liveLifecycle,
|
|
@@ -653,11 +580,8 @@ class GalacticaStatusRuntime {
|
|
|
653
580
|
const facts = this.cockpitFacts;
|
|
654
581
|
// The left title is Human-owned — session name, subject, directory —
|
|
655
582
|
// never openspec text (truthful-rendered-state law 2026-11-10); the
|
|
656
|
-
//
|
|
657
|
-
const
|
|
658
|
-
facts.openSpec.mode === 'task' && this.gateAct?.change === facts.openSpec.change
|
|
659
|
-
? this.gateAct.line
|
|
660
|
-
: undefined;
|
|
583
|
+
// right tile prints the room aim verbatim (observe-room-aim law).
|
|
584
|
+
const aim = readRoomAim(snapshot.projectRoot);
|
|
661
585
|
const subjectTitle = formatActiveWorkTitle({
|
|
662
586
|
subject: facts.subject,
|
|
663
587
|
...(this.sessionName ? { sessionName: this.sessionName } : {}),
|
|
@@ -666,11 +590,8 @@ class GalacticaStatusRuntime {
|
|
|
666
590
|
});
|
|
667
591
|
return buildTitleState({
|
|
668
592
|
subject: { state: 'set', title: subjectTitle },
|
|
669
|
-
|
|
670
|
-
openspec: snapshot.openspec,
|
|
593
|
+
aim,
|
|
671
594
|
activity: this.titleActivity.current(),
|
|
672
|
-
focusAt: this.focusAt,
|
|
673
|
-
...(gateAct ? { gateAct } : {}),
|
|
674
595
|
});
|
|
675
596
|
}
|
|
676
597
|
|
|
@@ -684,10 +605,6 @@ class GalacticaStatusRuntime {
|
|
|
684
605
|
return snapshotFromFacts(this.cockpitFacts, this.cockpitVersion);
|
|
685
606
|
}
|
|
686
607
|
|
|
687
|
-
currentFocus(): OpenSpecFocus {
|
|
688
|
-
return this.focus;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
608
|
currentSubject(): SessionSubject {
|
|
692
609
|
return this.subject;
|
|
693
610
|
}
|
|
@@ -704,24 +621,6 @@ class GalacticaStatusRuntime {
|
|
|
704
621
|
this.setLiveProjection('understanding', { kind: 'understanding' });
|
|
705
622
|
}
|
|
706
623
|
|
|
707
|
-
setFocus(focus: OpenSpecFocus): void {
|
|
708
|
-
if (sameOpenSpecFocus(this.focus, focus)) return;
|
|
709
|
-
this.focus = focus;
|
|
710
|
-
this.focusAt = Date.now();
|
|
711
|
-
if (focus.mode !== 'task' || this.gateAct?.change !== focus.change) {
|
|
712
|
-
this.gateAct = undefined;
|
|
713
|
-
}
|
|
714
|
-
this.recordEvent({ type: 'focus/openspec', at: Date.now(), focus });
|
|
715
|
-
this.store.clearOpenSpec();
|
|
716
|
-
if (focus.mode === 'task' && focus.awaitingValidation) {
|
|
717
|
-
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
718
|
-
} else if (focus.mode === 'none') {
|
|
719
|
-
this.setLiveProjection('listening');
|
|
720
|
-
}
|
|
721
|
-
this.publish();
|
|
722
|
-
this.launch(this.refresh(false), 'openspec');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
624
|
setSubject(subject: SessionSubject): void {
|
|
726
625
|
if (sameSessionSubject(this.subject, subject)) return;
|
|
727
626
|
this.subject = subject;
|
|
@@ -904,10 +803,6 @@ class GalacticaStatusRuntime {
|
|
|
904
803
|
this.publish();
|
|
905
804
|
return;
|
|
906
805
|
}
|
|
907
|
-
if (this.focus.mode === 'task') {
|
|
908
|
-
this.setLiveProjection('waiting', { kind: 'awaiting-validation' });
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
806
|
this.setLiveProjection('listening');
|
|
912
807
|
}
|
|
913
808
|
|
|
@@ -932,13 +827,10 @@ class GalacticaStatusRuntime {
|
|
|
932
827
|
);
|
|
933
828
|
lines.push(
|
|
934
829
|
snapshot.openspec
|
|
935
|
-
? `OpenSpec
|
|
936
|
-
snapshot.openspec,
|
|
937
|
-
this.focus.mode === 'task' ? this.focus.taskId : null,
|
|
938
|
-
)}`
|
|
830
|
+
? `OpenSpec: ${formatOpenSpec(snapshot.openspec)}`
|
|
939
831
|
: this.openSpecFeedback
|
|
940
|
-
? `OpenSpec
|
|
941
|
-
: 'OpenSpec
|
|
832
|
+
? `OpenSpec: ${this.openSpecFeedback}`
|
|
833
|
+
: 'OpenSpec: no aim',
|
|
942
834
|
);
|
|
943
835
|
lines.push(
|
|
944
836
|
snapshot.diagnostics
|
|
@@ -960,7 +852,6 @@ class GalacticaStatusRuntime {
|
|
|
960
852
|
projectRoot: snapshot.projectRoot,
|
|
961
853
|
configPath: snapshot.configPath,
|
|
962
854
|
configWarnings: snapshot.configWarnings,
|
|
963
|
-
focus: this.focus,
|
|
964
855
|
openSpecFeedback: this.openSpecFeedback ?? null,
|
|
965
856
|
openSpecOverview: this.openSpecOverview ?? null,
|
|
966
857
|
detected: {
|
|
@@ -1011,30 +902,12 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1011
902
|
}
|
|
1012
903
|
});
|
|
1013
904
|
|
|
1014
|
-
// work-state owns focus writes and emits `focus-changed` after each
|
|
1015
|
-
// successful one (pi.events); the renderer subscribes for immediate
|
|
1016
|
-
// repaint (pure-renderer-split Slice 2).
|
|
1017
|
-
const stopFocusRepaint = pi.events.on('focus-changed', (data: unknown) => {
|
|
1018
|
-
const focus = parseOpenSpecFocus(data);
|
|
1019
|
-
if (!focus) return;
|
|
1020
|
-
runtime?.setFocus(focus);
|
|
1021
|
-
runtime?.settle();
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
905
|
pi.on('session_start', (_event, ctx) => {
|
|
1025
906
|
runtime?.stop();
|
|
1026
907
|
const branch = ctx.sessionManager.getBranch();
|
|
1027
|
-
const focus = restoreOpenSpecFocus(branch);
|
|
1028
908
|
const subject = restoreSessionSubject(branch);
|
|
1029
909
|
const sessionName = pi.getSessionName() ?? ctx.sessionManager.getSessionName();
|
|
1030
|
-
runtime = new GalacticaStatusRuntime(
|
|
1031
|
-
pi,
|
|
1032
|
-
ctx,
|
|
1033
|
-
publisher,
|
|
1034
|
-
focus,
|
|
1035
|
-
subject,
|
|
1036
|
-
sessionName,
|
|
1037
|
-
);
|
|
910
|
+
runtime = new GalacticaStatusRuntime(pi, ctx, publisher, subject, sessionName);
|
|
1038
911
|
runtime.start();
|
|
1039
912
|
});
|
|
1040
913
|
|
|
@@ -1044,7 +917,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1044
917
|
|
|
1045
918
|
pi.on('session_tree', (_event, ctx) => {
|
|
1046
919
|
const branch = ctx.sessionManager.getBranch();
|
|
1047
|
-
runtime?.setFocus(restoreOpenSpecFocus(branch));
|
|
1048
920
|
runtime?.setSubject(restoreSessionSubject(branch));
|
|
1049
921
|
runtime?.settle();
|
|
1050
922
|
});
|
|
@@ -1131,7 +1003,6 @@ export default function galacticaStatus(pi: ExtensionAPI): void {
|
|
|
1131
1003
|
runtime = undefined;
|
|
1132
1004
|
publisher.removeAll();
|
|
1133
1005
|
stopReady();
|
|
1134
|
-
stopFocusRepaint();
|
|
1135
1006
|
});
|
|
1136
1007
|
|
|
1137
1008
|
pi.registerCommand('galactica-status', {
|
|
@@ -1,19 +1,7 @@
|
|
|
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' };
|
|
@@ -42,29 +30,6 @@ function explicitId(value: unknown): string | undefined {
|
|
|
42
30
|
return normalized || undefined;
|
|
43
31
|
}
|
|
44
32
|
|
|
45
|
-
export function taskFocus(
|
|
46
|
-
change: unknown,
|
|
47
|
-
taskId: unknown,
|
|
48
|
-
awaitingValidation?: unknown,
|
|
49
|
-
): TaskOpenSpecFocus | undefined {
|
|
50
|
-
const normalizedChange = explicitId(change);
|
|
51
|
-
const normalizedTaskId = explicitId(taskId);
|
|
52
|
-
if (!normalizedChange || !normalizedTaskId) return undefined;
|
|
53
|
-
return {
|
|
54
|
-
mode: 'task',
|
|
55
|
-
change: normalizedChange,
|
|
56
|
-
taskId: normalizedTaskId,
|
|
57
|
-
awaitingValidation: awaitingValidation === true,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
|
|
62
|
-
if (!isRecord(value)) return undefined;
|
|
63
|
-
if (value.mode === 'none') return NO_OPEN_SPEC_FOCUS;
|
|
64
|
-
if (value.mode !== 'task') return undefined;
|
|
65
|
-
return taskFocus(value.change, value.taskId, value.awaitingValidation);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
33
|
export function sessionSubject(
|
|
69
34
|
state: unknown,
|
|
70
35
|
title?: unknown,
|
|
@@ -84,22 +49,6 @@ export function parseSessionSubject(value: unknown): SessionSubject | undefined
|
|
|
84
49
|
return sessionSubject(value.state, value.title);
|
|
85
50
|
}
|
|
86
51
|
|
|
87
|
-
export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus {
|
|
88
|
-
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
89
|
-
const entry = entries[index];
|
|
90
|
-
if (
|
|
91
|
-
!isRecord(entry) ||
|
|
92
|
-
entry.type !== 'custom' ||
|
|
93
|
-
entry.customType !== OPEN_SPEC_FOCUS_ENTRY_TYPE
|
|
94
|
-
) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const focus = parseOpenSpecFocus(entry.data);
|
|
98
|
-
if (focus) return focus;
|
|
99
|
-
}
|
|
100
|
-
return NO_OPEN_SPEC_FOCUS;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
52
|
export function restoreSessionSubject(entries: readonly unknown[]): SessionSubject {
|
|
104
53
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
105
54
|
const entry = entries[index];
|
|
@@ -116,39 +65,6 @@ export function restoreSessionSubject(entries: readonly unknown[]): SessionSubje
|
|
|
116
65
|
return NO_SESSION_SUBJECT;
|
|
117
66
|
}
|
|
118
67
|
|
|
119
|
-
export function taskflowRunFocus(input: unknown): TaskOpenSpecFocus | undefined {
|
|
120
|
-
if (!isRecord(input) || input.action !== 'run' || !isRecord(input.args)) {
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
const focus = taskFocus(input.args.change, input.args.task);
|
|
124
|
-
return focus && focus.change !== 'none' && focus.taskId !== 'none'
|
|
125
|
-
? focus
|
|
126
|
-
: undefined;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Task identity (mode · change · taskId) — the replace-safeguard's
|
|
130
|
-
* question; the validation flag is state, not identity. */
|
|
131
|
-
export function sameTaskFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
|
|
132
|
-
if (left.mode !== right.mode) return false;
|
|
133
|
-
return (
|
|
134
|
-
left.mode === 'none' ||
|
|
135
|
-
(right.mode === 'task' &&
|
|
136
|
-
left.change === right.change &&
|
|
137
|
-
left.taskId === right.taskId)
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): boolean {
|
|
142
|
-
if (left.mode !== right.mode) return false;
|
|
143
|
-
return (
|
|
144
|
-
left.mode === 'none' ||
|
|
145
|
-
(right.mode === 'task' &&
|
|
146
|
-
left.change === right.change &&
|
|
147
|
-
left.taskId === right.taskId &&
|
|
148
|
-
left.awaitingValidation === right.awaitingValidation)
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
68
|
export function sameSessionSubject(
|
|
153
69
|
left: SessionSubject,
|
|
154
70
|
right: SessionSubject,
|
|
@@ -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 }
|
|
@@ -270,7 +270,7 @@ export function parseApplyInstructions(value: unknown): ApplyInstructionsResult
|
|
|
270
270
|
complete: finiteNumber(value.progress.complete),
|
|
271
271
|
}
|
|
272
272
|
: undefined;
|
|
273
|
-
// A blocked apply (
|
|
273
|
+
// A blocked apply (part-way through, artifacts missing) reports zero progress;
|
|
274
274
|
// those zeros must never shadow the honest `openspec list` counts.
|
|
275
275
|
const progress =
|
|
276
276
|
state !== 'blocked' && (reportedProgress?.total ?? 0) > 0
|
|
@@ -550,7 +550,7 @@ export async function collectOpenSpec(options: {
|
|
|
550
550
|
// tile at 0/N (simplify-deck-defaults). CLI progress and list counts
|
|
551
551
|
// remain fallbacks; the blocked-apply zero-guard stays for the
|
|
552
552
|
// no-tasks case.
|
|
553
|
-
// File-truth
|
|
553
|
+
// File-truth precedence: checkbox tasks first, then done/total artifacts
|
|
554
554
|
// (file presence via `openspec status`) — CLI task opinion cannot count
|
|
555
555
|
// custom schemas and pins 0/N (truthful-rendered-state). With neither
|
|
556
556
|
// readable, counts stay absent — absence never becomes zero.
|