@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,391 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useRef, useMemo } from "react";
|
|
3
|
-
import { useFrame } from "@react-three/fiber";
|
|
4
|
-
import { Html } from "@react-three/drei";
|
|
5
|
-
import type { Group, Mesh } from "three";
|
|
6
|
-
import type { AgentState, MinifigState, RoomId } from "@/lib/types";
|
|
7
|
-
import { ROOM_RECTS, ROAM_POOL, type RoomRect } from "@/lib/iso";
|
|
8
|
-
import { homeDeskFor, deskSlotsForRoom } from "@/lib/furniture";
|
|
9
|
-
import { planPath } from "@/lib/path-planning";
|
|
10
|
-
|
|
11
|
-
const AURA: Record<MinifigState, { color: string; emissive: number }> = {
|
|
12
|
-
idle: { color: "#8696b8", emissive: 0.0 },
|
|
13
|
-
queued: { color: "#60a5fa", emissive: 0.25 },
|
|
14
|
-
typing: { color: "#22d3ee", emissive: 0.5 },
|
|
15
|
-
talking: { color: "#fbbf24", emissive: 0.4 },
|
|
16
|
-
"red-alert": { color: "#ef4444", emissive: 0.7 },
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function hatColor(id: string): string {
|
|
20
|
-
const palette = [
|
|
21
|
-
"#e25555", "#5577e8", "#37a872", "#d68a2d", "#a05ad6", "#2da6c2", "#c54a8a", "#e0a02d",
|
|
22
|
-
"#5a7ad6", "#54b07a", "#b06fce", "#d6624a", "#3aa5b5", "#c19425",
|
|
23
|
-
];
|
|
24
|
-
let h = 0;
|
|
25
|
-
for (const ch of id) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
26
|
-
return palette[h % palette.length];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface MinifigProps {
|
|
30
|
-
agent: AgentState;
|
|
31
|
-
index: number;
|
|
32
|
-
// Where typing pins them; undefined for archive (no desk).
|
|
33
|
-
deskTarget?: { pos: [number, number, number]; facing: number };
|
|
34
|
-
// Where talking pins them (a meeting-room seat).
|
|
35
|
-
talkingTarget?: { pos: [number, number, number]; facing: number };
|
|
36
|
-
// Phase offset so 14 minifigs don't move in lock-step.
|
|
37
|
-
phase: number;
|
|
38
|
-
// Initial spawn point so each minifig starts in their home room.
|
|
39
|
-
spawn: [number, number];
|
|
40
|
-
onClick?: () => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function lerp(a: number, b: number, t: number) {
|
|
44
|
-
return a + (b - a) * t;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function approach(current: number, target: number, maxStep: number) {
|
|
48
|
-
const diff = target - current;
|
|
49
|
-
if (Math.abs(diff) <= maxStep) return target;
|
|
50
|
-
return current + Math.sign(diff) * maxStep;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function Minifig({ agent, index, deskTarget, talkingTarget, phase, spawn, onClick }: MinifigProps) {
|
|
54
|
-
const groupRef = useRef<Group>(null!);
|
|
55
|
-
const leftLegRef = useRef<Mesh>(null!);
|
|
56
|
-
const rightLegRef = useRef<Mesh>(null!);
|
|
57
|
-
const aura = AURA[agent.minifigState];
|
|
58
|
-
const hat = useMemo(() => hatColor(agent.id), [agent.id]);
|
|
59
|
-
// Per-minifig RNG seeded with the agent id so random walks are distinct but
|
|
60
|
-
// stable on a remount.
|
|
61
|
-
const rngState = useRef<number>(
|
|
62
|
-
Array.from(agent.id).reduce((h, ch) => (h * 31 + ch.charCodeAt(0)) >>> 0, 7)
|
|
63
|
-
);
|
|
64
|
-
const rng = () => {
|
|
65
|
-
let x = rngState.current;
|
|
66
|
-
x ^= x << 13;
|
|
67
|
-
x ^= x >>> 17;
|
|
68
|
-
x ^= x << 5;
|
|
69
|
-
rngState.current = x >>> 0;
|
|
70
|
-
return (rngState.current % 100000) / 100000;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const lastPos = useRef<{ x: number; z: number; yaw: number }>({
|
|
74
|
-
x: spawn[0],
|
|
75
|
-
z: spawn[1],
|
|
76
|
-
yaw: 0,
|
|
77
|
-
});
|
|
78
|
-
// Free-roam path-queue state (idle only). `path[0]` is the immediate target;
|
|
79
|
-
// when reached we shift it off and walk to the next. Empty queue → pick a
|
|
80
|
-
// new destination via planPath which routes through doors, never walls.
|
|
81
|
-
const roam = useRef<{ path: Array<[number, number]>; dwellUntil: number }>({
|
|
82
|
-
path: [],
|
|
83
|
-
dwellUntil: 0,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
function pickRoamTarget(): [number, number] {
|
|
87
|
-
return ROAM_POOL[Math.floor(rng() * ROAM_POOL.length)];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
useFrame((state, delta) => {
|
|
91
|
-
if (!groupRef.current) return;
|
|
92
|
-
const t = state.clock.elapsedTime + phase;
|
|
93
|
-
|
|
94
|
-
let tx = lastPos.current.x;
|
|
95
|
-
let tz = lastPos.current.z;
|
|
96
|
-
let targetYaw = lastPos.current.yaw;
|
|
97
|
-
let isMoving = false;
|
|
98
|
-
|
|
99
|
-
if ((agent.minifigState === "typing" || agent.minifigState === "queued") && deskTarget) {
|
|
100
|
-
// Route to the desk through doors if we're currently outside the home room.
|
|
101
|
-
const desiredX = deskTarget.pos[0];
|
|
102
|
-
const desiredZ = deskTarget.pos[2];
|
|
103
|
-
const distToDesk = Math.hypot(
|
|
104
|
-
desiredX - lastPos.current.x,
|
|
105
|
-
desiredZ - lastPos.current.z
|
|
106
|
-
);
|
|
107
|
-
if (roam.current.path.length === 0 && distToDesk > 0.05) {
|
|
108
|
-
roam.current.path = planPath(
|
|
109
|
-
[lastPos.current.x, lastPos.current.z],
|
|
110
|
-
[desiredX, desiredZ]
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
if (roam.current.path.length > 0) {
|
|
114
|
-
[tx, tz] = roam.current.path[0];
|
|
115
|
-
const dx = tx - lastPos.current.x;
|
|
116
|
-
const dz = tz - lastPos.current.z;
|
|
117
|
-
if (Math.abs(dx) + Math.abs(dz) > 0.01) {
|
|
118
|
-
targetYaw = Math.atan2(dx, dz);
|
|
119
|
-
isMoving = true;
|
|
120
|
-
}
|
|
121
|
-
} else {
|
|
122
|
-
tx = desiredX;
|
|
123
|
-
tz = desiredZ;
|
|
124
|
-
targetYaw = deskTarget.facing;
|
|
125
|
-
}
|
|
126
|
-
} else if (agent.minifigState === "talking" && talkingTarget) {
|
|
127
|
-
const desiredX = talkingTarget.pos[0];
|
|
128
|
-
const desiredZ = talkingTarget.pos[2];
|
|
129
|
-
const distToSeat = Math.hypot(
|
|
130
|
-
desiredX - lastPos.current.x,
|
|
131
|
-
desiredZ - lastPos.current.z
|
|
132
|
-
);
|
|
133
|
-
if (roam.current.path.length === 0 && distToSeat > 0.05) {
|
|
134
|
-
roam.current.path = planPath(
|
|
135
|
-
[lastPos.current.x, lastPos.current.z],
|
|
136
|
-
[desiredX, desiredZ]
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
if (roam.current.path.length > 0) {
|
|
140
|
-
[tx, tz] = roam.current.path[0];
|
|
141
|
-
const dx = tx - lastPos.current.x;
|
|
142
|
-
const dz = tz - lastPos.current.z;
|
|
143
|
-
if (Math.abs(dx) + Math.abs(dz) > 0.01) {
|
|
144
|
-
targetYaw = Math.atan2(dx, dz);
|
|
145
|
-
isMoving = true;
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
tx = desiredX;
|
|
149
|
-
tz = desiredZ;
|
|
150
|
-
targetYaw = talkingTarget.facing;
|
|
151
|
-
}
|
|
152
|
-
} else if (agent.minifigState === "red-alert") {
|
|
153
|
-
const home = deskTarget;
|
|
154
|
-
if (home) {
|
|
155
|
-
tx = home.pos[0] + Math.sin(t * 22) * 0.04;
|
|
156
|
-
tz = home.pos[2] + Math.cos(t * 22) * 0.04;
|
|
157
|
-
targetYaw = home.facing;
|
|
158
|
-
}
|
|
159
|
-
roam.current.path = [];
|
|
160
|
-
} else {
|
|
161
|
-
// idle — random destinations, but every transition between zones is
|
|
162
|
-
// routed through door waypoints so we never clip walls.
|
|
163
|
-
const now = state.clock.elapsedTime;
|
|
164
|
-
if (roam.current.path.length === 0 && now >= roam.current.dwellUntil) {
|
|
165
|
-
const dest = pickRoamTarget();
|
|
166
|
-
roam.current.path = planPath(
|
|
167
|
-
[lastPos.current.x, lastPos.current.z],
|
|
168
|
-
dest
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (roam.current.path.length > 0) {
|
|
173
|
-
[tx, tz] = roam.current.path[0];
|
|
174
|
-
const dx = tx - lastPos.current.x;
|
|
175
|
-
const dz = tz - lastPos.current.z;
|
|
176
|
-
if (Math.abs(dx) + Math.abs(dz) > 0.01) {
|
|
177
|
-
targetYaw = Math.atan2(dx, dz);
|
|
178
|
-
isMoving = true;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Path step advance — once we're close enough to the immediate waypoint,
|
|
184
|
-
// pop it and continue to the next, or start dwelling if the queue is
|
|
185
|
-
// exhausted.
|
|
186
|
-
if (roam.current.path.length > 0) {
|
|
187
|
-
const [pt0x, pt0z] = roam.current.path[0];
|
|
188
|
-
const reach = Math.hypot(pt0x - lastPos.current.x, pt0z - lastPos.current.z);
|
|
189
|
-
if (reach < 0.12) {
|
|
190
|
-
roam.current.path.shift();
|
|
191
|
-
if (
|
|
192
|
-
roam.current.path.length === 0 &&
|
|
193
|
-
agent.minifigState !== "typing" &&
|
|
194
|
-
agent.minifigState !== "queued" &&
|
|
195
|
-
agent.minifigState !== "talking"
|
|
196
|
-
) {
|
|
197
|
-
roam.current.dwellUntil = state.clock.elapsedTime + 0.4 + rng() * 1.4;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const speed = isMoving ? 1.6 : 1.4; // walking cadence — slower than before
|
|
203
|
-
const dist = Math.hypot(tx - lastPos.current.x, tz - lastPos.current.z);
|
|
204
|
-
if (dist > 0.001) {
|
|
205
|
-
const step = Math.min(dist, speed * delta);
|
|
206
|
-
const ratio = step / dist;
|
|
207
|
-
lastPos.current.x = lerp(lastPos.current.x, tx, ratio);
|
|
208
|
-
lastPos.current.z = lerp(lastPos.current.z, tz, ratio);
|
|
209
|
-
if (step > 0.005) isMoving = true;
|
|
210
|
-
}
|
|
211
|
-
const TAU = Math.PI * 2;
|
|
212
|
-
let yawDiff = ((targetYaw - lastPos.current.yaw + Math.PI) % TAU) - Math.PI;
|
|
213
|
-
if (yawDiff < -Math.PI) yawDiff += TAU;
|
|
214
|
-
lastPos.current.yaw = approach(lastPos.current.yaw, lastPos.current.yaw + yawDiff, 8 * delta);
|
|
215
|
-
|
|
216
|
-
const bob = isMoving ? Math.abs(Math.sin(t * 8)) * 0.05 : Math.sin(t * 1.6) * 0.02;
|
|
217
|
-
groupRef.current.position.set(lastPos.current.x, 0.18 + bob, lastPos.current.z);
|
|
218
|
-
groupRef.current.rotation.y = lastPos.current.yaw;
|
|
219
|
-
|
|
220
|
-
if (leftLegRef.current && rightLegRef.current) {
|
|
221
|
-
const swing = isMoving ? Math.sin(t * 8) * 0.4 : 0;
|
|
222
|
-
leftLegRef.current.rotation.x = swing;
|
|
223
|
-
rightLegRef.current.rotation.x = -swing;
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
return (
|
|
228
|
-
<group
|
|
229
|
-
ref={groupRef}
|
|
230
|
-
userData={{ agentId: agent.id, minifigState: agent.minifigState, index }}
|
|
231
|
-
onClick={(e) => {
|
|
232
|
-
e.stopPropagation();
|
|
233
|
-
onClick?.();
|
|
234
|
-
}}
|
|
235
|
-
onPointerOver={(e) => {
|
|
236
|
-
e.stopPropagation();
|
|
237
|
-
document.body.style.cursor = "pointer";
|
|
238
|
-
}}
|
|
239
|
-
onPointerOut={() => {
|
|
240
|
-
document.body.style.cursor = "default";
|
|
241
|
-
}}
|
|
242
|
-
>
|
|
243
|
-
{/* Body */}
|
|
244
|
-
<mesh castShadow position={[0, 0.18, 0]}>
|
|
245
|
-
<boxGeometry args={[0.32, 0.36, 0.22]} />
|
|
246
|
-
<meshStandardMaterial color="#dde3ee" roughness={0.7} />
|
|
247
|
-
</mesh>
|
|
248
|
-
{/* Legs — pivoted at the hip so leg swing rotates the whole leg. */}
|
|
249
|
-
<group position={[-0.08, 0, 0]}>
|
|
250
|
-
<mesh ref={leftLegRef} castShadow position={[0, -0.06, 0]}>
|
|
251
|
-
<boxGeometry args={[0.12, 0.16, 0.12]} />
|
|
252
|
-
<meshStandardMaterial color="#3b4256" roughness={0.85} />
|
|
253
|
-
</mesh>
|
|
254
|
-
</group>
|
|
255
|
-
<group position={[0.08, 0, 0]}>
|
|
256
|
-
<mesh ref={rightLegRef} castShadow position={[0, -0.06, 0]}>
|
|
257
|
-
<boxGeometry args={[0.12, 0.16, 0.12]} />
|
|
258
|
-
<meshStandardMaterial color="#3b4256" roughness={0.85} />
|
|
259
|
-
</mesh>
|
|
260
|
-
</group>
|
|
261
|
-
{/* Head */}
|
|
262
|
-
<mesh castShadow position={[0, 0.5, 0]}>
|
|
263
|
-
<sphereGeometry args={[0.16, 16, 16]} />
|
|
264
|
-
<meshStandardMaterial color="#f3d6a8" roughness={0.6} />
|
|
265
|
-
</mesh>
|
|
266
|
-
{/* Cap base */}
|
|
267
|
-
<mesh castShadow position={[0, 0.6, -0.02]}>
|
|
268
|
-
<cylinderGeometry args={[0.17, 0.17, 0.1, 16]} />
|
|
269
|
-
<meshStandardMaterial color={hat} roughness={0.7} />
|
|
270
|
-
</mesh>
|
|
271
|
-
{/* Cap brim */}
|
|
272
|
-
<mesh castShadow position={[0, 0.59, 0.12]} rotation={[0.1, 0, 0]}>
|
|
273
|
-
<boxGeometry args={[0.22, 0.04, 0.12]} />
|
|
274
|
-
<meshStandardMaterial color={hat} roughness={0.7} />
|
|
275
|
-
</mesh>
|
|
276
|
-
|
|
277
|
-
{/* Aura ring */}
|
|
278
|
-
<mesh
|
|
279
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
280
|
-
position={[0, -0.17, 0]}
|
|
281
|
-
scale={agent.minifigState === "red-alert" ? [1.1, 1.1, 1.1] : [1, 1, 1]}
|
|
282
|
-
>
|
|
283
|
-
<ringGeometry args={[0.22, 0.3, 32]} />
|
|
284
|
-
<meshStandardMaterial
|
|
285
|
-
color={aura.color}
|
|
286
|
-
emissive={aura.color}
|
|
287
|
-
emissiveIntensity={aura.emissive}
|
|
288
|
-
opacity={agent.minifigState === "idle" ? 0.35 : 0.85}
|
|
289
|
-
transparent
|
|
290
|
-
/>
|
|
291
|
-
</mesh>
|
|
292
|
-
|
|
293
|
-
{/* Talking speech bubble — Drei <Html> with transform mode keeps the
|
|
294
|
-
DOM size stable under the orthographic camera, unlike distanceFactor
|
|
295
|
-
which doesn't respect ortho zoom. */}
|
|
296
|
-
{agent.minifigState === "talking" && (
|
|
297
|
-
<>
|
|
298
|
-
<mesh position={[0.18, 0.78, 0]}>
|
|
299
|
-
<sphereGeometry args={[0.06, 12, 12]} />
|
|
300
|
-
<meshStandardMaterial
|
|
301
|
-
color="#fbbf24"
|
|
302
|
-
emissive="#fbbf24"
|
|
303
|
-
emissiveIntensity={0.6}
|
|
304
|
-
/>
|
|
305
|
-
</mesh>
|
|
306
|
-
<Html
|
|
307
|
-
position={[0, 1.0, 0]}
|
|
308
|
-
transform
|
|
309
|
-
occlude={false}
|
|
310
|
-
scale={0.18}
|
|
311
|
-
pointerEvents="none"
|
|
312
|
-
zIndexRange={[10, 0]}
|
|
313
|
-
>
|
|
314
|
-
<div
|
|
315
|
-
data-testid={`speech-${agent.id}`}
|
|
316
|
-
className="rounded-md bg-aura-talking text-black px-2 py-1 text-[14px] font-mono shadow-md whitespace-nowrap border border-yellow-500"
|
|
317
|
-
style={{ minWidth: 60 }}
|
|
318
|
-
>
|
|
319
|
-
{(agent.talkingPreview ?? "…").slice(0, 40)}
|
|
320
|
-
</div>
|
|
321
|
-
</Html>
|
|
322
|
-
</>
|
|
323
|
-
)}
|
|
324
|
-
</group>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
interface Minifigs3DProps {
|
|
329
|
-
agents: AgentState[];
|
|
330
|
-
onAgentClick?: (id: import("@/lib/types").AgentId) => void;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Build a meeting-room seat assignment table. Talking agents fill the meeting
|
|
334
|
-
// desks in stable order so they don't fight for the same chair.
|
|
335
|
-
function meetingSeats(): Array<{ pos: [number, number, number]; facing: number }> {
|
|
336
|
-
const meetingRect = ROOM_RECTS.find((r) => r.id === "meeting")!;
|
|
337
|
-
return deskSlotsForRoom(meetingRect).map((s) => ({ pos: s.chair, facing: s.facing }));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export function Minifigs3D({ agents, onAgentClick }: Minifigs3DProps) {
|
|
341
|
-
const meetingSeatList = useMemo(() => meetingSeats(), []);
|
|
342
|
-
const homeRoomCache = useMemo(() => {
|
|
343
|
-
const out = new Map<RoomId, RoomRect>();
|
|
344
|
-
for (const r of ROOM_RECTS) out.set(r.id, r);
|
|
345
|
-
return out;
|
|
346
|
-
}, []);
|
|
347
|
-
|
|
348
|
-
// Stable index for talkers so each gets the same seat.
|
|
349
|
-
const talkers = agents.filter((a) => a.minifigState === "talking").map((a) => a.id);
|
|
350
|
-
const talkerSeatIdx = new Map<string, number>();
|
|
351
|
-
talkers.forEach((id, i) => talkerSeatIdx.set(id, i));
|
|
352
|
-
|
|
353
|
-
return (
|
|
354
|
-
<group>
|
|
355
|
-
{agents.map((agent, i) => {
|
|
356
|
-
const homeRect = homeRoomCache.get(agent.homeRoom);
|
|
357
|
-
if (!homeRect) return null;
|
|
358
|
-
|
|
359
|
-
const homeDesk = homeDeskFor(agent.id);
|
|
360
|
-
const deskTarget = homeDesk
|
|
361
|
-
? { pos: homeDesk.chair, facing: homeDesk.facing }
|
|
362
|
-
: undefined;
|
|
363
|
-
|
|
364
|
-
let talkingTarget: { pos: [number, number, number]; facing: number } | undefined;
|
|
365
|
-
if (agent.minifigState === "talking") {
|
|
366
|
-
const idx = talkerSeatIdx.get(agent.id) ?? 0;
|
|
367
|
-
const seat = meetingSeatList[idx % meetingSeatList.length];
|
|
368
|
-
talkingTarget = seat;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Spawn each agent at their home desk's chair (or room center if no desk).
|
|
372
|
-
const spawn: [number, number] = homeDesk
|
|
373
|
-
? [homeDesk.chair[0], homeDesk.chair[2]]
|
|
374
|
-
: [homeRect.wx + homeRect.ww / 2, homeRect.wy + homeRect.wh / 2];
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<Minifig
|
|
378
|
-
key={agent.id}
|
|
379
|
-
agent={agent}
|
|
380
|
-
index={i}
|
|
381
|
-
deskTarget={deskTarget}
|
|
382
|
-
talkingTarget={talkingTarget}
|
|
383
|
-
phase={i * 0.7}
|
|
384
|
-
spawn={spawn}
|
|
385
|
-
onClick={() => onAgentClick?.(agent.id)}
|
|
386
|
-
/>
|
|
387
|
-
);
|
|
388
|
-
})}
|
|
389
|
-
</group>
|
|
390
|
-
);
|
|
391
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useRef } from "react";
|
|
3
|
-
import { useFrame } from "@react-three/fiber";
|
|
4
|
-
import { Html } from "@react-three/drei";
|
|
5
|
-
import type { Mesh } from "three";
|
|
6
|
-
import { ROOM_RECTS } from "@/lib/iso";
|
|
7
|
-
import type { IncidentEntry } from "@/lib/types";
|
|
8
|
-
|
|
9
|
-
interface OpsSirenProps {
|
|
10
|
-
incidents: IncidentEntry[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Rotating light fixture mounted on the Service-Ops ceiling. When there's no
|
|
14
|
-
// open incident, the bulb is a dim grey dome — drawing the eye to the room
|
|
15
|
-
// only when something is actually wrong.
|
|
16
|
-
export function OpsSiren({ incidents }: OpsSirenProps) {
|
|
17
|
-
const room = ROOM_RECTS.find((r) => r.id === "service-ops")!;
|
|
18
|
-
const cx = room.wx + room.ww / 2;
|
|
19
|
-
const cz = room.wy + 0.4;
|
|
20
|
-
const bulbRef = useRef<Mesh>(null);
|
|
21
|
-
const beamRef = useRef<Mesh>(null);
|
|
22
|
-
const active = incidents.length > 0;
|
|
23
|
-
const critical = incidents.some((i) => i.severity === "critical" || i.severity === "high");
|
|
24
|
-
|
|
25
|
-
useFrame((state, _delta) => {
|
|
26
|
-
if (!bulbRef.current || !beamRef.current) return;
|
|
27
|
-
const t = state.clock.elapsedTime;
|
|
28
|
-
if (active) {
|
|
29
|
-
// Strobe pulse — faster when there's a high/critical incident.
|
|
30
|
-
const speed = critical ? 6 : 3;
|
|
31
|
-
const intensity = (Math.sin(t * speed) + 1) / 2;
|
|
32
|
-
const mat = bulbRef.current.material as { emissiveIntensity?: number };
|
|
33
|
-
if (mat.emissiveIntensity !== undefined) {
|
|
34
|
-
mat.emissiveIntensity = 0.4 + intensity * 1.6;
|
|
35
|
-
}
|
|
36
|
-
beamRef.current.rotation.y = t * (critical ? 4 : 2);
|
|
37
|
-
beamRef.current.visible = true;
|
|
38
|
-
} else {
|
|
39
|
-
const mat = bulbRef.current.material as { emissiveIntensity?: number };
|
|
40
|
-
if (mat.emissiveIntensity !== undefined) {
|
|
41
|
-
mat.emissiveIntensity = 0.05;
|
|
42
|
-
}
|
|
43
|
-
beamRef.current.visible = false;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const color = active ? (critical ? "#ef4444" : "#f59e0b") : "#3b4256";
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<group data-testid="ops-siren" position={[cx, 0, cz]}>
|
|
51
|
-
{/* Wall-mounted dome */}
|
|
52
|
-
<mesh position={[0, 1.05, 0]} castShadow>
|
|
53
|
-
<cylinderGeometry args={[0.12, 0.12, 0.06, 16]} />
|
|
54
|
-
<meshStandardMaterial color="#1a1f2e" roughness={0.6} />
|
|
55
|
-
</mesh>
|
|
56
|
-
<mesh ref={bulbRef} position={[0, 1.0, 0]}>
|
|
57
|
-
<sphereGeometry args={[0.14, 24, 24]} />
|
|
58
|
-
<meshStandardMaterial
|
|
59
|
-
color={color}
|
|
60
|
-
emissive={color}
|
|
61
|
-
emissiveIntensity={active ? 1.2 : 0.05}
|
|
62
|
-
roughness={0.3}
|
|
63
|
-
/>
|
|
64
|
-
</mesh>
|
|
65
|
-
{/* Sweeping beam — a thin cone made visible only when active. */}
|
|
66
|
-
<group ref={beamRef} position={[0, 0.95, 0]}>
|
|
67
|
-
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, 0, 0.6]}>
|
|
68
|
-
<coneGeometry args={[0.4, 1.2, 16, 1, true]} />
|
|
69
|
-
<meshBasicMaterial
|
|
70
|
-
color={color}
|
|
71
|
-
transparent
|
|
72
|
-
opacity={0.18}
|
|
73
|
-
depthWrite={false}
|
|
74
|
-
/>
|
|
75
|
-
</mesh>
|
|
76
|
-
</group>
|
|
77
|
-
{active && (
|
|
78
|
-
<Html
|
|
79
|
-
position={[0, 1.4, 0]}
|
|
80
|
-
transform
|
|
81
|
-
occlude={false}
|
|
82
|
-
scale={0.1}
|
|
83
|
-
pointerEvents="none"
|
|
84
|
-
>
|
|
85
|
-
<div
|
|
86
|
-
data-testid="ops-incident-badge"
|
|
87
|
-
className={`rounded px-1.5 py-0.5 text-[10px] font-mono uppercase ${
|
|
88
|
-
critical
|
|
89
|
-
? "bg-aura-alert text-black"
|
|
90
|
-
: "bg-aura-talking text-black"
|
|
91
|
-
}`}
|
|
92
|
-
>
|
|
93
|
-
{incidents.length} incident{incidents.length === 1 ? "" : "s"}
|
|
94
|
-
</div>
|
|
95
|
-
</Html>
|
|
96
|
-
)}
|
|
97
|
-
</group>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { Canvas } from "@react-three/fiber";
|
|
3
|
-
import { ContactShadows } from "@react-three/drei";
|
|
4
|
-
import type { AgentId, HarnessSnapshot, RoomId } from "@/lib/types";
|
|
5
|
-
import { Floor3D } from "./Floor3D";
|
|
6
|
-
import { Minifigs3D } from "./Minifig3D";
|
|
7
|
-
import { GoalCard3D } from "./GoalCard3D";
|
|
8
|
-
import { ArchiveBoxes } from "./ArchiveBoxes";
|
|
9
|
-
import { MeetingWhiteboard } from "./MeetingWhiteboard";
|
|
10
|
-
import { DeptBoard } from "./DeptBoard";
|
|
11
|
-
import { OpsSiren } from "./OpsSiren";
|
|
12
|
-
import { HypothesisCards } from "./HypothesisCards";
|
|
13
|
-
import { TrackRibbons } from "./TrackRibbons";
|
|
14
|
-
|
|
15
|
-
interface Stage3DProps {
|
|
16
|
-
snapshot: HarnessSnapshot;
|
|
17
|
-
lang?: "ko" | "en";
|
|
18
|
-
onAgentClick?: (id: AgentId) => void;
|
|
19
|
-
onRoomClick?: (id: RoomId) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Fixed isometric framing. R3F's built-in camera prop (with orthographic=true)
|
|
23
|
-
// auto-aims at the scene origin, which is exactly what we want.
|
|
24
|
-
export function Stage3D({ snapshot, lang = "ko", onAgentClick, onRoomClick }: Stage3DProps) {
|
|
25
|
-
const ctoRoom = snapshot.rooms.find((r) => r.id === "cto-team");
|
|
26
|
-
const cqoRoom = snapshot.rooms.find((r) => r.id === "cqo-team");
|
|
27
|
-
return (
|
|
28
|
-
<Canvas
|
|
29
|
-
orthographic
|
|
30
|
-
shadows
|
|
31
|
-
dpr={[1, 2]}
|
|
32
|
-
style={{
|
|
33
|
-
width: "100%",
|
|
34
|
-
height: "100%",
|
|
35
|
-
display: "block",
|
|
36
|
-
background: "#11151a",
|
|
37
|
-
}}
|
|
38
|
-
gl={{ antialias: true, preserveDrawingBuffer: true }}
|
|
39
|
-
camera={{ position: [14, 14, 14], zoom: 60, near: -80, far: 120 }}
|
|
40
|
-
data-testid="brick-office-canvas"
|
|
41
|
-
>
|
|
42
|
-
<ambientLight intensity={0.48} />
|
|
43
|
-
<hemisphereLight args={["#d7f8f0", "#2f342b", 0.42]} />
|
|
44
|
-
<directionalLight
|
|
45
|
-
position={[8, 14, 6]}
|
|
46
|
-
intensity={1.4}
|
|
47
|
-
castShadow
|
|
48
|
-
shadow-mapSize-width={2048}
|
|
49
|
-
shadow-mapSize-height={2048}
|
|
50
|
-
shadow-camera-near={1}
|
|
51
|
-
shadow-camera-far={40}
|
|
52
|
-
shadow-camera-left={-15}
|
|
53
|
-
shadow-camera-right={15}
|
|
54
|
-
shadow-camera-top={15}
|
|
55
|
-
shadow-camera-bottom={-15}
|
|
56
|
-
/>
|
|
57
|
-
|
|
58
|
-
{/* World grid spans x:[0..11] z:[0..9]; shift so the building (and its
|
|
59
|
-
surrounding lawn/corridors) center on the camera target. */}
|
|
60
|
-
<group position={[-5.5, 0, -4.5]}>
|
|
61
|
-
<Floor3D rooms={snapshot.rooms} lang={lang} onRoomClick={onRoomClick} />
|
|
62
|
-
<Minifigs3D agents={snapshot.agents} onAgentClick={onAgentClick} />
|
|
63
|
-
<GoalCard3D
|
|
64
|
-
goal={snapshot.goal}
|
|
65
|
-
lang={lang}
|
|
66
|
-
contract={snapshot.contract}
|
|
67
|
-
escalations={snapshot.escalations}
|
|
68
|
-
/>
|
|
69
|
-
<MeetingWhiteboard meetings={snapshot.meetings} tracks={snapshot.tracks} />
|
|
70
|
-
<TrackRibbons tracks={snapshot.tracks} />
|
|
71
|
-
<HypothesisCards hypothesis={snapshot.hypothesis} />
|
|
72
|
-
<OpsSiren incidents={snapshot.incidents} />
|
|
73
|
-
{ctoRoom && (
|
|
74
|
-
<DeptBoard
|
|
75
|
-
roomId="cto-team"
|
|
76
|
-
contract={snapshot.contract}
|
|
77
|
-
evalScores={snapshot.evalScores}
|
|
78
|
-
passRate={ctoRoom.metrics?.pass_rate ?? null}
|
|
79
|
-
openArchRisks={ctoRoom.metrics?.open_arch_risks}
|
|
80
|
-
/>
|
|
81
|
-
)}
|
|
82
|
-
{cqoRoom && (
|
|
83
|
-
<DeptBoard
|
|
84
|
-
roomId="cqo-team"
|
|
85
|
-
contract={snapshot.contract}
|
|
86
|
-
evalScores={snapshot.evalScores}
|
|
87
|
-
passRate={cqoRoom.metrics?.pass_rate ?? null}
|
|
88
|
-
openRegressions={cqoRoom.metrics?.open_regressions}
|
|
89
|
-
sprintVerdict={cqoRoom.metrics?.sprint_verdict}
|
|
90
|
-
/>
|
|
91
|
-
)}
|
|
92
|
-
<ArchiveBoxes archive={snapshot.archive} />
|
|
93
|
-
</group>
|
|
94
|
-
|
|
95
|
-
<ContactShadows
|
|
96
|
-
position={[0, 0.01, 0]}
|
|
97
|
-
opacity={0.3}
|
|
98
|
-
scale={6}
|
|
99
|
-
blur={1.5}
|
|
100
|
-
far={3}
|
|
101
|
-
/>
|
|
102
|
-
</Canvas>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { Html } from "@react-three/drei";
|
|
3
|
-
import { ROOM_RECTS } from "@/lib/iso";
|
|
4
|
-
import type { ParallelTrack, RoomId } from "@/lib/types";
|
|
5
|
-
|
|
6
|
-
interface TrackRibbonsProps {
|
|
7
|
-
tracks: ParallelTrack[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const STATUS_COLOR: Record<string, string> = {
|
|
11
|
-
dispatched: "#fbbf24",
|
|
12
|
-
in_progress: "#22d3ee",
|
|
13
|
-
joined: "#34d399",
|
|
14
|
-
blocked: "#ef4444",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
function roomCenter(roomId: RoomId): [number, number] | null {
|
|
18
|
-
const r = ROOM_RECTS.find((x) => x.id === roomId);
|
|
19
|
-
if (!r) return null;
|
|
20
|
-
return [r.wx + r.ww / 2, r.wy + r.wh / 2];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Floor ribbon connecting the meeting room to each fork target room. Each
|
|
24
|
-
// track is a flat plane at floor height, coloured by status. Renders a small
|
|
25
|
-
// floating tag at the midpoint with the track id.
|
|
26
|
-
export function TrackRibbons({ tracks }: TrackRibbonsProps) {
|
|
27
|
-
if (tracks.length === 0) return null;
|
|
28
|
-
const meetingCenter = roomCenter("meeting");
|
|
29
|
-
if (!meetingCenter) return null;
|
|
30
|
-
const [mx, mz] = meetingCenter;
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<group data-testid="track-ribbons">
|
|
34
|
-
{tracks.map((t) => {
|
|
35
|
-
const target = roomCenter(t.to_room);
|
|
36
|
-
if (!target) return null;
|
|
37
|
-
const [tx, tz] = target;
|
|
38
|
-
const dx = tx - mx;
|
|
39
|
-
const dz = tz - mz;
|
|
40
|
-
const length = Math.hypot(dx, dz);
|
|
41
|
-
if (length < 0.01) return null;
|
|
42
|
-
const yaw = Math.atan2(dx, dz);
|
|
43
|
-
const cx = (mx + tx) / 2;
|
|
44
|
-
const cz = (mz + tz) / 2;
|
|
45
|
-
const color = STATUS_COLOR[t.status] ?? STATUS_COLOR.dispatched;
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<group key={t.id}>
|
|
49
|
-
<mesh
|
|
50
|
-
position={[cx, 0.22, cz]}
|
|
51
|
-
rotation={[-Math.PI / 2, 0, -yaw]}
|
|
52
|
-
>
|
|
53
|
-
<planeGeometry args={[0.3, length]} />
|
|
54
|
-
<meshBasicMaterial
|
|
55
|
-
color={color}
|
|
56
|
-
transparent
|
|
57
|
-
opacity={t.status === "joined" ? 0.35 : 0.55}
|
|
58
|
-
depthWrite={false}
|
|
59
|
-
/>
|
|
60
|
-
</mesh>
|
|
61
|
-
<Html
|
|
62
|
-
position={[cx, 0.4, cz]}
|
|
63
|
-
transform
|
|
64
|
-
occlude={false}
|
|
65
|
-
scale={0.08}
|
|
66
|
-
pointerEvents="none"
|
|
67
|
-
>
|
|
68
|
-
<div
|
|
69
|
-
data-testid={`track-${t.id}`}
|
|
70
|
-
className="rounded bg-black/80 px-1.5 py-0.5 text-[10px] font-mono whitespace-nowrap"
|
|
71
|
-
style={{ color }}
|
|
72
|
-
>
|
|
73
|
-
{t.id} · {t.status}
|
|
74
|
-
</div>
|
|
75
|
-
</Html>
|
|
76
|
-
</group>
|
|
77
|
-
);
|
|
78
|
-
})}
|
|
79
|
-
</group>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { t } from "../i18n";
|
|
3
|
-
|
|
4
|
-
describe("i18n.t", () => {
|
|
5
|
-
it("returns ko by default", () => {
|
|
6
|
-
expect(t("drawer.tab.agent_log")).toBe("에이전트 로그");
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("returns en when lang=en", () => {
|
|
10
|
-
expect(t("drawer.tab.agent_log", "en")).toBe("Agent Log");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("falls back to the key when missing", () => {
|
|
14
|
-
expect(t("nonexistent.key", "en")).toBe("nonexistent.key");
|
|
15
|
-
});
|
|
16
|
-
});
|