@aemonge-dev/pi-stardeck 0.0.0-stage → 0.6.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/LICENSE +21 -0
- package/README.md +240 -2
- package/demo/pi-stardeck.tape +59 -0
- package/docs/architecture.md +106 -0
- package/docs/assets/pi-stardeck.gif +0 -0
- package/docs/development.md +128 -0
- package/licenses/README.md +19 -0
- package/licenses/pi-vim-LICENSE +21 -0
- package/package.json +81 -4
- package/packages/footer/README.md +15 -0
- package/packages/footer/index.ts +37 -0
- package/packages/footer/package.json +32 -0
- package/packages/footer/src/protocol.ts +10 -0
- package/packages/footer/src/quota.ts +26 -0
- package/packages/footer/src/telemetry.ts +59 -0
- package/packages/pi-stardeck/README.md +220 -0
- package/packages/pi-stardeck/index.ts +691 -0
- package/packages/pi-stardeck/package.json +32 -0
- package/packages/pi-stardeck/src/activity-age.ts +184 -0
- package/packages/pi-stardeck/src/capabilities.ts +105 -0
- package/packages/pi-stardeck/src/deck.ts +807 -0
- package/packages/pi-stardeck/src/gauge.ts +1323 -0
- package/packages/pi-stardeck/src/git-refresh.ts +5 -0
- package/packages/pi-stardeck/src/process-resources.ts +428 -0
- package/packages/pi-stardeck/src/title-state.ts +195 -0
- package/packages/pi-stardeck/src/title.ts +443 -0
- package/packages/pi-vim-top-border/LICENSE +21 -0
- package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
- package/packages/pi-vim-top-border/README.md +577 -0
- package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
- package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
- package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
- package/packages/pi-vim-top-border/index.ts +4195 -0
- package/packages/pi-vim-top-border/input-keys.ts +49 -0
- package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
- package/packages/pi-vim-top-border/mode-colors.ts +97 -0
- package/packages/pi-vim-top-border/mode-label.ts +62 -0
- package/packages/pi-vim-top-border/motions.ts +297 -0
- package/packages/pi-vim-top-border/package.json +47 -0
- package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
- package/packages/pi-vim-top-border/prompt-status.ts +131 -0
- package/packages/pi-vim-top-border/settings.ts +287 -0
- package/packages/pi-vim-top-border/text-objects.ts +389 -0
- package/packages/pi-vim-top-border/types.ts +51 -0
- package/packages/pi-vim-top-border/visual.ts +65 -0
- package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
- package/packages/status/README.md +398 -0
- package/packages/status/index.ts +1717 -0
- package/packages/status/package.json +33 -0
- package/packages/status/src/active-work.ts +235 -0
- package/packages/status/src/config.ts +218 -0
- package/packages/status/src/diagnostics.ts +238 -0
- package/packages/status/src/goal-block.ts +377 -0
- package/packages/status/src/goal.ts +170 -0
- package/packages/status/src/ledger.ts +101 -0
- package/packages/status/src/openspec.ts +394 -0
- package/packages/status/src/orchestration.ts +237 -0
- package/packages/status/src/publisher.ts +1294 -0
- package/packages/status/src/reducer.ts +190 -0
- package/packages/status/src/runtime-runs.ts +267 -0
- package/packages/status/src/state.ts +178 -0
- package/packages/status/src/title-activity.ts +224 -0
- package/packages/status/src/title-state.ts +218 -0
- package/packages/status/src/turn-cost.ts +63 -0
- package/packages/status/src/types.ts +228 -0
- package/src/deck-surface.ts +46 -0
- package/src/health.ts +227 -0
- package/src/index.ts +27 -0
- package/src/pi-status.ts +334 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import type {
|
|
4
|
+
GalacticaStatusConfig,
|
|
5
|
+
OrchestrationState,
|
|
6
|
+
WorkflowState,
|
|
7
|
+
} from './types.ts';
|
|
8
|
+
|
|
9
|
+
const ORCHESTRATION_CANDIDATES = [
|
|
10
|
+
'.pi/status/orchestration.json',
|
|
11
|
+
'.pi/orchestration.json',
|
|
12
|
+
'.taskflow/state.json',
|
|
13
|
+
'.taskflow/run-state.json',
|
|
14
|
+
'.galactica/orchestration.json',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function text(value: unknown): string | undefined {
|
|
22
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function number(value: unknown): number | undefined {
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function timestamp(value: unknown): number | undefined {
|
|
30
|
+
const numeric = number(value);
|
|
31
|
+
if (numeric !== undefined) return numeric;
|
|
32
|
+
const string = text(value);
|
|
33
|
+
if (!string) return undefined;
|
|
34
|
+
const parsed = Date.parse(string);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeWorkflowState(value: unknown): WorkflowState {
|
|
39
|
+
const state = text(value)?.toLowerCase();
|
|
40
|
+
switch (state) {
|
|
41
|
+
case 'running':
|
|
42
|
+
case 'active':
|
|
43
|
+
case 'in-progress':
|
|
44
|
+
case 'working':
|
|
45
|
+
return 'running';
|
|
46
|
+
case 'blocked':
|
|
47
|
+
case 'failed':
|
|
48
|
+
case 'error':
|
|
49
|
+
case 'failed-verification':
|
|
50
|
+
return 'blocked';
|
|
51
|
+
case 'complete':
|
|
52
|
+
case 'completed':
|
|
53
|
+
case 'done':
|
|
54
|
+
case 'success':
|
|
55
|
+
return 'complete';
|
|
56
|
+
case 'idle':
|
|
57
|
+
case 'paused':
|
|
58
|
+
case 'waiting':
|
|
59
|
+
return 'idle';
|
|
60
|
+
default:
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function workerList(value: Record<string, unknown>): unknown[] {
|
|
66
|
+
return Array.isArray(value.workers)
|
|
67
|
+
? value.workers
|
|
68
|
+
: Array.isArray(value.agents)
|
|
69
|
+
? value.agents
|
|
70
|
+
: [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function activeWorkerCount(value: Record<string, unknown>): number {
|
|
74
|
+
const explicit = number(
|
|
75
|
+
value.activeWorkers ?? value.workerCount ?? value.activeAgents ?? value.agentCount,
|
|
76
|
+
);
|
|
77
|
+
if (explicit !== undefined) return Math.max(0, Math.trunc(explicit));
|
|
78
|
+
|
|
79
|
+
const workers = workerList(value);
|
|
80
|
+
return workers.filter((worker) => {
|
|
81
|
+
if (!isRecord(worker)) return false;
|
|
82
|
+
return ['running', 'active', 'working', 'in-progress'].includes(
|
|
83
|
+
text(worker.status ?? worker.state)?.toLowerCase() ?? '',
|
|
84
|
+
);
|
|
85
|
+
}).length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseOrchestrationState(
|
|
89
|
+
value: unknown,
|
|
90
|
+
options: { source: string; now?: number; fallbackUpdatedAt?: number },
|
|
91
|
+
): OrchestrationState {
|
|
92
|
+
if (!isRecord(value)) throw new Error('orchestration state is not an object');
|
|
93
|
+
const now = options.now ?? Date.now();
|
|
94
|
+
const workflow = isRecord(value.workflow) ? value.workflow : {};
|
|
95
|
+
const openspec = isRecord(value.openspec) ? value.openspec : {};
|
|
96
|
+
const task = isRecord(value.task) ? value.task : {};
|
|
97
|
+
|
|
98
|
+
const phase = text(value.phase ?? workflow.phase ?? workflow.node ?? value.role);
|
|
99
|
+
const state = normalizeWorkflowState(value.state ?? value.status ?? workflow.state);
|
|
100
|
+
const currentTask = text(
|
|
101
|
+
value.currentTask ??
|
|
102
|
+
value.current_task ??
|
|
103
|
+
task.title ??
|
|
104
|
+
task.name ??
|
|
105
|
+
value.blockedReason,
|
|
106
|
+
);
|
|
107
|
+
const openspecTaskId = text(
|
|
108
|
+
value.openspecTaskId ??
|
|
109
|
+
value.openspec_task_id ??
|
|
110
|
+
openspec.taskId ??
|
|
111
|
+
openspec.task_id,
|
|
112
|
+
);
|
|
113
|
+
const startedAt = timestamp(
|
|
114
|
+
value.startedAt ?? value.started_at ?? workflow.startedAt ?? workflow.started_at,
|
|
115
|
+
);
|
|
116
|
+
const explicitElapsed = number(
|
|
117
|
+
value.elapsedMs ?? value.elapsed_ms ?? workflow.elapsedMs,
|
|
118
|
+
);
|
|
119
|
+
const elapsedMs =
|
|
120
|
+
explicitElapsed ?? (startedAt ? Math.max(0, now - startedAt) : undefined);
|
|
121
|
+
const updatedAt =
|
|
122
|
+
timestamp(value.updatedAt ?? value.updated_at ?? value.timestamp) ??
|
|
123
|
+
options.fallbackUpdatedAt ??
|
|
124
|
+
now;
|
|
125
|
+
const staleAfterMs = 600_000;
|
|
126
|
+
const activeWorkers = activeWorkerCount(value);
|
|
127
|
+
const explicitTotalWorkers = number(
|
|
128
|
+
value.totalWorkers ?? value.totalWorkerCount ?? value.totalAgents,
|
|
129
|
+
);
|
|
130
|
+
const totalWorkers = Math.max(
|
|
131
|
+
activeWorkers,
|
|
132
|
+
Math.trunc(explicitTotalWorkers ?? workerList(value).length),
|
|
133
|
+
);
|
|
134
|
+
const progress = isRecord(value.progress) ? value.progress : {};
|
|
135
|
+
const fileProgress = isRecord(value.files)
|
|
136
|
+
? value.files
|
|
137
|
+
: isRecord(progress.files)
|
|
138
|
+
? progress.files
|
|
139
|
+
: {};
|
|
140
|
+
const rawCompletedFiles = number(
|
|
141
|
+
value.completedFiles ?? value.processedFiles ?? fileProgress.completed,
|
|
142
|
+
);
|
|
143
|
+
const rawTotalFiles = number(value.totalFiles ?? fileProgress.total);
|
|
144
|
+
const totalFiles =
|
|
145
|
+
rawTotalFiles === undefined ? undefined : Math.max(0, Math.trunc(rawTotalFiles));
|
|
146
|
+
const completedFiles =
|
|
147
|
+
rawCompletedFiles === undefined || totalFiles === undefined
|
|
148
|
+
? undefined
|
|
149
|
+
: Math.max(0, Math.min(totalFiles, Math.trunc(rawCompletedFiles)));
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
state === 'unknown' &&
|
|
153
|
+
!phase &&
|
|
154
|
+
!currentTask &&
|
|
155
|
+
activeWorkers === 0 &&
|
|
156
|
+
elapsedMs === undefined
|
|
157
|
+
) {
|
|
158
|
+
throw new Error('orchestration state has no recognized workflow fields');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...(phase ? { phase } : {}),
|
|
163
|
+
state,
|
|
164
|
+
activeWorkers,
|
|
165
|
+
totalWorkers,
|
|
166
|
+
...(completedFiles === undefined ? {} : { completedFiles }),
|
|
167
|
+
...(totalFiles === undefined ? {} : { totalFiles }),
|
|
168
|
+
...(currentTask ? { currentTask } : {}),
|
|
169
|
+
...(openspecTaskId ? { openspecTaskId } : {}),
|
|
170
|
+
...(startedAt ? { startedAt } : {}),
|
|
171
|
+
...(elapsedMs !== undefined ? { elapsedMs: Math.max(0, elapsedMs) } : {}),
|
|
172
|
+
meta: {
|
|
173
|
+
source: options.source,
|
|
174
|
+
refreshedAt: now,
|
|
175
|
+
attemptedAt: now,
|
|
176
|
+
updatedAt,
|
|
177
|
+
staleAfterMs,
|
|
178
|
+
stale: now - updatedAt > staleAfterMs,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function existingFile(paths: readonly string[]): Promise<string | undefined> {
|
|
184
|
+
for (const path of paths) {
|
|
185
|
+
try {
|
|
186
|
+
if ((await fs.stat(path)).isFile()) return path;
|
|
187
|
+
} catch {
|
|
188
|
+
// Optional state files may only exist while a workflow is active.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function detectOrchestrationPath(
|
|
195
|
+
projectRoot: string,
|
|
196
|
+
configuredPath?: string | null,
|
|
197
|
+
): Promise<string | undefined> {
|
|
198
|
+
if (configuredPath) {
|
|
199
|
+
const path = isAbsolute(configuredPath)
|
|
200
|
+
? configuredPath
|
|
201
|
+
: resolve(projectRoot, configuredPath);
|
|
202
|
+
return (await existingFile([path])) ?? path;
|
|
203
|
+
}
|
|
204
|
+
return existingFile(
|
|
205
|
+
ORCHESTRATION_CANDIDATES.map((path) => resolve(projectRoot, path)),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function collectOrchestration(options: {
|
|
210
|
+
projectRoot: string;
|
|
211
|
+
config: GalacticaStatusConfig['orchestration'];
|
|
212
|
+
now?: number;
|
|
213
|
+
}): Promise<{ state?: OrchestrationState; sourcePath?: string }> {
|
|
214
|
+
const sourcePath = await detectOrchestrationPath(
|
|
215
|
+
options.projectRoot,
|
|
216
|
+
options.config.stateFile,
|
|
217
|
+
);
|
|
218
|
+
if (!sourcePath) return {};
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const stat = await fs.stat(sourcePath);
|
|
222
|
+
const parsed = JSON.parse(await fs.readFile(sourcePath, 'utf8')) as unknown;
|
|
223
|
+
return {
|
|
224
|
+
sourcePath,
|
|
225
|
+
state: parseOrchestrationState(parsed, {
|
|
226
|
+
source: sourcePath,
|
|
227
|
+
now: options.now,
|
|
228
|
+
fallbackUpdatedAt: stat.mtimeMs,
|
|
229
|
+
}),
|
|
230
|
+
};
|
|
231
|
+
} catch (error) {
|
|
232
|
+
const code = isRecord(error) ? error.code : undefined;
|
|
233
|
+
if (code === 'ENOENT' && !options.config.stateFile) return {};
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
throw new Error(`failed to read orchestration state: ${message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|