@walwal-harness/cli 7.1.39 → 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 (74) 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 +20 -2
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +105 -0
  8. package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
  9. package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
  10. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  11. package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
  12. package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
  13. package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
  14. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +381 -0
  15. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  16. package/apps/harness-dashboard/components/Scene.tsx +568 -35
  17. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  18. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  19. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  20. package/apps/harness-dashboard/lib/types.ts +22 -222
  21. package/apps/harness-dashboard/playwright.config.ts +26 -7
  22. package/assets/templates/AGENTS-ko.md.template +1 -1
  23. package/assets/templates/AGENTS.md.template +12 -2
  24. package/assets/templates/HARNESS.md +2 -0
  25. package/assets/templates/company-pipeline-manifest.json +5 -14
  26. package/assets/templates/worker-report.md.template +38 -0
  27. package/bin/init.js +4 -3
  28. package/commands/goal.md +4 -2
  29. package/commands/hot-fix.md +1 -1
  30. package/commands/submission.md +1 -1
  31. package/package.json +1 -1
  32. package/scripts/conductor-tick.sh +12 -0
  33. package/scripts/harness-agenda.sh +105 -0
  34. package/scripts/harness-company-block.sh +89 -0
  35. package/scripts/harness-company-complete.sh +17 -8
  36. package/scripts/harness-company-cycle.sh +54 -0
  37. package/scripts/harness-progress-set.sh +8 -2
  38. package/scripts/harness-stop.sh +76 -0
  39. package/scripts/harness-wake-control.sh +151 -0
  40. package/scripts/harness-wake.sh +5 -0
  41. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  42. package/apps/harness-dashboard/components/Header.tsx +0 -40
  43. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  44. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  45. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  46. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  47. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  48. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  49. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  50. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  51. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  52. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  53. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  54. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  55. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  56. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  57. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  58. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  59. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  60. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  61. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  62. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  63. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  64. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  65. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  66. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  67. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  68. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  69. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  70. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  71. package/apps/harness-dashboard/lib/iso.ts +0 -150
  72. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  73. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  74. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -7,6 +7,7 @@ import type {
7
7
  ConventionEntry,
8
8
  CxxTodo,
9
9
  GotchaEntry,
10
+ HarnessFileEntry,
10
11
  HarnessSnapshot,
11
12
  MissionDoc,
12
13
  OwnerPromptEntry,
@@ -53,6 +54,14 @@ type HeatSample = {
53
54
  };
54
55
 
55
56
  type GoalGroup = { goal: MissionDoc; children: MissionDoc[] };
57
+ type HealthTone = "green" | "orange" | "red" | "cyan" | "slate";
58
+ type RuntimeHealth = {
59
+ label: string;
60
+ detail: string;
61
+ tone: HealthTone;
62
+ ageMs: number | null;
63
+ lastSignalAt: string | null;
64
+ };
56
65
 
57
66
  const CXX_ROLES = ["ceo", "coo", "cdo", "cto", "cqo", "ops"] as const;
58
67
  type CxxRole = typeof CXX_ROLES[number];
@@ -220,10 +229,12 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
220
229
  title: string;
221
230
  content: string;
222
231
  } | null>(null);
232
+ const [selectedHarnessPath, setSelectedHarnessPath] = useState<string | null>(null);
223
233
 
224
234
  // Selecting a mission or lane returns the viewer to mission-context mode.
225
235
  useEffect(() => {
226
236
  setCustomDoc(null);
237
+ setSelectedHarnessPath(null);
227
238
  }, [selectedMissionId, selectedLaneId]);
228
239
 
229
240
  void lang;
@@ -233,12 +244,35 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
233
244
  const activeWorkers =
234
245
  (snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0])?.workers.filter((w) => w.active).length ?? 0;
235
246
  const hotfixCount = snapshot.missions.filter((m) => m.type === "hotfix").length;
247
+ const runtimeHealth = useMemo(() => deriveRuntimeHealth(snapshot), [snapshot]);
248
+ const highlightedPaths = useMemo(
249
+ () => buildHighlightedPaths(selectedMission, selectedLane, snapshot.todos),
250
+ [selectedMission, selectedLane, snapshot.todos]
251
+ );
236
252
 
237
253
  const handleSelectWorker = (mission: MissionDoc, worker: WorkerDocEntry) => {
238
254
  setSelectedMissionId(mission.missionId);
239
255
  setSelectedLaneId(`${worker.owner}:${worker.name}`);
240
256
  };
241
257
 
258
+ const handleSelectHarnessFile = (entry: HarnessFileEntry) => {
259
+ setSelectedHarnessPath(entry.path);
260
+ setCustomDoc({
261
+ source: "Harness file",
262
+ title: entry.path,
263
+ content: [
264
+ `# ${entry.path}`,
265
+ "",
266
+ `- kind: ${entry.kind}`,
267
+ `- category: ${entry.category}`,
268
+ `- updated: ${entry.updatedAt ?? "(unknown)"}`,
269
+ `- size: ${entry.size === null ? "(directory)" : `${entry.size} bytes`}`,
270
+ "",
271
+ "Select a mission, CXX lane, or worker report to inspect document contents.",
272
+ ].join("\n"),
273
+ });
274
+ };
275
+
242
276
  return (
243
277
  <div className="flex h-[100dvh] flex-col overflow-hidden bg-[#070d1d] text-slate-200">
244
278
  <TopHeader
@@ -261,24 +295,23 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
261
295
  />
262
296
 
263
297
  <main className="flex min-w-0 flex-1 flex-col gap-2 p-2">
264
- {/* Top row: Knowledge | Layer Activity | Recent Report */}
265
- <section className="grid shrink-0 grid-cols-3 gap-2" style={{ height: 280 }}>
266
- <KnowledgeBasePanel
267
- gotchas={snapshot.gotchas}
268
- conventions={snapshot.conventions}
269
- baselineTs={selectedGoal?.ts ?? null}
270
- onSelect={(entry, kind) =>
271
- setCustomDoc({
272
- source: kind,
273
- title: `${entry.id}.md`,
274
- content: entry.content,
275
- })
276
- }
298
+ {/* Top row: Owner situational awareness */}
299
+ <section className="grid shrink-0 grid-cols-4 gap-2" style={{ height: 310 }}>
300
+ <OperationalStatusPanel
301
+ snapshot={snapshot}
302
+ health={runtimeHealth}
303
+ mission={selectedMission}
277
304
  />
278
- <LayerActivityPanel
279
- goalScope={goalScopeMissions}
305
+ <StepProgressPanel
306
+ mission={selectedMission}
307
+ runtimeAgent={snapshot.runtime.currentAgent}
280
308
  todos={snapshot.todos}
281
- label={selectedGoal?.missionId ?? "no goal"}
309
+ />
310
+ <HarnessFileMapPanel
311
+ files={snapshot.files}
312
+ highlightedPaths={highlightedPaths}
313
+ selectedPath={selectedHarnessPath}
314
+ onSelect={handleSelectHarnessFile}
282
315
  />
283
316
  <RecentReportPanel
284
317
  missions={snapshot.missions}
@@ -384,12 +417,82 @@ function TopHeader({
384
417
  />
385
418
  <HeaderBadge label="Workers" value={String(activeWorkers)} tone="cyan" />
386
419
  <HeaderBadge label="Hot-fix" value={String(hotfixCount)} tone="red" />
420
+ <WakeToggle />
387
421
  <PresenceLegend />
388
422
  </div>
389
423
  </header>
390
424
  );
391
425
  }
392
426
 
427
+ // Toggles the per-project hourly wake launchd job (keyed by HARNESS_BASE_PORT)
428
+ // that drives the perpetual operating loop across sessions.
429
+ interface WakeStatus {
430
+ supported?: boolean;
431
+ enabled?: boolean;
432
+ base_port?: number | string | null;
433
+ label?: string;
434
+ error?: string;
435
+ }
436
+
437
+ function WakeToggle() {
438
+ const [status, setStatus] = useState<WakeStatus | null>(null);
439
+ const [busy, setBusy] = useState(false);
440
+
441
+ useEffect(() => {
442
+ let alive = true;
443
+ fetch("/api/wake")
444
+ .then((r) => r.json())
445
+ .then((s) => { if (alive) setStatus(s); })
446
+ .catch(() => { if (alive) setStatus({ supported: false }); });
447
+ return () => { alive = false; };
448
+ }, []);
449
+
450
+ const toggle = async () => {
451
+ if (busy || !status?.supported) return;
452
+ setBusy(true);
453
+ try {
454
+ const r = await fetch("/api/wake", {
455
+ method: "POST",
456
+ headers: { "content-type": "application/json" },
457
+ body: JSON.stringify({ action: status?.enabled ? "off" : "on" }),
458
+ });
459
+ setStatus(await r.json());
460
+ } catch {
461
+ /* keep prior status */
462
+ } finally {
463
+ setBusy(false);
464
+ }
465
+ };
466
+
467
+ if (!status) return null;
468
+ const supported = !!status.supported;
469
+ const on = !!status.enabled;
470
+ const port = status.base_port ? `:${status.base_port}` : "";
471
+ const tone = !supported
472
+ ? "border-slate-600/30 bg-slate-600/10 text-slate-500"
473
+ : on
474
+ ? "border-emerald-400/40 bg-emerald-400/10 text-emerald-200"
475
+ : "border-slate-500/30 bg-slate-500/10 text-slate-300";
476
+ return (
477
+ <button
478
+ type="button"
479
+ onClick={toggle}
480
+ disabled={busy || !supported}
481
+ title={
482
+ supported
483
+ ? `Hourly wake launchd job ${status.label ?? ""} — click to turn ${on ? "off" : "on"} (drives the perpetual operating loop across sessions)`
484
+ : "launchd not available on this OS"
485
+ }
486
+ className={`rounded-full border px-2.5 py-1 font-mono text-[10px] transition-colors ${tone} ${
487
+ supported ? "cursor-pointer hover:brightness-125" : "cursor-default"
488
+ } ${busy ? "opacity-60" : ""}`}
489
+ >
490
+ <span className="text-slate-500">Wake</span>
491
+ <span className="ml-1.5">{!supported ? "n/a" : on ? `● on${port}` : `○ off${port}`}</span>
492
+ </button>
493
+ );
494
+ }
495
+
393
496
  function HeaderBadge({
394
497
  label,
395
498
  value,
@@ -565,14 +668,22 @@ function HistoryNavigator({
565
668
  <button
566
669
  type="button"
567
670
  onClick={() => onSelect(goal.missionId)}
568
- className={`w-full truncate px-2 py-2 text-left ${
671
+ className={`flex w-full items-center gap-1 px-2 py-2 text-left ${
569
672
  activeMissionId === goal.missionId
570
673
  ? "bg-cyan-400/[0.12] text-cyan-100"
571
674
  : "text-slate-300 hover:bg-white/[0.04]"
572
675
  }`}
573
676
  title={goal.label || goal.missionId}
574
677
  >
575
- {goal.label || goal.missionId}
678
+ {goal.lifecycle === "operating" && (
679
+ <span
680
+ className="shrink-0 rounded bg-emerald-500/20 px-1 py-0.5 font-mono text-[8px] font-semibold text-emerald-300"
681
+ title={`영구 운영 중${typeof goal.operatingCycles === "number" ? ` · ${goal.operatingCycles} cycles` : ""}`}
682
+ >
683
+ ⟳ OPER{typeof goal.agendaOpen === "number" && goal.agendaOpen > 0 ? ` ${goal.agendaOpen}` : ""}
684
+ </span>
685
+ )}
686
+ <span className="truncate">{goal.label || goal.missionId}</span>
576
687
  </button>
577
688
  {children.length > 0 && (
578
689
  <div className="border-t border-slate-800/70 py-1 pl-3">
@@ -701,6 +812,389 @@ function formatTime(iso: string | null | undefined) {
701
812
  }
702
813
  }
703
814
 
815
+ function formatRelativeTime(iso: string | null | undefined) {
816
+ if (!iso) return "no signal";
817
+ const t = new Date(iso).getTime();
818
+ if (!Number.isFinite(t)) return "bad timestamp";
819
+ const diff = Math.max(0, Date.now() - t);
820
+ const seconds = Math.floor(diff / 1000);
821
+ if (seconds < 60) return `${seconds}s ago`;
822
+ const minutes = Math.floor(seconds / 60);
823
+ if (minutes < 60) return `${minutes}m ago`;
824
+ const hours = Math.floor(minutes / 60);
825
+ if (hours < 48) return `${hours}h ago`;
826
+ return `${Math.floor(hours / 24)}d ago`;
827
+ }
828
+
829
+ function newestIso(values: Array<string | null | undefined>) {
830
+ let best: string | null = null;
831
+ let bestTime = 0;
832
+ for (const value of values) {
833
+ if (!value) continue;
834
+ const time = Date.parse(value);
835
+ if (!Number.isFinite(time) || time <= bestTime) continue;
836
+ best = value;
837
+ bestTime = time;
838
+ }
839
+ return best;
840
+ }
841
+
842
+ function deriveRuntimeHealth(snapshot: HarnessSnapshot): RuntimeHealth {
843
+ const rt = snapshot.runtime;
844
+ const status = (rt.agentStatus ?? "").toLowerCase();
845
+ const conductor = (rt.conductorState ?? "").toLowerCase();
846
+ const nextAgent = (rt.nextAgent ?? "").toLowerCase();
847
+ const hasNextAgent = nextAgent !== "" && nextAgent !== "none";
848
+ const hasActiveDispatch =
849
+ status === "running" ||
850
+ conductor === "running" ||
851
+ conductor === "active" ||
852
+ conductor === "dispatching" ||
853
+ hasNextAgent;
854
+ const lastSignalAt = newestIso([
855
+ rt.updatedAt,
856
+ rt.lastDispatchAt,
857
+ snapshot.activitySamples.at(-1)?.ts,
858
+ snapshot.events[0]?.ts,
859
+ ...snapshot.todos.map((todo) => todo.lastHeartbeatAt ?? todo.updatedAt),
860
+ ...snapshot.missions.flatMap((mission) => [
861
+ mission.ts,
862
+ ...mission.workers.map((worker) => worker.updatedAt),
863
+ ]),
864
+ ]);
865
+ const ageMs = lastSignalAt ? Date.now() - Date.parse(lastSignalAt) : null;
866
+ const completedLoop =
867
+ status === "completed" ||
868
+ status === "complete" ||
869
+ conductor === "completed" ||
870
+ conductor === "complete";
871
+ const idle =
872
+ !hasActiveDispatch &&
873
+ (completedLoop || ((rt.currentAgent ?? null) === null && !hasNextAgent));
874
+
875
+ if (status === "blocked" || conductor === "blocked" || rt.ownerPrompt?.status === "awaiting-authority") {
876
+ return {
877
+ label: "BLOCKED",
878
+ detail: rt.ownerPrompt?.summary || rt.currentAction || "external authority required",
879
+ tone: "red",
880
+ ageMs,
881
+ lastSignalAt,
882
+ };
883
+ }
884
+ if (idle) {
885
+ return {
886
+ label: conductor === "operating" || status === "operating" ? "OPERATING" : "IDLE",
887
+ detail: conductor === "operating" || status === "operating" ? "yielded to hourly wake" : "runtime loop is closed",
888
+ tone: conductor === "operating" || status === "operating" ? "cyan" : "slate",
889
+ ageMs,
890
+ lastSignalAt,
891
+ };
892
+ }
893
+ if (ageMs === null) {
894
+ return { label: "UNKNOWN", detail: "no heartbeat yet", tone: "orange", ageMs, lastSignalAt };
895
+ }
896
+ if (ageMs > 15 * 60_000) {
897
+ return {
898
+ label: "STUCK?",
899
+ detail: "running state with no recent file or heartbeat movement",
900
+ tone: "red",
901
+ ageMs,
902
+ lastSignalAt,
903
+ };
904
+ }
905
+ if (ageMs > 3 * 60_000) {
906
+ return {
907
+ label: "STALE",
908
+ detail: "running, but no recent visible movement",
909
+ tone: "orange",
910
+ ageMs,
911
+ lastSignalAt,
912
+ };
913
+ }
914
+ return {
915
+ label: "ALIVE",
916
+ detail: rt.currentAction || `${rt.currentAgent ?? rt.nextAgent ?? "company"} is active`,
917
+ tone: "green",
918
+ ageMs,
919
+ lastSignalAt,
920
+ };
921
+ }
922
+
923
+ function healthToneClass(tone: HealthTone) {
924
+ return {
925
+ green: "border-emerald-400/40 bg-emerald-400/10 text-emerald-200",
926
+ orange: "border-orange-400/40 bg-orange-400/10 text-orange-200",
927
+ red: "border-rose-400/50 bg-rose-400/10 text-rose-200",
928
+ cyan: "border-cyan-400/40 bg-cyan-400/10 text-cyan-200",
929
+ slate: "border-slate-500/30 bg-slate-500/10 text-slate-300",
930
+ }[tone];
931
+ }
932
+
933
+ function buildHighlightedPaths(
934
+ mission: MissionDoc | null,
935
+ lane: AgentLane | null,
936
+ todos: CxxTodo[]
937
+ ) {
938
+ const paths = new Set<string>();
939
+ if (mission) {
940
+ const base = `.harness/documents/${mission.missionId}`;
941
+ paths.add(base);
942
+ paths.add(`${base}/mission-state.json`);
943
+ for (const role of ["ceo", "coo", "cdo", "cto", "cqo", "ops"]) {
944
+ const key = role as keyof Pick<MissionDoc, "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops">;
945
+ if (mission[key]) paths.add(`${base}/${role}.md`);
946
+ }
947
+ for (const worker of mission.workers) {
948
+ if (worker.reportPath) paths.add(worker.reportPath);
949
+ }
950
+ }
951
+ if (lane?.worker?.reportPath) paths.add(lane.worker.reportPath);
952
+ for (const todo of todos) {
953
+ if (todo.missionPath) paths.add(todo.missionPath);
954
+ for (const artifact of todo.requiredArtifacts) paths.add(artifact);
955
+ }
956
+ paths.add(".harness/progress.json");
957
+ paths.add(".harness/todos/state.json");
958
+ return paths;
959
+ }
960
+
961
+ function OperationalStatusPanel({
962
+ snapshot,
963
+ health,
964
+ mission,
965
+ }: {
966
+ snapshot: HarnessSnapshot;
967
+ health: RuntimeHealth;
968
+ mission: MissionDoc | null;
969
+ }) {
970
+ const rt = snapshot.runtime;
971
+ const openTodos = snapshot.todos.filter((todo) => !["done", "completed"].includes(todo.status)).length;
972
+ const activeMission = mission ?? snapshot.missions.find((m) => m.active) ?? null;
973
+ return (
974
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
975
+ <SectionLabel>Now · Runtime</SectionLabel>
976
+ <div className={`mt-2 rounded border px-3 py-2 ${healthToneClass(health.tone)}`}>
977
+ <div className="flex items-center justify-between gap-2">
978
+ <span className="font-mono text-lg font-semibold tracking-wide">{health.label}</span>
979
+ <span className="font-mono text-[10px] opacity-80">{formatRelativeTime(health.lastSignalAt)}</span>
980
+ </div>
981
+ <div className="mt-1 line-clamp-2 text-[11px] leading-4 opacity-90">{health.detail}</div>
982
+ </div>
983
+ <div className="mt-2 grid grid-cols-2 gap-1.5 text-[10px]">
984
+ <RuntimeMetric label="agent" value={rt.currentAgent ?? rt.nextAgent ?? "none"} />
985
+ <RuntimeMetric label="status" value={rt.agentStatus ?? "unknown"} />
986
+ <RuntimeMetric label="loop" value={rt.conductorState ?? rt.companyState ?? "n/a"} />
987
+ <RuntimeMetric label="workers" value={String(rt.activeWorkers ?? 0)} />
988
+ </div>
989
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-hidden p-2">
990
+ <div className="font-mono text-[9px] uppercase tracking-wider text-slate-500">Current Work</div>
991
+ <div className="mt-1 truncate text-[11px] text-cyan-100" title={activeMission?.missionId}>
992
+ {activeMission?.missionId ?? "no mission selected"}
993
+ </div>
994
+ <div className="mt-1 line-clamp-2 text-[10px] leading-4 text-slate-300" title={rt.currentAction ?? ""}>
995
+ {rt.currentAction ?? rt.ownerPrompt?.summary ?? "no current action recorded"}
996
+ </div>
997
+ <div className="mt-2 flex items-center justify-between font-mono text-[9px] text-slate-500">
998
+ <span>{openTodos} open todos</span>
999
+ <span>{snapshot.events.length} recent events</span>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+ );
1004
+ }
1005
+
1006
+ function RuntimeMetric({ label, value }: { label: string; value: string }) {
1007
+ return (
1008
+ <div className="inset-shell min-w-0 px-2 py-1">
1009
+ <div className="font-mono text-[8px] uppercase tracking-wider text-slate-600">{label}</div>
1010
+ <div className="truncate font-mono text-[10px] text-slate-200" title={value}>{value}</div>
1011
+ </div>
1012
+ );
1013
+ }
1014
+
1015
+ function StepProgressPanel({
1016
+ mission,
1017
+ runtimeAgent,
1018
+ todos,
1019
+ }: {
1020
+ mission: MissionDoc | null;
1021
+ runtimeAgent: string | null;
1022
+ todos: CxxTodo[];
1023
+ }) {
1024
+ const steps = useMemo(() => buildMissionSteps(mission, runtimeAgent, todos), [mission, runtimeAgent, todos]);
1025
+ return (
1026
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1027
+ <SectionLabel>Plan · Current Step</SectionLabel>
1028
+ <div className="mt-2 min-h-0 flex-1 space-y-1 overflow-auto pr-1">
1029
+ {steps.map((step, idx) => (
1030
+ <div
1031
+ key={step.id}
1032
+ className={`flex items-center gap-2 rounded border px-2 py-1.5 ${
1033
+ step.state === "active"
1034
+ ? "border-cyan-400/50 bg-cyan-400/10"
1035
+ : step.state === "done"
1036
+ ? "border-emerald-400/20 bg-emerald-400/[0.06]"
1037
+ : step.state === "blocked"
1038
+ ? "border-rose-400/40 bg-rose-400/10"
1039
+ : "border-slate-800 bg-slate-950/30"
1040
+ }`}
1041
+ >
1042
+ <span className={`grid h-5 w-5 shrink-0 place-items-center rounded font-mono text-[9px] ${
1043
+ step.state === "active"
1044
+ ? "bg-cyan-400 text-slate-950"
1045
+ : step.state === "done"
1046
+ ? "bg-emerald-500/70 text-slate-950"
1047
+ : step.state === "blocked"
1048
+ ? "bg-rose-500/80 text-white"
1049
+ : "bg-slate-800 text-slate-500"
1050
+ }`}>
1051
+ {idx + 1}
1052
+ </span>
1053
+ <div className="min-w-0 flex-1">
1054
+ <div className="truncate text-[11px] font-semibold text-slate-100">{step.label}</div>
1055
+ <div className="truncate font-mono text-[9px] text-slate-500">{step.detail}</div>
1056
+ </div>
1057
+ </div>
1058
+ ))}
1059
+ </div>
1060
+ </div>
1061
+ );
1062
+ }
1063
+
1064
+ function buildMissionSteps(
1065
+ mission: MissionDoc | null,
1066
+ runtimeAgent: string | null,
1067
+ todos: CxxTodo[]
1068
+ ) {
1069
+ const roleDone = (role: keyof Pick<MissionDoc, "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops">) => !!mission?.[role];
1070
+ const workerTotal = mission?.workers.length ?? 0;
1071
+ const workerDone = mission?.workers.filter((w) => w.status === "COMPLETE").length ?? 0;
1072
+ const blockedOwners = new Set(todos.filter((todo) => todo.status === "blocked").map((todo) => todo.owner));
1073
+ const terminal = mission ? isTerminalLifecycle(mission.lifecycle) || mission.lifecycle === "blocked" : false;
1074
+ const activeRole = (runtimeAgent ?? "").replace(/^harness-/, "");
1075
+ const rows = [
1076
+ { id: "owner", label: "Owner intake", done: !!mission, detail: mission?.type ?? "waiting for command" },
1077
+ { id: "ceo", label: "CEO routing/report", done: roleDone("ceo"), detail: mission?.ceo ? "ceo.md present" : "needs CEO record" },
1078
+ { id: "coo", label: "COO plan/research", done: roleDone("coo"), detail: mission?.coo ? "coo.md present" : "not routed yet" },
1079
+ { id: "cdo", label: "CDO design", done: roleDone("cdo"), detail: mission?.cdoPreview ? "preview available" : mission?.cdo ? "cdo.md present" : "not routed yet" },
1080
+ { id: "cto", label: "CTO implementation", done: roleDone("cto"), detail: mission?.cto ? "cto.md present" : "not routed yet" },
1081
+ { id: "cqo", label: "CQO verification", done: roleDone("cqo"), detail: mission?.cqo ? "cqo.md present" : "not routed yet" },
1082
+ { id: "ops", label: "OPS watch", done: roleDone("ops"), detail: mission?.ops ? "ops.md present" : "not routed yet" },
1083
+ { id: "workers", label: "Hired workers", done: workerTotal > 0 && workerDone >= workerTotal, detail: `${workerDone}/${workerTotal} reports complete` },
1084
+ { id: "terminal", label: "Runtime transition", done: terminal, detail: mission?.lifecycle ?? "unknown" },
1085
+ ];
1086
+ return rows.map((row) => ({
1087
+ ...row,
1088
+ state: blockedOwners.has(row.id)
1089
+ ? "blocked"
1090
+ : activeRole === row.id || (row.id === "workers" && workerTotal > workerDone)
1091
+ ? "active"
1092
+ : row.done
1093
+ ? "done"
1094
+ : "pending",
1095
+ }));
1096
+ }
1097
+
1098
+ function HarnessFileMapPanel({
1099
+ files,
1100
+ highlightedPaths,
1101
+ selectedPath,
1102
+ onSelect,
1103
+ }: {
1104
+ files: HarnessFileEntry[];
1105
+ highlightedPaths: Set<string>;
1106
+ selectedPath: string | null;
1107
+ onSelect: (entry: HarnessFileEntry) => void;
1108
+ }) {
1109
+ const visibleFiles = useMemo(() => {
1110
+ const priority = new Map<HarnessFileEntry["category"], number>([
1111
+ ["documents", 0],
1112
+ ["todos", 1],
1113
+ ["activity", 2],
1114
+ ["knowledge", 3],
1115
+ ["runtime", 4],
1116
+ ["config", 5],
1117
+ ["other", 6],
1118
+ ]);
1119
+ return [...files]
1120
+ .sort((a, b) => {
1121
+ const hotA = isHighlightedPath(a.path, highlightedPaths) ? -1 : 0;
1122
+ const hotB = isHighlightedPath(b.path, highlightedPaths) ? -1 : 0;
1123
+ if (hotA !== hotB) return hotA - hotB;
1124
+ const cat = (priority.get(a.category) ?? 9) - (priority.get(b.category) ?? 9);
1125
+ if (cat !== 0) return cat;
1126
+ return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
1127
+ })
1128
+ .slice(0, 90);
1129
+ }, [files, highlightedPaths]);
1130
+ return (
1131
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1132
+ <div className="flex items-center justify-between">
1133
+ <SectionLabel>.harness · File Map</SectionLabel>
1134
+ <span className="font-mono text-[9px] text-slate-500">{files.length} indexed</span>
1135
+ </div>
1136
+ <div className="mt-2 min-h-0 flex-1 overflow-auto pr-1">
1137
+ {visibleFiles.length === 0 ? (
1138
+ <div className="text-[10px] text-slate-500">No .harness files indexed.</div>
1139
+ ) : (
1140
+ visibleFiles.map((entry) => {
1141
+ const active = isHighlightedPath(entry.path, highlightedPaths);
1142
+ const selected = selectedPath === entry.path;
1143
+ return (
1144
+ <button
1145
+ key={entry.path}
1146
+ type="button"
1147
+ onClick={() => onSelect(entry)}
1148
+ className={`mb-0.5 grid w-full grid-cols-[1fr_auto] items-center gap-2 rounded border px-1.5 py-1 text-left ${
1149
+ selected
1150
+ ? "border-cyan-300 bg-cyan-400/10"
1151
+ : active
1152
+ ? "border-emerald-400/40 bg-emerald-400/10"
1153
+ : "border-transparent hover:border-cyan-400/20 hover:bg-white/[0.03]"
1154
+ }`}
1155
+ title={entry.path}
1156
+ >
1157
+ <span className="min-w-0 truncate font-mono text-[10px] text-slate-200" style={{ paddingLeft: `${Math.max(0, entry.depth - 1) * 10}px` }}>
1158
+ <span className={entry.kind === "dir" ? "text-amber-300" : "text-slate-500"}>
1159
+ {entry.kind === "dir" ? "▸" : "•"}
1160
+ </span>{" "}
1161
+ {entry.name}
1162
+ </span>
1163
+ <span className={`rounded px-1 py-0.5 font-mono text-[8px] ${fileCategoryClass(entry.category)}`}>
1164
+ {active ? "focus" : entry.category}
1165
+ </span>
1166
+ <span className="col-span-2 truncate pl-4 font-mono text-[8px] text-slate-600">
1167
+ {entry.path} · {formatRelativeTime(entry.updatedAt)}
1168
+ </span>
1169
+ </button>
1170
+ );
1171
+ })
1172
+ )}
1173
+ </div>
1174
+ </div>
1175
+ );
1176
+ }
1177
+
1178
+ function isHighlightedPath(pathValue: string, highlightedPaths: Set<string>) {
1179
+ if (highlightedPaths.has(pathValue)) return true;
1180
+ for (const candidate of highlightedPaths) {
1181
+ if (candidate && pathValue.startsWith(`${candidate}/`)) return true;
1182
+ }
1183
+ return false;
1184
+ }
1185
+
1186
+ function fileCategoryClass(category: HarnessFileEntry["category"]) {
1187
+ return {
1188
+ documents: "bg-cyan-400/10 text-cyan-200",
1189
+ todos: "bg-orange-400/10 text-orange-200",
1190
+ activity: "bg-emerald-400/10 text-emerald-200",
1191
+ knowledge: "bg-violet-400/10 text-violet-200",
1192
+ runtime: "bg-slate-500/10 text-slate-300",
1193
+ config: "bg-amber-400/10 text-amber-200",
1194
+ other: "bg-slate-800 text-slate-400",
1195
+ }[category];
1196
+ }
1197
+
704
1198
  // ===== Knowledge base panel =====
705
1199
 
706
1200
  type KnowledgeKind = "Gotcha" | "Convention";
@@ -1732,6 +2226,14 @@ function DocumentViewer({
1732
2226
  | undefined;
1733
2227
  const roleDoc = role ? mission?.[role] : null;
1734
2228
  const workerDoc = lane?.worker?.content ?? null;
2229
+ // CDO ships a self-contained HTML preview (cdo/preview.html). When the CDO
2230
+ // lane is selected and a preview exists, surface the visual deliverable in a
2231
+ // sandboxed iframe (the "click harness-cdo to view the Preview" contract),
2232
+ // with a toggle back to the cdo.md decision record.
2233
+ const cdoPreview = !customDoc && !workerDoc && role === "cdo" ? mission?.cdoPreview ?? null : null;
2234
+ const [cdoMode, setCdoMode] = useState<"preview" | "doc">("preview");
2235
+ const showPreview = !!cdoPreview && cdoMode === "preview";
2236
+
1735
2237
  const rawContent = customDoc
1736
2238
  ? customDoc.content
1737
2239
  : workerDoc ?? roleDoc ?? mission?.ceo ?? "_No document selected._";
@@ -1745,28 +2247,59 @@ function DocumentViewer({
1745
2247
  : role
1746
2248
  ? role.toUpperCase()
1747
2249
  : "CEO"
1748
- }`;
2250
+ }${cdoPreview ? (showPreview ? " · preview" : " · doc") : ""}`;
1749
2251
  return (
1750
2252
  <section className="panel-shell flex min-h-0 flex-1 flex-col p-2">
1751
2253
  <div className="flex items-center justify-between gap-2">
1752
2254
  <SectionLabel>Document Viewer · {subtitle}</SectionLabel>
1753
- <button
1754
- type="button"
1755
- onClick={onToggle}
1756
- className="grid h-6 w-6 shrink-0 place-items-center rounded text-[11px] text-slate-400 hover:bg-white/10 hover:text-cyan-200"
1757
- title={expanded ? "Minimize viewer" : "Expand to 50%"}
1758
- aria-label={expanded ? "Minimize viewer" : "Expand viewer"}
1759
- >
1760
- <span className={`doc-toggle-icon ${expanded ? "rotate-180" : ""}`}>↗</span>
1761
- </button>
1762
- </div>
1763
- <div className="inset-shell mt-2 min-h-0 flex-1 overflow-auto p-3">
1764
- <article className="markdown-doc text-[11px] leading-5 text-slate-300">
1765
- <ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
1766
- {content}
1767
- </ReactMarkdown>
1768
- </article>
2255
+ <div className="flex shrink-0 items-center gap-1">
2256
+ {cdoPreview && (
2257
+ <div className="mr-1 flex overflow-hidden rounded border border-slate-700 font-mono text-[9px]">
2258
+ <button
2259
+ type="button"
2260
+ onClick={() => setCdoMode("preview")}
2261
+ className={`px-1.5 py-0.5 ${showPreview ? "bg-violet-500/30 text-violet-200" : "text-slate-400 hover:text-violet-200"}`}
2262
+ >
2263
+ Preview
2264
+ </button>
2265
+ <button
2266
+ type="button"
2267
+ onClick={() => setCdoMode("doc")}
2268
+ className={`px-1.5 py-0.5 ${!showPreview ? "bg-violet-500/30 text-violet-200" : "text-slate-400 hover:text-violet-200"}`}
2269
+ >
2270
+ Doc
2271
+ </button>
2272
+ </div>
2273
+ )}
2274
+ <button
2275
+ type="button"
2276
+ onClick={onToggle}
2277
+ className="grid h-6 w-6 place-items-center rounded text-[11px] text-slate-400 hover:bg-white/10 hover:text-cyan-200"
2278
+ title={expanded ? "Minimize viewer" : "Expand to 50%"}
2279
+ aria-label={expanded ? "Minimize viewer" : "Expand viewer"}
2280
+ >
2281
+ <span className={`doc-toggle-icon ${expanded ? "rotate-180" : ""}`}>↗</span>
2282
+ </button>
2283
+ </div>
1769
2284
  </div>
2285
+ {showPreview ? (
2286
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-hidden p-1">
2287
+ <iframe
2288
+ title="CDO design preview"
2289
+ sandbox=""
2290
+ className="h-full w-full rounded bg-white"
2291
+ srcDoc={cdoPreview ?? ""}
2292
+ />
2293
+ </div>
2294
+ ) : (
2295
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-auto p-3">
2296
+ <article className="markdown-doc text-[11px] leading-5 text-slate-300">
2297
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
2298
+ {content}
2299
+ </ReactMarkdown>
2300
+ </article>
2301
+ </div>
2302
+ )}
1770
2303
  </section>
1771
2304
  );
1772
2305
  }