@aemonge-dev/pi-stardeck 0.11.0 → 0.13.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.
@@ -1,7 +1,6 @@
1
1
  import { basename } from 'node:path';
2
2
 
3
3
  export const OPEN_SPEC_FOCUS_ENTRY_TYPE = 'galactica-status.openspec-focus.v1';
4
- export const SESSION_WORK_FOCUS_ENTRY_TYPE = 'galactica-status.session-work.v1';
5
4
  export const SESSION_SUBJECT_ENTRY_TYPE = 'galactica-status.session-subject.v1';
6
5
 
7
6
  export type TaskOpenSpecFocus = {
@@ -15,11 +14,6 @@ export type OpenSpecFocus = { mode: 'none' } | TaskOpenSpecFocus;
15
14
 
16
15
  export const NO_OPEN_SPEC_FOCUS: OpenSpecFocus = { mode: 'none' };
17
16
 
18
- export type SessionWorkFocus =
19
- { state: 'clear' } | { state: 'active' | 'validation'; intent: string };
20
-
21
- export const NO_SESSION_WORK_FOCUS: SessionWorkFocus = { state: 'clear' };
22
-
23
17
  export type SessionSubject = { state: 'clear' } | { state: 'set'; title: string };
24
18
 
25
19
  export const NO_SESSION_SUBJECT: SessionSubject = { state: 'clear' };
@@ -27,7 +21,6 @@ export const NO_SESSION_SUBJECT: SessionSubject = { state: 'clear' };
27
21
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
28
22
  const MAX_TITLE_POINTS = 180;
29
23
  const MAX_TASK_TITLE_POINTS = 72;
30
- const MAX_WORK_INTENT_POINTS = 160;
31
24
  const MAX_SUBJECT_POINTS = 120;
32
25
 
33
26
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -73,25 +66,6 @@ export function parseOpenSpecFocus(value: unknown): OpenSpecFocus | undefined {
73
66
  return taskFocus(value.change, value.taskId, value.awaitingValidation);
74
67
  }
75
68
 
76
- export function sessionWorkFocus(
77
- state: unknown,
78
- intent?: unknown,
79
- ): SessionWorkFocus | undefined {
80
- if (state === 'clear') return NO_SESSION_WORK_FOCUS;
81
- if (state !== 'active' && state !== 'validation') return undefined;
82
- const normalizedIntent = explicitId(intent);
83
- if (!normalizedIntent) return undefined;
84
- return {
85
- state,
86
- intent: shorten(normalizedIntent, MAX_WORK_INTENT_POINTS),
87
- };
88
- }
89
-
90
- export function parseSessionWorkFocus(value: unknown): SessionWorkFocus | undefined {
91
- if (!isRecord(value)) return undefined;
92
- return sessionWorkFocus(value.state, value.intent);
93
- }
94
-
95
69
  export function sessionSubject(
96
70
  state: unknown,
97
71
  title?: unknown,
@@ -111,18 +85,6 @@ export function parseSessionSubject(value: unknown): SessionSubject | undefined
111
85
  return sessionSubject(value.state, value.title);
112
86
  }
113
87
 
114
- /** Derive a session title from a raw user prompt — the extension-side
115
- * fallback that makes the agent's first-turn `title` call optional
116
- * polish instead of a load-bearing bet (every agent skipped it). The
117
- * same sanitizer bounds it; slash commands are commands, not subjects:
118
- * they never title. */
119
- export function titleFromPrompt(prompt: unknown): SessionSubject | undefined {
120
- if (typeof prompt !== 'string') return undefined;
121
- const normalized = normalizeTitlePart(prompt);
122
- if (!normalized || normalized.startsWith('/')) return undefined;
123
- return sessionSubject('set', normalized);
124
- }
125
-
126
88
  export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus {
127
89
  for (let index = entries.length - 1; index >= 0; index -= 1) {
128
90
  const entry = entries[index];
@@ -139,22 +101,6 @@ export function restoreOpenSpecFocus(entries: readonly unknown[]): OpenSpecFocus
139
101
  return NO_OPEN_SPEC_FOCUS;
140
102
  }
141
103
 
142
- export function restoreSessionWorkFocus(entries: readonly unknown[]): SessionWorkFocus {
143
- for (let index = entries.length - 1; index >= 0; index -= 1) {
144
- const entry = entries[index];
145
- if (
146
- !isRecord(entry) ||
147
- entry.type !== 'custom' ||
148
- entry.customType !== SESSION_WORK_FOCUS_ENTRY_TYPE
149
- ) {
150
- continue;
151
- }
152
- const focus = parseSessionWorkFocus(entry.data);
153
- if (focus) return focus;
154
- }
155
- return NO_SESSION_WORK_FOCUS;
156
- }
157
-
158
104
  export function restoreSessionSubject(entries: readonly unknown[]): SessionSubject {
159
105
  for (let index = entries.length - 1; index >= 0; index -= 1) {
160
106
  const entry = entries[index];
@@ -204,16 +150,6 @@ export function sameOpenSpecFocus(left: OpenSpecFocus, right: OpenSpecFocus): bo
204
150
  );
205
151
  }
206
152
 
207
- export function sameSessionWorkFocus(
208
- left: SessionWorkFocus,
209
- right: SessionWorkFocus,
210
- ): boolean {
211
- if (left.state !== right.state) return false;
212
- return (
213
- left.state === 'clear' || (right.state !== 'clear' && left.intent === right.intent)
214
- );
215
- }
216
-
217
153
  export function sameSessionSubject(
218
154
  left: SessionSubject,
219
155
  right: SessionSubject,
@@ -226,7 +162,6 @@ export function sameSessionSubject(
226
162
 
227
163
  export function formatActiveWorkTitle(options: {
228
164
  focus: OpenSpecFocus;
229
- workFocus?: SessionWorkFocus;
230
165
  subject?: SessionSubject;
231
166
  taskTitle?: string;
232
167
  sessionName?: string;
@@ -239,11 +174,8 @@ export function formatActiveWorkTitle(options: {
239
174
  ? shorten(normalizeTitlePart(options.taskTitle), MAX_TASK_TITLE_POINTS)
240
175
  : '';
241
176
  detail = `${options.focus.change} › ${options.focus.taskId}${taskTitle ? ` ${taskTitle}` : ''}`;
242
- } else if (options.workFocus && options.workFocus.state !== 'clear') {
243
- detail =
244
- options.workFocus.state === 'validation'
245
- ? `Validate › ${options.workFocus.intent}`
246
- : options.workFocus.intent;
177
+ } else if (normalizeTitlePart(options.sessionName ?? '')) {
178
+ detail = normalizeTitlePart(options.sessionName ?? '');
247
179
  } else if (options.subject && options.subject.state === 'set') {
248
180
  detail = options.subject.title;
249
181
  } else {
@@ -254,5 +186,8 @@ export function formatActiveWorkTitle(options: {
254
186
  'pi';
255
187
  }
256
188
 
257
- return shorten(`π ${normalizeTitlePart(detail)}`, MAX_TITLE_POINTS);
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);
258
193
  }
@@ -5,7 +5,7 @@ import type {
5
5
  RunSpan,
6
6
  StageValue,
7
7
  } from './types.ts';
8
- import type { OpenSpecFocus, SessionSubject, SessionWorkFocus } from './active-work.ts';
8
+ import type { OpenSpecFocus, SessionSubject } from './active-work.ts';
9
9
 
10
10
  /**
11
11
  * Append-only event ledger for the cockpit status core.
@@ -18,7 +18,6 @@ import type { OpenSpecFocus, SessionSubject, SessionWorkFocus } from './active-w
18
18
 
19
19
  export const LEDGER_EVENT_TYPES = [
20
20
  'focus/openspec',
21
- 'focus/work',
22
21
  'focus/subject',
23
22
  'run/start',
24
23
  'run/stage',
@@ -37,7 +36,6 @@ export type LedgerEvent =
37
36
  at: number;
38
37
  focus: OpenSpecFocus;
39
38
  }
40
- | { type: 'focus/work'; at: number; work: SessionWorkFocus }
41
39
  | { type: 'focus/subject'; at: number; subject: SessionSubject }
42
40
  | { type: 'run/start'; at: number; span: RunSpan }
43
41
  | { type: 'run/stage'; at: number; id: string; stage: StageValue; declared: boolean }
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
2
2
  import { resolve, sep } from 'node:path';
3
3
  import type {
4
4
  CommandRunner,
5
+ OpenSpecArtifact,
5
6
  OpenSpecHierarchyProgress,
6
7
  OpenSpecOverview,
7
8
  OpenSpecState,
@@ -27,6 +28,19 @@ interface ApplyInstructionsResult {
27
28
  tasksPath?: string;
28
29
  }
29
30
 
31
+ /** Parsed `openspec status` artifacts plus their resolved files. Paths stay
32
+ * internal: they feed the file-truth item counter, never the contract. */
33
+ interface ParsedArtifact extends OpenSpecArtifact {
34
+ resolvedPath?: string;
35
+ }
36
+
37
+ interface ParsedOpenSpecStatus {
38
+ schemaName?: string;
39
+ artifacts?: ParsedArtifact[];
40
+ currentArtifact?: string;
41
+ changeRoot?: string;
42
+ }
43
+
30
44
  function isRecord(value: unknown): value is Record<string, unknown> {
31
45
  return typeof value === 'object' && value !== null && !Array.isArray(value);
32
46
  }
@@ -117,6 +131,51 @@ export function parseTaskMarkdown(content: string): OpenSpecTask[] {
117
131
  return tasks;
118
132
  }
119
133
 
134
+ /** File-truth checkbox counting: `{ done, total }` over `- [x]` / `- [ ]`
135
+ * items. Zero checkboxes is absence, not zero — `undefined` emits only
136
+ * when a readable source file actually carries countable items. */
137
+ export function parseCheckboxCounts(
138
+ content: string,
139
+ ): { done: number; total: number } | undefined {
140
+ const pattern = /^\s*-\s*\[([ xX])\]/gmu;
141
+ let total = 0;
142
+ let done = 0;
143
+ let match: RegExpExecArray | null;
144
+ while ((match = pattern.exec(content)) !== null) {
145
+ total += 1;
146
+ if ((match[1] ?? '').toLowerCase() === 'x') done += 1;
147
+ }
148
+ return total > 0 ? { done, total } : undefined;
149
+ }
150
+
151
+ /** mtime-keyed parse cache: a file is re-read only after its mtime moves.
152
+ * Unreadable files stay uncached so the next refresh retries honestly. */
153
+ const artifactItemsCache = new Map<
154
+ string,
155
+ { mtimeMs: number; counts: { done: number; total: number } | undefined }
156
+ >();
157
+
158
+ export async function readArtifactItems(
159
+ path: string,
160
+ ): Promise<{ done: number; total: number } | undefined> {
161
+ let mtimeMs: number;
162
+ try {
163
+ mtimeMs = (await fs.stat(path)).mtimeMs;
164
+ } catch {
165
+ return undefined;
166
+ }
167
+ const cached = artifactItemsCache.get(path);
168
+ if (cached && cached.mtimeMs === mtimeMs) return cached.counts;
169
+ let counts: { done: number; total: number } | undefined;
170
+ try {
171
+ counts = parseCheckboxCounts(await fs.readFile(path, 'utf8'));
172
+ } catch {
173
+ return undefined;
174
+ }
175
+ artifactItemsCache.set(path, { mtimeMs, counts });
176
+ return counts;
177
+ }
178
+
120
179
  export function parseTaskHierarchy(
121
180
  content: string,
122
181
  ): OpenSpecHierarchyProgress | undefined {
@@ -234,31 +293,60 @@ function chooseChange(
234
293
  return selected;
235
294
  }
236
295
 
237
- function parseOpenSpecStatus(value: unknown): {
238
- schemaName?: string;
239
- ladder?: { done: number; total: number; rung?: string };
240
- } {
296
+ function parseOpenSpecStatus(value: unknown): ParsedOpenSpecStatus {
241
297
  if (!isRecord(value)) return {};
242
298
  const schemaName =
243
299
  typeof value.schemaName === 'string' && value.schemaName.trim()
244
300
  ? value.schemaName.trim()
245
301
  : undefined;
246
- let ladder: { done: number; total: number; rung?: string } | undefined;
302
+ const changeRoot =
303
+ typeof value.changeRoot === 'string' && value.changeRoot.trim()
304
+ ? value.changeRoot.trim()
305
+ : undefined;
306
+ let artifacts: ParsedArtifact[] | undefined;
307
+ let currentArtifact: string | undefined;
247
308
  if (Array.isArray(value.artifacts) && value.artifacts.length > 0) {
248
- let done = 0;
249
- let rung: string | undefined;
309
+ const parsed: ParsedArtifact[] = [];
310
+ let lastDone: string | undefined;
250
311
  for (const artifact of value.artifacts) {
251
312
  if (!isRecord(artifact)) continue;
252
- const id = typeof artifact.id === 'string' ? artifact.id : undefined;
253
- if (rung === undefined && id) rung = id;
254
- if (artifact.status === 'done') {
255
- done += 1;
256
- if (id) rung = id;
313
+ const id =
314
+ typeof artifact.id === 'string' && artifact.id.trim()
315
+ ? artifact.id.trim()
316
+ : undefined;
317
+ if (!id) continue;
318
+ const done = artifact.status === 'done';
319
+ if (done) lastDone = id;
320
+ // The artifacts array carries a relative outputPath; the item counter
321
+ // resolves it against the change root — contained paths only.
322
+ let resolvedPath: string | undefined;
323
+ const outputPath =
324
+ typeof artifact.outputPath === 'string' && artifact.outputPath.trim()
325
+ ? artifact.outputPath.trim()
326
+ : undefined;
327
+ if (changeRoot && outputPath) {
328
+ const root = resolve(changeRoot);
329
+ const candidate = resolve(root, outputPath);
330
+ if (candidate === root || candidate.startsWith(`${root}${sep}`)) {
331
+ resolvedPath = candidate;
332
+ }
257
333
  }
334
+ parsed.push({ id, done, ...(resolvedPath ? { resolvedPath } : {}) });
335
+ }
336
+ if (parsed.length > 0) {
337
+ // The current artifact is the one the work is in: the first not yet
338
+ // done; once everything is complete, the last done artifact.
339
+ const inProgress = parsed.find((artifact) => !artifact.done);
340
+ currentArtifact = inProgress ? inProgress.id : lastDone;
341
+ artifacts = parsed;
258
342
  }
259
- ladder = { done, total: value.artifacts.length, ...(rung ? { rung } : {}) };
260
343
  }
261
- return { ...(schemaName ? { schemaName } : {}), ...(ladder ? { ladder } : {}) };
344
+ return {
345
+ ...(schemaName ? { schemaName } : {}),
346
+ ...(artifacts ? { artifacts } : {}),
347
+ ...(currentArtifact ? { currentArtifact } : {}),
348
+ ...(changeRoot ? { changeRoot } : {}),
349
+ };
262
350
  }
263
351
 
264
352
  async function runJson(
@@ -295,6 +383,37 @@ async function isIdeaChange(projectRoot: string, changeName: string): Promise<bo
295
383
  }
296
384
  }
297
385
 
386
+ /** Newest mtime over the change directory's direct files — the file-truth
387
+ * "the change moved on" timestamp attention staleness compares against.
388
+ * Change directories are flat; absent or unreadable dirs stay undefined. */
389
+ async function changeDirectoryMtime(
390
+ projectRoot: string,
391
+ changeName: string,
392
+ ): Promise<number | undefined> {
393
+ const changesRoot = resolve(projectRoot, 'openspec', 'changes');
394
+ const changeRoot = resolve(changesRoot, changeName);
395
+ if (changeRoot !== changesRoot && !changeRoot.startsWith(`${changesRoot}${sep}`)) {
396
+ return undefined;
397
+ }
398
+ let entries;
399
+ try {
400
+ entries = await fs.readdir(changeRoot, { withFileTypes: true });
401
+ } catch {
402
+ return undefined;
403
+ }
404
+ let newest: number | undefined;
405
+ for (const entry of entries) {
406
+ if (!entry.isFile()) continue;
407
+ try {
408
+ const mtimeMs = (await fs.stat(resolve(changeRoot, entry.name))).mtimeMs;
409
+ newest = newest === undefined ? mtimeMs : Math.max(newest, mtimeMs);
410
+ } catch {
411
+ // A file vanishing between readdir and stat is honest absence.
412
+ }
413
+ }
414
+ return newest;
415
+ }
416
+
298
417
  export async function collectOpenSpecOverview(options: {
299
418
  cwd: string;
300
419
  runner: CommandRunner;
@@ -361,7 +480,8 @@ export async function collectOpenSpec(options: {
361
480
  }
362
481
 
363
482
  let schemaName: string | undefined;
364
- let ladder: OpenSpecState['ladder'];
483
+ let artifacts: OpenSpecArtifact[] | undefined;
484
+ let currentArtifact: string | undefined;
365
485
  try {
366
486
  const status = parseOpenSpecStatus(
367
487
  await runJson(
@@ -372,10 +492,28 @@ export async function collectOpenSpec(options: {
372
492
  ),
373
493
  );
374
494
  schemaName = status.schemaName;
375
- ladder = status.ladder;
495
+ currentArtifact = status.currentArtifact;
496
+ if (status.artifacts) {
497
+ // Item counts come from the artifact FILES, not CLI opinion: the CLI
498
+ // cannot count custom-schema items (taskTrackingConfigured: false),
499
+ // and only readable files emit counts — absence never becomes zero.
500
+ artifacts = await Promise.all(
501
+ status.artifacts.map(async (artifact) => {
502
+ const items = artifact.resolvedPath
503
+ ? await readArtifactItems(artifact.resolvedPath)
504
+ : undefined;
505
+ return {
506
+ id: artifact.id,
507
+ done: artifact.done,
508
+ ...(items ? { items } : {}),
509
+ };
510
+ }),
511
+ );
512
+ }
376
513
  } catch {
377
- // Ladder facts are optional; the list counts remain valid without them.
514
+ // Artifact facts are optional; the list counts remain valid without them.
378
515
  }
516
+ const changeModifiedAt = await changeDirectoryMtime(projectRoot, change.name);
379
517
 
380
518
  let tasks: OpenSpecTask[] = [];
381
519
  let tasksPath: string | undefined;
@@ -441,7 +579,9 @@ export async function collectOpenSpec(options: {
441
579
  ...(tasksPath ? { tasksPath } : {}),
442
580
  ...(hierarchy ? { hierarchy } : {}),
443
581
  ...(schemaName ? { schemaName } : {}),
444
- ...(ladder ? { ladder } : {}),
582
+ ...(artifacts ? { artifacts } : {}),
583
+ ...(currentArtifact ? { currentArtifact } : {}),
584
+ ...(changeModifiedAt !== undefined ? { changeModifiedAt } : {}),
445
585
  meta: {
446
586
  source: 'openspec-cli',
447
587
  refreshedAt: now,
@@ -1,4 +1,4 @@
1
- import type { SessionSubject, SessionWorkFocus } from './active-work.ts';
1
+ import type { SessionSubject } from './active-work.ts';
2
2
  import type { ActiveRunSpanView, ActiveRuntimeRuns } from './runtime-runs.ts';
3
3
  import type { TitleState } from './title-state.ts';
4
4
  import type { TurnCostSnapshot } from './turn-cost.ts';
@@ -24,7 +24,6 @@ export const GALACTICA_HEADER_CHANNEL = 'galactica-status:header';
24
24
  const OPEN_SPEC_WIDGET_ID = 'galactica.openspec';
25
25
  const OPEN_SPEC_SCOPE_SEPARATOR_WIDGET_ID = 'galactica.openspec-scope-separator';
26
26
  const OPEN_SPEC_PROGRESS_WIDGET_ID = 'galactica.openspec-progress';
27
- const SESSION_WORK_WIDGET_ID = 'galactica.work';
28
27
  const DIAGNOSTICS_WIDGET_ID = 'galactica.diagnostics';
29
28
  const ORCHESTRATION_WIDGET_ID = 'galactica.orchestration';
30
29
 
@@ -88,13 +87,6 @@ function openSpecColor(state: OpenSpecState): WidgetColor {
88
87
  return 'accent';
89
88
  }
90
89
 
91
- export function formatSessionWork(focus: SessionWorkFocus): string {
92
- if (focus.state === 'clear') return 'Work · clear';
93
- return focus.state === 'validation'
94
- ? `Validate · ${focus.intent}`
95
- : `Work · ${focus.intent}`;
96
- }
97
-
98
90
  export function formatOpenSpecOverview(overview: OpenSpecOverview): string {
99
91
  return (
100
92
  `${overview.completedPlans}/${overview.totalPlans}` +
@@ -209,21 +201,13 @@ export function buildWidgetSnapshots(
209
201
  openSpecFeedback?: OpenSpecFeedback;
210
202
  openSpecOverview?: OpenSpecOverview;
211
203
  openSpecProjectDetected?: boolean;
212
- workFocus?: SessionWorkFocus;
213
204
  } = {},
214
205
  ): FancyFooterWidget[] {
215
206
  const compact = options.compact ?? false;
216
207
  const now = options.now ?? Date.now();
217
208
  const widgets: FancyFooterWidget[] = [];
218
209
 
219
- const hasActiveFallback =
220
- !options.openSpecOverview &&
221
- Boolean(options.workFocus && options.workFocus.state !== 'clear');
222
-
223
- if (
224
- options.openSpecOverview ||
225
- (options.openSpecProjectDetected === false && !hasActiveFallback)
226
- ) {
210
+ if (options.openSpecOverview || options.openSpecProjectDetected === false) {
227
211
  widgets.push({
228
212
  id: OPEN_SPEC_SCOPE_SEPARATOR_WIDGET_ID,
229
213
  label: 'OpenSpec scope separator',
@@ -265,41 +249,7 @@ export function buildWidgetSnapshots(
265
249
  // Frozen contract: without an OpenSpec focus there is no progress surface —
266
250
  // no aggregate overview, no 'no OpenSpec' narration.
267
251
 
268
- if (
269
- !snapshot.openspec &&
270
- !options.openSpecOverview &&
271
- options.workFocus &&
272
- options.workFocus.state !== 'clear'
273
- ) {
274
- const color: WidgetColor =
275
- options.workFocus.state === 'validation' ? 'warning' : 'accent';
276
- const glyph =
277
- options.workFocus.state === 'validation'
278
- ? { nerd: '◇⠀', emoji: '◇⠀', unicode: '◇⠀', ascii: '? ' }
279
- : { nerd: '▶⠀', emoji: '▶⠀', unicode: '▶⠀', ascii: '> ' };
280
- widgets.push({
281
- id: SESSION_WORK_WIDGET_ID,
282
- label: options.workFocus.state === 'validation' ? 'Validate' : 'Focus',
283
- description:
284
- options.workFocus.state === 'validation'
285
- ? 'Session focus awaiting Human validation; its title remains in the header'
286
- : 'Active Session Work focus; its title remains in the header',
287
- content: {
288
- type: 'text',
289
- text: options.workFocus.state === 'validation' ? 'Validate' : 'Focus',
290
- },
291
- icon: { glyphs: glyph, color },
292
- style: { textColor: color },
293
- layout: {
294
- row: 0,
295
- position: 0,
296
- align: 'right',
297
- fill: 'none',
298
- },
299
- });
300
- }
301
-
302
- if (!snapshot.openspec && !options.workFocus && options.openSpecFeedback) {
252
+ if (!snapshot.openspec && options.openSpecFeedback) {
303
253
  const unavailable = options.openSpecFeedback === 'unavailable';
304
254
  const color: WidgetColor = unavailable ? 'warning' : 'dim';
305
255
  const stateText =
@@ -466,11 +416,7 @@ export const HEADER_LIFECYCLES = [
466
416
 
467
417
  export type HeaderLifecycle = (typeof HEADER_LIFECYCLES)[number];
468
418
 
469
- export const HEADER_SELECTION_SOURCES = [
470
- 'openspec',
471
- 'session-work',
472
- 'subject',
473
- ] as const;
419
+ export const HEADER_SELECTION_SOURCES = ['openspec', 'subject'] as const;
474
420
 
475
421
  export type HeaderSelection = {
476
422
  source: (typeof HEADER_SELECTION_SOURCES)[number];
@@ -547,7 +493,6 @@ function selectedOpenSpecTitles(
547
493
  export function headerSelection(options: {
548
494
  openSpec?: OpenSpecState | null;
549
495
  focusedTaskId?: string;
550
- workFocus?: SessionWorkFocus;
551
496
  subject?: SessionSubject;
552
497
  }): HeaderSelection | null {
553
498
  if (options.openSpec) {
@@ -557,13 +502,6 @@ export function headerSelection(options: {
557
502
  color: openSpecColor(options.openSpec),
558
503
  };
559
504
  }
560
- if (options.workFocus && options.workFocus.state !== 'clear') {
561
- return {
562
- source: 'session-work',
563
- titles: [normalizeInline(options.workFocus.intent)],
564
- color: options.workFocus.state === 'validation' ? 'warning' : 'accent',
565
- };
566
- }
567
505
  if (options.subject && options.subject.state === 'set') {
568
506
  return {
569
507
  source: 'subject',
@@ -884,7 +822,6 @@ export function buildHeaderStatusEvent(
884
822
  options: {
885
823
  openSpec?: OpenSpecState | null;
886
824
  focusedTaskId?: string;
887
- workFocus?: SessionWorkFocus;
888
825
  subject?: SessionSubject;
889
826
  orchestration?: OrchestrationState | null;
890
827
  lifecycle?: HeaderLifecycle;
@@ -932,7 +869,7 @@ export function buildHeaderStatusEvent(
932
869
  orchestrationLifecycle ??
933
870
  options.lifecycle ??
934
871
  headerLifecycleForActivity(activity) ??
935
- (options.workFocus?.state === 'validation' ? 'waiting' : 'listening');
872
+ 'listening';
936
873
  const lifecycle: HeaderLifecycle = blocked
937
874
  ? 'blocked'
938
875
  : options.lifecycle === 'aborted'
@@ -942,7 +879,7 @@ export function buildHeaderStatusEvent(
942
879
  const projectedActivity =
943
880
  activityPath !== undefined ? { activityPath } : activity ? { activity } : {};
944
881
  const selection = headerSelection(options);
945
- const approvalRequired = options.workFocus?.state === 'validation';
882
+ const approvalRequired = false;
946
883
  const suggestion = headerSuggestion({
947
884
  lifecycle,
948
885
  activity,
@@ -960,13 +897,6 @@ export function buildHeaderStatusEvent(
960
897
  color: openSpecColor(state),
961
898
  ...projectedActivity,
962
899
  };
963
- } else if (options.workFocus && options.workFocus.state !== 'clear') {
964
- work = {
965
- lifecycle,
966
- titles: [normalizeInline(options.workFocus.intent)],
967
- color: options.workFocus.state === 'validation' ? 'warning' : 'accent',
968
- ...projectedActivity,
969
- };
970
900
  } else if (options.orchestration) {
971
901
  work = {
972
902
  lifecycle,
@@ -1,5 +1,5 @@
1
1
  import type { LedgerEvent } from './ledger.ts';
2
- import type { OpenSpecFocus, SessionSubject, SessionWorkFocus } from './active-work.ts';
2
+ import type { OpenSpecFocus, SessionSubject } from './active-work.ts';
3
3
  import type {
4
4
  CockpitSnapshot,
5
5
  ProgressFact,
@@ -19,7 +19,6 @@ import type {
19
19
 
20
20
  export type ReducerFacts = {
21
21
  openSpec: OpenSpecFocus;
22
- work: SessionWorkFocus;
23
22
  subject: SessionSubject;
24
23
  runs: ReadonlyMap<string, RunSpan>;
25
24
  progress: ProgressFact | null;
@@ -30,7 +29,6 @@ export const NO_SELECTION: SelectionState = { kind: 'none' };
30
29
  export function emptyFacts(): ReducerFacts {
31
30
  return {
32
31
  openSpec: { mode: 'none' },
33
- work: { state: 'clear' },
34
32
  subject: { state: 'clear' },
35
33
  runs: new Map(),
36
34
  progress: null,
@@ -40,7 +38,6 @@ export function emptyFacts(): ReducerFacts {
40
38
  /** The single selection resolver. Priority order is contract-frozen. */
41
39
  export function resolveSelection(facts: {
42
40
  openSpec: OpenSpecFocus;
43
- work: SessionWorkFocus;
44
41
  subject: SessionSubject;
45
42
  }): SelectionState {
46
43
  if (facts.openSpec.mode === 'task') {
@@ -50,13 +47,6 @@ export function resolveSelection(facts: {
50
47
  task: facts.openSpec.taskId,
51
48
  };
52
49
  }
53
- if (facts.work.state !== 'clear') {
54
- return {
55
- kind: 'session-work',
56
- intent: facts.work.intent,
57
- phase: facts.work.state === 'validation' ? 'validation' : 'active',
58
- };
59
- }
60
50
  if (facts.subject.state === 'set') {
61
51
  return { kind: 'subject', title: facts.subject.title };
62
52
  }
@@ -73,8 +63,6 @@ export function applyEvent(facts: ReducerFacts, event: LedgerEvent): ReducerFact
73
63
  switch (event.type) {
74
64
  case 'focus/openspec':
75
65
  return { ...facts, openSpec: event.focus };
76
- case 'focus/work':
77
- return { ...facts, work: event.work };
78
66
  case 'focus/subject':
79
67
  return { ...facts, subject: event.subject };
80
68
  case 'run/start': {
@@ -153,12 +141,6 @@ export function deriveTitle(snapshot: CockpitSnapshot): string {
153
141
  case 'openspec-task':
154
142
  detail = `${selection.change} › ${selection.task}`;
155
143
  break;
156
- case 'session-work':
157
- detail =
158
- selection.phase === 'validation'
159
- ? `Validate › ${selection.intent}`
160
- : selection.intent;
161
- break;
162
144
  case 'subject':
163
145
  detail = selection.title;
164
146
  break;