@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.
Files changed (69) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  11. package/apps/harness-dashboard/components/Scene.tsx +568 -35
  12. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  13. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  14. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  15. package/apps/harness-dashboard/lib/types.ts +22 -222
  16. package/apps/harness-dashboard/playwright.config.ts +26 -7
  17. package/assets/templates/AGENTS-ko.md.template +1 -1
  18. package/assets/templates/AGENTS.md.template +12 -2
  19. package/assets/templates/HARNESS.md +2 -0
  20. package/assets/templates/company-pipeline-manifest.json +5 -14
  21. package/assets/templates/worker-report.md.template +38 -0
  22. package/bin/init.js +2 -1
  23. package/commands/goal.md +4 -2
  24. package/commands/hot-fix.md +1 -1
  25. package/commands/submission.md +1 -1
  26. package/package.json +1 -1
  27. package/scripts/conductor-tick.sh +12 -0
  28. package/scripts/harness-agenda.sh +105 -0
  29. package/scripts/harness-company-block.sh +89 -0
  30. package/scripts/harness-company-complete.sh +17 -8
  31. package/scripts/harness-company-cycle.sh +54 -0
  32. package/scripts/harness-progress-set.sh +8 -2
  33. package/scripts/harness-stop.sh +76 -0
  34. package/scripts/harness-wake-control.sh +151 -0
  35. package/scripts/harness-wake.sh +5 -0
  36. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  37. package/apps/harness-dashboard/components/Header.tsx +0 -40
  38. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  39. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  40. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  41. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  42. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  43. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  44. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  45. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  46. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  47. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  48. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  49. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  50. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  51. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  52. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  53. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  54. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  55. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  56. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  57. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  58. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  59. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  60. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  61. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  62. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  63. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  64. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  65. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  66. package/apps/harness-dashboard/lib/iso.ts +0 -150
  67. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  68. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  69. 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
- }