@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,576 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
import type { MissionDoc, OwnerPromptEntry, RuntimeSnapshot } from "@/lib/types";
|
|
4
|
-
import { MarkdownView } from "@/lib/markdown";
|
|
5
|
-
|
|
6
|
-
type DocView = "overview" | "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops" | string;
|
|
7
|
-
|
|
8
|
-
interface Props {
|
|
9
|
-
mission: MissionDoc | null;
|
|
10
|
-
ownerHistory: OwnerPromptEntry[];
|
|
11
|
-
runtime: RuntimeSnapshot;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Section extractors
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/** Extract up to maxLines non-empty lines from a ## section */
|
|
19
|
-
function extractSection(content: string, heading: string, maxLines = 3): string | null {
|
|
20
|
-
const body = content.replace(/^---[\s\S]*?---\n+/, "");
|
|
21
|
-
const pattern = new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "m");
|
|
22
|
-
const match = pattern.exec(body);
|
|
23
|
-
if (!match) return null;
|
|
24
|
-
const start = match.index + match[0].length;
|
|
25
|
-
const rest = body.slice(start);
|
|
26
|
-
const lines = rest.split("\n");
|
|
27
|
-
const snippet: string[] = [];
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
if (line.startsWith("## ") || line.startsWith("# ")) break;
|
|
30
|
-
const trimmed = line.trim();
|
|
31
|
-
if (trimmed && !trimmed.startsWith("---")) snippet.push(trimmed);
|
|
32
|
-
if (snippet.length >= maxLines) break;
|
|
33
|
-
}
|
|
34
|
-
const result = snippet.join(" · ").slice(0, 240);
|
|
35
|
-
return result || null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Try multiple heading alternatives, return first match */
|
|
39
|
-
function extractSectionAny(content: string, ...headings: string[]): string | null {
|
|
40
|
-
for (const h of headings) {
|
|
41
|
-
const r = extractSection(content, h, 4);
|
|
42
|
-
if (r) return r;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Parse markdown table rows from "생성/수정 파일" or "구현 파일 목록" sections */
|
|
48
|
-
function extractFilesTable(content: string, maxRows = 5): Array<{ file: string; action: string }> {
|
|
49
|
-
const body = content.replace(/^---[\s\S]*?---\n+/, "");
|
|
50
|
-
const lines = body.split("\n");
|
|
51
|
-
const results: Array<{ file: string; action: string }> = [];
|
|
52
|
-
let inFileTable = false;
|
|
53
|
-
|
|
54
|
-
for (const line of lines) {
|
|
55
|
-
// Detect table header with "파일" column
|
|
56
|
-
if (line.startsWith("|") && (line.includes("파일") || line.includes("File"))) {
|
|
57
|
-
inFileTable = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (inFileTable) {
|
|
61
|
-
if (line.trim() === "" || (!line.startsWith("|") && !line.startsWith("|-"))) {
|
|
62
|
-
inFileTable = false;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (line.includes("---")) continue; // divider row
|
|
66
|
-
if (!line.startsWith("|")) continue;
|
|
67
|
-
const cells = line.split("|").map((c) => c.trim()).filter(Boolean);
|
|
68
|
-
if (cells.length < 1) continue;
|
|
69
|
-
const file = cells[0].replace(/`/g, "").trim();
|
|
70
|
-
if (!file || file.toLowerCase() === "파일" || file.toLowerCase() === "file") continue;
|
|
71
|
-
// Truncate long action text
|
|
72
|
-
const raw = cells[1]?.replace(/`/g, "").trim() ?? "";
|
|
73
|
-
const action = raw.length > 60 ? raw.slice(0, 60) + "…" : raw;
|
|
74
|
-
results.push({ file, action });
|
|
75
|
-
if (results.length >= maxRows) break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return results;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Extract CTO's worker instruction section */
|
|
82
|
-
function extractCtoWorkerInstructions(content: string): string | null {
|
|
83
|
-
return extractSection(content, "워커 지시", 6);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// Chip helpers
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
function StepChip({
|
|
90
|
-
role,
|
|
91
|
-
label,
|
|
92
|
-
onClick,
|
|
93
|
-
}: {
|
|
94
|
-
role: string;
|
|
95
|
-
label: string;
|
|
96
|
-
onClick?: () => void;
|
|
97
|
-
}) {
|
|
98
|
-
const colorMap: Record<string, string> = {
|
|
99
|
-
owner: "bg-amber-500/15 text-amber-300 border-amber-500/30",
|
|
100
|
-
ceo: "bg-cyan-400/15 text-cyan-300 border-cyan-400/30",
|
|
101
|
-
coo: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30",
|
|
102
|
-
cdo: "bg-purple-500/15 text-purple-300 border-purple-500/30",
|
|
103
|
-
cto: "bg-blue-400/15 text-blue-300 border-blue-400/30",
|
|
104
|
-
cqo: "bg-amber-400/15 text-amber-300 border-amber-400/30",
|
|
105
|
-
ops: "bg-rose-400/15 text-rose-300 border-rose-400/30",
|
|
106
|
-
worker: "bg-gray-500/15 text-gray-300 border-gray-500/30",
|
|
107
|
-
};
|
|
108
|
-
const cls = colorMap[role] ?? "bg-gray-600/15 text-gray-400 border-gray-600/30";
|
|
109
|
-
return (
|
|
110
|
-
<span
|
|
111
|
-
className={`inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[9px] ${cls} ${onClick ? "cursor-pointer hover:opacity-80" : ""}`}
|
|
112
|
-
onClick={onClick}
|
|
113
|
-
>
|
|
114
|
-
{label}
|
|
115
|
-
</span>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// DocRow: one step in the flow tree
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
function DocRow({
|
|
123
|
-
step,
|
|
124
|
-
roleLabel,
|
|
125
|
-
roleKey,
|
|
126
|
-
snippet,
|
|
127
|
-
onViewDoc,
|
|
128
|
-
}: {
|
|
129
|
-
step: number;
|
|
130
|
-
roleLabel: string;
|
|
131
|
-
roleKey: string;
|
|
132
|
-
snippet?: string | null;
|
|
133
|
-
onViewDoc?: () => void;
|
|
134
|
-
}) {
|
|
135
|
-
return (
|
|
136
|
-
<div className="relative pl-4 border-l border-gray-700/40 ml-2 space-y-1">
|
|
137
|
-
<div className="flex items-center justify-between gap-2">
|
|
138
|
-
<div className="flex items-center gap-1.5">
|
|
139
|
-
<span className="font-mono text-[9px] text-gray-600">{step}</span>
|
|
140
|
-
<StepChip role={roleKey} label={roleLabel} />
|
|
141
|
-
</div>
|
|
142
|
-
{onViewDoc && (
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
onClick={onViewDoc}
|
|
146
|
-
className="shrink-0 font-mono text-[9px] text-cyan-400/70 hover:text-cyan-300 transition-colors"
|
|
147
|
-
>
|
|
148
|
-
View Doc →
|
|
149
|
-
</button>
|
|
150
|
-
)}
|
|
151
|
-
</div>
|
|
152
|
-
{snippet && (
|
|
153
|
-
<p className="text-[10px] text-gray-500 leading-relaxed pl-5 line-clamp-3">
|
|
154
|
-
{snippet}
|
|
155
|
-
</p>
|
|
156
|
-
)}
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// WorkerCard: shows worker + file list
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
function WorkerCard({
|
|
165
|
-
worker,
|
|
166
|
-
onViewDoc,
|
|
167
|
-
active,
|
|
168
|
-
}: {
|
|
169
|
-
worker: { name: string; content: string; status: string; owner?: string };
|
|
170
|
-
onViewDoc?: () => void;
|
|
171
|
-
active?: boolean;
|
|
172
|
-
}) {
|
|
173
|
-
const files = extractFilesTable(worker.content, 4);
|
|
174
|
-
return (
|
|
175
|
-
<div
|
|
176
|
-
className={`relative pl-3 border-l border-gray-700/30 ml-2 space-y-1 pb-1 ${active ? "border-cyan-400/40" : ""}`}
|
|
177
|
-
>
|
|
178
|
-
<div className="flex items-center justify-between gap-2">
|
|
179
|
-
<div className="flex items-center gap-1.5">
|
|
180
|
-
<span className="font-mono text-[9px] text-gray-600">├─</span>
|
|
181
|
-
<StepChip role="worker" label={worker.name} />
|
|
182
|
-
<span
|
|
183
|
-
className={`font-mono text-[9px] ${
|
|
184
|
-
worker.status === "COMPLETE" ? "text-emerald-400" : "text-amber-400"
|
|
185
|
-
}`}
|
|
186
|
-
>
|
|
187
|
-
{worker.status === "COMPLETE" ? "COMPLETE ✅" : "⏳"}
|
|
188
|
-
</span>
|
|
189
|
-
</div>
|
|
190
|
-
{onViewDoc && (
|
|
191
|
-
<button
|
|
192
|
-
type="button"
|
|
193
|
-
onClick={onViewDoc}
|
|
194
|
-
className="shrink-0 font-mono text-[9px] text-cyan-400/70 hover:text-cyan-300 transition-colors"
|
|
195
|
-
>
|
|
196
|
-
View Doc →
|
|
197
|
-
</button>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
{/* Changed files */}
|
|
202
|
-
{files.length > 0 && (
|
|
203
|
-
<div className="pl-5 space-y-0.5">
|
|
204
|
-
{files.map((f, i) => (
|
|
205
|
-
<div key={i} className="flex items-start gap-1.5">
|
|
206
|
-
<span className="font-mono text-[9px] text-gray-600 shrink-0 mt-0.5">▸</span>
|
|
207
|
-
<div className="min-w-0">
|
|
208
|
-
<span className="font-mono text-[9px] text-blue-300/80 break-all leading-tight">
|
|
209
|
-
{f.file}
|
|
210
|
-
</span>
|
|
211
|
-
{f.action && (
|
|
212
|
-
<span className="font-mono text-[9px] text-gray-600 ml-1">— {f.action}</span>
|
|
213
|
-
)}
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
))}
|
|
217
|
-
</div>
|
|
218
|
-
)}
|
|
219
|
-
</div>
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// Main component
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
function RuntimeStrip({ runtime }: { runtime: RuntimeSnapshot }) {
|
|
227
|
-
const isActive = runtime.agentStatus === "running";
|
|
228
|
-
const command = runtime.ownerPrompt?.command ?? "input";
|
|
229
|
-
const summary = runtime.ownerPrompt?.summary ?? null;
|
|
230
|
-
return (
|
|
231
|
-
<div
|
|
232
|
-
data-testid="runtime-strip"
|
|
233
|
-
data-current-agent={runtime.currentAgent ?? ""}
|
|
234
|
-
data-agent-status={runtime.agentStatus}
|
|
235
|
-
className={`rounded border px-2.5 py-2 ${
|
|
236
|
-
isActive
|
|
237
|
-
? "border-cyan-400/30 bg-cyan-400/10"
|
|
238
|
-
: runtime.agentStatus === "blocked" || runtime.agentStatus === "failed"
|
|
239
|
-
? "border-rose-400/30 bg-rose-400/10"
|
|
240
|
-
: "border-gray-700/60 bg-black/20"
|
|
241
|
-
}`}
|
|
242
|
-
>
|
|
243
|
-
<div className="flex flex-wrap items-center gap-1.5">
|
|
244
|
-
<span className="font-mono text-[9px] uppercase tracking-[0.18em] text-gray-500">
|
|
245
|
-
live runtime
|
|
246
|
-
</span>
|
|
247
|
-
<StepChip role={runtime.currentAgent ?? "worker"} label={runtime.currentAgent ?? "none"} />
|
|
248
|
-
<span
|
|
249
|
-
className={`font-mono text-[9px] ${
|
|
250
|
-
isActive ? "text-cyan-300" : "text-gray-400"
|
|
251
|
-
}`}
|
|
252
|
-
>
|
|
253
|
-
{runtime.agentStatus}
|
|
254
|
-
</span>
|
|
255
|
-
{runtime.nextAgent && (
|
|
256
|
-
<span className="font-mono text-[9px] text-gray-500">
|
|
257
|
-
next: {runtime.nextAgent}
|
|
258
|
-
</span>
|
|
259
|
-
)}
|
|
260
|
-
</div>
|
|
261
|
-
{summary && (
|
|
262
|
-
<p className="mt-1.5 text-[10px] leading-relaxed text-gray-400 line-clamp-2">
|
|
263
|
-
<span className="font-mono text-gray-500">{command}</span>
|
|
264
|
-
<span className="text-gray-600"> · </span>
|
|
265
|
-
{summary}
|
|
266
|
-
</p>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function MissionFlowTab({ mission, ownerHistory, runtime }: Props) {
|
|
273
|
-
const [docView, setDocView] = useState<DocView>("overview");
|
|
274
|
-
|
|
275
|
-
useEffect(() => {
|
|
276
|
-
setDocView("overview");
|
|
277
|
-
}, [mission?.missionId]);
|
|
278
|
-
|
|
279
|
-
if (!mission) {
|
|
280
|
-
return (
|
|
281
|
-
<div className="space-y-3">
|
|
282
|
-
<RuntimeStrip runtime={runtime} />
|
|
283
|
-
<div className="text-gray-500 text-xs">
|
|
284
|
-
No mission selected. Click a mission from the timeline below.
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// -- Doc view mode --
|
|
291
|
-
if (docView !== "overview") {
|
|
292
|
-
let content: string | null = null;
|
|
293
|
-
let docTitle = docView;
|
|
294
|
-
|
|
295
|
-
if (["ceo", "cto", "cqo", "coo", "cdo", "ops"].includes(docView)) {
|
|
296
|
-
content = mission[docView as "ceo" | "cto" | "cqo" | "coo" | "cdo" | "ops"];
|
|
297
|
-
docTitle = `harness-${docView}`;
|
|
298
|
-
} else {
|
|
299
|
-
const w = mission.workers.find((x) => x.name === docView);
|
|
300
|
-
content = w?.content ?? null;
|
|
301
|
-
docTitle = docView;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<div className="space-y-3">
|
|
306
|
-
<RuntimeStrip runtime={runtime} />
|
|
307
|
-
<button
|
|
308
|
-
type="button"
|
|
309
|
-
onClick={() => setDocView("overview")}
|
|
310
|
-
className="flex items-center gap-1.5 font-mono text-[10px] text-cyan-400/70 hover:text-cyan-300 transition-colors"
|
|
311
|
-
>
|
|
312
|
-
← Back
|
|
313
|
-
</button>
|
|
314
|
-
<div className="font-mono text-[10px] text-gray-500">
|
|
315
|
-
{mission.missionId} · {docTitle}
|
|
316
|
-
</div>
|
|
317
|
-
{content ? (
|
|
318
|
-
<MarkdownView source={content} />
|
|
319
|
-
) : (
|
|
320
|
-
<div className="text-gray-500 text-xs">No document available.</div>
|
|
321
|
-
)}
|
|
322
|
-
</div>
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// -- Overview mode --
|
|
327
|
-
|
|
328
|
-
// Find owner prompt closest to mission date
|
|
329
|
-
let ownerPromptSnippet: string | null = null;
|
|
330
|
-
if (ownerHistory.length > 0) {
|
|
331
|
-
const missionTime = Date.parse(mission.ts);
|
|
332
|
-
let closest: OwnerPromptEntry | null = null;
|
|
333
|
-
let closestDiff = Infinity;
|
|
334
|
-
for (const entry of ownerHistory) {
|
|
335
|
-
const t = Date.parse(entry.ts);
|
|
336
|
-
if (!Number.isNaN(t)) {
|
|
337
|
-
const diff = Math.abs(t - missionTime);
|
|
338
|
-
if (diff < closestDiff) { closestDiff = diff; closest = entry; }
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
if (closest) ownerPromptSnippet = closest.content.slice(0, 140);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const typeBadge =
|
|
345
|
-
mission.type === "hotfix" ? "[🔥 hot-fix]"
|
|
346
|
-
: mission.type === "submission" ? "[+ submission]"
|
|
347
|
-
: mission.type === "goal" || mission.type === "feature" ? "[✦ goal]"
|
|
348
|
-
: "[— unknown]";
|
|
349
|
-
|
|
350
|
-
let dateStr = "—";
|
|
351
|
-
try {
|
|
352
|
-
dateStr = new Date(mission.ts).toLocaleDateString(undefined, {
|
|
353
|
-
year: "numeric", month: "short", day: "numeric",
|
|
354
|
-
});
|
|
355
|
-
} catch { /* ignore */ }
|
|
356
|
-
|
|
357
|
-
// Routing breadcrumb
|
|
358
|
-
const routeChips: string[] = ["Owner", "CEO"];
|
|
359
|
-
const cxxOrder: Array<"coo" | "cdo" | "cto" | "cqo" | "ops"> = ["coo", "cdo", "cto", "cqo", "ops"];
|
|
360
|
-
for (const r of cxxOrder) {
|
|
361
|
-
if (mission.cxxPresent.includes(r)) routeChips.push(r.toUpperCase());
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// CEO snippets
|
|
365
|
-
const ceoRequestSnippet = mission.ceo
|
|
366
|
-
? extractSectionAny(mission.ceo, "Owner 요청 요약", "Owner Request")
|
|
367
|
-
: null;
|
|
368
|
-
const ceoDecisionSnippet = mission.ceo
|
|
369
|
-
? extractSectionAny(mission.ceo, "CEO 결정", "CXX Decisions")
|
|
370
|
-
: null;
|
|
371
|
-
const ceoSnippet = [ceoRequestSnippet, ceoDecisionSnippet].filter(Boolean).join(" | ").slice(0, 240) || null;
|
|
372
|
-
|
|
373
|
-
// CTO snippet + worker instructions
|
|
374
|
-
const ctoSnippet = mission.cto
|
|
375
|
-
? extractSectionAny(mission.cto, "변경 요약", "Summary")
|
|
376
|
-
: null;
|
|
377
|
-
const ctoWorkerInstructions = mission.cto
|
|
378
|
-
? extractCtoWorkerInstructions(mission.cto)
|
|
379
|
-
: null;
|
|
380
|
-
|
|
381
|
-
// CQO verdict
|
|
382
|
-
let cqoVerdict: string | null = null;
|
|
383
|
-
if (mission.cqo) {
|
|
384
|
-
if (mission.cqo.includes("ACCEPTED")) cqoVerdict = "검증 결과: ACCEPTED ✅";
|
|
385
|
-
else if (mission.cqo.includes("REJECTED")) cqoVerdict = "검증 결과: REJECTED ❌";
|
|
386
|
-
else cqoVerdict = extractSectionAny(mission.cqo, "검증 결과", "Verdict") ?? null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
let stepNum = 1;
|
|
390
|
-
|
|
391
|
-
return (
|
|
392
|
-
<div className="space-y-3">
|
|
393
|
-
<RuntimeStrip runtime={runtime} />
|
|
394
|
-
{/* Mission header */}
|
|
395
|
-
<div>
|
|
396
|
-
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
397
|
-
<span
|
|
398
|
-
className={`font-mono text-[9px] rounded border px-1.5 py-0.5 ${
|
|
399
|
-
mission.type === "hotfix"
|
|
400
|
-
? "bg-rose-500/15 text-rose-300 border-rose-500/30"
|
|
401
|
-
: mission.type === "submission"
|
|
402
|
-
? "bg-sky-500/15 text-sky-300 border-sky-500/30"
|
|
403
|
-
: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30"
|
|
404
|
-
}`}
|
|
405
|
-
>
|
|
406
|
-
{typeBadge}
|
|
407
|
-
</span>
|
|
408
|
-
<span className="font-mono text-[10px] text-gray-300 truncate flex-1 min-w-0">
|
|
409
|
-
{mission.missionId}
|
|
410
|
-
</span>
|
|
411
|
-
<span className="font-mono text-[9px] text-gray-500 shrink-0">{dateStr}</span>
|
|
412
|
-
</div>
|
|
413
|
-
{/* Routing breadcrumb */}
|
|
414
|
-
<div className="flex flex-wrap items-center gap-1 mt-1">
|
|
415
|
-
{routeChips.map((label, idx) => {
|
|
416
|
-
const roleKey = label === "Owner" ? "owner" : label === "CEO" ? "ceo" : label.toLowerCase();
|
|
417
|
-
return (
|
|
418
|
-
<span key={idx} className="flex items-center gap-1">
|
|
419
|
-
{idx > 0 && <span className="text-gray-600 text-[9px]">→</span>}
|
|
420
|
-
<StepChip role={roleKey} label={label} />
|
|
421
|
-
</span>
|
|
422
|
-
);
|
|
423
|
-
})}
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
|
|
427
|
-
{/* Flow tree */}
|
|
428
|
-
<div className="space-y-2">
|
|
429
|
-
{/* OWNER */}
|
|
430
|
-
<DocRow step={stepNum++} roleLabel="OWNER" roleKey="owner" />
|
|
431
|
-
{ownerPromptSnippet && (
|
|
432
|
-
<p className="pl-7 text-[10px] text-gray-500 italic leading-relaxed line-clamp-2">
|
|
433
|
-
{ownerPromptSnippet}{ownerPromptSnippet.length >= 140 ? "…" : ""}
|
|
434
|
-
</p>
|
|
435
|
-
)}
|
|
436
|
-
|
|
437
|
-
{/* CEO */}
|
|
438
|
-
<div className="pl-3 space-y-1">
|
|
439
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ dispatched to</p>
|
|
440
|
-
<DocRow
|
|
441
|
-
step={stepNum++}
|
|
442
|
-
roleLabel="harness-ceo"
|
|
443
|
-
roleKey="ceo"
|
|
444
|
-
snippet={ceoSnippet}
|
|
445
|
-
onViewDoc={mission.ceo ? () => setDocView("ceo") : undefined}
|
|
446
|
-
/>
|
|
447
|
-
{/* Legacy flat workers: protocol violation / unknown owner */}
|
|
448
|
-
{mission.workers.some((w) => w.owner === "unknown") && (
|
|
449
|
-
<div className="pl-4 space-y-2 mt-1">
|
|
450
|
-
<p className="font-mono text-[9px] text-rose-300/80 pl-1">
|
|
451
|
-
Unowned legacy workers (CEO/CXX bypass suspected):
|
|
452
|
-
</p>
|
|
453
|
-
{mission.workers.filter((w) => w.owner === "unknown").map((worker) => (
|
|
454
|
-
<WorkerCard
|
|
455
|
-
key={worker.name}
|
|
456
|
-
worker={worker}
|
|
457
|
-
onViewDoc={worker.content ? () => setDocView(worker.name) : undefined}
|
|
458
|
-
/>
|
|
459
|
-
))}
|
|
460
|
-
</div>
|
|
461
|
-
)}
|
|
462
|
-
</div>
|
|
463
|
-
|
|
464
|
-
{/* COO */}
|
|
465
|
-
{mission.cxxPresent.includes("coo") && (
|
|
466
|
-
<div className="pl-6 space-y-1">
|
|
467
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ routed to</p>
|
|
468
|
-
<DocRow
|
|
469
|
-
step={stepNum++}
|
|
470
|
-
roleLabel="harness-coo"
|
|
471
|
-
roleKey="coo"
|
|
472
|
-
snippet={mission.coo ? extractSectionAny(mission.coo, "요약", "Summary") : null}
|
|
473
|
-
onViewDoc={mission.coo ? () => setDocView("coo") : undefined}
|
|
474
|
-
/>
|
|
475
|
-
</div>
|
|
476
|
-
)}
|
|
477
|
-
|
|
478
|
-
{/* CDO */}
|
|
479
|
-
{mission.cxxPresent.includes("cdo") && (
|
|
480
|
-
<div className="pl-6 space-y-1">
|
|
481
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ routed to</p>
|
|
482
|
-
<DocRow
|
|
483
|
-
step={stepNum++}
|
|
484
|
-
roleLabel="harness-cdo"
|
|
485
|
-
roleKey="cdo"
|
|
486
|
-
snippet={mission.cdo ? extractSectionAny(mission.cdo, "요약", "Summary") : null}
|
|
487
|
-
onViewDoc={mission.cdo ? () => setDocView("cdo") : undefined}
|
|
488
|
-
/>
|
|
489
|
-
</div>
|
|
490
|
-
)}
|
|
491
|
-
|
|
492
|
-
{/* CTO + Workers */}
|
|
493
|
-
{mission.cxxPresent.includes("cto") && (
|
|
494
|
-
<div className="pl-6 space-y-1">
|
|
495
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ routed to</p>
|
|
496
|
-
<DocRow
|
|
497
|
-
step={stepNum++}
|
|
498
|
-
roleLabel="harness-cto"
|
|
499
|
-
roleKey="cto"
|
|
500
|
-
snippet={ctoSnippet}
|
|
501
|
-
onViewDoc={mission.cto ? () => setDocView("cto") : undefined}
|
|
502
|
-
/>
|
|
503
|
-
|
|
504
|
-
{/* CTO worker instructions */}
|
|
505
|
-
{ctoWorkerInstructions && (
|
|
506
|
-
<div className="pl-7 mt-1">
|
|
507
|
-
<div className="rounded border border-blue-400/15 bg-blue-400/5 px-2 py-1.5">
|
|
508
|
-
<span className="font-mono text-[9px] text-blue-300/50 uppercase tracking-wider">
|
|
509
|
-
워커 지시
|
|
510
|
-
</span>
|
|
511
|
-
<p className="text-[10px] text-gray-400 leading-relaxed mt-0.5">
|
|
512
|
-
{ctoWorkerInstructions}
|
|
513
|
-
</p>
|
|
514
|
-
</div>
|
|
515
|
-
</div>
|
|
516
|
-
)}
|
|
517
|
-
|
|
518
|
-
{/* Workers */}
|
|
519
|
-
{mission.workers.some((w) => w.owner === "cto") && (
|
|
520
|
-
<div className="pl-4 space-y-2 mt-1">
|
|
521
|
-
<p className="font-mono text-[9px] text-gray-500 pl-1">CTO workers:</p>
|
|
522
|
-
{mission.workers.filter((w) => w.owner === "cto").map((worker) => (
|
|
523
|
-
<WorkerCard
|
|
524
|
-
key={worker.name}
|
|
525
|
-
worker={worker}
|
|
526
|
-
onViewDoc={worker.content ? () => setDocView(worker.name) : undefined}
|
|
527
|
-
/>
|
|
528
|
-
))}
|
|
529
|
-
</div>
|
|
530
|
-
)}
|
|
531
|
-
</div>
|
|
532
|
-
)}
|
|
533
|
-
|
|
534
|
-
{/* OPS */}
|
|
535
|
-
{mission.cxxPresent.includes("ops") && (
|
|
536
|
-
<div className="pl-6 space-y-1">
|
|
537
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ routed to</p>
|
|
538
|
-
<DocRow
|
|
539
|
-
step={stepNum++}
|
|
540
|
-
roleLabel="harness-ops"
|
|
541
|
-
roleKey="ops"
|
|
542
|
-
snippet={mission.ops ? extractSectionAny(mission.ops, "요약", "Summary") : null}
|
|
543
|
-
onViewDoc={mission.ops ? () => setDocView("ops") : undefined}
|
|
544
|
-
/>
|
|
545
|
-
</div>
|
|
546
|
-
)}
|
|
547
|
-
|
|
548
|
-
{/* CQO */}
|
|
549
|
-
{mission.cxxPresent.includes("cqo") && (
|
|
550
|
-
<div className="pl-6 space-y-1">
|
|
551
|
-
<p className="font-mono text-[9px] text-gray-600 pl-2">↓ routed to</p>
|
|
552
|
-
<DocRow
|
|
553
|
-
step={stepNum++}
|
|
554
|
-
roleLabel="harness-cqo"
|
|
555
|
-
roleKey="cqo"
|
|
556
|
-
snippet={cqoVerdict}
|
|
557
|
-
onViewDoc={mission.cqo ? () => setDocView("cqo") : undefined}
|
|
558
|
-
/>
|
|
559
|
-
{mission.workers.some((w) => w.owner === "cqo") && (
|
|
560
|
-
<div className="pl-4 space-y-2 mt-1">
|
|
561
|
-
<p className="font-mono text-[9px] text-gray-500 pl-1">CQO evaluators:</p>
|
|
562
|
-
{mission.workers.filter((w) => w.owner === "cqo").map((worker) => (
|
|
563
|
-
<WorkerCard
|
|
564
|
-
key={worker.name}
|
|
565
|
-
worker={worker}
|
|
566
|
-
onViewDoc={worker.content ? () => setDocView(worker.name) : undefined}
|
|
567
|
-
/>
|
|
568
|
-
))}
|
|
569
|
-
</div>
|
|
570
|
-
)}
|
|
571
|
-
</div>
|
|
572
|
-
)}
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
);
|
|
576
|
-
}
|