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.
Files changed (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * design-tree/design-card — Rich card rendering for design nodes.
3
+ *
4
+ * Renders a structured, information-dense card for the conversation window.
5
+ * Used by the design-focus message renderer and the design_tree tool result.
6
+ *
7
+ * Layout:
8
+ * ── ◈ design:focus → node-id ──────────────────────────────────────
9
+ * │ ● decided │ P2 high │ ★ feature
10
+ * │
11
+ * │ Overview text truncated to fit...
12
+ * │
13
+ * │ Decisions ── 5● 2◐
14
+ * │ ● Agent unit model ● WorkspaceBackend trait
15
+ * │ ◐ API surface ● Server/worker modes
16
+ * │
17
+ * │ Open Questions ── 3
18
+ * │ 1. What API surface?
19
+ * │ 2. Binary composition?
20
+ * │
21
+ * │ Dependencies ── 2
22
+ * │ ● omega-memory-backend ◐ omega-workspace-backend
23
+ * │
24
+ * │ File Scope ── 7
25
+ * │ + src/memory/decay.rs ~ src/memory/vectors.rs
26
+ * ╰──────────────────────────────────────────────────────────────────
27
+ */
28
+
29
+ import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@cwilson613/pi-tui";
30
+ import type { Component } from "@cwilson613/pi-tui";
31
+ import type { Theme } from "@cwilson613/pi-coding-agent";
32
+ import { STATUS_ICONS, STATUS_COLORS, ISSUE_TYPE_ICONS, PRIORITY_LABELS } from "./types.ts";
33
+ import type { NodeStatus, IssueType, Priority } from "./types.ts";
34
+
35
+ /** Extract ThemeColor type from Theme.fg signature */
36
+ type ThemeColor = Parameters<Theme["fg"]>[0];
37
+
38
+ // ─── Card Details Shape ──────────────────────────────────────────────────────
39
+
40
+ export interface DesignCardDetails {
41
+ id: string;
42
+ title: string;
43
+ status: NodeStatus;
44
+ priority?: Priority;
45
+ issue_type?: IssueType;
46
+ overview?: string;
47
+ decisions?: Array<{ title: string; status: string }>;
48
+ openQuestions?: string[];
49
+ dependencies?: Array<{ id: string; title: string; status: NodeStatus }>;
50
+ children?: Array<{ id: string; title: string; status: NodeStatus }>;
51
+ fileScope?: Array<{ path: string; action?: string; description?: string }>;
52
+ constraints?: string[];
53
+ openspec_change?: string;
54
+ branches?: string[];
55
+ }
56
+
57
+ // ─── Action Glyphs ──────────────────────────────────────────────────────────
58
+
59
+ const FILE_ACTION_ICONS: Record<string, string> = {
60
+ new: "+",
61
+ modified: "~",
62
+ deleted: "−",
63
+ };
64
+
65
+ // ─── SciDesignCard Component ─────────────────────────────────────────────────
66
+
67
+ export class SciDesignCard implements Component {
68
+ private label: string;
69
+ private details: DesignCardDetails;
70
+ private theme: Theme;
71
+ private cachedLines?: string[];
72
+ private cachedWidth?: number;
73
+
74
+ constructor(label: string, details: DesignCardDetails, theme: Theme) {
75
+ this.label = label;
76
+ this.details = details;
77
+ this.theme = theme;
78
+ }
79
+
80
+ invalidate(): void {
81
+ this.cachedLines = undefined;
82
+ this.cachedWidth = undefined;
83
+ }
84
+
85
+ render(width: number): string[] {
86
+ if (this.cachedLines && this.cachedWidth === width) {
87
+ return this.cachedLines;
88
+ }
89
+
90
+ const th = this.theme;
91
+ const d = this.details;
92
+ const lines: string[] = [];
93
+
94
+ // ── Header bar ───────────────────────────────────────────────
95
+ const headerLabel = ` ${th.fg("accent", "◈")} ${th.fg("muted", this.label)} `;
96
+ const headerLabelVw = visibleWidth(headerLabel);
97
+ const leftDash = th.fg("dim", "──");
98
+ const leftDashVw = 2;
99
+ const rightFillLen = Math.max(0, width - leftDashVw - headerLabelVw);
100
+ const rightFill = th.fg("dim", "─".repeat(rightFillLen));
101
+ lines.push(truncateToWidth(leftDash + headerLabel + rightFill, width));
102
+
103
+ const brd = th.fg("dim", " │");
104
+ const innerWidth = Math.max(20, width - 5); // 5 = " │ " + 1 margin
105
+
106
+ // ── Metadata row ─────────────────────────────────────────────
107
+ const metaParts: string[] = [];
108
+
109
+ // Status with icon
110
+ const statusIcon = STATUS_ICONS[d.status] || "·";
111
+ const statusColor = (STATUS_COLORS[d.status] || "muted") as ThemeColor;
112
+ metaParts.push(th.fg(statusColor, `${statusIcon} ${d.status}`));
113
+
114
+ // Priority
115
+ if (d.priority != null) {
116
+ const pLabel = PRIORITY_LABELS[d.priority as Priority];
117
+ if (pLabel) {
118
+ metaParts.push(th.fg("muted", `P${d.priority} ${pLabel}`));
119
+ }
120
+ }
121
+
122
+ // Issue type
123
+ if (d.issue_type) {
124
+ const itIcon = ISSUE_TYPE_ICONS[d.issue_type as IssueType] || "";
125
+ metaParts.push(th.fg("muted", `${itIcon} ${d.issue_type}`));
126
+ }
127
+
128
+ // OpenSpec binding
129
+ if (d.openspec_change) {
130
+ metaParts.push(th.fg("accent", `◎ ${d.openspec_change}`));
131
+ }
132
+
133
+ const metaLine = metaParts.join(th.fg("dim", " │ "));
134
+ lines.push(truncateToWidth(`${brd} ${metaLine}`, width));
135
+
136
+ // ── Overview ─────────────────────────────────────────────────
137
+ if (d.overview && d.overview.trim()) {
138
+ lines.push(brd);
139
+ const overviewText = d.overview.trim().replace(/\n+/g, " ");
140
+ const wrapped = wrapTextWithAnsi(th.fg("muted", overviewText), innerWidth);
141
+ const maxLines = 3;
142
+ for (let i = 0; i < Math.min(wrapped.length, maxLines); i++) {
143
+ const line = wrapped[i]!;
144
+ if (i === maxLines - 1 && wrapped.length > maxLines) {
145
+ lines.push(truncateToWidth(`${brd} ${line}${th.fg("dim", "…")}`, width));
146
+ } else {
147
+ lines.push(truncateToWidth(`${brd} ${line}`, width));
148
+ }
149
+ }
150
+ }
151
+
152
+ // ── Decisions ────────────────────────────────────────────────
153
+ if (d.decisions && d.decisions.length > 0) {
154
+ lines.push(brd);
155
+ const decided = d.decisions.filter((x) => x.status === "decided").length;
156
+ const exploring = d.decisions.filter((x) => x.status === "exploring").length;
157
+ const rejected = d.decisions.filter((x) => x.status === "rejected").length;
158
+ const countParts: string[] = [];
159
+ if (decided > 0) countParts.push(th.fg("success", `${decided}●`));
160
+ if (exploring > 0) countParts.push(th.fg("accent", `${exploring}◐`));
161
+ if (rejected > 0) countParts.push(th.fg("error", `${rejected}✕`));
162
+ const sectionHeader = `${th.fg("muted", "Decisions")} ${th.fg("dim", "──")} ${countParts.join(" ")}`;
163
+ lines.push(truncateToWidth(`${brd} ${sectionHeader}`, width));
164
+
165
+ this.renderTwoColumn(lines, d.decisions, innerWidth, width, brd, (item) => {
166
+ const icon = item.status === "decided" ? "●" : item.status === "exploring" ? "◐" : "✕";
167
+ const color = item.status === "decided" ? "success" : item.status === "exploring" ? "accent" : "error";
168
+ return `${th.fg(color, icon)} ${th.fg("muted", item.title)}`;
169
+ });
170
+ }
171
+
172
+ // ── Open Questions ───────────────────────────────────────────
173
+ if (d.openQuestions && d.openQuestions.length > 0) {
174
+ lines.push(brd);
175
+ const qHeader = `${th.fg("muted", "Open Questions")} ${th.fg("dim", "──")} ${th.fg("warning", String(d.openQuestions.length))}`;
176
+ lines.push(truncateToWidth(`${brd} ${qHeader}`, width));
177
+
178
+ const maxQ = 5;
179
+ for (let i = 0; i < Math.min(d.openQuestions.length, maxQ); i++) {
180
+ const q = d.openQuestions[i]!;
181
+ const num = th.fg("dim", `${i + 1}.`);
182
+ const qText = truncateToWidth(th.fg("muted", q), innerWidth - 4);
183
+ lines.push(truncateToWidth(`${brd} ${num} ${qText}`, width));
184
+ }
185
+ if (d.openQuestions.length > maxQ) {
186
+ lines.push(truncateToWidth(`${brd} ${th.fg("dim", `… +${d.openQuestions.length - maxQ} more`)}`, width));
187
+ }
188
+ }
189
+
190
+ // ── Dependencies ─────────────────────────────────────────────
191
+ if (d.dependencies && d.dependencies.length > 0) {
192
+ lines.push(brd);
193
+ const depHeader = `${th.fg("muted", "Dependencies")} ${th.fg("dim", "──")} ${th.fg("muted", String(d.dependencies.length))}`;
194
+ lines.push(truncateToWidth(`${brd} ${depHeader}`, width));
195
+
196
+ this.renderTwoColumn(lines, d.dependencies, innerWidth, width, brd, (item) => {
197
+ const icon = STATUS_ICONS[item.status] || "·";
198
+ const color = (STATUS_COLORS[item.status] || "muted") as ThemeColor;
199
+ return `${th.fg(color, icon)} ${th.fg("muted", item.title)}`;
200
+ });
201
+ }
202
+
203
+ // ── Children ─────────────────────────────────────────────────
204
+ if (d.children && d.children.length > 0) {
205
+ lines.push(brd);
206
+ const childHeader = `${th.fg("muted", "Children")} ${th.fg("dim", "──")} ${th.fg("muted", String(d.children.length))}`;
207
+ lines.push(truncateToWidth(`${brd} ${childHeader}`, width));
208
+
209
+ this.renderTwoColumn(lines, d.children, innerWidth, width, brd, (item) => {
210
+ const icon = STATUS_ICONS[item.status] || "·";
211
+ const color = (STATUS_COLORS[item.status] || "muted") as ThemeColor;
212
+ return `${th.fg(color, icon)} ${th.fg("muted", item.title)}`;
213
+ });
214
+ }
215
+
216
+ // ── File Scope ───────────────────────────────────────────────
217
+ if (d.fileScope && d.fileScope.length > 0) {
218
+ lines.push(brd);
219
+ const fsHeader = `${th.fg("muted", "File Scope")} ${th.fg("dim", "──")} ${th.fg("muted", String(d.fileScope.length) + " files")}`;
220
+ lines.push(truncateToWidth(`${brd} ${fsHeader}`, width));
221
+
222
+ const maxFiles = 8;
223
+ this.renderTwoColumn(
224
+ lines,
225
+ d.fileScope.slice(0, maxFiles),
226
+ innerWidth,
227
+ width,
228
+ brd,
229
+ (item) => {
230
+ const actionIcon = FILE_ACTION_ICONS[item.action ?? "modified"] ?? "·";
231
+ const actionColor = item.action === "new" ? "success" : item.action === "deleted" ? "error" : "accent";
232
+ return `${th.fg(actionColor, actionIcon)} ${th.fg("muted", item.path)}`;
233
+ },
234
+ );
235
+ if (d.fileScope.length > maxFiles) {
236
+ lines.push(truncateToWidth(`${brd} ${th.fg("dim", `… +${d.fileScope.length - maxFiles} more`)}`, width));
237
+ }
238
+ }
239
+
240
+ // ── Constraints ──────────────────────────────────────────────
241
+ if (d.constraints && d.constraints.length > 0) {
242
+ lines.push(brd);
243
+ const cHeader = `${th.fg("muted", "Constraints")} ${th.fg("dim", "──")} ${th.fg("muted", String(d.constraints.length))}`;
244
+ lines.push(truncateToWidth(`${brd} ${cHeader}`, width));
245
+
246
+ const maxC = 4;
247
+ for (let i = 0; i < Math.min(d.constraints.length, maxC); i++) {
248
+ const c = d.constraints[i]!;
249
+ const cText = truncateToWidth(th.fg("muted", c), innerWidth - 4);
250
+ lines.push(truncateToWidth(`${brd} ${th.fg("dim", "▸")} ${cText}`, width));
251
+ }
252
+ if (d.constraints.length > maxC) {
253
+ lines.push(truncateToWidth(`${brd} ${th.fg("dim", `… +${d.constraints.length - maxC} more`)}`, width));
254
+ }
255
+ }
256
+
257
+ // ── Footer bar ───────────────────────────────────────────────
258
+ const footerFillLen = Math.max(0, width - 5);
259
+ lines.push(th.fg("dim", ` ╰${"─".repeat(footerFillLen)}`));
260
+
261
+ this.cachedLines = lines;
262
+ this.cachedWidth = width;
263
+ return lines;
264
+ }
265
+
266
+ /**
267
+ * Render items in a two-column layout within the bordered card.
268
+ * Falls back to single column if the terminal is too narrow.
269
+ */
270
+ private renderTwoColumn<T>(
271
+ lines: string[],
272
+ items: T[],
273
+ innerWidth: number,
274
+ totalWidth: number,
275
+ brd: string,
276
+ format: (item: T) => string,
277
+ ): void {
278
+ const colWidth = Math.floor((innerWidth - 2) / 2); // 2 for gap between columns
279
+ const useTwoCols = colWidth >= 20 && items.length > 1;
280
+
281
+ if (useTwoCols) {
282
+ for (let i = 0; i < items.length; i += 2) {
283
+ const left = truncateToWidth(format(items[i]!), colWidth);
284
+ if (i + 1 < items.length) {
285
+ const right = truncateToWidth(format(items[i + 1]!), colWidth);
286
+ const leftPad = " ".repeat(Math.max(0, colWidth - visibleWidth(left)));
287
+ lines.push(truncateToWidth(`${brd} ${left}${leftPad} ${right}`, totalWidth));
288
+ } else {
289
+ lines.push(truncateToWidth(`${brd} ${left}`, totalWidth));
290
+ }
291
+ }
292
+ } else {
293
+ for (const item of items) {
294
+ const formatted = truncateToWidth(format(item), innerWidth - 2);
295
+ lines.push(truncateToWidth(`${brd} ${formatted}`, totalWidth));
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ // ─── Builder ─────────────────────────────────────────────────────────────────
302
+
303
+ /**
304
+ * Build a DesignCardDetails from a design node and its sections.
305
+ * Used to populate the `details` field of design-focus messages.
306
+ */
307
+ export function buildCardDetails(
308
+ node: {
309
+ id: string;
310
+ title: string;
311
+ status: NodeStatus;
312
+ priority?: Priority;
313
+ issue_type?: IssueType;
314
+ dependencies: string[];
315
+ open_questions: string[];
316
+ openspec_change?: string;
317
+ branches: string[];
318
+ },
319
+ sections: {
320
+ overview: string;
321
+ decisions: Array<{ title: string; status: string }>;
322
+ implementationNotes: {
323
+ fileScope: Array<{ path: string; action?: string; description?: string }>;
324
+ constraints: string[];
325
+ };
326
+ },
327
+ tree: { nodes: Map<string, { id: string; title: string; status: NodeStatus; parent?: string }> },
328
+ ): DesignCardDetails {
329
+ // Resolve dependencies to titles
330
+ const deps = node.dependencies
331
+ .map((id) => {
332
+ const d = tree.nodes.get(id);
333
+ return d ? { id: d.id, title: d.title, status: d.status } : null;
334
+ })
335
+ .filter((x): x is NonNullable<typeof x> => x !== null);
336
+
337
+ // Find children
338
+ const children = Array.from(tree.nodes.values())
339
+ .filter((n) => n.parent === node.id)
340
+ .map((n) => ({ id: n.id, title: n.title, status: n.status }));
341
+
342
+ return {
343
+ id: node.id,
344
+ title: node.title,
345
+ status: node.status,
346
+ priority: node.priority,
347
+ issue_type: node.issue_type,
348
+ overview: sections.overview,
349
+ decisions: sections.decisions.map((d) => ({ title: d.title, status: d.status })),
350
+ openQuestions: node.open_questions,
351
+ dependencies: deps,
352
+ children: children.length > 0 ? children : undefined,
353
+ fileScope: sections.implementationNotes.fileScope.length > 0
354
+ ? sections.implementationNotes.fileScope
355
+ : undefined,
356
+ constraints: sections.implementationNotes.constraints.length > 0
357
+ ? sections.implementationNotes.constraints
358
+ : undefined,
359
+ openspec_change: node.openspec_change,
360
+ branches: node.branches.length > 0 ? node.branches : undefined,
361
+ };
362
+ }