@walwal-harness/cli 7.1.40 → 7.1.42

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 (84) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
  11. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  12. package/apps/harness-dashboard/app/brick.css +482 -0
  13. package/apps/harness-dashboard/app/layout.tsx +13 -0
  14. package/apps/harness-dashboard/app/page.tsx +2 -2
  15. package/apps/harness-dashboard/components/Scene.tsx +690 -36
  16. package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
  17. package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
  18. package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
  19. package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
  20. package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
  21. package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
  22. package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
  23. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  24. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  25. package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
  26. package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
  27. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  28. package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
  29. package/apps/harness-dashboard/lib/types.ts +22 -222
  30. package/apps/harness-dashboard/playwright.config.ts +26 -7
  31. package/assets/templates/AGENTS-ko.md.template +1 -1
  32. package/assets/templates/AGENTS.md.template +12 -2
  33. package/assets/templates/HARNESS.md +2 -0
  34. package/assets/templates/company-pipeline-manifest.json +5 -14
  35. package/assets/templates/worker-report.md.template +38 -0
  36. package/bin/init.js +2 -1
  37. package/commands/goal.md +4 -2
  38. package/commands/hot-fix.md +1 -1
  39. package/commands/submission.md +1 -1
  40. package/package.json +1 -1
  41. package/scripts/conductor-tick.sh +12 -0
  42. package/scripts/harness-agenda.sh +105 -0
  43. package/scripts/harness-company-block.sh +89 -0
  44. package/scripts/harness-company-complete.sh +17 -8
  45. package/scripts/harness-company-cycle.sh +54 -0
  46. package/scripts/harness-progress-set.sh +8 -2
  47. package/scripts/harness-stop.sh +76 -0
  48. package/scripts/harness-wake-control.sh +151 -0
  49. package/scripts/harness-wake.sh +5 -0
  50. package/scripts/harness-worker-dispatch.sh +31 -7
  51. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  52. package/apps/harness-dashboard/components/Header.tsx +0 -40
  53. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  54. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  55. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  56. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  57. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  58. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  59. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  60. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  61. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  62. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  63. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  64. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  65. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  66. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  67. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  68. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  69. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  70. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  71. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  72. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  73. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  74. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  75. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  76. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  77. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  78. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  79. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  80. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  81. package/apps/harness-dashboard/lib/iso.ts +0 -150
  82. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  83. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  84. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -0,0 +1,423 @@
1
+ /* =============================================================
2
+ walwal-harness · BRICK OFFICE real-data adapter
3
+ Maps a real HarnessSnapshot onto the design's Contract shape.
4
+
5
+ Honesty over fake values: where the real feed has no granular
6
+ source (token/min, cost, cpu, per-task %, per-worker %), the
7
+ field is `null` and the UI renders "—". Nothing is fabricated.
8
+
9
+ This module is pure (no React, no DOM).
10
+ ============================================================= */
11
+
12
+ import type {
13
+ HarnessSnapshot,
14
+ WorkerDocEntry,
15
+ MissionDoc,
16
+ CxxTodo,
17
+ HarnessEvent,
18
+ ActivitySample,
19
+ OwnerPromptEntry,
20
+ } from "../types";
21
+ import {
22
+ AGENT_DEFS,
23
+ AGENT_ROLES,
24
+ type AgentRole,
25
+ type AgentStatus,
26
+ type ContractState,
27
+ type ContractAgent,
28
+ type ContractWorker,
29
+ type ContractTask,
30
+ type ContractTasks,
31
+ type ContractEvent,
32
+ type ContractAlert,
33
+ type ContractMetrics,
34
+ } from "./contract";
35
+
36
+ /* ---- constants mirrored from Scene.tsx bucketing ---------------------- */
37
+ const BUCKET_MS = 10 * 60_000; // 10-minute heat buckets (matches Scene heatmap)
38
+ const SPARK_BUCKETS = 24; // 24 buckets of recent activity -> the bar sparkline
39
+ const HOUR_MS = 60 * 60 * 1000;
40
+ const CADENCE_BUCKETS = 24; // 24 hourly buckets of owner prompts
41
+ const STALE_MS = 120_000; // running but no visible movement > 120s -> stale
42
+ const THROUGHPUT_WINDOW_MS = 60_000; // events in the last 60s
43
+
44
+ const ROLE_SET = new Set<string>(AGENT_ROLES);
45
+
46
+ /* ---- helpers ---------------------------------------------------------- */
47
+
48
+ /** Strip the `harness-` prefix and lowercase, returning a role or null. */
49
+ function toRole(value: string | null | undefined): AgentRole | null {
50
+ const r = (value ?? "").toLowerCase().replace(/^harness-/, "").trim();
51
+ return ROLE_SET.has(r) ? (r as AgentRole) : null;
52
+ }
53
+
54
+ /** Parse an ISO timestamp to epoch ms, or null when unparseable. */
55
+ function parseTs(ts: string | null | undefined): number | null {
56
+ if (!ts) return null;
57
+ const t = Date.parse(ts);
58
+ return Number.isFinite(t) ? t : null;
59
+ }
60
+
61
+ /** Strip leading YAML frontmatter from a markdown string. */
62
+ export function stripFrontmatter(md: string | null | undefined): string {
63
+ if (!md) return "";
64
+ return md.replace(/^---[\s\S]*?---\n+/, "");
65
+ }
66
+
67
+ /** The active mission (active flag), else the first mission, else null. */
68
+ function pickActiveMission(snapshot: HarnessSnapshot): MissionDoc | null {
69
+ return snapshot.missions.find((m) => m.active) ?? snapshot.missions[0] ?? null;
70
+ }
71
+
72
+ /** Read a role's report markdown off a mission doc. */
73
+ function missionReportFor(
74
+ mission: MissionDoc | null,
75
+ role: AgentRole
76
+ ): string | undefined {
77
+ if (!mission) return undefined;
78
+ const raw = mission[role];
79
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
80
+ }
81
+
82
+ /**
83
+ * Build a per-role spark array: SPARK_BUCKETS buckets of activitySamples,
84
+ * newest bucket last (mirrors Scene.tsx heat bucketing — x = nowMinute -
85
+ * sampleMinute, keep the strongest count seen in each bucket).
86
+ */
87
+ function buildSpark(samples: ActivitySample[], role: AgentRole, nowMs: number): number[] {
88
+ const nowBucket = Math.floor(nowMs / BUCKET_MS);
89
+ const buckets = new Array<number>(SPARK_BUCKETS).fill(0);
90
+ for (const sample of samples) {
91
+ // A role owns its own lane (id === role) and all of its worker lanes
92
+ // (id === `${role}:...`). Both contribute to the role's activity spark.
93
+ const lane = sample.laneId;
94
+ if (lane !== role && !lane.startsWith(role + ":")) continue;
95
+ const t = parseTs(sample.ts);
96
+ if (t == null) continue;
97
+ const x = nowBucket - Math.floor(t / BUCKET_MS);
98
+ if (x < 0 || x >= SPARK_BUCKETS) continue;
99
+ // index SPARK_BUCKETS-1 = now (newest), index 0 = oldest visible bucket
100
+ const idx = SPARK_BUCKETS - 1 - x;
101
+ buckets[idx] = Math.max(buckets[idx], sample.count);
102
+ }
103
+ return buckets;
104
+ }
105
+
106
+ /** Newest activity epoch across a role's lanes + its workers + events. */
107
+ function heartbeatFor(
108
+ snapshot: HarnessSnapshot,
109
+ role: AgentRole,
110
+ workers: WorkerDocEntry[],
111
+ eventEpochByRole: Map<AgentRole, number>
112
+ ): number | null {
113
+ let hb: number | null = null;
114
+ const bump = (t: number | null) => {
115
+ if (t != null && (hb == null || t > hb)) hb = t;
116
+ };
117
+ for (const sample of snapshot.activitySamples) {
118
+ const lane = sample.laneId;
119
+ if (lane === role || lane.startsWith(role + ":")) bump(parseTs(sample.ts));
120
+ }
121
+ for (const w of workers) bump(parseTs(w.updatedAt));
122
+ bump(eventEpochByRole.get(role) ?? null);
123
+ return hb;
124
+ }
125
+
126
+ /** Determine if the company loop is active for status/loop derivation. */
127
+ function loopIsActive(snapshot: HarnessSnapshot): boolean {
128
+ const rt = snapshot.runtime;
129
+ const agentStatus = (rt.agentStatus ?? "").toLowerCase();
130
+ const conductor = (rt.conductorState ?? "").toLowerCase();
131
+ const agentDone = agentStatus === "completed" || agentStatus === "complete";
132
+ const conductorDone =
133
+ conductor === "completed" || conductor === "complete" || conductor === "blocked";
134
+ return !agentDone && !conductorDone;
135
+ }
136
+
137
+ /* ---- the adapter ------------------------------------------------------ */
138
+
139
+ /**
140
+ * Map a real HarnessSnapshot onto the design Contract shape.
141
+ *
142
+ * @param snapshot the real harness feed snapshot
143
+ * @param connected whether the SSE stream is currently open
144
+ * @param nowMs the shell's ~1/s clock value (epoch ms)
145
+ */
146
+ export function toContractState(
147
+ snapshot: HarnessSnapshot,
148
+ connected: boolean,
149
+ nowMs: number
150
+ ): ContractState {
151
+ const rt = snapshot.runtime;
152
+ const currentRole = toRole(rt.currentAgent);
153
+ const loopActive = loopIsActive(snapshot);
154
+ const activeMission = pickActiveMission(snapshot);
155
+
156
+ // Workers from the active mission(s) — index by role.
157
+ const activeMissions = snapshot.missions.filter((m) => m.active);
158
+ const missionsForWorkers = activeMissions.length > 0 ? activeMissions : snapshot.missions;
159
+ const workerEntries: Array<{ worker: WorkerDocEntry; mission: MissionDoc }> = [];
160
+ const workersByRole = new Map<AgentRole, WorkerDocEntry[]>();
161
+ for (const role of AGENT_ROLES) workersByRole.set(role, []);
162
+ for (const mission of missionsForWorkers) {
163
+ for (const worker of mission.workers) {
164
+ const role = toRole(worker.owner);
165
+ if (!role) continue;
166
+ workerEntries.push({ worker, mission });
167
+ workersByRole.get(role)!.push(worker);
168
+ }
169
+ }
170
+
171
+ // Events newest-first; capture per-role newest epoch for heartbeats.
172
+ const eventEpochByRole = new Map<AgentRole, number>();
173
+ const firstLaneRole = (): AgentRole | null => {
174
+ const s = snapshot.activitySamples[snapshot.activitySamples.length - 1];
175
+ return s ? toRole(s.laneId.split(":")[0]) : null;
176
+ };
177
+ for (const ev of snapshot.events) {
178
+ const role = toRole(ev.owner);
179
+ const t = parseTs(ev.ts);
180
+ if (role && t != null) {
181
+ const prev = eventEpochByRole.get(role);
182
+ if (prev == null || t > prev) eventEpochByRole.set(role, t);
183
+ }
184
+ }
185
+
186
+ // ---- agents --------------------------------------------------------
187
+ const hasRecentSample = (role: AgentRole): boolean =>
188
+ snapshot.activitySamples.some(
189
+ (s) => (s.laneId === role || s.laneId.startsWith(role + ":")) && s.count > 0
190
+ );
191
+ const runtimeBlocked =
192
+ (rt.agentStatus ?? "").toLowerCase() === "blocked" ||
193
+ (rt.conductorState ?? "").toLowerCase() === "blocked";
194
+
195
+ const agents: ContractAgent[] = AGENT_DEFS.map((def) => {
196
+ const role = def.id;
197
+ const isCurrent = role === currentRole;
198
+ const roleWorkers = workersByRole.get(role) ?? [];
199
+ const openTodos = snapshot.todos.filter(
200
+ (t) => toRole(t.owner) === role && !isTerminalTodo(t)
201
+ ).length;
202
+ const liveWorkers = roleWorkers.filter(
203
+ (w) => w.active || w.status === "IN_PROGRESS"
204
+ ).length;
205
+
206
+ let status: AgentStatus;
207
+ if (isCurrent && loopActive) status = "live";
208
+ else if (hasRecentSample(role)) status = "active";
209
+ else if (runtimeBlocked && isCurrent) status = "alert";
210
+ else status = "idle";
211
+
212
+ const loop = (isCurrent && loopActive) || openTodos > 0 ? "running" : "idle";
213
+ const work = isCurrent ? rt.currentAction ?? "—" : "—";
214
+
215
+ return {
216
+ id: role,
217
+ name: def.name,
218
+ role: def.role,
219
+ hue: def.hue,
220
+ status,
221
+ loop,
222
+ work,
223
+ todos: openTodos,
224
+ workers: liveWorkers,
225
+ spark: buildSpark(snapshot.activitySamples, role, nowMs),
226
+ heartbeat: heartbeatFor(snapshot, role, roleWorkers, eventEpochByRole),
227
+ report: missionReportFor(activeMission, role),
228
+ };
229
+ });
230
+
231
+ // ---- workers -------------------------------------------------------
232
+ const workers: ContractWorker[] = workerEntries.map(({ worker }) => {
233
+ const role = toRole(worker.owner)!;
234
+ const status: AgentStatus =
235
+ worker.status === "IN_PROGRESS" ? "active" : "idle";
236
+ return {
237
+ id: `${worker.owner}:${worker.name}`,
238
+ agent: role,
239
+ name: worker.displayName,
240
+ status,
241
+ started: parseTs(worker.updatedAt),
242
+ progress: worker.status === "COMPLETE" ? 100 : null,
243
+ report: stripFrontmatter(worker.content) || undefined,
244
+ };
245
+ });
246
+
247
+ // ---- tasks ---------------------------------------------------------
248
+ const tasks = buildTasks(snapshot.todos);
249
+
250
+ // ---- events --------------------------------------------------------
251
+ const events: ContractEvent[] = snapshot.events
252
+ .slice(-60)
253
+ .map((ev, i): ContractEvent => {
254
+ const role = toRole(ev.owner) ?? firstLaneRole() ?? "";
255
+ return {
256
+ id: eventId(ev, i),
257
+ at: parseTs(ev.ts),
258
+ agent: role,
259
+ worker: "",
260
+ verb: ev.type || ev.status || "",
261
+ obj: ev.title || ev.summary || ev.command || "",
262
+ };
263
+ });
264
+
265
+ // ---- cadence -------------------------------------------------------
266
+ const cadence = buildCadence(snapshot.ownerHistory, nowMs);
267
+
268
+ // ---- alerts (synthesized) -----------------------------------------
269
+ const alerts = buildAlerts(snapshot, agents, currentRole, runtimeBlocked, nowMs);
270
+
271
+ // ---- metrics -------------------------------------------------------
272
+ const throughput = snapshot.events.filter((ev) => {
273
+ const t = parseTs(ev.ts);
274
+ return t != null && nowMs - t <= THROUGHPUT_WINDOW_MS;
275
+ }).length;
276
+
277
+ const metrics: ContractMetrics = {
278
+ sse: connected ? "open" : "closed",
279
+ tokensPerMin: null,
280
+ costToday: null,
281
+ throughput,
282
+ cpu: null,
283
+ hotfix: snapshot.missions.filter((m) => m.type === "hotfix").length,
284
+ eventsTotal: snapshot.events.length,
285
+ };
286
+
287
+ return {
288
+ now: nowMs,
289
+ agents,
290
+ workers,
291
+ tasks,
292
+ events,
293
+ cadence,
294
+ alerts,
295
+ metrics,
296
+ mtrend: [],
297
+ ctrend: [],
298
+ };
299
+ }
300
+
301
+ /* ---- task building ---------------------------------------------------- */
302
+
303
+ function isTerminalTodo(t: CxxTodo): boolean {
304
+ const s = (t.status ?? "").toLowerCase();
305
+ return s === "done" || s === "completed";
306
+ }
307
+
308
+ function buildTasks(todos: CxxTodo[]): ContractTasks {
309
+ const running: ContractTask[] = [];
310
+ const queued: ContractTask[] = [];
311
+ const done: ContractTask[] = [];
312
+
313
+ for (const t of todos) {
314
+ const role = ((t.owner ?? "").toLowerCase().replace(/^harness-/, "")) as AgentRole;
315
+ const status = (t.status ?? "").toLowerCase();
316
+ const at = parseTs(t.updatedAt);
317
+ const base: ContractTask = {
318
+ id: t.id,
319
+ owner: (t.owner ?? "").toUpperCase(),
320
+ title: t.title,
321
+ agent: role,
322
+ status: "queued",
323
+ progress: null,
324
+ started: at,
325
+ at,
326
+ };
327
+ if (status === "active") {
328
+ running.push({ ...base, status: "running" });
329
+ } else if (status === "pending" || status === "paused") {
330
+ queued.push({ ...base, status: "queued" });
331
+ } else if (status === "done" || status === "completed") {
332
+ done.push({ ...base, status: "done" });
333
+ }
334
+ }
335
+
336
+ // done: newest first, cap ~14
337
+ done.sort((a, b) => (b.at ?? 0) - (a.at ?? 0));
338
+ return { running, queued, done: done.slice(0, 14) };
339
+ }
340
+
341
+ /* ---- cadence building (mirrors Scene.tsx CadenceStrip) ---------------- */
342
+
343
+ function buildCadence(ownerHistory: OwnerPromptEntry[], nowMs: number): number[] {
344
+ const buckets = new Array<number>(CADENCE_BUCKETS).fill(0);
345
+ for (const entry of ownerHistory) {
346
+ const t = parseTs(entry.ts);
347
+ if (t == null) continue;
348
+ const hoursAgo = Math.floor((nowMs - t) / HOUR_MS);
349
+ if (hoursAgo < 0 || hoursAgo >= CADENCE_BUCKETS) continue;
350
+ buckets[hoursAgo] += 1;
351
+ }
352
+ return buckets;
353
+ }
354
+
355
+ /* ---- alert synthesis -------------------------------------------------- */
356
+
357
+ function buildAlerts(
358
+ snapshot: HarnessSnapshot,
359
+ agents: ContractAgent[],
360
+ currentRole: AgentRole | null,
361
+ runtimeBlocked: boolean,
362
+ nowMs: number
363
+ ): ContractAlert[] {
364
+ const alerts: ContractAlert[] = [];
365
+ const rt = snapshot.runtime;
366
+ let seq = 0;
367
+ const nextId = (k: string) => `alert-${k}-${seq++}`;
368
+
369
+ // runtime blocked OR ownerPrompt awaiting-authority -> attention
370
+ const awaiting = (rt.ownerPrompt?.status ?? "").toLowerCase() === "awaiting-authority";
371
+ if (runtimeBlocked || awaiting) {
372
+ const agent = currentRole ?? "ceo";
373
+ const msg =
374
+ rt.currentAction ?? rt.ownerPrompt?.summary ?? (runtimeBlocked ? "runtime blocked" : "awaiting authority");
375
+ alerts.push({
376
+ id: nextId("rt"),
377
+ level: "attention",
378
+ agent,
379
+ msg,
380
+ at: parseTs(rt.updatedAt) ?? nowMs,
381
+ });
382
+ }
383
+
384
+ // running-but-stale heartbeat -> stale
385
+ for (const a of agents) {
386
+ if (a.loop !== "running") continue;
387
+ if (a.heartbeat == null) continue;
388
+ if (nowMs - a.heartbeat > STALE_MS) {
389
+ alerts.push({
390
+ id: nextId("stale-" + a.id),
391
+ level: "stale",
392
+ agent: a.id,
393
+ msg: "running, but no recent visible movement",
394
+ at: a.heartbeat,
395
+ });
396
+ }
397
+ }
398
+
399
+ // protocolViolations -> attention
400
+ for (const mission of snapshot.missions) {
401
+ for (const v of mission.protocolViolations) {
402
+ const agent = currentRole ?? "ceo";
403
+ alerts.push({
404
+ id: nextId("pv"),
405
+ level: "attention",
406
+ agent,
407
+ msg: v,
408
+ at: parseTs(mission.ts) ?? nowMs,
409
+ });
410
+ }
411
+ }
412
+
413
+ return alerts;
414
+ }
415
+
416
+ /* ---- misc ------------------------------------------------------------- */
417
+
418
+ function eventId(ev: HarnessEvent, i: number): string {
419
+ const t = ev.ts ?? "";
420
+ const owner = ev.owner ?? "";
421
+ const kind = ev.type ?? ev.status ?? "";
422
+ return `ev-${i}-${t}-${owner}-${kind}`;
423
+ }
@@ -0,0 +1,247 @@
1
+ /* =============================================================
2
+ walwal-harness · BRICK OFFICE Data Contract
3
+ The single state shape every view renders from. Ported from the
4
+ design-handoff simulation (harness/sim.jsx, harness/ui.jsx) — but
5
+ here it is produced from REAL harness data by lib/brick/adapter.ts.
6
+
7
+ This module is pure (no React, no DOM). It exports:
8
+ - the Contract TypeScript interfaces
9
+ - AGENT_DEFS (the 6 CXX roles with id/name/role/hue)
10
+ - fmt utils (time, hm, ago, k, money)
11
+ - statusColor + STATUS_COLOR
12
+ ============================================================= */
13
+
14
+ /** The role ids of the 6 exec agents, in canonical display order. */
15
+ export type AgentRole = "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops";
16
+
17
+ /**
18
+ * Status enum shared by agents and workers. The simulation used the full set
19
+ * (idle | typing | talking | active | alert | live); the real adapter only
20
+ * emits the subset {live, active, alert, idle}, but the renderer/statusColor
21
+ * still understands every value so the design's legend stays faithful.
22
+ */
23
+ export type AgentStatus =
24
+ | "idle"
25
+ | "typing"
26
+ | "talking"
27
+ | "active"
28
+ | "alert"
29
+ | "live";
30
+
31
+ /** The agent's autonomous loop state. */
32
+ export type LoopState = "running" | "idle";
33
+
34
+ /** Task lifecycle, as rendered by the task panel. */
35
+ export type TaskStatus = "running" | "queued" | "done";
36
+
37
+ /** Attention-queue severity. */
38
+ export type AlertLevel = "stale" | "attention";
39
+
40
+ /** A reference to a document the DocViewer can render. */
41
+ export type DocTarget =
42
+ | { type: "agent"; agent: AgentRole }
43
+ | { type: "worker"; agent: AgentRole; worker: ContractWorker };
44
+
45
+ /** Static, design-fixed definition of one exec agent. */
46
+ export interface AgentDef {
47
+ id: AgentRole;
48
+ name: string;
49
+ role: string;
50
+ hue: string;
51
+ }
52
+
53
+ /** One exec agent row in the live state. */
54
+ export interface ContractAgent {
55
+ id: AgentRole;
56
+ name: string;
57
+ role: string;
58
+ hue: string;
59
+ status: AgentStatus;
60
+ loop: LoopState;
61
+ /** One-line "current work" string. "—" when there is no live work string. */
62
+ work: string;
63
+ /** Open todo count. */
64
+ todos: number;
65
+ /** Count of live workers (derived from `workers` below). */
66
+ workers: number;
67
+ /** ~24 ints, recent activity, drives the bar sparkline. */
68
+ spark: number[];
69
+ /** epoch ms of last visible movement -> drives "stale"; null when unknown. */
70
+ heartbeat: number | null;
71
+ /** Full report markdown for this agent (mission[role]); undefined when none. */
72
+ report?: string;
73
+ }
74
+
75
+ /** One spawned worker across all agents. */
76
+ export interface ContractWorker {
77
+ id: string;
78
+ agent: AgentRole;
79
+ name: string;
80
+ status: AgentStatus;
81
+ /** epoch ms; null when unknown. */
82
+ started: number | null;
83
+ /** 0..100, or null when there is no real granular progress source. */
84
+ progress: number | null;
85
+ /** Full worker report/brief markdown (worker.content); undefined when none. */
86
+ report?: string;
87
+ }
88
+
89
+ /** One task in the running/queued/done lanes. */
90
+ export interface ContractTask {
91
+ id: string;
92
+ /** Owner display label, e.g. "CTO". */
93
+ owner: string;
94
+ title: string;
95
+ agent: AgentRole;
96
+ status: TaskStatus;
97
+ /** 0..100, or null — there is no real granular progress source. */
98
+ progress: number | null;
99
+ /** epoch ms the task started, or null. */
100
+ started?: number | null;
101
+ /** epoch ms the task completed (done lane only), or null. */
102
+ at?: number | null;
103
+ }
104
+
105
+ /** The three task lanes. */
106
+ export interface ContractTasks {
107
+ running: ContractTask[];
108
+ queued: ContractTask[];
109
+ done: ContractTask[];
110
+ }
111
+
112
+ /** One activity-stream event. */
113
+ export interface ContractEvent {
114
+ id: string;
115
+ /** epoch ms; null when unknown. */
116
+ at: number | null;
117
+ agent: AgentRole | "";
118
+ worker: string;
119
+ /** The verb; "COMPLETE"/"complete" renders green. */
120
+ verb: string;
121
+ /** The object the verb acted on. */
122
+ obj: string;
123
+ }
124
+
125
+ /** One attention-queue alert. */
126
+ export interface ContractAlert {
127
+ id: string;
128
+ level: AlertLevel;
129
+ agent: AgentRole;
130
+ msg: string;
131
+ /** epoch ms; null when unknown. */
132
+ at: number | null;
133
+ }
134
+
135
+ /**
136
+ * System-health metrics. Values with no real telemetry source are `null`
137
+ * and MUST render as "—" (never a fabricated number).
138
+ */
139
+ export interface ContractMetrics {
140
+ /** "open" when the SSE stream is connected, else "closed". */
141
+ sse: "open" | "closed";
142
+ /** Rolling tokens/min from /api/metrics ring buffer; null until sourced. */
143
+ tokensPerMin: number | null;
144
+ /** USD cost; an ESTIMATE from /api/metrics (see costEstimated); null when unsourced. */
145
+ costToday: number | null;
146
+ /** True when costToday is a price-table estimate (not a billed figure). */
147
+ costEstimated?: boolean;
148
+ /** 0..100 context-window occupancy of the most-active session; null when unknown. */
149
+ contextPct?: number | null;
150
+ /** Count of events in the last 60s (derived). */
151
+ throughput: number | null;
152
+ /** Summed %cpu of discovered company PIDs; null when no telemetry. */
153
+ cpu: number | null;
154
+ /** Count of missions of type 'hotfix'. */
155
+ hotfix: number;
156
+ /** Total events in the snapshot. */
157
+ eventsTotal: number;
158
+ }
159
+
160
+ /** The single source-of-truth state every view renders from. */
161
+ export interface ContractState {
162
+ /** epoch ms, ticked ~1/s by the shell. */
163
+ now: number;
164
+ agents: ContractAgent[];
165
+ workers: ContractWorker[];
166
+ tasks: ContractTasks;
167
+ events: ContractEvent[];
168
+ /** 24 hourly buckets = owner prompts/hour -> heatmap. */
169
+ cadence: number[];
170
+ alerts: ContractAlert[];
171
+ metrics: ContractMetrics;
172
+ /** tokens/min sparkline samples; [] when no source. */
173
+ mtrend: number[];
174
+ /** cost sparkline samples; [] when no source. */
175
+ ctrend: number[];
176
+ }
177
+
178
+ /* ---- agent (exec) definitions -----------------------------------------
179
+ The 6 CXX roles in canonical order, with per-role accent hue.
180
+ Ported from sim.jsx AGENTS, with hues per the adapter spec. */
181
+ export const AGENT_DEFS: readonly AgentDef[] = [
182
+ { id: "ceo", name: "CEO", role: "Chief Executive", hue: "#4cc2ff" },
183
+ { id: "coo", name: "COO", role: "Operations", hue: "#3fd17a" },
184
+ { id: "cdo", name: "CDO", role: "Design", hue: "#8b95a4" },
185
+ { id: "cto", name: "CTO", role: "Technology", hue: "#f0a23b" },
186
+ { id: "cqo", name: "CQO", role: "Quality", hue: "#c084fc" },
187
+ { id: "ops", name: "OPS", role: "Runtime Watch", hue: "#f97e4d" },
188
+ ] as const;
189
+
190
+ /** The 6 role ids in canonical order. */
191
+ export const AGENT_ROLES: readonly AgentRole[] = AGENT_DEFS.map((a) => a.id);
192
+
193
+ /** Quick lookup by role id. */
194
+ export const AGENT_DEF_BY_ID: Record<AgentRole, AgentDef> = AGENT_DEFS.reduce(
195
+ (acc, def) => {
196
+ acc[def.id] = def;
197
+ return acc;
198
+ },
199
+ {} as Record<AgentRole, AgentDef>
200
+ );
201
+
202
+ /* ---- status colors (ported from ui.jsx) ------------------------------- */
203
+ export const STATUS_COLOR: Record<string, string> = {
204
+ live: "#3fd17a",
205
+ active: "#3fd17a",
206
+ typing: "#4cc2ff",
207
+ talking: "#c084fc",
208
+ idle: "#5a6472",
209
+ alert: "#f0506b",
210
+ running: "#3fd17a",
211
+ stale: "#f0a23b",
212
+ };
213
+
214
+ export const statusColor = (s: string): string => STATUS_COLOR[s] ?? "#5a6472";
215
+
216
+ /* ---- fmt utils (ported from sim.jsx) ----------------------------------
217
+ Pure formatting helpers. `time`/`hm` take a Date; `ago` takes a ms delta;
218
+ `k`/`money` take numbers (and tolerate null -> "—" for honest blanks). */
219
+ export const fmt = {
220
+ /** "HH:MM:SS" */
221
+ time: (d: Date): string => d.toTimeString().slice(0, 8),
222
+ /** "HH:MM" */
223
+ hm: (d: Date): string => d.toTimeString().slice(0, 5),
224
+ /** humanize a ms delta -> "Ns ago" / "Nm ago" / "Nh ago" */
225
+ ago: (ms: number): string => {
226
+ const s = Math.floor(ms / 1000);
227
+ if (s < 60) return s + "s ago";
228
+ const m = Math.floor(s / 60);
229
+ if (m < 60) return m + "m ago";
230
+ const h = Math.floor(m / 60);
231
+ return h + "h ago";
232
+ },
233
+ /** compact thousands: 11200 -> "11.2k"; null -> "—" */
234
+ k: (n: number | null | undefined): string => {
235
+ if (n == null || !Number.isFinite(n)) return "—";
236
+ return n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(Math.round(n));
237
+ },
238
+ /** "$42.18"; null -> "—" */
239
+ money: (n: number | null | undefined): string => {
240
+ if (n == null || !Number.isFinite(n)) return "—";
241
+ return "$" + n.toFixed(2);
242
+ },
243
+ };
244
+
245
+ /** Render a possibly-null number as the design's em-dash blank. */
246
+ export const dashIfNull = (n: number | null | undefined): string =>
247
+ n == null || !Number.isFinite(n) ? "—" : String(n);