omegon 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/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- package/themes/alpharius.json +88 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ControlPlaneState snapshot builder.
|
|
3
|
+
*
|
|
4
|
+
* Derives a versioned, JSON-serialisable snapshot from:
|
|
5
|
+
* - sharedState (live in-process data)
|
|
6
|
+
* - on-demand scans of the design-tree and OpenSpec directories
|
|
7
|
+
*
|
|
8
|
+
* This module is pure logic — no HTTP, no side-effects. The HTTP layer
|
|
9
|
+
* calls buildControlPlaneState() on every GET /api/state request.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { sharedState } from "../shared-state.ts";
|
|
15
|
+
import { listChanges, listDesignChanges } from "../openspec/spec.ts";
|
|
16
|
+
import { scanDesignDocs, countAcceptanceCriteria } from "../design-tree/tree.ts";
|
|
17
|
+
import {
|
|
18
|
+
SCHEMA_VERSION,
|
|
19
|
+
type ControlPlaneState,
|
|
20
|
+
type SessionSnapshot,
|
|
21
|
+
type DashboardSnapshot,
|
|
22
|
+
type DesignTreeSnapshot,
|
|
23
|
+
type DesignNodeSummary,
|
|
24
|
+
type DesignSpecBinding,
|
|
25
|
+
type ACSummary,
|
|
26
|
+
type AssessmentResult,
|
|
27
|
+
type OpenSpecSnapshot,
|
|
28
|
+
type OpenSpecChangeSummary,
|
|
29
|
+
type CleaveSnapshot,
|
|
30
|
+
type ModelsSnapshot,
|
|
31
|
+
type MemorySnapshot,
|
|
32
|
+
type HealthSnapshot,
|
|
33
|
+
type RecoverySnapshot,
|
|
34
|
+
type OperatorMetadataSnapshot,
|
|
35
|
+
type DesignPipelineSnapshot,
|
|
36
|
+
type DesignChangeSummary,
|
|
37
|
+
type DesignFunnelCounts,
|
|
38
|
+
} from "./types.ts";
|
|
39
|
+
|
|
40
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/** Resolve the package version from the nearest package.json */
|
|
43
|
+
function readPiKitVersion(repoRoot: string): string {
|
|
44
|
+
try {
|
|
45
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
46
|
+
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
48
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
49
|
+
} catch {
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Attempt to read the current git branch without shelling out if possible. */
|
|
55
|
+
function readGitBranch(repoRoot: string): string | null {
|
|
56
|
+
try {
|
|
57
|
+
const headPath = path.join(repoRoot, ".git", "HEAD");
|
|
58
|
+
const head = fs.readFileSync(headPath, "utf8").trim();
|
|
59
|
+
const match = /^ref: refs\/heads\/(.+)$/.exec(head);
|
|
60
|
+
return match ? match[1] : head.slice(0, 12); // detached HEAD → short SHA
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Section builders ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function buildSession(repoRoot: string): SessionSnapshot {
|
|
69
|
+
return {
|
|
70
|
+
capturedAt: new Date().toISOString(),
|
|
71
|
+
piKitVersion: readPiKitVersion(repoRoot),
|
|
72
|
+
repoRoot,
|
|
73
|
+
gitBranch: readGitBranch(repoRoot),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Recovery actions that require operator attention — the web UI should treat
|
|
79
|
+
* these as "actionable" and may surface prompts, alerts, or indicators.
|
|
80
|
+
*/
|
|
81
|
+
const ACTIONABLE_RECOVERY_ACTIONS = new Set([
|
|
82
|
+
"escalate",
|
|
83
|
+
"retry",
|
|
84
|
+
"switch_candidate",
|
|
85
|
+
"switch_offline",
|
|
86
|
+
"cooldown",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
function buildDashboard(): DashboardSnapshot {
|
|
90
|
+
|
|
91
|
+
let recovery: RecoverySnapshot | null = null;
|
|
92
|
+
const r = sharedState.recovery;
|
|
93
|
+
if (r) {
|
|
94
|
+
recovery = {
|
|
95
|
+
provider: r.provider,
|
|
96
|
+
modelId: r.modelId,
|
|
97
|
+
classification: r.classification,
|
|
98
|
+
summary: r.summary,
|
|
99
|
+
action: r.action,
|
|
100
|
+
retryCount: r.retryCount ?? null,
|
|
101
|
+
timestamp: r.timestamp,
|
|
102
|
+
escalated: r.escalated ?? false,
|
|
103
|
+
// Structural actionability flag — avoids web consumers parsing action strings.
|
|
104
|
+
actionable:
|
|
105
|
+
(r.escalated ?? false) || ACTIONABLE_RECOVERY_ACTIONS.has(r.action),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const effort = sharedState.effort;
|
|
110
|
+
const effortLevel = effort?.name ?? null;
|
|
111
|
+
|
|
112
|
+
const routingPolicy = sharedState.routingPolicy
|
|
113
|
+
? (sharedState.routingPolicy as unknown as Record<string, unknown>)
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
const inj = sharedState.lastMemoryInjection;
|
|
117
|
+
const operatorMetadata: OperatorMetadataSnapshot = {
|
|
118
|
+
effortName: effort?.name ?? null,
|
|
119
|
+
effortLevel: effort?.level ?? null,
|
|
120
|
+
driverTier: effort?.driver ?? null,
|
|
121
|
+
thinkingLevel: effort?.thinking ?? null,
|
|
122
|
+
effortCapped: effort?.capped ?? false,
|
|
123
|
+
memoryTokenEstimate: sharedState.memoryTokenEstimate,
|
|
124
|
+
workingMemoryCount: inj?.workingMemoryFactCount ?? null,
|
|
125
|
+
totalFactCount: inj
|
|
126
|
+
? (inj.projectFactCount ?? 0) +
|
|
127
|
+
(inj.globalFactCount ?? 0) +
|
|
128
|
+
(inj.workingMemoryFactCount ?? 0)
|
|
129
|
+
: null,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
mode: sharedState.dashboardMode ?? "compact",
|
|
134
|
+
turns: sharedState.dashboardTurns ?? 0,
|
|
135
|
+
memoryTokenEstimate: sharedState.memoryTokenEstimate,
|
|
136
|
+
routingPolicy,
|
|
137
|
+
effortLevel,
|
|
138
|
+
recovery,
|
|
139
|
+
operatorMetadata,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveDesignSpecBinding(repoRoot: string, nodeId: string): DesignSpecBinding | null {
|
|
144
|
+
const activeDir = path.join(repoRoot, "openspec", "design", nodeId);
|
|
145
|
+
const archiveDir = path.join(repoRoot, "openspec", "design-archive", nodeId);
|
|
146
|
+
const [dir, isArchived] = fs.existsSync(activeDir)
|
|
147
|
+
? [activeDir, false]
|
|
148
|
+
: fs.existsSync(archiveDir)
|
|
149
|
+
? [archiveDir, true]
|
|
150
|
+
: [null, false];
|
|
151
|
+
|
|
152
|
+
if (!dir) return null;
|
|
153
|
+
|
|
154
|
+
const hasProposal = fs.existsSync(path.join(dir, "proposal.md"));
|
|
155
|
+
const hasSpec = fs.existsSync(path.join(dir, "spec.md"));
|
|
156
|
+
const hasTasks = fs.existsSync(path.join(dir, "tasks.md"));
|
|
157
|
+
const hasAssessment = fs.existsSync(path.join(dir, "assessment.json"));
|
|
158
|
+
|
|
159
|
+
let tasksDone = 0;
|
|
160
|
+
let tasksTotal = 0;
|
|
161
|
+
if (hasTasks) {
|
|
162
|
+
try {
|
|
163
|
+
const raw = fs.readFileSync(path.join(dir, "tasks.md"), "utf8");
|
|
164
|
+
tasksTotal = (raw.match(/^\s*-\s+\[[ xX]\]/gm) ?? []).length;
|
|
165
|
+
tasksDone = (raw.match(/^\s*-\s+\[[xX]\]/gm) ?? []).length;
|
|
166
|
+
} catch { /* ignore */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Relative path from repoRoot for portability
|
|
170
|
+
const changePath = path.relative(repoRoot, dir);
|
|
171
|
+
|
|
172
|
+
return { changePath, hasProposal, hasSpec, hasTasks, hasAssessment, tasksDone, tasksTotal, isArchived };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readAssessmentResult(repoRoot: string, nodeId: string): AssessmentResult | null {
|
|
176
|
+
for (const subDir of ["design", "design-archive"]) {
|
|
177
|
+
const assessPath = path.join(repoRoot, "openspec", subDir, nodeId, "assessment.json");
|
|
178
|
+
if (fs.existsSync(assessPath)) {
|
|
179
|
+
try {
|
|
180
|
+
const raw = JSON.parse(fs.readFileSync(assessPath, "utf8")) as {
|
|
181
|
+
outcome?: string;
|
|
182
|
+
timestamp?: string;
|
|
183
|
+
};
|
|
184
|
+
if (raw.timestamp) {
|
|
185
|
+
return { pass: raw.outcome === "pass", capturedAt: raw.timestamp };
|
|
186
|
+
}
|
|
187
|
+
} catch { /* ignore */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Scan docs/ once and build the enriched DesignNodeSummary list.
|
|
195
|
+
* Called at most once per request; the result is shared between
|
|
196
|
+
* buildDesignTree and buildDesignPipeline to avoid duplicate scans.
|
|
197
|
+
*/
|
|
198
|
+
function scanDesignNodes(repoRoot: string): DesignNodeSummary[] {
|
|
199
|
+
const docsDir = path.join(repoRoot, "docs");
|
|
200
|
+
if (!fs.existsSync(docsDir)) return [];
|
|
201
|
+
try {
|
|
202
|
+
const tree = scanDesignDocs(docsDir);
|
|
203
|
+
const out: DesignNodeSummary[] = [];
|
|
204
|
+
for (const [, node] of tree.nodes) {
|
|
205
|
+
const acRaw = countAcceptanceCriteria(node);
|
|
206
|
+
const acSummary: ACSummary | null = acRaw
|
|
207
|
+
? { scenarios: acRaw.scenarios, falsifiability: acRaw.falsifiability, constraints: acRaw.constraints }
|
|
208
|
+
: null;
|
|
209
|
+
const designSpec = resolveDesignSpecBinding(repoRoot, node.id);
|
|
210
|
+
const assessmentResult = readAssessmentResult(repoRoot, node.id);
|
|
211
|
+
out.push({
|
|
212
|
+
id: node.id,
|
|
213
|
+
title: node.title,
|
|
214
|
+
status: node.status,
|
|
215
|
+
parent: node.parent ?? null,
|
|
216
|
+
questionCount: node.open_questions.length,
|
|
217
|
+
questions: node.open_questions,
|
|
218
|
+
tags: node.tags,
|
|
219
|
+
openspecChange: node.openspec_change ?? null,
|
|
220
|
+
branch: node.branch ?? null,
|
|
221
|
+
designSpec,
|
|
222
|
+
acSummary,
|
|
223
|
+
assessmentResult,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
} catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildDesignTree(repoRoot: string, scannedNodes?: DesignNodeSummary[]): DesignTreeSnapshot {
|
|
233
|
+
// Use sharedState.designTree if available (populated by design-tree extension),
|
|
234
|
+
// otherwise fall back to an on-demand file scan.
|
|
235
|
+
const live = sharedState.designTree;
|
|
236
|
+
|
|
237
|
+
// Accept pre-scanned nodes (shared with buildDesignPipeline) or scan now.
|
|
238
|
+
let nodes: DesignNodeSummary[] = scannedNodes ?? scanDesignNodes(repoRoot);
|
|
239
|
+
let focusedNodeId: string | null = null;
|
|
240
|
+
|
|
241
|
+
if (live?.focusedNode) {
|
|
242
|
+
focusedNodeId = live.focusedNode.id;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let statusCounts: Record<string, number> = {};
|
|
246
|
+
let openQuestionCount = 0;
|
|
247
|
+
let focusedNode: DesignNodeSummary | null = null;
|
|
248
|
+
|
|
249
|
+
let scanSucceeded = nodes.length > 0 || fs.existsSync(path.join(repoRoot, "docs"));
|
|
250
|
+
if (nodes.length > 0 || scannedNodes !== undefined) {
|
|
251
|
+
try {
|
|
252
|
+
for (const node of nodes) {
|
|
253
|
+
statusCounts[node.status] = (statusCounts[node.status] ?? 0) + 1;
|
|
254
|
+
openQuestionCount += node.questionCount;
|
|
255
|
+
if (node.id === focusedNodeId) focusedNode = node;
|
|
256
|
+
}
|
|
257
|
+
scanSucceeded = true;
|
|
258
|
+
} catch {
|
|
259
|
+
// fall through to live state fallback
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// No pre-scanned nodes and no docs dir — mark as not succeeded
|
|
263
|
+
scanSucceeded = false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!scanSucceeded && live) {
|
|
267
|
+
// No docs dir but we have live dashboard state — synthesise minimal summary
|
|
268
|
+
openQuestionCount = live.openQuestionCount;
|
|
269
|
+
statusCounts = {
|
|
270
|
+
decided: live.decidedCount,
|
|
271
|
+
exploring: live.exploringCount,
|
|
272
|
+
implementing: live.implementingCount,
|
|
273
|
+
implemented: live.implementedCount,
|
|
274
|
+
blocked: live.blockedCount,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
nodeCount: nodes.length || live?.nodeCount || 0,
|
|
280
|
+
statusCounts,
|
|
281
|
+
openQuestionCount,
|
|
282
|
+
focusedNode,
|
|
283
|
+
nodes,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildOpenSpec(repoRoot: string): OpenSpecSnapshot {
|
|
288
|
+
// On-demand scan
|
|
289
|
+
let changes: OpenSpecChangeSummary[] = [];
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const rawChanges = listChanges(repoRoot);
|
|
293
|
+
changes = rawChanges.map((c) => ({
|
|
294
|
+
name: c.name,
|
|
295
|
+
stage: c.stage,
|
|
296
|
+
hasProposal: c.hasProposal,
|
|
297
|
+
hasDesign: c.hasDesign,
|
|
298
|
+
hasSpecs: c.hasSpecs,
|
|
299
|
+
hasTasks: c.hasTasks,
|
|
300
|
+
tasksTotal: c.totalTasks,
|
|
301
|
+
tasksDone: c.doneTasks,
|
|
302
|
+
specDomains: c.specs.map((s) => s.domain),
|
|
303
|
+
}));
|
|
304
|
+
} catch {
|
|
305
|
+
// openspec dir may not exist
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { changes };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildCleave(): CleaveSnapshot {
|
|
312
|
+
const c = sharedState.cleave;
|
|
313
|
+
if (!c) {
|
|
314
|
+
return { status: "idle", runId: null, children: [], updatedAt: null };
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
status: c.status,
|
|
318
|
+
runId: c.runId ?? null,
|
|
319
|
+
children: (c.children ?? []).map((ch) => ({
|
|
320
|
+
label: ch.label,
|
|
321
|
+
status: ch.status,
|
|
322
|
+
elapsed: ch.elapsed ?? null,
|
|
323
|
+
})),
|
|
324
|
+
updatedAt: c.updatedAt ?? null,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildModels(): ModelsSnapshot {
|
|
329
|
+
const effort = sharedState.effort;
|
|
330
|
+
return {
|
|
331
|
+
routingPolicy: sharedState.routingPolicy
|
|
332
|
+
? (sharedState.routingPolicy as unknown as Record<string, unknown>)
|
|
333
|
+
: null,
|
|
334
|
+
effortLevel: effort?.name ?? null,
|
|
335
|
+
effortCapped: effort?.capped ?? false,
|
|
336
|
+
resolvedExtractionModelId: effort?.resolvedExtractionModelId ?? null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildMemory(): MemorySnapshot {
|
|
341
|
+
const inj = sharedState.lastMemoryInjection;
|
|
342
|
+
return {
|
|
343
|
+
tokenEstimate: sharedState.memoryTokenEstimate,
|
|
344
|
+
lastInjection: inj
|
|
345
|
+
? {
|
|
346
|
+
factCount:
|
|
347
|
+
(inj.projectFactCount ?? 0) +
|
|
348
|
+
(inj.globalFactCount ?? 0) +
|
|
349
|
+
(inj.workingMemoryFactCount ?? 0),
|
|
350
|
+
episodeCount: inj.episodeCount,
|
|
351
|
+
workingMemoryCount: inj.workingMemoryFactCount,
|
|
352
|
+
totalTokens: inj.estimatedTokens,
|
|
353
|
+
}
|
|
354
|
+
: null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function buildDesignPipeline(repoRoot: string, scannedNodes?: DesignNodeSummary[]): DesignPipelineSnapshot {
|
|
359
|
+
const raw = listDesignChanges(repoRoot);
|
|
360
|
+
|
|
361
|
+
const changes: DesignChangeSummary[] = raw.map((c) => ({
|
|
362
|
+
nodeId: c.nodeId,
|
|
363
|
+
changePath: path.relative(repoRoot, c.path),
|
|
364
|
+
hasProposal: c.hasProposal,
|
|
365
|
+
hasSpec: c.hasSpec,
|
|
366
|
+
hasTasks: c.hasTasks,
|
|
367
|
+
hasAssessment: c.hasAssessment,
|
|
368
|
+
assessmentPass: c.assessmentPass,
|
|
369
|
+
capturedAt: c.capturedAt,
|
|
370
|
+
tasksDone: c.tasksDone,
|
|
371
|
+
tasksTotal: c.tasksTotal,
|
|
372
|
+
isArchived: c.isArchived,
|
|
373
|
+
archivedPath: c.archivedPath ? path.relative(repoRoot, c.archivedPath) : undefined,
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
// Compute funnel counts from the already-scanned node list (shared with
|
|
377
|
+
// buildDesignTree to avoid a second full docs/ + openspec/design/ scan per request).
|
|
378
|
+
const nodes = scannedNodes ?? scanDesignNodes(repoRoot);
|
|
379
|
+
let total = nodes.length;
|
|
380
|
+
let bound = 0;
|
|
381
|
+
let tasksComplete = 0;
|
|
382
|
+
let assessed = 0;
|
|
383
|
+
const archived = raw.filter((c) => c.isArchived).length;
|
|
384
|
+
|
|
385
|
+
for (const node of nodes) {
|
|
386
|
+
if (!node.designSpec) continue;
|
|
387
|
+
bound++;
|
|
388
|
+
if (node.designSpec.tasksTotal > 0 && node.designSpec.tasksDone >= node.designSpec.tasksTotal) tasksComplete++;
|
|
389
|
+
if (node.assessmentResult?.pass) assessed++;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const funnelCounts: DesignFunnelCounts = { total, bound, tasksComplete, assessed, archived };
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
capturedAt: new Date().toISOString(),
|
|
396
|
+
changes,
|
|
397
|
+
funnelCounts,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildHealth(startedAt: number): HealthSnapshot {
|
|
402
|
+
return {
|
|
403
|
+
status: "ok",
|
|
404
|
+
uptimeMs: Date.now() - startedAt,
|
|
405
|
+
serverAlive: true,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Build a full ControlPlaneState snapshot.
|
|
413
|
+
*
|
|
414
|
+
* @param repoRoot Absolute path to the repository root.
|
|
415
|
+
* @param startedAt Unix epoch ms when the web UI server started (for uptime).
|
|
416
|
+
*/
|
|
417
|
+
export function buildControlPlaneState(
|
|
418
|
+
repoRoot: string,
|
|
419
|
+
startedAt: number
|
|
420
|
+
): ControlPlaneState {
|
|
421
|
+
// Scan design docs exactly once per request — result shared between
|
|
422
|
+
// buildDesignTree and buildDesignPipeline to avoid duplicate I/O.
|
|
423
|
+
const scannedNodes = scanDesignNodes(repoRoot);
|
|
424
|
+
return {
|
|
425
|
+
schemaVersion: SCHEMA_VERSION,
|
|
426
|
+
session: buildSession(repoRoot),
|
|
427
|
+
dashboard: buildDashboard(),
|
|
428
|
+
designTree: buildDesignTree(repoRoot, scannedNodes),
|
|
429
|
+
openspec: buildOpenSpec(repoRoot),
|
|
430
|
+
cleave: buildCleave(),
|
|
431
|
+
models: buildModels(),
|
|
432
|
+
memory: buildMemory(),
|
|
433
|
+
health: buildHealth(startedAt),
|
|
434
|
+
designPipeline: buildDesignPipeline(repoRoot, scannedNodes),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Build only the named top-level slice.
|
|
440
|
+
* Used by the slice routes (e.g. GET /api/design-tree).
|
|
441
|
+
*/
|
|
442
|
+
export function buildSlice(
|
|
443
|
+
slice: keyof Omit<ControlPlaneState, "schemaVersion">,
|
|
444
|
+
repoRoot: string,
|
|
445
|
+
startedAt: number
|
|
446
|
+
): ControlPlaneState[typeof slice] {
|
|
447
|
+
switch (slice) {
|
|
448
|
+
case "session": return buildSession(repoRoot);
|
|
449
|
+
case "dashboard": return buildDashboard();
|
|
450
|
+
case "designTree": return buildDesignTree(repoRoot);
|
|
451
|
+
case "openspec": return buildOpenSpec(repoRoot);
|
|
452
|
+
case "cleave": return buildCleave();
|
|
453
|
+
case "models": return buildModels();
|
|
454
|
+
case "memory": return buildMemory();
|
|
455
|
+
case "health": return buildHealth(startedAt);
|
|
456
|
+
case "designPipeline": return buildDesignPipeline(repoRoot);
|
|
457
|
+
default: {
|
|
458
|
+
const _exhaustive: never = slice;
|
|
459
|
+
throw new Error(`Unhandled slice: ${String(_exhaustive)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>pi-kit dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
11
|
+
background: #0d1117;
|
|
12
|
+
color: #c9d1d9;
|
|
13
|
+
padding: 1.25rem;
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
}
|
|
16
|
+
header {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
justify-content: space-between;
|
|
20
|
+
margin-bottom: 1rem;
|
|
21
|
+
border-bottom: 1px solid #30363d;
|
|
22
|
+
padding-bottom: 0.75rem;
|
|
23
|
+
}
|
|
24
|
+
header h1 { font-size: 1.1rem; color: #58a6ff; letter-spacing: 0.03em; }
|
|
25
|
+
#status { font-size: 0.75rem; color: #8b949e; }
|
|
26
|
+
.badge {
|
|
27
|
+
display: inline-block;
|
|
28
|
+
padding: 0.1rem 0.45rem;
|
|
29
|
+
border-radius: 3px;
|
|
30
|
+
font-size: 0.7rem;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
}
|
|
33
|
+
.badge-ok { background: #0d4429; color: #3fb950; }
|
|
34
|
+
.badge-err { background: #3d1c1c; color: #f85149; }
|
|
35
|
+
|
|
36
|
+
#grid {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
}
|
|
41
|
+
.card {
|
|
42
|
+
background: #161b22;
|
|
43
|
+
border: 1px solid #30363d;
|
|
44
|
+
border-radius: 6px;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
}
|
|
47
|
+
.card-header {
|
|
48
|
+
padding: 0.5rem 0.75rem;
|
|
49
|
+
background: #21262d;
|
|
50
|
+
border-bottom: 1px solid #30363d;
|
|
51
|
+
font-size: 0.8rem;
|
|
52
|
+
color: #79c0ff;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
letter-spacing: 0.02em;
|
|
55
|
+
}
|
|
56
|
+
.card pre {
|
|
57
|
+
padding: 0.75rem;
|
|
58
|
+
font-size: 0.72rem;
|
|
59
|
+
color: #c9d1d9;
|
|
60
|
+
white-space: pre-wrap;
|
|
61
|
+
word-break: break-word;
|
|
62
|
+
max-height: 340px;
|
|
63
|
+
overflow-y: auto;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Scrollbar styling */
|
|
67
|
+
pre::-webkit-scrollbar { width: 4px; }
|
|
68
|
+
pre::-webkit-scrollbar-track { background: #161b22; }
|
|
69
|
+
pre::-webkit-scrollbar-thumb { background: #30363d; border-radius: 2px; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<header>
|
|
74
|
+
<h1>⚡ pi-kit dashboard</h1>
|
|
75
|
+
<div id="status">Connecting…</div>
|
|
76
|
+
</header>
|
|
77
|
+
<div id="grid"></div>
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
'use strict';
|
|
81
|
+
|
|
82
|
+
const POLL_MS = 3000;
|
|
83
|
+
|
|
84
|
+
const SECTIONS = [
|
|
85
|
+
{ key: 'session', label: 'Session' },
|
|
86
|
+
{ key: 'dashboard', label: 'Dashboard' },
|
|
87
|
+
{ key: 'designTree', label: 'Design Tree' },
|
|
88
|
+
{ key: 'openspec', label: 'OpenSpec' },
|
|
89
|
+
{ key: 'cleave', label: 'Cleave' },
|
|
90
|
+
{ key: 'models', label: 'Models' },
|
|
91
|
+
{ key: 'memory', label: 'Memory' },
|
|
92
|
+
{ key: 'health', label: 'Health' },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// Build cards once, update pre content on each poll
|
|
96
|
+
const grid = document.getElementById('grid');
|
|
97
|
+
const pres = {};
|
|
98
|
+
|
|
99
|
+
for (const { key, label } of SECTIONS) {
|
|
100
|
+
const card = document.createElement('div');
|
|
101
|
+
card.className = 'card';
|
|
102
|
+
|
|
103
|
+
const header = document.createElement('div');
|
|
104
|
+
header.className = 'card-header';
|
|
105
|
+
header.textContent = label;
|
|
106
|
+
|
|
107
|
+
const pre = document.createElement('pre');
|
|
108
|
+
pre.id = 'pre-' + key;
|
|
109
|
+
pre.textContent = '…';
|
|
110
|
+
pres[key] = pre;
|
|
111
|
+
|
|
112
|
+
card.appendChild(header);
|
|
113
|
+
card.appendChild(pre);
|
|
114
|
+
grid.appendChild(card);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const statusEl = document.getElementById('status');
|
|
118
|
+
|
|
119
|
+
async function poll() {
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch('/api/state');
|
|
122
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
123
|
+
const state = await res.json();
|
|
124
|
+
|
|
125
|
+
for (const { key } of SECTIONS) {
|
|
126
|
+
if (key in state) {
|
|
127
|
+
pres[key].textContent = JSON.stringify(state[key], null, 2);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const now = new Date().toLocaleTimeString();
|
|
132
|
+
statusEl.innerHTML =
|
|
133
|
+
'<span class="badge badge-ok">live</span> ' +
|
|
134
|
+
'Updated ' + now + ' — schema v' + state.schemaVersion;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
statusEl.innerHTML =
|
|
137
|
+
'<span class="badge badge-err">error</span> ' + err.message;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
poll();
|
|
142
|
+
setInterval(poll, POLL_MS);
|
|
143
|
+
</script>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|