shipwright-cli 2.2.2 → 2.3.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 (143) hide show
  1. package/README.md +12 -11
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.ts +381 -0
  18. package/dashboard/src/core/helpers.ts +118 -0
  19. package/dashboard/src/core/router.ts +190 -0
  20. package/dashboard/src/core/sse.ts +38 -0
  21. package/dashboard/src/core/state.ts +150 -0
  22. package/dashboard/src/core/ws.ts +143 -0
  23. package/dashboard/src/design/icons.ts +131 -0
  24. package/dashboard/src/design/tokens.ts +160 -0
  25. package/dashboard/src/main.ts +68 -0
  26. package/dashboard/src/types/api.ts +337 -0
  27. package/dashboard/src/views/activity.ts +185 -0
  28. package/dashboard/src/views/agent-cockpit.ts +236 -0
  29. package/dashboard/src/views/agents.ts +72 -0
  30. package/dashboard/src/views/fleet-map.ts +299 -0
  31. package/dashboard/src/views/insights.ts +298 -0
  32. package/dashboard/src/views/machines.ts +162 -0
  33. package/dashboard/src/views/metrics.ts +420 -0
  34. package/dashboard/src/views/overview.ts +409 -0
  35. package/dashboard/src/views/pipeline-theater.ts +219 -0
  36. package/dashboard/src/views/pipelines.ts +595 -0
  37. package/dashboard/src/views/team.ts +362 -0
  38. package/dashboard/src/views/timeline.ts +389 -0
  39. package/dashboard/tsconfig.json +21 -0
  40. package/docs/AGI-WHATS-NEXT.md +15 -15
  41. package/package.json +8 -1
  42. package/scripts/lib/helpers.sh +30 -0
  43. package/scripts/lib/pipeline-quality-checks.sh +1 -1
  44. package/scripts/sw +86 -167
  45. package/scripts/sw-activity.sh +1 -1
  46. package/scripts/sw-adaptive.sh +1 -1
  47. package/scripts/sw-adversarial.sh +1 -1
  48. package/scripts/sw-architecture-enforcer.sh +1 -1
  49. package/scripts/sw-auth.sh +14 -6
  50. package/scripts/sw-autonomous.sh +1 -1
  51. package/scripts/sw-changelog.sh +2 -2
  52. package/scripts/sw-checkpoint.sh +1 -1
  53. package/scripts/sw-ci.sh +1 -1
  54. package/scripts/sw-cleanup.sh +1 -1
  55. package/scripts/sw-code-review.sh +1 -1
  56. package/scripts/sw-connect.sh +1 -1
  57. package/scripts/sw-context.sh +1 -1
  58. package/scripts/sw-cost.sh +1 -1
  59. package/scripts/sw-daemon.sh +2 -2
  60. package/scripts/sw-dashboard.sh +1 -1
  61. package/scripts/sw-db.sh +1 -1
  62. package/scripts/sw-decompose.sh +1 -1
  63. package/scripts/sw-deps.sh +1 -1
  64. package/scripts/sw-developer-simulation.sh +1 -1
  65. package/scripts/sw-discovery.sh +1 -1
  66. package/scripts/sw-doc-fleet.sh +1 -1
  67. package/scripts/sw-docs-agent.sh +1 -1
  68. package/scripts/sw-docs.sh +1 -1
  69. package/scripts/sw-doctor.sh +8 -1
  70. package/scripts/sw-dora.sh +1 -1
  71. package/scripts/sw-durable.sh +1 -1
  72. package/scripts/sw-e2e-orchestrator.sh +1 -1
  73. package/scripts/sw-eventbus.sh +1 -1
  74. package/scripts/sw-feedback.sh +1 -1
  75. package/scripts/sw-fix.sh +6 -5
  76. package/scripts/sw-fleet-discover.sh +1 -1
  77. package/scripts/sw-fleet-viz.sh +1 -1
  78. package/scripts/sw-fleet.sh +1 -1
  79. package/scripts/sw-github-app.sh +5 -2
  80. package/scripts/sw-github-checks.sh +1 -1
  81. package/scripts/sw-github-deploy.sh +1 -1
  82. package/scripts/sw-github-graphql.sh +1 -1
  83. package/scripts/sw-guild.sh +1 -1
  84. package/scripts/sw-heartbeat.sh +1 -1
  85. package/scripts/sw-hygiene.sh +1 -1
  86. package/scripts/sw-incident.sh +1 -1
  87. package/scripts/sw-init.sh +112 -9
  88. package/scripts/sw-instrument.sh +6 -1
  89. package/scripts/sw-intelligence.sh +5 -1
  90. package/scripts/sw-jira.sh +1 -1
  91. package/scripts/sw-launchd.sh +1 -1
  92. package/scripts/sw-linear.sh +20 -9
  93. package/scripts/sw-logs.sh +1 -1
  94. package/scripts/sw-loop.sh +2 -1
  95. package/scripts/sw-memory.sh +10 -1
  96. package/scripts/sw-mission-control.sh +1 -1
  97. package/scripts/sw-model-router.sh +4 -1
  98. package/scripts/sw-otel.sh +1 -1
  99. package/scripts/sw-oversight.sh +1 -1
  100. package/scripts/sw-pipeline-composer.sh +3 -1
  101. package/scripts/sw-pipeline-vitals.sh +4 -6
  102. package/scripts/sw-pipeline.sh +4 -1
  103. package/scripts/sw-pm.sh +5 -2
  104. package/scripts/sw-pr-lifecycle.sh +1 -1
  105. package/scripts/sw-predictive.sh +4 -1
  106. package/scripts/sw-prep.sh +3 -2
  107. package/scripts/sw-ps.sh +1 -1
  108. package/scripts/sw-public-dashboard.sh +10 -4
  109. package/scripts/sw-quality.sh +1 -1
  110. package/scripts/sw-reaper.sh +1 -1
  111. package/scripts/sw-recruit.sh +16 -0
  112. package/scripts/sw-regression.sh +2 -1
  113. package/scripts/sw-release-manager.sh +1 -1
  114. package/scripts/sw-release.sh +7 -5
  115. package/scripts/sw-remote.sh +1 -1
  116. package/scripts/sw-replay.sh +1 -1
  117. package/scripts/sw-retro.sh +1 -1
  118. package/scripts/sw-scale.sh +4 -1
  119. package/scripts/sw-security-audit.sh +1 -1
  120. package/scripts/sw-self-optimize.sh +15 -1
  121. package/scripts/sw-session.sh +1 -1
  122. package/scripts/sw-setup.sh +1 -1
  123. package/scripts/sw-standup.sh +2 -1
  124. package/scripts/sw-status.sh +1 -1
  125. package/scripts/sw-strategic.sh +2 -1
  126. package/scripts/sw-stream.sh +1 -1
  127. package/scripts/sw-swarm.sh +6 -1
  128. package/scripts/sw-team-stages.sh +1 -1
  129. package/scripts/sw-templates.sh +1 -1
  130. package/scripts/sw-testgen.sh +3 -2
  131. package/scripts/sw-tmux-pipeline.sh +2 -1
  132. package/scripts/sw-tmux.sh +1 -1
  133. package/scripts/sw-trace.sh +1 -1
  134. package/scripts/sw-tracker-jira.sh +1 -0
  135. package/scripts/sw-tracker-linear.sh +1 -0
  136. package/scripts/sw-tracker.sh +1 -1
  137. package/scripts/sw-triage.sh +1 -1
  138. package/scripts/sw-upgrade.sh +1 -1
  139. package/scripts/sw-ux.sh +1 -1
  140. package/scripts/sw-webhook.sh +1 -1
  141. package/scripts/sw-widgets.sh +2 -2
  142. package/scripts/sw-worktree.sh +1 -1
  143. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,38 @@
1
+ // Server-Sent Events client for live log streaming
2
+
3
+ type SSECallback = (data: string) => void;
4
+
5
+ export class SSEClient {
6
+ private eventSource: EventSource | null = null;
7
+ private url: string;
8
+ private onMessage: SSECallback;
9
+ private onError?: () => void;
10
+
11
+ constructor(url: string, onMessage: SSECallback, onError?: () => void) {
12
+ this.url = url;
13
+ this.onMessage = onMessage;
14
+ this.onError = onError;
15
+ }
16
+
17
+ connect(): void {
18
+ this.close();
19
+ this.eventSource = new EventSource(this.url);
20
+ this.eventSource.onmessage = (e) => {
21
+ this.onMessage(e.data);
22
+ };
23
+ this.eventSource.onerror = () => {
24
+ if (this.onError) this.onError();
25
+ };
26
+ }
27
+
28
+ close(): void {
29
+ if (this.eventSource) {
30
+ this.eventSource.close();
31
+ this.eventSource = null;
32
+ }
33
+ }
34
+
35
+ isConnected(): boolean {
36
+ return this.eventSource?.readyState === EventSource.OPEN;
37
+ }
38
+ }
@@ -0,0 +1,150 @@
1
+ // Global state store with typed subscriptions
2
+
3
+ import type {
4
+ FleetState,
5
+ TabId,
6
+ PipelineDetail,
7
+ InsightsData,
8
+ MetricsData,
9
+ MachineInfo,
10
+ JoinToken,
11
+ DaemonConfig,
12
+ AlertInfo,
13
+ TeamData,
14
+ TeamActivityEvent,
15
+ UserInfo,
16
+ } from "../types/api";
17
+
18
+ export interface AppState {
19
+ connected: boolean;
20
+ connectedAt: number | null;
21
+ fleetState: FleetState | null;
22
+ activeTab: TabId;
23
+ selectedPipelineIssue: number | null;
24
+ pipelineDetail: PipelineDetail | null;
25
+ pipelineFilter: string;
26
+ activityFilter: string;
27
+ activityIssueFilter: string;
28
+ activityEvents: Array<Record<string, unknown>>;
29
+ activityOffset: number;
30
+ activityHasMore: boolean;
31
+ metricsCache: MetricsData | null;
32
+ insightsCache: InsightsData | null;
33
+ machinesCache: MachineInfo[] | null;
34
+ joinTokensCache: JoinToken[] | null;
35
+ costBreakdownCache: Record<string, unknown> | null;
36
+ alertsCache: AlertInfo[] | null;
37
+ alertDismissed: boolean;
38
+ teamCache: TeamData | null;
39
+ teamActivityCache: TeamActivityEvent[] | null;
40
+ daemonConfig: DaemonConfig | null;
41
+ currentUser: UserInfo | null;
42
+ selectedIssues: Record<string, boolean>;
43
+ firstRender: boolean;
44
+ }
45
+
46
+ const initialState: AppState = {
47
+ connected: false,
48
+ connectedAt: null,
49
+ fleetState: null,
50
+ activeTab: "overview",
51
+ selectedPipelineIssue: null,
52
+ pipelineDetail: null,
53
+ pipelineFilter: "all",
54
+ activityFilter: "all",
55
+ activityIssueFilter: "",
56
+ activityEvents: [],
57
+ activityOffset: 0,
58
+ activityHasMore: false,
59
+ metricsCache: null,
60
+ insightsCache: null,
61
+ machinesCache: null,
62
+ joinTokensCache: null,
63
+ costBreakdownCache: null,
64
+ alertsCache: null,
65
+ alertDismissed: false,
66
+ teamCache: null,
67
+ teamActivityCache: null,
68
+ daemonConfig: null,
69
+ currentUser: null,
70
+ selectedIssues: {},
71
+ firstRender: true,
72
+ };
73
+
74
+ type Listener<K extends keyof AppState> = (
75
+ value: AppState[K],
76
+ prev: AppState[K],
77
+ ) => void;
78
+ type AnyListener = (state: AppState) => void;
79
+
80
+ class Store {
81
+ private state: AppState;
82
+ private listeners: Map<keyof AppState, Set<Listener<any>>> = new Map();
83
+ private globalListeners: Set<AnyListener> = new Set();
84
+
85
+ constructor() {
86
+ this.state = { ...initialState };
87
+ }
88
+
89
+ get<K extends keyof AppState>(key: K): AppState[K] {
90
+ return this.state[key];
91
+ }
92
+
93
+ getState(): Readonly<AppState> {
94
+ return this.state;
95
+ }
96
+
97
+ set<K extends keyof AppState>(key: K, value: AppState[K]): void {
98
+ const prev = this.state[key];
99
+ if (prev === value) return;
100
+ this.state = { ...this.state, [key]: value };
101
+ const keyListeners = this.listeners.get(key);
102
+ if (keyListeners) {
103
+ keyListeners.forEach((fn) => fn(value, prev));
104
+ }
105
+ this.globalListeners.forEach((fn) => fn(this.state));
106
+ }
107
+
108
+ update(partial: Partial<AppState>): void {
109
+ const keys = Object.keys(partial) as Array<keyof AppState>;
110
+ let changed = false;
111
+ const prevState = this.state;
112
+ const nextState = { ...this.state };
113
+ for (const key of keys) {
114
+ if (nextState[key] !== partial[key]) {
115
+ (nextState as any)[key] = partial[key];
116
+ changed = true;
117
+ }
118
+ }
119
+ if (!changed) return;
120
+ this.state = nextState;
121
+ for (const key of keys) {
122
+ if (prevState[key] !== this.state[key]) {
123
+ const keyListeners = this.listeners.get(key);
124
+ if (keyListeners) {
125
+ keyListeners.forEach((fn) => fn(this.state[key], prevState[key]));
126
+ }
127
+ }
128
+ }
129
+ this.globalListeners.forEach((fn) => fn(this.state));
130
+ }
131
+
132
+ subscribe<K extends keyof AppState>(key: K, fn: Listener<K>): () => void {
133
+ if (!this.listeners.has(key)) {
134
+ this.listeners.set(key, new Set());
135
+ }
136
+ this.listeners.get(key)!.add(fn);
137
+ return () => {
138
+ this.listeners.get(key)?.delete(fn);
139
+ };
140
+ }
141
+
142
+ onAny(fn: AnyListener): () => void {
143
+ this.globalListeners.add(fn);
144
+ return () => {
145
+ this.globalListeners.delete(fn);
146
+ };
147
+ }
148
+ }
149
+
150
+ export const store = new Store();
@@ -0,0 +1,143 @@
1
+ // WebSocket connection with automatic reconnection, stale data indicator, offline resilience
2
+
3
+ import { store } from "./state";
4
+ import type { FleetState } from "../types/api";
5
+
6
+ let ws: WebSocket | null = null;
7
+ let reconnectDelay = 1000;
8
+ let connectionTimer: ReturnType<typeof setInterval> | null = null;
9
+ let lastDataTime = 0;
10
+ let staleTimer: ReturnType<typeof setInterval> | null = null;
11
+ let reconnectAttempts = 0;
12
+
13
+ function startConnectionTimer(): void {
14
+ stopConnectionTimer();
15
+ connectionTimer = setInterval(() => {
16
+ const connectedAt = store.get("connectedAt");
17
+ if (connectedAt) {
18
+ const elapsed = Math.floor((Date.now() - connectedAt) / 1000);
19
+ const h = String(Math.floor(elapsed / 3600)).padStart(2, "0");
20
+ const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, "0");
21
+ const s = String(elapsed % 60).padStart(2, "0");
22
+ const el = document.getElementById("connection-text");
23
+ if (el) el.textContent = `LIVE \u2014 ${h}:${m}:${s}`;
24
+ }
25
+ }, 1000);
26
+ }
27
+
28
+ function stopConnectionTimer(): void {
29
+ if (connectionTimer) {
30
+ clearInterval(connectionTimer);
31
+ connectionTimer = null;
32
+ }
33
+ }
34
+
35
+ function startStaleDataTimer(): void {
36
+ if (staleTimer) clearInterval(staleTimer);
37
+ staleTimer = setInterval(() => {
38
+ if (!lastDataTime) return;
39
+ const ageS = Math.floor((Date.now() - lastDataTime) / 1000);
40
+ const banner = document.getElementById("stale-data-banner");
41
+ if (ageS > 30 && banner) {
42
+ banner.style.display = "";
43
+ const ageEl = document.getElementById("stale-data-age");
44
+ if (ageEl) {
45
+ if (ageS < 60) ageEl.textContent = `${ageS}s`;
46
+ else if (ageS < 3600)
47
+ ageEl.textContent = `${Math.floor(ageS / 60)}m ${ageS % 60}s`;
48
+ else
49
+ ageEl.textContent = `${Math.floor(ageS / 3600)}h ${Math.floor((ageS % 3600) / 60)}m`;
50
+ }
51
+ } else if (banner) {
52
+ banner.style.display = "none";
53
+ }
54
+ }, 5000);
55
+ }
56
+
57
+ function updateConnectionStatus(status: "LIVE" | "OFFLINE"): void {
58
+ const dot = document.getElementById("connection-dot");
59
+ const text = document.getElementById("connection-text");
60
+ if (!dot || !text) return;
61
+ if (status === "LIVE") {
62
+ dot.className = "connection-dot live";
63
+ text.textContent = "LIVE \u2014 00:00:00";
64
+ reconnectAttempts = 0;
65
+ } else {
66
+ dot.className = "connection-dot offline";
67
+ text.textContent = `OFFLINE (retry ${reconnectAttempts})`;
68
+ }
69
+ }
70
+
71
+ function showOfflineBanner(show: boolean): void {
72
+ let banner = document.getElementById("offline-banner");
73
+ if (!banner && show) {
74
+ banner = document.createElement("div");
75
+ banner.id = "offline-banner";
76
+ banner.className = "offline-banner";
77
+ banner.innerHTML =
78
+ `<span class="offline-icon">\u26A0</span>` +
79
+ `<span>Connection lost. Data may be stale.</span>` +
80
+ `<button class="btn-sm" id="manual-reconnect">Reconnect</button>`;
81
+ const main = document.querySelector(".main");
82
+ if (main) main.prepend(banner);
83
+ document
84
+ .getElementById("manual-reconnect")
85
+ ?.addEventListener("click", () => {
86
+ if (ws) {
87
+ try {
88
+ ws.close();
89
+ } catch {}
90
+ }
91
+ reconnectDelay = 1000;
92
+ reconnectAttempts = 0;
93
+ connect();
94
+ });
95
+ }
96
+ if (banner) banner.style.display = show ? "" : "none";
97
+ }
98
+
99
+ export function connect(): void {
100
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
101
+ const wsUrl = `${protocol}//${location.host}/ws`;
102
+ ws = new WebSocket(wsUrl);
103
+
104
+ ws.onopen = () => {
105
+ reconnectDelay = 1000;
106
+ reconnectAttempts = 0;
107
+ store.update({ connected: true, connectedAt: Date.now() });
108
+ updateConnectionStatus("LIVE");
109
+ startConnectionTimer();
110
+ showOfflineBanner(false);
111
+ };
112
+
113
+ ws.onclose = () => {
114
+ store.update({ connected: false, connectedAt: null });
115
+ stopConnectionTimer();
116
+ reconnectAttempts++;
117
+ updateConnectionStatus("OFFLINE");
118
+ showOfflineBanner(true);
119
+ setTimeout(connect, reconnectDelay);
120
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
121
+ };
122
+
123
+ ws.onerror = () => {};
124
+
125
+ ws.onmessage = (e: MessageEvent) => {
126
+ try {
127
+ const data: FleetState = JSON.parse(e.data);
128
+ lastDataTime = Date.now();
129
+ store.update({
130
+ fleetState: data,
131
+ firstRender: false,
132
+ });
133
+ } catch (err) {
134
+ console.error("Failed to parse message:", err);
135
+ }
136
+ };
137
+
138
+ startStaleDataTimer();
139
+ }
140
+
141
+ export function getWebSocket(): WebSocket | null {
142
+ return ws;
143
+ }
@@ -0,0 +1,131 @@
1
+ // Lucide Icons - inlined SVG strings
2
+ // Each icon is a function returning an SVG string with configurable size and color.
3
+
4
+ const iconPaths: Record<string, string> = {
5
+ // Navigation
6
+ anchor:
7
+ '<path d="M12 22V8"/><path d="M5 12H2a10 10 0 0 0 20 0h-3"/><circle cx="12" cy="5" r="3"/>',
8
+ "layout-dashboard":
9
+ '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/>',
10
+ users:
11
+ '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
12
+ "git-branch":
13
+ '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
14
+ clock:
15
+ '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
16
+ activity:
17
+ '<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/>',
18
+ "bar-chart-3":
19
+ '<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M7 16h8"/><path d="M7 11h12"/><path d="M7 6h3"/>',
20
+ server:
21
+ '<rect width="20" height="8" x="2" y="2" rx="2" ry="2"/><rect width="20" height="8" x="2" y="14" rx="2" ry="2"/><line x1="6" x2="6.01" y1="6" y2="6"/><line x1="6" x2="6.01" y1="18" y2="18"/>',
22
+ lightbulb:
23
+ '<path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/>',
24
+ "users-round":
25
+ '<path d="M18 21a8 8 0 0 0-16 0"/><circle cx="10" cy="8" r="5"/><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"/>',
26
+
27
+ // Status
28
+ "circle-check": '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
29
+ "circle-x":
30
+ '<circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/>',
31
+ "circle-alert":
32
+ '<circle cx="12" cy="12" r="10"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/>',
33
+ "circle-pause":
34
+ '<circle cx="12" cy="12" r="10"/><line x1="10" x2="10" y1="15" y2="9"/><line x1="14" x2="14" y1="15" y2="9"/>',
35
+ "circle-play":
36
+ '<circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/>',
37
+ loader:
38
+ '<path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/>',
39
+ signal:
40
+ '<path d="M2 20h.01"/><path d="M7 20v-4"/><path d="M12 20v-8"/><path d="M17 20V8"/><path d="M22 4v16"/>',
41
+
42
+ // Actions
43
+ play: '<polygon points="6 3 20 12 6 21 6 3"/>',
44
+ pause:
45
+ '<rect width="4" height="16" x="6" y="4"/><rect width="4" height="16" x="14" y="4"/>',
46
+ square: '<rect width="18" height="18" x="3" y="3" rx="2"/>',
47
+ send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
48
+ "message-square":
49
+ '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
50
+ "trash-2":
51
+ '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
52
+ plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
53
+ x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
54
+ copy: '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>',
55
+ "external-link":
56
+ '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
57
+ "chevron-down": '<path d="m6 9 6 6 6-6"/>',
58
+ "chevron-right": '<path d="m9 18 6-6-6-6"/>',
59
+ "chevron-left": '<path d="m15 18-6-6 6-6"/>',
60
+
61
+ // Data
62
+ cpu: '<rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/>',
63
+ "memory-stick":
64
+ '<path d="M6 19v-3"/><path d="M10 19v-3"/><path d="M14 19v-3"/><path d="M18 19v-3"/><path d="M8 11V9"/><path d="M16 11V9"/><path d="M12 11V9"/><path d="M2 15h20"/><path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V15H2v-3.063a2 2 0 0 0 0-3.837Z"/>',
65
+ "dollar-sign":
66
+ '<line x1="12" x2="12" y1="2" y2="22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
67
+ zap: '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
68
+ gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
69
+ timer:
70
+ '<line x1="10" x2="14" y1="2" y2="2"/><line x1="12" x2="15" y1="14" y2="11"/><circle cx="12" cy="14" r="8"/>',
71
+ "trending-up":
72
+ '<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>',
73
+ "trending-down":
74
+ '<polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/>',
75
+
76
+ // Fleet
77
+ ship: '<path d="M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1 .6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"/><path d="M19.38 20A11.6 11.6 0 0 0 21 14l-9-4-9 4c0 2.9.94 5.34 2.81 7.76"/><path d="M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6"/><path d="M12 10v4"/><path d="M12 2v3"/>',
78
+ radar:
79
+ '<path d="M19.07 4.93A10 10 0 0 0 6.99 3.34"/><path d="M4 6h.01"/><path d="M2.29 9.62A10 10 0 1 0 21.31 8.35"/><path d="M16.24 7.76A6 6 0 1 0 8.23 16.67"/><path d="M12 18h.01"/><path d="M17.99 11.66A6 6 0 0 1 15.77 16.67"/><circle cx="12" cy="12" r="2"/><path d="m13.41 10.59 5.66-5.66"/>',
80
+ map: '<path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"/><path d="M15 5.764v15"/><path d="M9 3.236v15"/>',
81
+ globe:
82
+ '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
83
+ terminal:
84
+ '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
85
+ "file-diff":
86
+ '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/>',
87
+ "git-pull-request":
88
+ '<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><line x1="6" x2="6" y1="9" y2="21"/>',
89
+ "shield-alert":
90
+ '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
91
+ "octagon-alert":
92
+ '<path d="M12 16h.01"/><path d="M12 8v4"/><path d="M15.312 2a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586l-4.688-4.688A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2z"/>',
93
+ "refresh-cw":
94
+ '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
95
+ eye: '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>',
96
+ brain:
97
+ '<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/>',
98
+ "shield-check":
99
+ '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/>',
100
+ webhook:
101
+ '<path d="M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"/><path d="m6 17 3.13-5.78c.53-.97.43-2.22-.26-3.07a4 4 0 0 1 6.72-4.07"/><path d="m11.44 11.79.87-.47c.98-.53 2.22-.43 3.07.26a4 4 0 0 1 2.72 6.93"/>',
102
+ "clipboard-list":
103
+ '<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/>',
104
+ "check-circle":
105
+ '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/>',
106
+ "user-check":
107
+ '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><polyline points="16 11 18 13 22 9"/>',
108
+ "mail-plus":
109
+ '<path d="M22 13V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h8"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/><path d="M19 16v6"/><path d="M16 19h6"/>',
110
+ "scroll-text":
111
+ '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2"/>',
112
+ settings:
113
+ '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
114
+ "log-out":
115
+ '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/>',
116
+ };
117
+
118
+ export type IconName = keyof typeof iconPaths;
119
+
120
+ export function icon(
121
+ name: IconName,
122
+ size: number = 16,
123
+ color?: string,
124
+ ): string {
125
+ const paths = iconPaths[name];
126
+ if (!paths) return "";
127
+ const c = color ? ` color="${color}"` : "";
128
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${c}>${paths}</svg>`;
129
+ }
130
+
131
+ export const iconNames = Object.keys(iconPaths) as IconName[];
@@ -0,0 +1,160 @@
1
+ // Design System Tokens
2
+ // Single source of truth for colors, typography, spacing, shadows, animations, and z-index.
3
+ // CSS custom properties mirror these values; Canvas2D code reads from here directly.
4
+
5
+ export const colors = {
6
+ bg: {
7
+ abyss: "#060a14",
8
+ deep: "#0a1628",
9
+ ocean: "#0d1f3c",
10
+ surface: "#132d56",
11
+ foam: "#1a3a6a",
12
+ },
13
+ accent: {
14
+ cyan: "#00d4ff",
15
+ cyanGlow: "rgba(0, 212, 255, 0.15)",
16
+ cyanDim: "rgba(0, 212, 255, 0.4)",
17
+ purple: "#7c3aed",
18
+ purpleGlow: "rgba(124, 58, 237, 0.15)",
19
+ blue: "#0066ff",
20
+ },
21
+ semantic: {
22
+ success: "#4ade80",
23
+ warning: "#fbbf24",
24
+ error: "#f43f5e",
25
+ },
26
+ text: {
27
+ primary: "#e8ecf4",
28
+ secondary: "#8899b8",
29
+ muted: "#5a6d8a",
30
+ },
31
+ } as const;
32
+
33
+ export const fonts = {
34
+ display: "'Instrument Serif', Georgia, serif",
35
+ body: "'Plus Jakarta Sans', system-ui, sans-serif",
36
+ mono: "'JetBrains Mono', 'SF Mono', monospace",
37
+ } as const;
38
+
39
+ export const typeScale = {
40
+ display: { size: 32, weight: 400, family: fonts.display },
41
+ heading: { size: 24, weight: 400, family: fonts.display },
42
+ title: { size: 20, weight: 600, family: fonts.body },
43
+ body: { size: 14, weight: 400, family: fonts.body },
44
+ caption: { size: 12, weight: 500, family: fonts.body },
45
+ tiny: { size: 11, weight: 400, family: fonts.body },
46
+ mono: { size: 13, weight: 400, family: fonts.mono },
47
+ monoSm: { size: 11, weight: 400, family: fonts.mono },
48
+ } as const;
49
+
50
+ export const spacing: Record<number, number> = {
51
+ 0: 0,
52
+ 1: 4,
53
+ 2: 8,
54
+ 3: 12,
55
+ 4: 16,
56
+ 5: 20,
57
+ 6: 24,
58
+ 8: 32,
59
+ 10: 40,
60
+ 12: 48,
61
+ 16: 64,
62
+ };
63
+
64
+ export const radius = {
65
+ sm: 4,
66
+ md: 8,
67
+ lg: 12,
68
+ xl: 16,
69
+ full: 9999,
70
+ } as const;
71
+
72
+ export const shadows = {
73
+ glow: {
74
+ cyan: "0 0 20px rgba(0, 212, 255, 0.15)",
75
+ purple: "0 0 20px rgba(124, 58, 237, 0.15)",
76
+ success: "0 0 12px rgba(74, 222, 128, 0.2)",
77
+ error: "0 0 12px rgba(244, 63, 94, 0.2)",
78
+ },
79
+ elevated: "0 8px 32px rgba(0, 0, 0, 0.4)",
80
+ } as const;
81
+
82
+ export const duration = {
83
+ fast: 150,
84
+ base: 300,
85
+ slow: 500,
86
+ glacial: 1000,
87
+ } as const;
88
+
89
+ export const easing = {
90
+ default: "ease",
91
+ smooth: "cubic-bezier(0.4, 0, 0.2, 1)",
92
+ spring: "cubic-bezier(0.34, 1.56, 0.64, 1)",
93
+ } as const;
94
+
95
+ export const zIndex = {
96
+ base: 1,
97
+ dropdown: 10,
98
+ sticky: 20,
99
+ overlay: 30,
100
+ modal: 40,
101
+ toast: 50,
102
+ } as const;
103
+
104
+ export const STAGES = [
105
+ "intake",
106
+ "plan",
107
+ "design",
108
+ "build",
109
+ "test",
110
+ "review",
111
+ "compound_quality",
112
+ "pr",
113
+ "merge",
114
+ "deploy",
115
+ "monitor",
116
+ ] as const;
117
+
118
+ export type StageName = (typeof STAGES)[number];
119
+
120
+ export const STAGE_SHORT: Record<StageName, string> = {
121
+ intake: "INT",
122
+ plan: "PLN",
123
+ design: "DSN",
124
+ build: "BLD",
125
+ test: "TST",
126
+ review: "REV",
127
+ compound_quality: "QA",
128
+ pr: "PR",
129
+ merge: "MRG",
130
+ deploy: "DPL",
131
+ monitor: "MON",
132
+ };
133
+
134
+ export const STAGE_COLORS: string[] = [
135
+ "c-cyan",
136
+ "c-blue",
137
+ "c-purple",
138
+ "c-green",
139
+ "c-amber",
140
+ "c-cyan",
141
+ "c-blue",
142
+ "c-purple",
143
+ "c-green",
144
+ "c-amber",
145
+ "c-cyan",
146
+ ];
147
+
148
+ export const STAGE_HEX: Record<StageName, string> = {
149
+ intake: "#00d4ff",
150
+ plan: "#0066ff",
151
+ design: "#7c3aed",
152
+ build: "#4ade80",
153
+ test: "#fbbf24",
154
+ review: "#00d4ff",
155
+ compound_quality: "#0066ff",
156
+ pr: "#7c3aed",
157
+ merge: "#4ade80",
158
+ deploy: "#fbbf24",
159
+ monitor: "#00d4ff",
160
+ };