@walwal-harness/cli 7.1.39 → 7.1.41

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 (74) 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 +20 -2
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +105 -0
  8. package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
  9. package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
  10. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  11. package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
  12. package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
  13. package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
  14. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +381 -0
  15. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  16. package/apps/harness-dashboard/components/Scene.tsx +568 -35
  17. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  18. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  19. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  20. package/apps/harness-dashboard/lib/types.ts +22 -222
  21. package/apps/harness-dashboard/playwright.config.ts +26 -7
  22. package/assets/templates/AGENTS-ko.md.template +1 -1
  23. package/assets/templates/AGENTS.md.template +12 -2
  24. package/assets/templates/HARNESS.md +2 -0
  25. package/assets/templates/company-pipeline-manifest.json +5 -14
  26. package/assets/templates/worker-report.md.template +38 -0
  27. package/bin/init.js +4 -3
  28. package/commands/goal.md +4 -2
  29. package/commands/hot-fix.md +1 -1
  30. package/commands/submission.md +1 -1
  31. package/package.json +1 -1
  32. package/scripts/conductor-tick.sh +12 -0
  33. package/scripts/harness-agenda.sh +105 -0
  34. package/scripts/harness-company-block.sh +89 -0
  35. package/scripts/harness-company-complete.sh +17 -8
  36. package/scripts/harness-company-cycle.sh +54 -0
  37. package/scripts/harness-progress-set.sh +8 -2
  38. package/scripts/harness-stop.sh +76 -0
  39. package/scripts/harness-wake-control.sh +151 -0
  40. package/scripts/harness-wake.sh +5 -0
  41. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  42. package/apps/harness-dashboard/components/Header.tsx +0 -40
  43. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  44. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  45. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  46. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  47. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  48. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  49. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  50. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  51. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  52. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  53. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  54. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  55. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  56. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  57. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  58. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  59. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  60. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  61. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  62. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  63. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  64. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  65. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  66. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  67. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  68. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  69. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  70. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  71. package/apps/harness-dashboard/lib/iso.ts +0 -150
  72. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  73. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  74. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -1,404 +0,0 @@
1
- "use client";
2
- import { Html } from "@react-three/drei";
3
- import { ROOM_RECTS, CORRIDOR_RECTS, type RoomRect } from "@/lib/iso";
4
- import { ROOM_LABELS } from "@/lib/agent-roster";
5
- import {
6
- deskSlotsForRoom,
7
- doorsForRoom,
8
- type DoorSpec,
9
- } from "@/lib/furniture";
10
- import type { RoomId, RoomState } from "@/lib/types";
11
-
12
- const ROOM_PALETTE: Record<string, { floor: string; wall: string; trim: string }> = {
13
- ceo: { floor: "#3a4a72", wall: "#4d5f8f", trim: "#88a2d4" },
14
- meeting: { floor: "#324063", wall: "#465782", trim: "#7e98c6" },
15
- coo: { floor: "#374566", wall: "#48598a", trim: "#83a0d3" },
16
- "cto-team": { floor: "#2a3a55", wall: "#3a4d72", trim: "#6a87b5" },
17
- "cqo-team": { floor: "#3a304a", wall: "#4d4068", trim: "#9476c0" },
18
- "service-ops": { floor: "#28394d", wall: "#385069", trim: "#6da4c2" },
19
- archive: { floor: "#2a2d3a", wall: "#3a3d4d", trim: "#7c8197" },
20
- };
21
-
22
- const FLOOR_THICKNESS = 0.2;
23
- const WALL_HEIGHT = 0.9;
24
- const WALL_THICKNESS = 0.12;
25
- const Y_BASE = 0;
26
-
27
- // Build the segments for one wall, removing the slice covered by each door.
28
- // Returns absolute world-space ranges along the wall axis (in absolute coords,
29
- // not local-to-room).
30
- function wallSegments(wallStart: number, wallEnd: number, doors: number[][]): Array<[number, number]> {
31
- const segments: Array<[number, number]> = [[wallStart, wallEnd]];
32
- for (const [doorStart, doorEnd] of doors) {
33
- const next: Array<[number, number]> = [];
34
- for (const [s, e] of segments) {
35
- if (doorEnd <= s || doorStart >= e) {
36
- next.push([s, e]);
37
- continue;
38
- }
39
- if (doorStart > s) next.push([s, doorStart]);
40
- if (doorEnd < e) next.push([doorEnd, e]);
41
- }
42
- segments.splice(0, segments.length, ...next);
43
- }
44
- return segments.filter(([s, e]) => e - s > 0.01);
45
- }
46
-
47
- function doorsBySide(rect: RoomRect, side: "north" | "south" | "east" | "west"): number[][] {
48
- // Returns gap intervals along the wall axis in absolute world coords.
49
- const out: number[][] = [];
50
- for (const d of doorsForRoom(rect)) {
51
- if (d.side !== side) continue;
52
- if (side === "north" || side === "south") {
53
- out.push([d.position[0] - d.width / 2, d.position[0] + d.width / 2]);
54
- } else {
55
- out.push([d.position[1] - d.width / 2, d.position[1] + d.width / 2]);
56
- }
57
- }
58
- return out;
59
- }
60
-
61
- interface RoomBlockProps {
62
- rect: RoomRect;
63
- room: RoomState | undefined;
64
- lang: "ko" | "en";
65
- onRoomClick?: (id: RoomId) => void;
66
- }
67
-
68
- function Wall({
69
- start,
70
- end,
71
- axis,
72
- fixedCoord,
73
- height,
74
- color,
75
- }: {
76
- start: number;
77
- end: number;
78
- axis: "x" | "z";
79
- fixedCoord: number;
80
- height: number;
81
- color: string;
82
- }) {
83
- const len = end - start;
84
- if (len <= 0.01) return null;
85
- const cx = axis === "x" ? (start + end) / 2 : fixedCoord;
86
- const cz = axis === "z" ? (start + end) / 2 : fixedCoord;
87
- const w = axis === "x" ? len : WALL_THICKNESS;
88
- const d = axis === "z" ? len : WALL_THICKNESS;
89
- return (
90
- <mesh
91
- castShadow
92
- receiveShadow
93
- position={[cx, FLOOR_THICKNESS + height / 2, cz]}
94
- >
95
- <boxGeometry args={[w, height, d]} />
96
- <meshStandardMaterial color={color} roughness={0.85} />
97
- </mesh>
98
- );
99
- }
100
-
101
- function Desk({
102
- position,
103
- facing,
104
- }: {
105
- position: [number, number, number];
106
- facing: number;
107
- }) {
108
- return (
109
- <group position={position} rotation={[0, facing, 0]}>
110
- {/* Desktop */}
111
- <mesh castShadow receiveShadow position={[0, 0.32, 0]}>
112
- <boxGeometry args={[0.7, 0.06, 0.4]} />
113
- <meshStandardMaterial color="#caa978" roughness={0.7} />
114
- </mesh>
115
- {/* Legs (front pair only — the back is hidden by the wall) */}
116
- <mesh castShadow position={[-0.3, 0.16, 0.16]}>
117
- <boxGeometry args={[0.06, 0.32, 0.06]} />
118
- <meshStandardMaterial color="#5a4a30" roughness={0.85} />
119
- </mesh>
120
- <mesh castShadow position={[0.3, 0.16, 0.16]}>
121
- <boxGeometry args={[0.06, 0.32, 0.06]} />
122
- <meshStandardMaterial color="#5a4a30" roughness={0.85} />
123
- </mesh>
124
- {/* Monitor */}
125
- <mesh castShadow position={[0, 0.5, -0.12]}>
126
- <boxGeometry args={[0.32, 0.22, 0.04]} />
127
- <meshStandardMaterial color="#1a1f2e" emissive="#22d3ee" emissiveIntensity={0.25} roughness={0.4} />
128
- </mesh>
129
- <mesh position={[0, 0.36, -0.12]}>
130
- <boxGeometry args={[0.06, 0.06, 0.04]} />
131
- <meshStandardMaterial color="#1a1f2e" />
132
- </mesh>
133
- </group>
134
- );
135
- }
136
-
137
- function Chair({ position, facing }: { position: [number, number, number]; facing: number }) {
138
- return (
139
- <group position={position} rotation={[0, facing, 0]}>
140
- <mesh castShadow position={[0, 0.18, 0]}>
141
- <boxGeometry args={[0.32, 0.06, 0.32]} />
142
- <meshStandardMaterial color="#2c3e63" roughness={0.7} />
143
- </mesh>
144
- <mesh castShadow position={[0, 0.34, 0.13]}>
145
- <boxGeometry args={[0.32, 0.32, 0.06]} />
146
- <meshStandardMaterial color="#2c3e63" roughness={0.7} />
147
- </mesh>
148
- <mesh position={[0, 0.06, 0]}>
149
- <cylinderGeometry args={[0.04, 0.04, 0.12, 8]} />
150
- <meshStandardMaterial color="#1a1f2e" />
151
- </mesh>
152
- </group>
153
- );
154
- }
155
-
156
- function RoomBlock({ rect, room, lang, onRoomClick }: RoomBlockProps) {
157
- const palette = ROOM_PALETTE[rect.id];
158
- const cx = rect.wx + rect.ww / 2;
159
- const cz = rect.wy + rect.wh / 2;
160
- const label = lang === "ko" ? ROOM_LABELS[rect.id].ko : ROOM_LABELS[rect.id].en;
161
- const verdict = room?.metrics?.sprint_verdict;
162
- const desks = deskSlotsForRoom(rect);
163
-
164
- // Walls with door gaps. North/south run along x-axis; east/west run along z-axis.
165
- const northSegs = wallSegments(rect.wx, rect.wx + rect.ww, doorsBySide(rect, "north"));
166
- const southSegs = wallSegments(rect.wx, rect.wx + rect.ww, doorsBySide(rect, "south"));
167
- const westSegs = wallSegments(rect.wy, rect.wy + rect.wh, doorsBySide(rect, "west"));
168
- const eastSegs = wallSegments(rect.wy, rect.wy + rect.wh, doorsBySide(rect, "east"));
169
-
170
- return (
171
- <group userData={{ roomId: rect.id }}>
172
- {/* Floor slab — placed at the room center in absolute world coords.
173
- Clicking the floor opens the room-metrics drawer. */}
174
- <mesh
175
- receiveShadow
176
- position={[cx, FLOOR_THICKNESS / 2, cz]}
177
- onClick={(e) => {
178
- e.stopPropagation();
179
- onRoomClick?.(rect.id);
180
- }}
181
- onPointerOver={(e) => {
182
- e.stopPropagation();
183
- document.body.style.cursor = "pointer";
184
- }}
185
- onPointerOut={() => {
186
- document.body.style.cursor = "default";
187
- }}
188
- >
189
- <boxGeometry args={[rect.ww, FLOOR_THICKNESS, rect.wh]} />
190
- <meshStandardMaterial color={palette.floor} roughness={0.85} metalness={0.05} />
191
- </mesh>
192
-
193
- {/* Trim border on top of floor */}
194
- <mesh position={[cx, FLOOR_THICKNESS + 0.02, cz]}>
195
- <boxGeometry args={[rect.ww, 0.04, rect.wh]} />
196
- <meshStandardMaterial color={palette.trim} roughness={0.5} metalness={0.2} opacity={0.35} transparent />
197
- </mesh>
198
-
199
- {/* North wall (z = rect.wy) */}
200
- {northSegs.map(([s, e], i) => (
201
- <Wall
202
- key={`n${i}`}
203
- axis="x"
204
- start={s}
205
- end={e}
206
- fixedCoord={rect.wy}
207
- height={WALL_HEIGHT}
208
- color={palette.wall}
209
- />
210
- ))}
211
- {/* South wall (z = rect.wy + rect.wh) */}
212
- {southSegs.map(([s, e], i) => (
213
- <Wall
214
- key={`s${i}`}
215
- axis="x"
216
- start={s}
217
- end={e}
218
- fixedCoord={rect.wy + rect.wh}
219
- height={WALL_HEIGHT}
220
- color={palette.wall}
221
- />
222
- ))}
223
- {/* West wall (x = rect.wx) */}
224
- {westSegs.map(([s, e], i) => (
225
- <Wall
226
- key={`w${i}`}
227
- axis="z"
228
- start={s}
229
- end={e}
230
- fixedCoord={rect.wx}
231
- height={WALL_HEIGHT}
232
- color={palette.wall}
233
- />
234
- ))}
235
- {/* East wall (x = rect.wx + rect.ww) */}
236
- {eastSegs.map(([s, e], i) => (
237
- <Wall
238
- key={`e${i}`}
239
- axis="z"
240
- start={s}
241
- end={e}
242
- fixedCoord={rect.wx + rect.ww}
243
- height={WALL_HEIGHT}
244
- color={palette.wall}
245
- />
246
- ))}
247
-
248
- {/* Door frame trim — emphasises openings */}
249
- {doorsForRoom(rect).map((d, i) => (
250
- <DoorFrame key={`door${i}`} rect={rect} door={d} color={palette.trim} />
251
- ))}
252
-
253
- {/* Desks + chairs */}
254
- {desks.map((slot) => (
255
- <group key={`desk-${slot.index}`}>
256
- <Desk position={slot.desk} facing={slot.facing} />
257
- <Chair position={slot.chair} facing={slot.facing} />
258
- </group>
259
- ))}
260
-
261
- {/* Room label as DOM overlay */}
262
- <Html
263
- position={[cx, FLOOR_THICKNESS + 0.05, cz + rect.wh / 2 - 0.3]}
264
- center
265
- distanceFactor={20}
266
- pointerEvents="none"
267
- >
268
- <span
269
- className="select-none rounded bg-black/55 px-2 py-0.5 text-[11px] font-mono uppercase tracking-widest text-gray-100"
270
- style={{ whiteSpace: "nowrap" }}
271
- >
272
- {label}
273
- </span>
274
- </Html>
275
-
276
- {verdict && verdict !== "pending" && (
277
- <Html
278
- position={[cx, FLOOR_THICKNESS + 0.06, cz - rect.wh / 2 + 0.3]}
279
- center
280
- distanceFactor={20}
281
- pointerEvents="none"
282
- >
283
- <span
284
- className={`select-none rounded px-1.5 py-0.5 text-[9px] font-mono ${
285
- verdict === "PASS"
286
- ? "bg-aura-typing/30 text-aura-typing"
287
- : "bg-aura-alert/30 text-aura-alert"
288
- }`}
289
- >
290
- {verdict}
291
- </span>
292
- </Html>
293
- )}
294
- </group>
295
- );
296
- }
297
-
298
- function DoorFrame({
299
- rect,
300
- door,
301
- color,
302
- }: {
303
- rect: RoomRect;
304
- door: DoorSpec;
305
- color: string;
306
- }) {
307
- const cy = FLOOR_THICKNESS + WALL_HEIGHT;
308
- if (door.side === "north" || door.side === "south") {
309
- const z = door.side === "north" ? rect.wy : rect.wy + rect.wh;
310
- return (
311
- <mesh position={[door.position[0], cy + 0.05, z]} castShadow>
312
- <boxGeometry args={[door.width + 0.1, 0.1, WALL_THICKNESS + 0.04]} />
313
- <meshStandardMaterial color={color} metalness={0.3} roughness={0.5} />
314
- </mesh>
315
- );
316
- }
317
- const x = door.side === "west" ? rect.wx : rect.wx + rect.ww;
318
- return (
319
- <mesh position={[x, cy + 0.05, door.position[1]]} castShadow>
320
- <boxGeometry args={[WALL_THICKNESS + 0.04, 0.1, door.width + 0.1]} />
321
- <meshStandardMaterial color={color} metalness={0.3} roughness={0.5} />
322
- </mesh>
323
- );
324
- }
325
-
326
- interface Floor3DProps {
327
- rooms: RoomState[];
328
- lang?: "ko" | "en";
329
- onRoomClick?: (id: RoomId) => void;
330
- }
331
-
332
- export function Floor3D({ rooms, lang = "ko", onRoomClick }: Floor3DProps) {
333
- const byId = new Map(rooms.map((r) => [r.id, r]));
334
- return (
335
- <group>
336
- {/* Ground / lawn — extends well past the building to give the figures
337
- room to stroll outside the office. */}
338
- <mesh receiveShadow position={[5.5, -0.05, 4.5]} rotation={[-Math.PI / 2, 0, 0]}>
339
- <planeGeometry args={[26, 22]} />
340
- <meshStandardMaterial color="#7ba27a" roughness={1} />
341
- </mesh>
342
-
343
- {/* Corridor floors — same height as room floors, lighter palette so the
344
- path between rooms reads as a connector. */}
345
- {CORRIDOR_RECTS.map((c, i) => (
346
- <group key={`corr-${i}`}>
347
- <mesh
348
- receiveShadow
349
- position={[c.wx + c.ww / 2, FLOOR_THICKNESS / 2, c.wy + c.wh / 2]}
350
- >
351
- <boxGeometry args={[c.ww, FLOOR_THICKNESS, c.wh]} />
352
- <meshStandardMaterial color="#9aa6b8" roughness={0.85} />
353
- </mesh>
354
- <mesh position={[c.wx + c.ww / 2, FLOOR_THICKNESS + 0.011, c.wy + c.wh / 2]}>
355
- <boxGeometry args={[c.ww, 0.02, c.wh]} />
356
- <meshStandardMaterial color="#cdd5e0" roughness={0.7} />
357
- </mesh>
358
- </group>
359
- ))}
360
-
361
- {/* Sidewalk strip leading to the entrance */}
362
- <mesh position={[5.5, 0.0, -1.2]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
363
- <planeGeometry args={[14, 0.7]} />
364
- <meshStandardMaterial color="#bcc4d4" roughness={1} />
365
- </mesh>
366
-
367
- {ROOM_RECTS.map((rect) => (
368
- <RoomBlock
369
- key={rect.id}
370
- rect={rect}
371
- room={byId.get(rect.id)}
372
- lang={lang}
373
- onRoomClick={onRoomClick}
374
- />
375
- ))}
376
-
377
- {/* Decorative trees — placed where the corridor meets the lawn so the
378
- building doesn't sit on a flat green disk. */}
379
- {[
380
- [-1.8, 0.8],
381
- [-1.8, 4.5],
382
- [-1.8, 8.5],
383
- [12.5, 0.8],
384
- [12.5, 4.5],
385
- [12.5, 8.5],
386
- [3, 10.5],
387
- [8, 10.5],
388
- [5, -1.2],
389
- [9, -1.2],
390
- ].map(([x, z], i) => (
391
- <group key={i} position={[x, 0, z]}>
392
- <mesh castShadow position={[0, 0.5, 0]}>
393
- <cylinderGeometry args={[0.08, 0.1, 1, 8]} />
394
- <meshStandardMaterial color="#7a4f2e" roughness={0.95} />
395
- </mesh>
396
- <mesh castShadow position={[0, 1.2, 0]}>
397
- <icosahedronGeometry args={[0.55, 0]} />
398
- <meshStandardMaterial color="#5a8a52" roughness={0.9} />
399
- </mesh>
400
- </group>
401
- ))}
402
- </group>
403
- );
404
- }
@@ -1,123 +0,0 @@
1
- "use client";
2
- import { Html } from "@react-three/drei";
3
- import type {
4
- ContractSnapshot,
5
- EscalationEntry,
6
- GoalCard,
7
- } from "@/lib/types";
8
- import { ROOM_RECTS } from "@/lib/iso";
9
-
10
- interface GoalCard3DProps {
11
- goal: GoalCard | null;
12
- lang?: "ko" | "en";
13
- contract?: ContractSnapshot;
14
- escalations?: EscalationEntry[];
15
- }
16
-
17
- const PIPELINE_TONE: Record<string, string> = {
18
- FULLSTACK: "bg-aura-typing/30 text-aura-typing",
19
- "FE-ONLY": "bg-aura-talking/30 text-aura-talking",
20
- "BE-ONLY": "bg-aura-talking/30 text-aura-talking",
21
- META_REFACTOR: "bg-aura-alert/20 text-aura-alert",
22
- };
23
-
24
- // Mounted on the back wall of the CEO room. The Html node renders a card-like
25
- // DOM element so long titles/descriptions stay readable instead of being
26
- // crammed into a 3D text mesh that would otherwise need a font load.
27
- //
28
- // CEO wall now also carries the pipeline + sprint badge and an open-escalation
29
- // counter — these are CEO-only signals (Owner-facing). Putting them here keeps
30
- // the doctrine "Owner ↔ Dispatcher only" intuition visible.
31
- export function GoalCard3D({
32
- goal,
33
- lang = "ko",
34
- contract,
35
- escalations = [],
36
- }: GoalCard3DProps) {
37
- const ceo = ROOM_RECTS.find((r) => r.id === "ceo")!;
38
- const cx = ceo.wx + ceo.ww / 2;
39
- const cz = ceo.wy + 0.15;
40
-
41
- const pipelineTone = contract?.pipeline
42
- ? PIPELINE_TONE[contract.pipeline] ?? "bg-gray-500/30 text-gray-300"
43
- : "bg-gray-500/30 text-gray-400";
44
-
45
- return (
46
- <Html
47
- position={[cx, 0.85, cz]}
48
- transform
49
- occlude={false}
50
- scale={0.16}
51
- pointerEvents="none"
52
- zIndexRange={[5, 0]}
53
- >
54
- <div
55
- data-testid="goal-card"
56
- className="rounded-md border border-aura-typing/60 bg-brick-bg/95 px-3 py-2 shadow-lg text-gray-100 font-mono"
57
- style={{ minWidth: 240, maxWidth: 320 }}
58
- >
59
- {goal ? (
60
- <>
61
- <div className="flex items-center justify-between gap-2">
62
- <div className="text-[9px] uppercase tracking-widest text-aura-typing">
63
- GOAL
64
- </div>
65
- <div className="flex items-center gap-1">
66
- {contract?.sprint_number !== null && contract?.sprint_number !== undefined && (
67
- <span
68
- data-testid="ceo-sprint-pill"
69
- className="rounded bg-aura-typing/20 px-1 py-0.5 text-[9px] uppercase text-aura-typing"
70
- >
71
- sprint {contract.sprint_number}
72
- </span>
73
- )}
74
- {contract?.pipeline && (
75
- <span
76
- data-testid="ceo-pipeline-pill"
77
- className={`rounded px-1 py-0.5 text-[9px] uppercase ${pipelineTone}`}
78
- >
79
- {contract.pipeline}
80
- </span>
81
- )}
82
- </div>
83
- </div>
84
- <div className="text-[14px] leading-tight font-semibold">
85
- {goal.title}
86
- </div>
87
- {goal.description_truncated && (
88
- <div className="mt-1 text-[10px] leading-snug text-gray-300">
89
- {goal.description_truncated}
90
- </div>
91
- )}
92
- <div className="mt-1 flex items-center justify-between">
93
- {typeof goal.adherence === "number" ? (
94
- <span className="text-[9px] text-gray-500">
95
- adherence {(goal.adherence * 100).toFixed(0)}%
96
- </span>
97
- ) : (
98
- <span className="text-[9px] text-gray-600">adherence —</span>
99
- )}
100
- {escalations.length > 0 && (
101
- <span
102
- data-testid="ceo-escalation-pill"
103
- className="rounded bg-aura-alert/30 px-1.5 py-0.5 text-[9px] uppercase text-aura-alert"
104
- >
105
- ↑ {escalations.length} escalation{escalations.length === 1 ? "" : "s"}
106
- </span>
107
- )}
108
- </div>
109
- </>
110
- ) : (
111
- <>
112
- <div className="text-[9px] uppercase tracking-widest text-gray-500">
113
- GOAL
114
- </div>
115
- <div className="text-[12px] text-gray-500">
116
- {lang === "en" ? "No active goal" : "활성 GOAL 없음"}
117
- </div>
118
- </>
119
- )}
120
- </div>
121
- </Html>
122
- );
123
- }
@@ -1,71 +0,0 @@
1
- "use client";
2
- import { Html } from "@react-three/drei";
3
- import { ROOM_RECTS } from "@/lib/iso";
4
- import type { HypothesisEntry } from "@/lib/types";
5
-
6
- interface HypothesisCardsProps {
7
- hypothesis: HypothesisEntry[];
8
- }
9
-
10
- const VERDICT_TONE: Record<string, string> = {
11
- pending: "bg-aura-talking/30 text-aura-talking border-aura-talking/40",
12
- valid: "bg-aura-typing/30 text-aura-typing border-aura-typing/40",
13
- invalid: "bg-aura-alert/30 text-aura-alert border-aura-alert/40",
14
- };
15
-
16
- // Stack of cards on the COO room wall — each card is one hypothesis from the
17
- // COO Hypothesis Cell. Limited to the latest 3 to keep the wall readable.
18
- export function HypothesisCards({ hypothesis }: HypothesisCardsProps) {
19
- const room = ROOM_RECTS.find((r) => r.id === "coo")!;
20
- const cx = room.wx + room.ww / 2;
21
- const cz = room.wy + 0.18;
22
-
23
- if (hypothesis.length === 0) return null;
24
- const visible = hypothesis.slice(0, 3);
25
- const overflow = hypothesis.length - visible.length;
26
-
27
- return (
28
- <Html
29
- position={[cx, 0.75, cz]}
30
- transform
31
- occlude={false}
32
- scale={0.12}
33
- pointerEvents="none"
34
- zIndexRange={[5, 0]}
35
- >
36
- <div
37
- data-testid="hypothesis-cards"
38
- className="flex flex-col gap-1 rounded-md border border-brick-wall bg-brick-bg/95 px-2 py-2 shadow-lg font-mono"
39
- style={{ minWidth: 220, maxWidth: 260 }}
40
- >
41
- <div className="text-[9px] uppercase tracking-widest text-gray-400">
42
- COO HYPOTHESIS CELL
43
- </div>
44
- {visible.map((h) => (
45
- <div
46
- key={h.id}
47
- data-testid={`hypothesis-${h.id}`}
48
- className={`rounded border px-1.5 py-0.5 text-[10px] ${
49
- VERDICT_TONE[h.verdict]
50
- }`}
51
- >
52
- <div className="flex items-center justify-between">
53
- <span className="opacity-70 tracking-wider">{h.id}</span>
54
- <span className="uppercase tracking-wide text-[9px]">
55
- {h.verdict}
56
- </span>
57
- </div>
58
- <div className="text-[10px] leading-tight text-gray-100 truncate">
59
- {h.brief}
60
- </div>
61
- </div>
62
- ))}
63
- {overflow > 0 && (
64
- <div className="text-[9px] text-gray-500 text-right">
65
- +{overflow} more
66
- </div>
67
- )}
68
- </div>
69
- </Html>
70
- );
71
- }
@@ -1,98 +0,0 @@
1
- "use client";
2
- import { Html } from "@react-three/drei";
3
- import { ROOM_RECTS } from "@/lib/iso";
4
- import type { MeetingsState, ParallelTrack } from "@/lib/types";
5
-
6
- interface MeetingWhiteboardProps {
7
- meetings: MeetingsState;
8
- tracks: ParallelTrack[];
9
- }
10
-
11
- const TYPE_LABEL: Record<string, { ko: string; tone: string }> = {
12
- standup: { ko: "스탠드업", tone: "bg-aura-typing/30 text-aura-typing" },
13
- "sprint-review": { ko: "스프린트 리뷰", tone: "bg-aura-talking/30 text-aura-talking" },
14
- "spec-review": { ko: "스펙 리뷰", tone: "bg-aura-talking/30 text-aura-talking" },
15
- "incident-war-room": { ko: "워룸", tone: "bg-aura-alert/30 text-aura-alert" },
16
- "all-hands": { ko: "올핸즈", tone: "bg-aura-typing/30 text-aura-typing" },
17
- "followup-review": { ko: "팔로우업", tone: "bg-aura-talking/30 text-aura-talking" },
18
- };
19
-
20
- const CADENCE_TONE: Record<string, string> = {
21
- light: "bg-aura-idle/40 text-gray-200",
22
- normal: "bg-aura-typing/30 text-aura-typing",
23
- heavy: "bg-aura-alert/30 text-aura-alert",
24
- };
25
-
26
- // Whiteboard mounted on the back wall of the meeting room. Surfaces:
27
- // - active meeting type (typed badge)
28
- // - cadence (light/normal/heavy)
29
- // - active parallel track count
30
- // When no meeting is in session and no tracks are active, the board reads
31
- // "idle" so the room never looks broken.
32
- export function MeetingWhiteboard({ meetings, tracks }: MeetingWhiteboardProps) {
33
- const room = ROOM_RECTS.find((r) => r.id === "meeting")!;
34
- const cx = room.wx + room.ww / 2;
35
- const cz = room.wy + 0.18;
36
- const meeting = meetings.current;
37
- const typeInfo = meeting ? TYPE_LABEL[meeting.type] : null;
38
- const cadenceTone = CADENCE_TONE[meetings.cadence] ?? CADENCE_TONE.normal;
39
-
40
- return (
41
- <Html
42
- position={[cx, 0.85, cz]}
43
- transform
44
- occlude={false}
45
- scale={0.14}
46
- pointerEvents="none"
47
- zIndexRange={[5, 0]}
48
- >
49
- <div
50
- data-testid="meeting-whiteboard"
51
- className="rounded-md border border-brick-wall bg-brick-bg/95 px-3 py-2 shadow-lg text-gray-100 font-mono"
52
- style={{ minWidth: 240, maxWidth: 320 }}
53
- >
54
- <div className="text-[9px] uppercase tracking-widest text-gray-400">
55
- MEETING ROOM
56
- </div>
57
- <div className="mt-1 flex flex-wrap items-center gap-1">
58
- {typeInfo ? (
59
- <span
60
- data-testid={`meeting-type-${meeting!.type}`}
61
- className={`rounded px-1.5 py-0.5 text-[10px] uppercase ${typeInfo.tone}`}
62
- >
63
- {typeInfo.ko}
64
- </span>
65
- ) : (
66
- <span className="rounded bg-aura-idle/40 px-1.5 py-0.5 text-[10px] uppercase text-gray-300">
67
- 유휴
68
- </span>
69
- )}
70
- <span
71
- data-testid={`cadence-${meetings.cadence}`}
72
- className={`rounded px-1.5 py-0.5 text-[10px] uppercase ${cadenceTone}`}
73
- >
74
- cadence: {meetings.cadence}
75
- </span>
76
- {tracks.length > 0 && (
77
- <span
78
- data-testid="track-count"
79
- className="rounded bg-aura-talking/30 px-1.5 py-0.5 text-[10px] uppercase text-aura-talking"
80
- >
81
- tracks ×{tracks.length}
82
- </span>
83
- )}
84
- </div>
85
- {meeting?.topic && (
86
- <div className="mt-1 text-[10px] text-gray-300 leading-snug">
87
- {meeting.topic}
88
- </div>
89
- )}
90
- {meetings.next_scheduled && (
91
- <div className="mt-1 text-[9px] text-gray-500">
92
- next {meetings.next_scheduled}
93
- </div>
94
- )}
95
- </div>
96
- </Html>
97
- );
98
- }