@aemonge-dev/pi-stardeck 0.9.1 → 0.9.3

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.
@@ -62,6 +62,7 @@ export const HEADER_ACTIVITY_KINDS = [
62
62
  'regression',
63
63
  'verification',
64
64
  'result-review',
65
+ 'answering',
65
66
  'recovery',
66
67
  'awaiting-validation',
67
68
  'awaiting-input',
@@ -97,27 +98,15 @@ export type HeaderWork = {
97
98
 
98
99
  export const HEADER_SELECTION_SOURCES = [
99
100
  'openspec',
100
- 'goal',
101
101
  'session-work',
102
102
  'subject',
103
103
  'legacy',
104
104
  ] as const;
105
105
 
106
- export type HeaderSelectionGoalDetail = {
107
- status: string;
108
- elapsedSeconds?: number;
109
- tasksDone?: number;
110
- tasksActive?: number;
111
- tasksTotal?: number;
112
- tokensUsed?: number;
113
- tokenBudget?: number;
114
- };
115
-
116
106
  export type HeaderSelection = {
117
107
  source: (typeof HEADER_SELECTION_SOURCES)[number];
118
108
  titles: string[];
119
109
  color: HeaderColor;
120
- goal?: HeaderSelectionGoalDetail;
121
110
  };
122
111
 
123
112
  export const HEADER_SUGGESTIONS = [
@@ -131,7 +120,6 @@ export const HEADER_SUGGESTIONS = [
131
120
  export type HeaderSuggestion = (typeof HEADER_SUGGESTIONS)[number];
132
121
 
133
122
  export type HeaderSnapshot = {
134
- openGoals?: number;
135
123
  activity?: HeaderActivity | null;
136
124
  selection?: HeaderSelection | null;
137
125
  suggestion?: HeaderSuggestion | null;
@@ -954,46 +942,10 @@ function parseHeaderSelection(value: unknown): HeaderSelection | null {
954
942
  });
955
943
  if (titles.length !== candidate.titles.length) return null;
956
944
 
957
- let goal: HeaderSelectionGoalDetail | undefined;
958
- if (
959
- candidate.source === 'goal' &&
960
- candidate.goal &&
961
- typeof candidate.goal === 'object' &&
962
- !Array.isArray(candidate.goal)
963
- ) {
964
- const raw = candidate.goal as Record<string, unknown>;
965
- const boundedNumber = (input: unknown): number | undefined =>
966
- typeof input === 'number' &&
967
- Number.isFinite(input) &&
968
- input >= 0 &&
969
- input <= 9_999_999_999
970
- ? input
971
- : undefined;
972
- if (typeof raw.status === 'string' && raw.status.length <= 24) {
973
- const detail: HeaderSelectionGoalDetail = { status: raw.status };
974
- const elapsed = boundedNumber(raw.elapsedSeconds);
975
- if (elapsed !== undefined) detail.elapsedSeconds = elapsed;
976
- for (const [key, value] of [
977
- ['tasksDone', raw.tasksDone],
978
- ['tasksActive', raw.tasksActive],
979
- ['tasksTotal', raw.tasksTotal],
980
- ['tokenBudget', raw.tokenBudget],
981
- ] as const) {
982
- const parsed = boundedNumber(value);
983
- if (parsed !== undefined && parsed <= 9_999) {
984
- detail[key] = parsed;
985
- }
986
- }
987
- const tokensUsed = boundedNumber(raw.tokensUsed);
988
- if (tokensUsed !== undefined) detail.tokensUsed = tokensUsed;
989
- goal = detail;
990
- }
991
- }
992
945
  return {
993
946
  source: candidate.source as HeaderSelection['source'],
994
947
  titles,
995
948
  color: candidate.color,
996
- ...(goal !== undefined ? { goal } : {}),
997
949
  };
998
950
  }
999
951
 
@@ -1066,14 +1018,6 @@ export function parseHeaderSnapshot(raw: unknown): HeaderSnapshot | null {
1066
1018
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
1067
1019
  const message = raw as Record<string, unknown>;
1068
1020
  if (message.protocol !== 1) return null;
1069
- const openGoals =
1070
- typeof message.openGoals === 'number' &&
1071
- Number.isInteger(message.openGoals) &&
1072
- message.openGoals >= 0 &&
1073
- message.openGoals <= 99
1074
- ? message.openGoals
1075
- : 0;
1076
-
1077
1021
  const standaloneActivity = parseHeaderActivity(message.activity) ?? null;
1078
1022
  let work: HeaderWork | null = null;
1079
1023
  if (
@@ -1190,7 +1134,6 @@ export function parseHeaderSnapshot(raw: unknown): HeaderSnapshot | null {
1190
1134
  const hasExplicitTurn = Object.prototype.hasOwnProperty.call(message, 'turn');
1191
1135
 
1192
1136
  return {
1193
- openGoals,
1194
1137
  activity: standaloneActivity,
1195
1138
  selection: hasExplicitSelection
1196
1139
  ? parseHeaderSelection(message.selection)
@@ -42,8 +42,10 @@ const TURN_ELAPSED_MAX = (99 * 60 + 59) * 1_000 + 999;
42
42
  export type DeckTitleWorkState = {
43
43
  change: string;
44
44
  task?: string;
45
- done: number;
46
- total: number;
45
+ done?: number;
46
+ total?: number;
47
+ schemaName?: string;
48
+ ladder?: { done: number; total: number; rung?: string };
47
49
  };
48
50
 
49
51
  export type DeckTitleActivityState = {
@@ -116,12 +118,47 @@ function parseTitleWork(value: unknown): DeckTitleWorkState | null | undefined {
116
118
  if (value === null) return null;
117
119
  if (!isRecord(value)) return undefined;
118
120
  const change = boundedText(value.change, IDENTIFIER_MAX);
119
- const done = boundedTotal(value.done);
120
- const total = boundedTotal(value.total);
121
- if (!change || done === null || total === null) return undefined;
121
+ if (!change) return undefined;
122
+ // Counts are optional as a pair: absent means unknown (never fabricated
123
+ // zeros); present-but-invalid rejects the object.
124
+ const hasCounts = 'done' in value && 'total' in value;
125
+ if ('done' in value !== 'total' in value) return undefined;
126
+ const boundedDone = hasCounts ? boundedTotal(value.done) : undefined;
127
+ const boundedTotalValue = hasCounts ? boundedTotal(value.total) : undefined;
128
+ if (hasCounts && (boundedDone === null || boundedTotalValue === null)) {
129
+ return undefined;
130
+ }
122
131
  const task = boundedText(value.task, TASK_ID_MAX, { required: false });
123
132
  if (task === null) return undefined;
124
- return task === undefined ? { change, done, total } : { change, task, done, total };
133
+ const schemaName = boundedText(value.schemaName, IDENTIFIER_MAX, {
134
+ required: false,
135
+ });
136
+ if (schemaName === null) return undefined;
137
+ let ladder: { done: number; total: number; rung?: string } | undefined;
138
+ if ('ladder' in value) {
139
+ if (!isRecord(value.ladder)) return undefined;
140
+ const ladderDone = boundedTotal(value.ladder.done);
141
+ const ladderTotal = boundedTotal(value.ladder.total);
142
+ if (ladderDone === null || ladderTotal === null) return undefined;
143
+ const ladderRung = boundedText(value.ladder.rung, 32, { required: false });
144
+ if (ladderRung === null) return undefined;
145
+ ladder = {
146
+ done: ladderDone,
147
+ total: ladderTotal,
148
+ ...(ladderRung ? { rung: ladderRung } : {}),
149
+ };
150
+ }
151
+ const counts =
152
+ typeof boundedDone === 'number' && typeof boundedTotalValue === 'number'
153
+ ? { done: boundedDone, total: boundedTotalValue }
154
+ : {};
155
+ const extras = {
156
+ ...(schemaName ? { schemaName } : {}),
157
+ ...(ladder ? { ladder } : {}),
158
+ };
159
+ return task === undefined
160
+ ? { change, ...counts, ...extras }
161
+ : { change, task, ...counts, ...extras };
125
162
  }
126
163
 
127
164
  function parseTitleWaiver(value: unknown): DeckTitleWaiverState | null | undefined {
@@ -189,7 +226,46 @@ export function parseDeckTurnCost(value: unknown): DeckTurnCost | null {
189
226
  }
190
227
  }
191
228
 
192
- /** `change · done/total` — the Work label from the accepted mockups. */
229
+ /** One colored piece of the Work label. */
230
+ export type DeckWorkSegment = { text: string; color: string; bold?: boolean };
231
+
232
+ /**
233
+ * The Work label as colored segments — the human-chosen design: `change ⟩
234
+ * done/total rung › 󰄱 done/total` in one hue per state (accent, success at
235
+ * complete with a filled checkbox and bold counts). The rung name comes
236
+ * straight from OpenSpec's status artifacts — never an invented phase
237
+ * word. Separators are chrome with hard deck-wide colors — `⟩`
238
+ * thinkingHigh-bold like every semantic separator, `›` accent like every
239
+ * minor one. Unknown facts degrade honestly; the bare change is the floor.
240
+ */
241
+ export function deckWorkSegments(work: DeckTitleWorkState): DeckWorkSegment[] {
242
+ const complete =
243
+ work.done !== undefined &&
244
+ work.total !== undefined &&
245
+ work.total > 0 &&
246
+ work.done >= work.total;
247
+ const base = complete ? 'success' : 'accent';
248
+ const segments: DeckWorkSegment[] = [{ text: work.change, color: base }];
249
+ const ladder = work.ladder;
250
+ if (ladder) {
251
+ segments.push({ text: ' ⟩ ', color: 'thinkingHigh', bold: true });
252
+ const group = `${ladder.done}/${ladder.total}${ladder.rung ? ` ${ladder.rung}` : ''}`;
253
+ segments.push({ text: group, color: base });
254
+ }
255
+ if (work.done !== undefined && work.total !== undefined) {
256
+ segments.push({ text: ' › ', color: 'accent' });
257
+ segments.push({
258
+ text: `${complete ? '✔' : '󰄱'} ${work.done}/${work.total}`,
259
+ color: base,
260
+ ...(complete ? { bold: true } : {}),
261
+ });
262
+ }
263
+ return segments;
264
+ }
265
+
266
+ /** The plain-text Work label — the segment join without colors. */
193
267
  export function formatDeckWorkLabel(work: DeckTitleWorkState): string {
194
- return `${work.change} · ${work.done}/${work.total}`;
268
+ return deckWorkSegments(work)
269
+ .map((segment) => segment.text)
270
+ .join('');
195
271
  }
@@ -45,6 +45,7 @@ const ACTIVITY_LABELS: Record<HeaderActivity['kind'], ActivityLabels> = {
45
45
  full: 'reviewing check results',
46
46
  compact: 'reviewing results',
47
47
  },
48
+ answering: { full: 'answering the Human', compact: 'answering' },
48
49
  recovery: { full: 'repairing bounded failure', compact: 'repair' },
49
50
  'awaiting-validation': {
50
51
  full: 'Human validation',
@@ -6,7 +6,6 @@ const TRACKED_WIDGET_IDS = new Set([
6
6
  'galactica.openspec-progress',
7
7
  'galactica.openspec',
8
8
  'galactica.work',
9
- 'galactica.goal',
10
9
  'galactica.project-capabilities',
11
10
  'galactica.session-memory',
12
11
  'galactica.session-cpu',
@@ -214,7 +213,6 @@ function workWidget(widgets: ReadonlyMap<string, RailWidget>): RailWidget | unde
214
213
  for (const id of [
215
214
  'galactica.openspec-progress',
216
215
  'galactica.openspec',
217
- 'galactica.goal',
218
216
  'galactica.work',
219
217
  ]) {
220
218
  const widget = widgets.get(id);
@@ -7,7 +7,7 @@ editor widget itself.
7
7
  ## Architecture
8
8
 
9
9
  ```text
10
- Pi tool lifecycle + session/Goal metadata + OpenSpec CLI + aggregate JSON state
10
+ Pi tool lifecycle + session metadata + OpenSpec CLI + aggregate JSON state
11
11
  │
12
12
  ▼
13
13
  normalized immutable state
@@ -64,9 +64,9 @@ active OpenSpec footer by themselves.
64
64
 
65
65
  ## Session focus and terminal title
66
66
 
67
- A Pi session has zero or one explicit OpenSpec task focus, optional read-only Pi Goal
68
- state, and an independent ephemeral Session Work state. Display priority is OpenSpec
69
- Task → Goal → Session Work → safe direct fallback. OpenSpec focus clears active Session
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
70
  Work when acquired, preventing hidden or stale work from resurfacing later.
71
71
 
72
72
  Session Work is stored only as versioned custom entries on the active Pi session branch;
@@ -80,7 +80,7 @@ it never creates a repository status file. It supports this lifecycle:
80
80
  ```
81
81
 
82
82
  When no OpenSpec portfolio is available, the matching row-one footer fallback is the
83
- compact `▶ Focus` or `◇ Validate`; an active Goal uses `󰊕 Goal`, and an entirely empty
83
+ compact `▶ Focus` or `◇ Validate`; an entirely empty
84
84
  project uses ` no OpenSpec`. The detailed Session Work intent remains in the header
85
85
  instead of being duplicated in the footer. Active and validation states also replace the
86
86
  fallback terminal title. Reload, resume, and tree navigation restore the newest valid
@@ -97,25 +97,14 @@ publication.
97
97
 
98
98
  The header projection uses the rich lifecycle `understanding`, `working`, `waiting`,
99
99
  `assuring`, `learning`, `answering`, `blocked`, `listening`, or `aborted`. It separately
100
- publishes one explicit selection with OpenSpec Task → Goal → Session Work precedence,
100
+ publishes one explicit selection with OpenSpec Task → Session Work precedence,
101
101
  or `null` when no work is selected. A controlled suggestion enum communicates only
102
102
  Human-facing reporting cues such as `requesting-validation`,
103
103
  `awaiting-continuation`, or `requesting-redirection`; it never carries generated
104
104
  prose. Direct tool
105
- categories, read-only Pi Goal state, and known Taskflow phases move the lifecycle
106
- without exposing arguments or raw node prose. Goal state is restored from Pi's canonical
107
- `goal-state` session entries without enabling Goal RPC. An active Goal renders bounded
108
- automatic-turn progress from `pi-goal.json`; paused, blocked, limited, external-wait,
109
- and complete states render controlled labels. Complete projects as
110
- `awaiting Human validation` rather than Human acceptance. Raw Goal objectives, wait
111
- reasons, completion summaries, paths, and Goal IDs never enter the visible header. While
112
- a Goal exists, a session-scoped 500 ms in-memory branch monitor catches idle
113
- `/goal pause`, clear, and menu transitions; it performs no filesystem, process,
114
- provider, or network work and stops when Goal state is cleared. A stopped Goal remains
115
- visible while Pi is idle so its terminal reason is honest, but the next ordinary
116
- non-Goal turn automatically returns the title to current Ramona, OpenSpec, Session Work,
117
- or direct activity. This display transition never mutates Goal or focus state; a later
118
- Goal resume automatically reclaims the title. During a Taskflow tool run, structured
105
+ categories and known Taskflow phases move the lifecycle
106
+ without exposing arguments or raw node prose. Complete projects as
107
+ `awaiting Human validation` rather than Human acceptance. During a Taskflow tool run, structured
119
108
  streaming state temporarily replaces the normal focus with the running top-level phase
120
109
  ID, such as `working ⟩ work-fragment`; promoted child IDs and generated output remain
121
110
  hidden. Visible assistant text moves it to `answering`; settling with selected work
@@ -128,7 +117,7 @@ operation transitions to bounded recovery. This prevents a completed command fro
128
117
  looking permanently active while Pi prepares its next action. A Ctrl-C assistant stop
129
118
  reason moves it to `aborted` and remains visible until the next turn resets it to
130
119
  `understanding`. Legacy work titles still preserve the existing explicit fallback states for compatible
131
- consumers. The additive selection field contains only an exact OpenSpec Task, Goal, or
120
+ consumers. The additive selection field contains only an exact OpenSpec Task or
132
121
  Session Work; otherwise it is `null`, so the deck renders a stable dim `󰓾 —`. `Direct
133
122
  response` is never presented as selection and Human prompt text is never copied into it.
134
123
 
@@ -181,7 +170,7 @@ Aggregate portfolio progress is independent of focus and remains visible as:
181
170
  It occupies the right side of footer row one, opposite the full PWD and Git state. It
182
171
  updates from `openspec list --json`; task-list `` replaces `Plans` and notebook ``
183
172
  replaces `Tasks`. If no OpenSpec portfolio exists, the slot prefers compact active scope
184
- (`󰊕 Goal`, then `▶ Focus` or `◇ Validate`) before falling back to dim ` no OpenSpec`.
173
+ (`▶ Focus` or `◇ Validate`) before falling back to dim ` no OpenSpec`.
185
174
  Focused-change metadata remains separate and does not replace these totals. The
186
175
  no-argument form reports the current focus without changing it. The matching tool calls
187
176
  are:
@@ -220,7 +209,7 @@ legacy focused-status producer also gives quiet state feedback:
220
209
 
221
210
  - actionable task work exists: dim `○ OpenSpec · no focus`
222
211
  - no actionable task work exists: dim `○ OpenSpec · clear`
223
- - no `openspec/` project exists: row one shows Goal or Focus when active, otherwise dim
212
+ - no `openspec/` project exists: row one shows Focus when active, otherwise dim
224
213
  ` no OpenSpec`
225
214
  - OpenSpec status cannot be read: the last aggregate remains while focused status
226
215
  reports unavailable
@@ -282,7 +271,7 @@ A configured `orchestration.stateFile` takes precedence. Useful fields include `
282
271
  or node vocabulary is classified into an allowlisted title activity—understanding,
283
272
  inspection, synthesis, planning, skill proposal, mutation, implementation, change
284
273
  review, regression, verification, result review, recovery, waiting reason, or abort—and
285
- attached to the current OpenSpec, Goal, Session Work, or explicit empty focus. Task
274
+ attached to the current OpenSpec, Session Work, or explicit empty focus. Task
286
275
  text, paths, generated prose, and promoted child IDs never enter the lifecycle title;
287
276
  unknown vocabulary produces no activity. The bounded top-level phase ID is the only
288
277
  exact Taskflow identifier shown. Named Taskflow runs invoked by the assistant through