@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.
- package/docs/architecture.md +1 -1
- package/package.json +1 -1
- package/packages/pi-stardeck/README.md +4 -4
- package/packages/pi-stardeck/index.ts +10 -0
- package/packages/pi-stardeck/src/deck.ts +132 -158
- package/packages/pi-stardeck/src/gauge.ts +1 -58
- package/packages/pi-stardeck/src/title-state.ts +84 -8
- package/packages/pi-stardeck/src/title.ts +1 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +0 -2
- package/packages/status/README.md +13 -24
- package/packages/status/index.ts +17 -144
- package/packages/status/src/active-work.ts +0 -9
- package/packages/status/src/ledger.ts +0 -3
- package/packages/status/src/openspec.ts +54 -1
- package/packages/status/src/publisher.ts +8 -173
- package/packages/status/src/reducer.ts +0 -21
- package/packages/status/src/title-activity.ts +18 -2
- package/packages/status/src/title-state.ts +57 -8
- package/packages/status/src/types.ts +2 -7
- package/packages/status/src/goal-block.ts +0 -377
- package/packages/status/src/goal.ts +0 -170
|
@@ -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
|
|
46
|
-
total
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
68
|
-
|
|
69
|
-
Task →
|
|
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
|
|
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 →
|
|
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
|
|
106
|
-
without exposing arguments or raw node prose.
|
|
107
|
-
`
|
|
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
|
|
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
|
-
(
|
|
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
|
|
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,
|
|
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
|