@walwal-harness/cli 7.1.8 → 7.1.10

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 (86) hide show
  1. package/HR-Resource/ceo/SKILL.md +4 -4
  2. package/HR-Resource/cqo/SKILL.md +3 -3
  3. package/HR-Resource/cto/SKILL.md +4 -4
  4. package/README.md +58 -5
  5. package/apps/harness-dashboard/app/api/env/route.ts +62 -0
  6. package/apps/harness-dashboard/app/api/log/route.ts +19 -0
  7. package/apps/harness-dashboard/app/api/missions/route.ts +15 -0
  8. package/apps/harness-dashboard/app/api/snapshot/route.ts +15 -0
  9. package/apps/harness-dashboard/app/api/stream/route.ts +71 -0
  10. package/apps/harness-dashboard/app/globals.css +17 -0
  11. package/apps/harness-dashboard/app/layout.tsx +29 -0
  12. package/apps/harness-dashboard/app/page.tsx +35 -0
  13. package/apps/harness-dashboard/components/Drawer.tsx +97 -0
  14. package/apps/harness-dashboard/components/Header.tsx +40 -0
  15. package/apps/harness-dashboard/components/MissionTimeline.tsx +214 -0
  16. package/apps/harness-dashboard/components/OrgTree.tsx +330 -0
  17. package/apps/harness-dashboard/components/Scene.tsx +563 -0
  18. package/apps/harness-dashboard/components/Wordmark.tsx +14 -0
  19. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +56 -0
  20. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +89 -0
  21. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +42 -0
  22. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +50 -0
  23. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +92 -0
  24. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +520 -0
  25. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +160 -0
  26. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +90 -0
  27. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +47 -0
  28. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +95 -0
  29. package/apps/harness-dashboard/components/three/DeptBoard.tsx +175 -0
  30. package/apps/harness-dashboard/components/three/Floor3D.tsx +404 -0
  31. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +123 -0
  32. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +71 -0
  33. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +98 -0
  34. package/apps/harness-dashboard/components/three/Minifig3D.tsx +391 -0
  35. package/apps/harness-dashboard/components/three/OpsSiren.tsx +99 -0
  36. package/apps/harness-dashboard/components/three/Stage3D.tsx +104 -0
  37. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +81 -0
  38. package/apps/harness-dashboard/hooks/useHarnessStream.ts +53 -0
  39. package/apps/harness-dashboard/lib/__tests__/company-flow.test.ts +270 -0
  40. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +194 -0
  41. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +16 -0
  42. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +42 -0
  43. package/apps/harness-dashboard/lib/__tests__/safe-json.test.ts +46 -0
  44. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +159 -0
  45. package/apps/harness-dashboard/lib/__tests__/watcher.test.ts +48 -0
  46. package/apps/harness-dashboard/lib/agent-roster.ts +42 -0
  47. package/apps/harness-dashboard/lib/furniture.ts +165 -0
  48. package/apps/harness-dashboard/lib/harness-root.ts +20 -0
  49. package/apps/harness-dashboard/lib/harness-state.ts +1193 -0
  50. package/apps/harness-dashboard/lib/i18n.ts +26 -0
  51. package/apps/harness-dashboard/lib/iso.ts +150 -0
  52. package/apps/harness-dashboard/lib/log-filter.ts +23 -0
  53. package/apps/harness-dashboard/lib/markdown.tsx +306 -0
  54. package/apps/harness-dashboard/lib/path-planning.ts +72 -0
  55. package/apps/harness-dashboard/lib/safe-json.ts +24 -0
  56. package/apps/harness-dashboard/lib/state-mapping.ts +126 -0
  57. package/apps/harness-dashboard/lib/types.ts +329 -0
  58. package/apps/harness-dashboard/lib/watcher.ts +64 -0
  59. package/apps/harness-dashboard/next-env.d.ts +6 -0
  60. package/apps/harness-dashboard/next.config.mjs +9 -0
  61. package/apps/harness-dashboard/package-lock.json +4280 -0
  62. package/apps/harness-dashboard/package.json +38 -0
  63. package/apps/harness-dashboard/playwright.config.ts +23 -0
  64. package/apps/harness-dashboard/postcss.config.js +6 -0
  65. package/apps/harness-dashboard/tailwind.config.ts +49 -0
  66. package/apps/harness-dashboard/tsconfig.json +24 -0
  67. package/apps/harness-dashboard/vitest.config.ts +15 -0
  68. package/assets/templates/AGENTS-ko.md.template +19 -6
  69. package/assets/templates/AGENTS.md.template +20 -7
  70. package/assets/templates/HARNESS.md +75 -199
  71. package/assets/templates/config.json +2 -2
  72. package/bin/init.js +11 -11
  73. package/commands/goal.md +6 -4
  74. package/commands/hot-fix.md +16 -13
  75. package/commands/submission.md +28 -0
  76. package/conventions/ceo.md +3 -3
  77. package/conventions/shared.md +4 -3
  78. package/package.json +11 -1
  79. package/scripts/harness-dashboard-up.sh +24 -4
  80. package/scripts/harness-session-start.sh +1 -1
  81. package/scripts/harness-stop.sh +1 -1
  82. package/scripts/harness-user-prompt-submit.sh +12 -4
  83. package/scripts/harness-worker-evidence-validate.sh +8 -3
  84. package/scripts/import-agency-agents.js +1 -1
  85. package/scripts/init-agents-md.sh +17 -2
  86. package/assets/templates/AGENTS-ko /341/204/207/341/205/251/341/206/250/341/204/211/341/205/241/341/204/207/341/205/251/341/206/253.md" +0 -114
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ import type { MissionDoc } from "@/lib/types";
3
+
4
+ interface Props {
5
+ missions: MissionDoc[];
6
+ activeMissionId: string | null;
7
+ onMissionClick: (mission: MissionDoc) => void;
8
+ }
9
+
10
+ type ChipRole =
11
+ | "owner"
12
+ | "ceo"
13
+ | "coo"
14
+ | "cdo"
15
+ | "cto"
16
+ | "cqo"
17
+ | "ops"
18
+ | "worker";
19
+
20
+ function chipColor(role: ChipRole): string {
21
+ switch (role) {
22
+ case "owner":
23
+ return "bg-amber-500/15 text-amber-300 border-amber-500/30";
24
+ case "ceo":
25
+ return "bg-cyan-400/15 text-cyan-300 border-cyan-400/30";
26
+ case "coo":
27
+ return "bg-emerald-500/15 text-emerald-300 border-emerald-500/30";
28
+ case "cdo":
29
+ return "bg-purple-500/15 text-purple-300 border-purple-500/30";
30
+ case "cto":
31
+ return "bg-blue-400/15 text-blue-300 border-blue-400/30";
32
+ case "cqo":
33
+ return "bg-amber-400/15 text-amber-300 border-amber-400/30";
34
+ case "ops":
35
+ return "bg-rose-400/15 text-rose-300 border-rose-400/30";
36
+ case "worker":
37
+ return "bg-gray-500/15 text-gray-300 border-gray-500/30";
38
+ default:
39
+ return "bg-gray-500/15 text-gray-400 border-gray-600/30";
40
+ }
41
+ }
42
+
43
+ function Chip({
44
+ role,
45
+ label,
46
+ badge,
47
+ }: {
48
+ role: ChipRole;
49
+ label: string;
50
+ badge?: string;
51
+ }) {
52
+ return (
53
+ <span
54
+ className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[9px] ${chipColor(role)}`}
55
+ >
56
+ {label}
57
+ {badge && <span className="opacity-80">{badge}</span>}
58
+ </span>
59
+ );
60
+ }
61
+
62
+ function Arrow() {
63
+ return <span className="text-[10px] text-gray-600 shrink-0">→</span>;
64
+ }
65
+
66
+ function buildFlowChain(mission: MissionDoc): React.ReactNode {
67
+ // Pre-worker CXX: planning phase (before implementation)
68
+ const preWorker: Array<{ key: "coo" | "cdo" | "cto"; role: ChipRole }> = [
69
+ { key: "coo", role: "coo" },
70
+ { key: "cdo", role: "cdo" },
71
+ { key: "cto", role: "cto" },
72
+ ];
73
+ // Post-worker CXX: verification phase (after implementation)
74
+ const postWorker: Array<{ key: "cqo" | "ops"; role: ChipRole }> = [
75
+ { key: "cqo", role: "cqo" },
76
+ { key: "ops", role: "ops" },
77
+ ];
78
+
79
+ const isCqoAccepted =
80
+ typeof mission.cqo === "string" && mission.cqo.includes("ACCEPTED");
81
+
82
+ const chips: React.ReactNode[] = [];
83
+ let key = 0;
84
+
85
+ chips.push(<Chip key={key++} role="owner" label="Owner" />);
86
+ chips.push(<Arrow key={key++} />);
87
+ chips.push(<Chip key={key++} role="ceo" label="CEO" />);
88
+
89
+ // Planning phase: COO, CDO, CTO
90
+ for (const { key: roleKey, role } of preWorker) {
91
+ if (mission.cxxPresent.includes(roleKey)) {
92
+ chips.push(<Arrow key={key++} />);
93
+ chips.push(<Chip key={key++} role={role} label={roleKey.toUpperCase()} />);
94
+ }
95
+ }
96
+
97
+ // CTO-owned implementation workers. Legacy flat workers remain visible here
98
+ // with owner=unknown so protocol bypasses are not hidden.
99
+ for (const worker of mission.workers.filter((w) => w.owner === "cto" || w.owner === "unknown")) {
100
+ chips.push(<Arrow key={key++} />);
101
+ const badge = worker.status === "COMPLETE" ? "✅" : "⏳";
102
+ const shortName =
103
+ worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
104
+ chips.push(
105
+ <Chip key={key++} role="worker" label={shortName} badge={badge} />
106
+ );
107
+ }
108
+
109
+ // Verification phase: CQO, OPS
110
+ for (const { key: roleKey, role } of postWorker) {
111
+ if (mission.cxxPresent.includes(roleKey)) {
112
+ chips.push(<Arrow key={key++} />);
113
+ const badge = roleKey === "cqo" && isCqoAccepted ? "✅" : undefined;
114
+ chips.push(
115
+ <Chip key={key++} role={role} label={roleKey.toUpperCase()} badge={badge} />
116
+ );
117
+ if (roleKey === "cqo") {
118
+ for (const worker of mission.workers.filter((w) => w.owner === "cqo")) {
119
+ chips.push(<Arrow key={key++} />);
120
+ const workerBadge = worker.status === "COMPLETE" ? "✅" : "⏳";
121
+ const shortName =
122
+ worker.name.length > 12 ? worker.name.slice(0, 12) + "…" : worker.name;
123
+ chips.push(
124
+ <Chip key={key++} role="worker" label={shortName} badge={workerBadge} />
125
+ );
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ return (
132
+ <div className="flex flex-wrap items-center gap-1 min-w-0">{chips}</div>
133
+ );
134
+ }
135
+
136
+ export function MissionTimeline({
137
+ missions,
138
+ activeMissionId,
139
+ onMissionClick,
140
+ }: Props) {
141
+ if (!missions.length) {
142
+ return (
143
+ <div className="rounded border border-dashed border-gray-700 px-3 py-4 text-center text-[11px] text-gray-500">
144
+ No missions found in .harness/documents/
145
+ </div>
146
+ );
147
+ }
148
+
149
+ return (
150
+ <div className="space-y-1.5">
151
+ {missions.map((mission) => {
152
+ const isActive = mission.missionId === activeMissionId;
153
+ const typeBadge =
154
+ mission.type === "hotfix"
155
+ ? "[🔥 hot-fix]"
156
+ : mission.type === "submission"
157
+ ? "[+ submission]"
158
+ : mission.type === "goal" || mission.type === "feature"
159
+ ? "[✦ goal]"
160
+ : "[— unknown]";
161
+ const shortId =
162
+ mission.missionId.length > 28
163
+ ? mission.missionId.slice(0, 28) + "…"
164
+ : mission.missionId;
165
+
166
+ let dateStr = "—";
167
+ try {
168
+ dateStr = new Date(mission.ts).toLocaleDateString(undefined, {
169
+ month: "short",
170
+ day: "numeric",
171
+ });
172
+ } catch {
173
+ // ignore invalid date
174
+ }
175
+
176
+ return (
177
+ <button
178
+ key={mission.missionId}
179
+ type="button"
180
+ onClick={() => onMissionClick(mission)}
181
+ className={`w-full rounded border px-3 py-2.5 text-left transition-colors ${
182
+ isActive
183
+ ? "border-cyan-400/40 bg-cyan-400/5"
184
+ : "border-gray-700/40 bg-black/10 hover:border-gray-600/50"
185
+ }`}
186
+ >
187
+ <div className="flex items-start justify-between gap-2 mb-1.5">
188
+ <div className="flex items-center gap-2 min-w-0">
189
+ <span
190
+ className={`shrink-0 font-mono text-[9px] rounded px-1.5 py-0.5 border ${
191
+ mission.type === "hotfix"
192
+ ? "bg-rose-500/15 text-rose-300 border-rose-500/30"
193
+ : mission.type === "submission"
194
+ ? "bg-sky-500/15 text-sky-300 border-sky-500/30"
195
+ : "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
196
+ }`}
197
+ >
198
+ {typeBadge}
199
+ </span>
200
+ <span className="font-mono text-[10px] text-gray-200 truncate">
201
+ {shortId}
202
+ </span>
203
+ </div>
204
+ <span className="shrink-0 font-mono text-[9px] text-gray-500">
205
+ {dateStr}
206
+ </span>
207
+ </div>
208
+ {buildFlowChain(mission)}
209
+ </button>
210
+ );
211
+ })}
212
+ </div>
213
+ );
214
+ }
@@ -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 · /submission · /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
+ }