@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.
- package/HR-Resource/ceo/SKILL.md +4 -4
- package/HR-Resource/cqo/SKILL.md +3 -3
- package/HR-Resource/cto/SKILL.md +4 -4
- package/README.md +58 -5
- 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 +214 -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 +520 -0
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +160 -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 +1193 -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/assets/templates/AGENTS-ko.md.template +19 -6
- package/assets/templates/AGENTS.md.template +20 -7
- package/assets/templates/HARNESS.md +75 -199
- package/assets/templates/config.json +2 -2
- package/bin/init.js +11 -11
- package/commands/goal.md +6 -4
- package/commands/hot-fix.md +16 -13
- package/commands/submission.md +28 -0
- package/conventions/ceo.md +3 -3
- package/conventions/shared.md +4 -3
- package/package.json +11 -1
- package/scripts/harness-dashboard-up.sh +24 -4
- package/scripts/harness-session-start.sh +1 -1
- package/scripts/harness-stop.sh +1 -1
- package/scripts/harness-user-prompt-submit.sh +12 -4
- package/scripts/harness-worker-evidence-validate.sh +8 -3
- package/scripts/import-agency-agents.js +1 -1
- package/scripts/init-agents-md.sh +17 -2
- 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
|
+
}
|