@walwal-harness/cli 7.1.40 → 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.
- package/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +12 -4
- package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/components/Scene.tsx +568 -35
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +2 -1
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- 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
|
-
}
|