@walwal-harness/cli 7.1.8 → 7.1.9
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/README.md +40 -0
- package/apps/harness-dashboard/app/api/env/route.ts +62 -0
- package/apps/harness-dashboard/app/api/log/route.ts +19 -0
- package/apps/harness-dashboard/app/api/missions/route.ts +15 -0
- package/apps/harness-dashboard/app/api/snapshot/route.ts +15 -0
- package/apps/harness-dashboard/app/api/stream/route.ts +71 -0
- package/apps/harness-dashboard/app/globals.css +17 -0
- package/apps/harness-dashboard/app/layout.tsx +29 -0
- package/apps/harness-dashboard/app/page.tsx +35 -0
- package/apps/harness-dashboard/components/Drawer.tsx +97 -0
- package/apps/harness-dashboard/components/Header.tsx +40 -0
- package/apps/harness-dashboard/components/MissionTimeline.tsx +210 -0
- package/apps/harness-dashboard/components/OrgTree.tsx +330 -0
- package/apps/harness-dashboard/components/Scene.tsx +563 -0
- package/apps/harness-dashboard/components/Wordmark.tsx +14 -0
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +56 -0
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +89 -0
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +42 -0
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +50 -0
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +92 -0
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +517 -0
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +156 -0
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +90 -0
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +47 -0
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +95 -0
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +175 -0
- package/apps/harness-dashboard/components/three/Floor3D.tsx +404 -0
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +123 -0
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +71 -0
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +98 -0
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +391 -0
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +99 -0
- package/apps/harness-dashboard/components/three/Stage3D.tsx +104 -0
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +81 -0
- package/apps/harness-dashboard/hooks/useHarnessStream.ts +53 -0
- package/apps/harness-dashboard/lib/__tests__/company-flow.test.ts +270 -0
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +194 -0
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +16 -0
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +42 -0
- package/apps/harness-dashboard/lib/__tests__/safe-json.test.ts +46 -0
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +159 -0
- package/apps/harness-dashboard/lib/__tests__/watcher.test.ts +48 -0
- package/apps/harness-dashboard/lib/agent-roster.ts +42 -0
- package/apps/harness-dashboard/lib/furniture.ts +165 -0
- package/apps/harness-dashboard/lib/harness-root.ts +20 -0
- package/apps/harness-dashboard/lib/harness-state.ts +1162 -0
- package/apps/harness-dashboard/lib/i18n.ts +26 -0
- package/apps/harness-dashboard/lib/iso.ts +150 -0
- package/apps/harness-dashboard/lib/log-filter.ts +23 -0
- package/apps/harness-dashboard/lib/markdown.tsx +306 -0
- package/apps/harness-dashboard/lib/path-planning.ts +72 -0
- package/apps/harness-dashboard/lib/safe-json.ts +24 -0
- package/apps/harness-dashboard/lib/state-mapping.ts +126 -0
- package/apps/harness-dashboard/lib/types.ts +329 -0
- package/apps/harness-dashboard/lib/watcher.ts +64 -0
- package/apps/harness-dashboard/next-env.d.ts +6 -0
- package/apps/harness-dashboard/next.config.mjs +9 -0
- package/apps/harness-dashboard/package-lock.json +4280 -0
- package/apps/harness-dashboard/package.json +38 -0
- package/apps/harness-dashboard/playwright.config.ts +23 -0
- package/apps/harness-dashboard/postcss.config.js +6 -0
- package/apps/harness-dashboard/tailwind.config.ts +49 -0
- package/apps/harness-dashboard/tsconfig.json +24 -0
- package/apps/harness-dashboard/vitest.config.ts +15 -0
- package/package.json +11 -1
- package/scripts/harness-dashboard-up.sh +24 -4
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { AgentId, HarnessSnapshot, MinifigState, OrgNodeDef } from "@/lib/types";
|
|
3
|
+
|
|
4
|
+
interface OrgTreeProps {
|
|
5
|
+
snapshot: HarnessSnapshot;
|
|
6
|
+
activeNodeId: string | null;
|
|
7
|
+
onNodeClick: (node: OrgNodeDef) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function StatusDot({ status }: { status: MinifigState }) {
|
|
11
|
+
const cls = {
|
|
12
|
+
"red-alert": "bg-rose-500 animate-ping",
|
|
13
|
+
typing: "bg-cyan-400 animate-pulse",
|
|
14
|
+
talking: "bg-emerald-400",
|
|
15
|
+
queued: "bg-amber-400",
|
|
16
|
+
idle: "bg-gray-600",
|
|
17
|
+
}[status] ?? "bg-gray-600";
|
|
18
|
+
return <span className={`absolute top-2 right-2 h-2 w-2 rounded-full ${cls}`} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function OrgCard({
|
|
22
|
+
node,
|
|
23
|
+
size = "md",
|
|
24
|
+
active,
|
|
25
|
+
onClick,
|
|
26
|
+
}: {
|
|
27
|
+
node: OrgNodeDef;
|
|
28
|
+
size?: "lg" | "md" | "sm";
|
|
29
|
+
active: boolean;
|
|
30
|
+
onClick: (n: OrgNodeDef) => void;
|
|
31
|
+
}) {
|
|
32
|
+
const roleColor = {
|
|
33
|
+
owner: "border-amber-500/50 bg-amber-500/5",
|
|
34
|
+
ceo: "border-cyan-400/50 bg-cyan-400/5",
|
|
35
|
+
coo: "border-emerald-400/40 bg-emerald-500/5",
|
|
36
|
+
cdo: "border-purple-400/40 bg-purple-500/5",
|
|
37
|
+
cto: "border-blue-400/40 bg-blue-500/5",
|
|
38
|
+
cqo: "border-amber-400/40 bg-amber-500/5",
|
|
39
|
+
ops: "border-rose-400/40 bg-rose-500/5",
|
|
40
|
+
}[node.role] ?? "border-gray-600/40 bg-gray-800/30";
|
|
41
|
+
|
|
42
|
+
const widths = {
|
|
43
|
+
lg: "min-w-[220px] max-w-[260px]",
|
|
44
|
+
md: "min-w-[150px] max-w-[190px]",
|
|
45
|
+
sm: "min-w-[130px] max-w-[160px]",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onClick(node)}
|
|
52
|
+
className={`relative rounded-lg border p-3 text-left transition-all cursor-pointer ${roleColor} ${widths[size]} ${active ? "ring-2 ring-cyan-300 border-cyan-300/60" : "hover:border-white/30"}`}
|
|
53
|
+
>
|
|
54
|
+
<StatusDot status={node.status} />
|
|
55
|
+
<div className="font-mono text-[9px] uppercase tracking-[0.2em] text-gray-500 mb-0.5">{node.role}</div>
|
|
56
|
+
<div className="text-sm font-semibold text-gray-100 leading-tight">{node.label}</div>
|
|
57
|
+
{node.sublabel && (
|
|
58
|
+
<div className="text-[10px] text-gray-500 mt-0.5">{node.sublabel}</div>
|
|
59
|
+
)}
|
|
60
|
+
{node.activity && (
|
|
61
|
+
<div className="mt-2 text-[10px] text-gray-400 line-clamp-2 leading-relaxed">
|
|
62
|
+
{node.activity}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function BranchConnector({ count }: { count: number }) {
|
|
70
|
+
const h = 44;
|
|
71
|
+
const stepPct = 100 / count;
|
|
72
|
+
const midY = h * 0.5;
|
|
73
|
+
const centers = Array.from({ length: count }, (_, i) => stepPct * i + stepPct / 2);
|
|
74
|
+
return (
|
|
75
|
+
<svg width="100%" height={h} className="overflow-visible pointer-events-none">
|
|
76
|
+
<line x1="50%" y1="0" x2="50%" y2={midY} stroke="#374151" strokeWidth="1.5" />
|
|
77
|
+
<line
|
|
78
|
+
x1={`${centers[0]}%`}
|
|
79
|
+
y1={midY}
|
|
80
|
+
x2={`${centers[centers.length - 1]}%`}
|
|
81
|
+
y2={midY}
|
|
82
|
+
stroke="#374151"
|
|
83
|
+
strokeWidth="1.5"
|
|
84
|
+
/>
|
|
85
|
+
{centers.map((x, i) => (
|
|
86
|
+
<line
|
|
87
|
+
key={i}
|
|
88
|
+
x1={`${x}%`}
|
|
89
|
+
y1={midY}
|
|
90
|
+
x2={`${x}%`}
|
|
91
|
+
y2={h}
|
|
92
|
+
stroke="#374151"
|
|
93
|
+
strokeWidth="1.5"
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</svg>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function OrgTree({ snapshot, activeNodeId, onNodeClick }: OrgTreeProps) {
|
|
101
|
+
const agentMap = new Map(snapshot.agents.map((a) => [a.id, a]));
|
|
102
|
+
|
|
103
|
+
const getStatus = (ids: AgentId[]): MinifigState => {
|
|
104
|
+
const states = ids.map((id) => agentMap.get(id)?.minifigState ?? "idle");
|
|
105
|
+
if (states.includes("red-alert")) return "red-alert";
|
|
106
|
+
if (states.includes("typing")) return "typing";
|
|
107
|
+
if (states.includes("talking")) return "talking";
|
|
108
|
+
if (states.includes("queued")) return "queued";
|
|
109
|
+
return "idle";
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const currentMission = snapshot.missions?.[0] ?? null;
|
|
113
|
+
|
|
114
|
+
// Parse CEO activity from mission doc
|
|
115
|
+
const ceoActivity = (() => {
|
|
116
|
+
if (!currentMission?.ceo) return null;
|
|
117
|
+
const match = currentMission.ceo.match(/##\s*Owner 요청 요약\s*\n([\s\S]*?)(?=\n##)/);
|
|
118
|
+
return (
|
|
119
|
+
match?.[1]
|
|
120
|
+
?.split("\n")
|
|
121
|
+
.find((l) => l.trim().length > 0)
|
|
122
|
+
?.replace(/^[-*\d.]\s*/, "")
|
|
123
|
+
.trim()
|
|
124
|
+
.slice(0, 100) ?? null
|
|
125
|
+
);
|
|
126
|
+
})();
|
|
127
|
+
|
|
128
|
+
const ownerNode: OrgNodeDef = {
|
|
129
|
+
id: "owner",
|
|
130
|
+
role: "owner",
|
|
131
|
+
label: "Owner",
|
|
132
|
+
sublabel: "/goal · /hot-fix",
|
|
133
|
+
status: "idle",
|
|
134
|
+
activity: snapshot.ownerHistory?.[0]?.content.slice(0, 80) ?? null,
|
|
135
|
+
agentIds: [],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const ceoNode: OrgNodeDef = {
|
|
139
|
+
id: "ceo",
|
|
140
|
+
role: "ceo",
|
|
141
|
+
label: "harness-ceo",
|
|
142
|
+
sublabel: "Dispatcher · Brainstormer",
|
|
143
|
+
status: getStatus(["dispatcher", "brainstormer"] as AgentId[]),
|
|
144
|
+
activity: ceoActivity,
|
|
145
|
+
agentIds: ["dispatcher", "brainstormer"] as AgentId[],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const cxxNodes: OrgNodeDef[] = [
|
|
149
|
+
{
|
|
150
|
+
id: "coo",
|
|
151
|
+
role: "coo",
|
|
152
|
+
label: "harness-coo",
|
|
153
|
+
sublabel: "Planner · Research",
|
|
154
|
+
status: getStatus(["planner", "coo-developer", "documentationer"] as AgentId[]),
|
|
155
|
+
activity: null,
|
|
156
|
+
agentIds: ["planner", "coo-developer", "documentationer"] as AgentId[],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "cdo",
|
|
160
|
+
role: "cdo",
|
|
161
|
+
label: "harness-cdo",
|
|
162
|
+
sublabel: "Design · Branding",
|
|
163
|
+
status: "idle",
|
|
164
|
+
activity: null,
|
|
165
|
+
agentIds: [],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "cto",
|
|
169
|
+
role: "cto",
|
|
170
|
+
label: "harness-cto",
|
|
171
|
+
sublabel: "Conductor · Generators",
|
|
172
|
+
status: getStatus([
|
|
173
|
+
"cto",
|
|
174
|
+
"conductor",
|
|
175
|
+
"generator-backend",
|
|
176
|
+
"generator-frontend",
|
|
177
|
+
"generator-designer",
|
|
178
|
+
"generator-devops",
|
|
179
|
+
] as AgentId[]),
|
|
180
|
+
activity: currentMission?.cto ? "Architecture plan active" : null,
|
|
181
|
+
agentIds: [
|
|
182
|
+
"cto",
|
|
183
|
+
"conductor",
|
|
184
|
+
"generator-backend",
|
|
185
|
+
"generator-frontend",
|
|
186
|
+
"generator-designer",
|
|
187
|
+
"generator-devops",
|
|
188
|
+
] as AgentId[],
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "cqo",
|
|
192
|
+
role: "cqo",
|
|
193
|
+
label: "harness-cqo",
|
|
194
|
+
sublabel: "Evaluators",
|
|
195
|
+
status: getStatus([
|
|
196
|
+
"cqo",
|
|
197
|
+
"evaluator-functional",
|
|
198
|
+
"evaluator-visual",
|
|
199
|
+
"evaluator-code-quality",
|
|
200
|
+
"evaluator-architecture",
|
|
201
|
+
"evaluator-security",
|
|
202
|
+
] as AgentId[]),
|
|
203
|
+
activity: currentMission?.cqo?.includes("ACCEPTED")
|
|
204
|
+
? "ACCEPTED"
|
|
205
|
+
: currentMission?.cqo
|
|
206
|
+
? "Report available"
|
|
207
|
+
: null,
|
|
208
|
+
agentIds: [
|
|
209
|
+
"cqo",
|
|
210
|
+
"evaluator-functional",
|
|
211
|
+
"evaluator-visual",
|
|
212
|
+
"evaluator-code-quality",
|
|
213
|
+
"evaluator-architecture",
|
|
214
|
+
"evaluator-security",
|
|
215
|
+
] as AgentId[],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
id: "ops",
|
|
219
|
+
role: "ops",
|
|
220
|
+
label: "harness-ops",
|
|
221
|
+
sublabel: "Service Monitor",
|
|
222
|
+
status: getStatus(["service-ops"] as AgentId[]),
|
|
223
|
+
activity: null,
|
|
224
|
+
agentIds: ["service-ops"] as AgentId[],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// Workers from current mission's workers/, shown under CTO column
|
|
229
|
+
const workers = currentMission?.workers ?? [];
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className="w-full px-4 py-6 select-none">
|
|
233
|
+
{/* Mission label */}
|
|
234
|
+
{currentMission && (
|
|
235
|
+
<div className="mb-4 text-center">
|
|
236
|
+
<span className="font-mono text-[10px] uppercase tracking-[0.2em] text-cyan-300/60">
|
|
237
|
+
current mission
|
|
238
|
+
</span>
|
|
239
|
+
<div className="mt-0.5 font-mono text-xs text-gray-400">{currentMission.missionId}</div>
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
|
|
243
|
+
{/* Row 1: Owner */}
|
|
244
|
+
<div className="flex justify-center">
|
|
245
|
+
<OrgCard
|
|
246
|
+
node={ownerNode}
|
|
247
|
+
size="lg"
|
|
248
|
+
active={activeNodeId === "owner"}
|
|
249
|
+
onClick={onNodeClick}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Connector: Owner → CEO */}
|
|
254
|
+
<div className="flex justify-center">
|
|
255
|
+
<div className="w-px h-6 bg-gray-700" />
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Row 2: CEO */}
|
|
259
|
+
<div className="flex justify-center">
|
|
260
|
+
<OrgCard
|
|
261
|
+
node={ceoNode}
|
|
262
|
+
size="lg"
|
|
263
|
+
active={activeNodeId === "ceo"}
|
|
264
|
+
onClick={onNodeClick}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{/* Branch connector: CEO → CXX */}
|
|
269
|
+
<BranchConnector count={cxxNodes.length} />
|
|
270
|
+
|
|
271
|
+
{/* Row 3: CXX nodes with workers below */}
|
|
272
|
+
<div className="flex gap-3 justify-center">
|
|
273
|
+
{cxxNodes.map((node) => {
|
|
274
|
+
const nodeWorkers = node.role === "cto" ? workers : [];
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div key={node.id} className="flex flex-col items-center gap-2">
|
|
278
|
+
<OrgCard
|
|
279
|
+
node={node}
|
|
280
|
+
size="md"
|
|
281
|
+
active={activeNodeId === node.id}
|
|
282
|
+
onClick={onNodeClick}
|
|
283
|
+
/>
|
|
284
|
+
{/* Worker mini-cards under CTO */}
|
|
285
|
+
{nodeWorkers.length > 0 && (
|
|
286
|
+
<>
|
|
287
|
+
<div className="w-px h-3 bg-gray-700/60" />
|
|
288
|
+
<div className="flex flex-col gap-1.5 w-full">
|
|
289
|
+
{nodeWorkers.map((w) => (
|
|
290
|
+
<button
|
|
291
|
+
key={w.name}
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={() =>
|
|
294
|
+
onNodeClick({
|
|
295
|
+
id: `worker-${w.name}`,
|
|
296
|
+
role: "cto",
|
|
297
|
+
label: w.name,
|
|
298
|
+
sublabel: w.status,
|
|
299
|
+
status: w.status === "COMPLETE" ? "idle" : "typing",
|
|
300
|
+
activity: null,
|
|
301
|
+
agentIds: [],
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
className={`rounded border border-gray-700/40 bg-gray-800/30 px-2 py-1.5 text-left min-w-[130px] max-w-[160px] transition-all hover:border-white/20 ${
|
|
305
|
+
activeNodeId === `worker-${w.name}` ? "ring-1 ring-cyan-300" : ""
|
|
306
|
+
}`}
|
|
307
|
+
>
|
|
308
|
+
<div className="font-mono text-[9px] text-gray-500">worker</div>
|
|
309
|
+
<div className="text-[11px] font-medium text-gray-200 truncate">
|
|
310
|
+
{w.name}
|
|
311
|
+
</div>
|
|
312
|
+
<div
|
|
313
|
+
className={`text-[9px] font-mono mt-0.5 ${
|
|
314
|
+
w.status === "COMPLETE" ? "text-emerald-400" : "text-amber-400"
|
|
315
|
+
}`}
|
|
316
|
+
>
|
|
317
|
+
{w.status}
|
|
318
|
+
</div>
|
|
319
|
+
</button>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
</>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|