@walwal-harness/cli 7.1.40 → 7.1.42

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 (84) 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/metrics/route.ts +17 -0
  11. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  12. package/apps/harness-dashboard/app/brick.css +482 -0
  13. package/apps/harness-dashboard/app/layout.tsx +13 -0
  14. package/apps/harness-dashboard/app/page.tsx +2 -2
  15. package/apps/harness-dashboard/components/Scene.tsx +690 -36
  16. package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
  17. package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
  18. package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
  19. package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
  20. package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
  21. package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
  22. package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
  23. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  24. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  25. package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
  26. package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
  27. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  28. package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
  29. package/apps/harness-dashboard/lib/types.ts +22 -222
  30. package/apps/harness-dashboard/playwright.config.ts +26 -7
  31. package/assets/templates/AGENTS-ko.md.template +1 -1
  32. package/assets/templates/AGENTS.md.template +12 -2
  33. package/assets/templates/HARNESS.md +2 -0
  34. package/assets/templates/company-pipeline-manifest.json +5 -14
  35. package/assets/templates/worker-report.md.template +38 -0
  36. package/bin/init.js +2 -1
  37. package/commands/goal.md +4 -2
  38. package/commands/hot-fix.md +1 -1
  39. package/commands/submission.md +1 -1
  40. package/package.json +1 -1
  41. package/scripts/conductor-tick.sh +12 -0
  42. package/scripts/harness-agenda.sh +105 -0
  43. package/scripts/harness-company-block.sh +89 -0
  44. package/scripts/harness-company-complete.sh +17 -8
  45. package/scripts/harness-company-cycle.sh +54 -0
  46. package/scripts/harness-progress-set.sh +8 -2
  47. package/scripts/harness-stop.sh +76 -0
  48. package/scripts/harness-wake-control.sh +151 -0
  49. package/scripts/harness-wake.sh +5 -0
  50. package/scripts/harness-worker-dispatch.sh +31 -7
  51. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  52. package/apps/harness-dashboard/components/Header.tsx +0 -40
  53. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  54. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  55. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  56. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  57. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  58. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  59. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  60. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  61. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  62. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  63. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  64. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  65. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  66. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  67. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  68. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  69. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  70. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  71. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  72. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  73. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  74. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  75. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  76. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  77. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  78. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  79. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  80. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  81. package/apps/harness-dashboard/lib/iso.ts +0 -150
  82. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  83. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  84. 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];
@@ -190,6 +199,23 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
190
199
  missionId: activeMission?.missionId ?? null,
191
200
  });
192
201
  }
202
+ // Authoritative "who's working now": always light the runtime's current
203
+ // agent lane while the loop is active (we only reach here when not idle),
204
+ // even if that agent has no per-lane todo/worker row yet — work often runs
205
+ // in a background agent the runtime only names via current_agent.
206
+ const currentRole = (snapshot.runtime?.currentAgent ?? "").replace(/^harness-/, "");
207
+ if (currentRole) {
208
+ const currentLane = lanes.find((l) => l.kind === "cxx" && l.role === currentRole);
209
+ if (currentLane && !next.some((s) => s.laneId === currentLane.id)) {
210
+ next.push({
211
+ ts: now,
212
+ laneId: currentLane.id,
213
+ count: 2,
214
+ hotfix: activeMission?.type === "hotfix",
215
+ missionId: activeMission?.missionId ?? null,
216
+ });
217
+ }
218
+ }
193
219
  if (next.length === 0) return prev;
194
220
  return [...prev, ...next]
195
221
  .filter((sample) => now - sample.ts < 30 * 60 * 1000)
@@ -233,12 +259,22 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
233
259
  const activeWorkers =
234
260
  (snapshot.missions.find((mission) => mission.active) ?? snapshot.missions[0])?.workers.filter((w) => w.active).length ?? 0;
235
261
  const hotfixCount = snapshot.missions.filter((m) => m.type === "hotfix").length;
262
+ const runtimeHealth = useMemo(() => deriveRuntimeHealth(snapshot), [snapshot]);
236
263
 
237
264
  const handleSelectWorker = (mission: MissionDoc, worker: WorkerDocEntry) => {
238
265
  setSelectedMissionId(mission.missionId);
239
266
  setSelectedLaneId(`${worker.owner}:${worker.name}`);
240
267
  };
241
268
 
269
+ // Open a gotcha/convention in the document viewer with its FULL markdown body
270
+ // (entry.content is the real readFileSync'd file — restores full-document reading).
271
+ const handleSelectKnowledge = (
272
+ entry: GotchaEntry | ConventionEntry,
273
+ kind: KnowledgeKind
274
+ ) => {
275
+ setCustomDoc({ source: kind, title: `${entry.id}.md`, content: entry.content });
276
+ };
277
+
242
278
  return (
243
279
  <div className="flex h-[100dvh] flex-col overflow-hidden bg-[#070d1d] text-slate-200">
244
280
  <TopHeader
@@ -261,24 +297,27 @@ export function Scene({ snapshot: initial, lang = "ko" }: SceneProps) {
261
297
  />
262
298
 
263
299
  <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 }}>
300
+ {/* Top row: Owner situational awareness.
301
+ PULSE (trust verdict) · ACTIVE NOW (per-CXX activity, fixes the
302
+ "only CEO" heatmap illusion) · KNOWLEDGE (gotchas/conventions, full
303
+ body) · RECENT (worker reports, full body). All viewer-openable. */}
304
+ <section className="grid shrink-0 grid-cols-4 gap-2" style={{ height: 310 }}>
305
+ <OperationalStatusPanel
306
+ snapshot={snapshot}
307
+ health={runtimeHealth}
308
+ mission={selectedMission}
309
+ />
310
+ <ActiveNowPanel
311
+ lanes={lanes}
312
+ snapshot={snapshot}
313
+ selectedLaneId={selectedLaneId}
314
+ onSelectLane={setSelectedLaneId}
315
+ />
266
316
  <KnowledgeBasePanel
267
317
  gotchas={snapshot.gotchas}
268
318
  conventions={snapshot.conventions}
269
319
  baselineTs={selectedGoal?.ts ?? null}
270
- onSelect={(entry, kind) =>
271
- setCustomDoc({
272
- source: kind,
273
- title: `${entry.id}.md`,
274
- content: entry.content,
275
- })
276
- }
277
- />
278
- <LayerActivityPanel
279
- goalScope={goalScopeMissions}
280
- todos={snapshot.todos}
281
- label={selectedGoal?.missionId ?? "no goal"}
320
+ onSelect={handleSelectKnowledge}
282
321
  />
283
322
  <RecentReportPanel
284
323
  missions={snapshot.missions}
@@ -384,12 +423,82 @@ function TopHeader({
384
423
  />
385
424
  <HeaderBadge label="Workers" value={String(activeWorkers)} tone="cyan" />
386
425
  <HeaderBadge label="Hot-fix" value={String(hotfixCount)} tone="red" />
426
+ <WakeToggle />
387
427
  <PresenceLegend />
388
428
  </div>
389
429
  </header>
390
430
  );
391
431
  }
392
432
 
433
+ // Toggles the per-project hourly wake launchd job (keyed by HARNESS_BASE_PORT)
434
+ // that drives the perpetual operating loop across sessions.
435
+ interface WakeStatus {
436
+ supported?: boolean;
437
+ enabled?: boolean;
438
+ base_port?: number | string | null;
439
+ label?: string;
440
+ error?: string;
441
+ }
442
+
443
+ function WakeToggle() {
444
+ const [status, setStatus] = useState<WakeStatus | null>(null);
445
+ const [busy, setBusy] = useState(false);
446
+
447
+ useEffect(() => {
448
+ let alive = true;
449
+ fetch("/api/wake")
450
+ .then((r) => r.json())
451
+ .then((s) => { if (alive) setStatus(s); })
452
+ .catch(() => { if (alive) setStatus({ supported: false }); });
453
+ return () => { alive = false; };
454
+ }, []);
455
+
456
+ const toggle = async () => {
457
+ if (busy || !status?.supported) return;
458
+ setBusy(true);
459
+ try {
460
+ const r = await fetch("/api/wake", {
461
+ method: "POST",
462
+ headers: { "content-type": "application/json" },
463
+ body: JSON.stringify({ action: status?.enabled ? "off" : "on" }),
464
+ });
465
+ setStatus(await r.json());
466
+ } catch {
467
+ /* keep prior status */
468
+ } finally {
469
+ setBusy(false);
470
+ }
471
+ };
472
+
473
+ if (!status) return null;
474
+ const supported = !!status.supported;
475
+ const on = !!status.enabled;
476
+ const port = status.base_port ? `:${status.base_port}` : "";
477
+ const tone = !supported
478
+ ? "border-slate-600/30 bg-slate-600/10 text-slate-500"
479
+ : on
480
+ ? "border-emerald-400/40 bg-emerald-400/10 text-emerald-200"
481
+ : "border-slate-500/30 bg-slate-500/10 text-slate-300";
482
+ return (
483
+ <button
484
+ type="button"
485
+ onClick={toggle}
486
+ disabled={busy || !supported}
487
+ title={
488
+ supported
489
+ ? `Hourly wake launchd job ${status.label ?? ""} — click to turn ${on ? "off" : "on"} (drives the perpetual operating loop across sessions)`
490
+ : "launchd not available on this OS"
491
+ }
492
+ className={`rounded-full border px-2.5 py-1 font-mono text-[10px] transition-colors ${tone} ${
493
+ supported ? "cursor-pointer hover:brightness-125" : "cursor-default"
494
+ } ${busy ? "opacity-60" : ""}`}
495
+ >
496
+ <span className="text-slate-500">Wake</span>
497
+ <span className="ml-1.5">{!supported ? "n/a" : on ? `● on${port}` : `○ off${port}`}</span>
498
+ </button>
499
+ );
500
+ }
501
+
393
502
  function HeaderBadge({
394
503
  label,
395
504
  value,
@@ -565,14 +674,22 @@ function HistoryNavigator({
565
674
  <button
566
675
  type="button"
567
676
  onClick={() => onSelect(goal.missionId)}
568
- className={`w-full truncate px-2 py-2 text-left ${
677
+ className={`flex w-full items-center gap-1 px-2 py-2 text-left ${
569
678
  activeMissionId === goal.missionId
570
679
  ? "bg-cyan-400/[0.12] text-cyan-100"
571
680
  : "text-slate-300 hover:bg-white/[0.04]"
572
681
  }`}
573
682
  title={goal.label || goal.missionId}
574
683
  >
575
- {goal.label || goal.missionId}
684
+ {goal.lifecycle === "operating" && (
685
+ <span
686
+ className="shrink-0 rounded bg-emerald-500/20 px-1 py-0.5 font-mono text-[8px] font-semibold text-emerald-300"
687
+ title={`영구 운영 중${typeof goal.operatingCycles === "number" ? ` · ${goal.operatingCycles} cycles` : ""}`}
688
+ >
689
+ ⟳ OPER{typeof goal.agendaOpen === "number" && goal.agendaOpen > 0 ? ` ${goal.agendaOpen}` : ""}
690
+ </span>
691
+ )}
692
+ <span className="truncate">{goal.label || goal.missionId}</span>
576
693
  </button>
577
694
  {children.length > 0 && (
578
695
  <div className="border-t border-slate-800/70 py-1 pl-3">
@@ -701,6 +818,496 @@ function formatTime(iso: string | null | undefined) {
701
818
  }
702
819
  }
703
820
 
821
+ function formatRelativeTime(iso: string | null | undefined) {
822
+ if (!iso) return "no signal";
823
+ const t = new Date(iso).getTime();
824
+ if (!Number.isFinite(t)) return "bad timestamp";
825
+ const diff = Math.max(0, Date.now() - t);
826
+ const seconds = Math.floor(diff / 1000);
827
+ if (seconds < 60) return `${seconds}s ago`;
828
+ const minutes = Math.floor(seconds / 60);
829
+ if (minutes < 60) return `${minutes}m ago`;
830
+ const hours = Math.floor(minutes / 60);
831
+ if (hours < 48) return `${hours}h ago`;
832
+ return `${Math.floor(hours / 24)}d ago`;
833
+ }
834
+
835
+ function newestIso(values: Array<string | null | undefined>) {
836
+ let best: string | null = null;
837
+ let bestTime = 0;
838
+ for (const value of values) {
839
+ if (!value) continue;
840
+ const time = Date.parse(value);
841
+ if (!Number.isFinite(time) || time <= bestTime) continue;
842
+ best = value;
843
+ bestTime = time;
844
+ }
845
+ return best;
846
+ }
847
+
848
+ function deriveRuntimeHealth(snapshot: HarnessSnapshot): RuntimeHealth {
849
+ const rt = snapshot.runtime;
850
+ const status = (rt.agentStatus ?? "").toLowerCase();
851
+ const conductor = (rt.conductorState ?? "").toLowerCase();
852
+ const nextAgent = (rt.nextAgent ?? "").toLowerCase();
853
+ const hasNextAgent = nextAgent !== "" && nextAgent !== "none";
854
+ const hasActiveDispatch =
855
+ status === "running" ||
856
+ conductor === "running" ||
857
+ conductor === "active" ||
858
+ conductor === "dispatching" ||
859
+ hasNextAgent;
860
+ const lastSignalAt = newestIso([
861
+ rt.updatedAt,
862
+ rt.lastDispatchAt,
863
+ snapshot.activitySamples.at(-1)?.ts,
864
+ snapshot.events[0]?.ts,
865
+ ...snapshot.todos.map((todo) => todo.lastHeartbeatAt ?? todo.updatedAt),
866
+ ...snapshot.missions.flatMap((mission) => [
867
+ mission.ts,
868
+ ...mission.workers.map((worker) => worker.updatedAt),
869
+ ]),
870
+ ]);
871
+ const ageMs = lastSignalAt ? Date.now() - Date.parse(lastSignalAt) : null;
872
+ const completedLoop =
873
+ status === "completed" ||
874
+ status === "complete" ||
875
+ conductor === "completed" ||
876
+ conductor === "complete";
877
+ const idle =
878
+ !hasActiveDispatch &&
879
+ (completedLoop || ((rt.currentAgent ?? null) === null && !hasNextAgent));
880
+
881
+ if (status === "blocked" || conductor === "blocked" || rt.ownerPrompt?.status === "awaiting-authority") {
882
+ return {
883
+ label: "BLOCKED",
884
+ detail: rt.ownerPrompt?.summary || rt.currentAction || "external authority required",
885
+ tone: "red",
886
+ ageMs,
887
+ lastSignalAt,
888
+ };
889
+ }
890
+ if (idle) {
891
+ return {
892
+ label: conductor === "operating" || status === "operating" ? "OPERATING" : "IDLE",
893
+ detail: conductor === "operating" || status === "operating" ? "yielded to hourly wake" : "runtime loop is closed",
894
+ tone: conductor === "operating" || status === "operating" ? "cyan" : "slate",
895
+ ageMs,
896
+ lastSignalAt,
897
+ };
898
+ }
899
+ if (ageMs === null) {
900
+ return { label: "UNKNOWN", detail: "no heartbeat yet", tone: "orange", ageMs, lastSignalAt };
901
+ }
902
+ if (ageMs > 15 * 60_000) {
903
+ return {
904
+ label: "STUCK?",
905
+ detail: "running state with no recent file or heartbeat movement",
906
+ tone: "red",
907
+ ageMs,
908
+ lastSignalAt,
909
+ };
910
+ }
911
+ if (ageMs > 3 * 60_000) {
912
+ return {
913
+ label: "STALE",
914
+ detail: "running, but no recent visible movement",
915
+ tone: "orange",
916
+ ageMs,
917
+ lastSignalAt,
918
+ };
919
+ }
920
+ return {
921
+ label: "ALIVE",
922
+ detail: rt.currentAction || `${rt.currentAgent ?? rt.nextAgent ?? "company"} is active`,
923
+ tone: "green",
924
+ ageMs,
925
+ lastSignalAt,
926
+ };
927
+ }
928
+
929
+ function healthToneClass(tone: HealthTone) {
930
+ return {
931
+ green: "border-emerald-400/40 bg-emerald-400/10 text-emerald-200",
932
+ orange: "border-orange-400/40 bg-orange-400/10 text-orange-200",
933
+ red: "border-rose-400/50 bg-rose-400/10 text-rose-200",
934
+ cyan: "border-cyan-400/40 bg-cyan-400/10 text-cyan-200",
935
+ slate: "border-slate-500/30 bg-slate-500/10 text-slate-300",
936
+ }[tone];
937
+ }
938
+
939
+ function buildHighlightedPaths(
940
+ mission: MissionDoc | null,
941
+ lane: AgentLane | null,
942
+ todos: CxxTodo[]
943
+ ) {
944
+ const paths = new Set<string>();
945
+ if (mission) {
946
+ const base = `.harness/documents/${mission.missionId}`;
947
+ paths.add(base);
948
+ paths.add(`${base}/mission-state.json`);
949
+ for (const role of ["ceo", "coo", "cdo", "cto", "cqo", "ops"]) {
950
+ const key = role as keyof Pick<MissionDoc, "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops">;
951
+ if (mission[key]) paths.add(`${base}/${role}.md`);
952
+ }
953
+ for (const worker of mission.workers) {
954
+ if (worker.reportPath) paths.add(worker.reportPath);
955
+ }
956
+ }
957
+ if (lane?.worker?.reportPath) paths.add(lane.worker.reportPath);
958
+ for (const todo of todos) {
959
+ if (todo.missionPath) paths.add(todo.missionPath);
960
+ for (const artifact of todo.requiredArtifacts) paths.add(artifact);
961
+ }
962
+ paths.add(".harness/progress.json");
963
+ paths.add(".harness/todos/state.json");
964
+ return paths;
965
+ }
966
+
967
+ function OperationalStatusPanel({
968
+ snapshot,
969
+ health,
970
+ mission,
971
+ }: {
972
+ snapshot: HarnessSnapshot;
973
+ health: RuntimeHealth;
974
+ mission: MissionDoc | null;
975
+ }) {
976
+ const rt = snapshot.runtime;
977
+ const openTodos = snapshot.todos.filter((todo) => !["done", "completed"].includes(todo.status)).length;
978
+ const activeMission = mission ?? snapshot.missions.find((m) => m.active) ?? null;
979
+ return (
980
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
981
+ <SectionLabel>Now · Runtime</SectionLabel>
982
+ <div className={`mt-2 rounded border px-3 py-2 ${healthToneClass(health.tone)}`}>
983
+ <div className="flex items-center justify-between gap-2">
984
+ <span className="font-mono text-lg font-semibold tracking-wide">{health.label}</span>
985
+ <span className="font-mono text-[10px] opacity-80">{formatRelativeTime(health.lastSignalAt)}</span>
986
+ </div>
987
+ <div className="mt-1 line-clamp-2 text-[11px] leading-4 opacity-90">{health.detail}</div>
988
+ </div>
989
+ <div className="mt-2 grid grid-cols-2 gap-1.5 text-[10px]">
990
+ <RuntimeMetric label="agent" value={rt.currentAgent ?? rt.nextAgent ?? "none"} />
991
+ <RuntimeMetric label="status" value={rt.agentStatus ?? "unknown"} />
992
+ <RuntimeMetric label="loop" value={rt.conductorState ?? rt.companyState ?? "n/a"} />
993
+ <RuntimeMetric label="workers" value={String(rt.activeWorkers ?? 0)} />
994
+ </div>
995
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-hidden p-2">
996
+ <div className="font-mono text-[9px] uppercase tracking-wider text-slate-500">Current Work</div>
997
+ <div className="mt-1 truncate text-[11px] text-cyan-100" title={activeMission?.missionId}>
998
+ {activeMission?.missionId ?? "no mission selected"}
999
+ </div>
1000
+ <div className="mt-1 line-clamp-2 text-[10px] leading-4 text-slate-300" title={rt.currentAction ?? ""}>
1001
+ {rt.currentAction ?? rt.ownerPrompt?.summary ?? "no current action recorded"}
1002
+ </div>
1003
+ <div className="mt-2 flex items-center justify-between font-mono text-[9px] text-slate-500">
1004
+ <span>{openTodos} open todos</span>
1005
+ <span>{snapshot.events.length} recent events</span>
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+ );
1010
+ }
1011
+
1012
+ function RuntimeMetric({ label, value }: { label: string; value: string }) {
1013
+ return (
1014
+ <div className="inset-shell min-w-0 px-2 py-1">
1015
+ <div className="font-mono text-[8px] uppercase tracking-wider text-slate-600">{label}</div>
1016
+ <div className="truncate font-mono text-[10px] text-slate-200" title={value}>{value}</div>
1017
+ </div>
1018
+ );
1019
+ }
1020
+
1021
+ // ACTIVE NOW — a per-CXX roster so the whole company is legible at a glance,
1022
+ // independent of heatmap brightness. Each role shows LIVE (open todo / running
1023
+ // worker), RECENT (newest activity sample, relative age), or IDLE — in its own
1024
+ // CXX colour. This is the perception-level fix for "heatmap shows only CEO".
1025
+ function ActiveNowPanel({
1026
+ lanes,
1027
+ snapshot,
1028
+ selectedLaneId,
1029
+ onSelectLane,
1030
+ }: {
1031
+ lanes: AgentLane[];
1032
+ snapshot: HarnessSnapshot;
1033
+ selectedLaneId: string | null;
1034
+ onSelectLane: (laneId: string) => void;
1035
+ }) {
1036
+ const lastSampleByRole = useMemo(() => {
1037
+ const m = new Map<string, string>();
1038
+ for (const s of snapshot.activitySamples) {
1039
+ const role = s.laneId.split(":")[0];
1040
+ const prev = m.get(role);
1041
+ if (!prev || Date.parse(s.ts) > Date.parse(prev)) m.set(role, s.ts);
1042
+ }
1043
+ return m;
1044
+ }, [snapshot.activitySamples]);
1045
+
1046
+ // The runtime's current_agent is the authoritative "who is working now" signal:
1047
+ // a freshly-spawned agent often has no per-lane todo/worker row yet (work runs in
1048
+ // a background agent the runtime only names via current_agent + current_action),
1049
+ // so honour it directly instead of inferring liveness from todos/workers alone.
1050
+ const rt = snapshot.runtime;
1051
+ const currentRole = (rt.currentAgent ?? "").replace(/^harness-/, "");
1052
+ const loopActive =
1053
+ rt.agentStatus !== "completed" &&
1054
+ rt.agentStatus !== "complete" &&
1055
+ rt.conductorState !== "completed" &&
1056
+ rt.conductorState !== "blocked";
1057
+
1058
+ const rows = CXX_ROLES.map((role) => {
1059
+ const lane = lanes.find((l) => l.kind === "cxx" && l.role === role) ?? null;
1060
+ const openTodos = snapshot.todos.filter(
1061
+ (t) => t.owner === role && t.status !== "done" && t.status !== "completed"
1062
+ ).length;
1063
+ const runningWorkers = (lane?.workers ?? []).filter(
1064
+ (w) => w.active || w.status === "IN_PROGRESS"
1065
+ ).length;
1066
+ const lastTs = lastSampleByRole.get(role) ?? null;
1067
+ const isCurrent = loopActive && currentRole === role;
1068
+ const state: "live" | "recent" | "idle" =
1069
+ isCurrent || openTodos > 0 || runningWorkers > 0 ? "live" : lastTs ? "recent" : "idle";
1070
+ return { role, lane, openTodos, runningWorkers, lastTs, isCurrent, state };
1071
+ });
1072
+
1073
+ return (
1074
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1075
+ <SectionLabel>Active Now · Who · What</SectionLabel>
1076
+ <div className="mt-2 min-h-0 flex-1 space-y-1 overflow-auto pr-1">
1077
+ {rows.map(({ role, lane, openTodos, runningWorkers, lastTs, isCurrent, state }) => {
1078
+ const selected = !!lane && selectedLaneId === lane.id;
1079
+ return (
1080
+ <button
1081
+ key={role}
1082
+ type="button"
1083
+ disabled={!lane}
1084
+ onClick={() => lane && onSelectLane(lane.id)}
1085
+ title={lane ? `Open ${role.toUpperCase()} document` : `${role.toUpperCase()} not active in this mission`}
1086
+ className={`flex w-full items-center gap-2 rounded border px-2 py-1.5 text-left transition-colors ${
1087
+ selected
1088
+ ? "border-cyan-300 bg-cyan-400/10"
1089
+ : state === "live"
1090
+ ? "border-cyan-400/40 bg-cyan-400/5"
1091
+ : "border-slate-800 hover:border-cyan-400/20 hover:bg-white/[0.03]"
1092
+ } ${lane ? "cursor-pointer" : "cursor-default opacity-60"}`}
1093
+ >
1094
+ <span
1095
+ className={`h-2.5 w-2.5 shrink-0 rounded-full ${
1096
+ state === "idle" ? "bg-slate-700" : CXX_COLOR[role]
1097
+ } ${state === "live" ? "animate-pulse" : state === "recent" ? "opacity-60" : ""}`}
1098
+ />
1099
+ <span className={`w-9 shrink-0 font-mono text-[11px] font-semibold ${CXX_TEXT[role]}`}>
1100
+ {role.toUpperCase()}
1101
+ </span>
1102
+ <span className="min-w-0 flex-1 truncate text-[10px] text-slate-300">
1103
+ {state === "live"
1104
+ ? isCurrent
1105
+ ? rt.currentAction ?? "current agent · running"
1106
+ : runningWorkers > 0
1107
+ ? `${runningWorkers} worker${runningWorkers > 1 ? "s" : ""} running`
1108
+ : `${openTodos} open todo${openTodos > 1 ? "s" : ""}`
1109
+ : state === "recent"
1110
+ ? `active ${formatRelativeTime(lastTs)}`
1111
+ : "idle"}
1112
+ </span>
1113
+ <span
1114
+ className={`shrink-0 font-mono text-[8px] uppercase tracking-wider ${
1115
+ state === "live" ? "text-cyan-300" : state === "recent" ? "text-slate-500" : "text-slate-600"
1116
+ }`}
1117
+ >
1118
+ {state}
1119
+ </span>
1120
+ </button>
1121
+ );
1122
+ })}
1123
+ </div>
1124
+ </div>
1125
+ );
1126
+ }
1127
+
1128
+ function StepProgressPanel({
1129
+ mission,
1130
+ runtimeAgent,
1131
+ todos,
1132
+ }: {
1133
+ mission: MissionDoc | null;
1134
+ runtimeAgent: string | null;
1135
+ todos: CxxTodo[];
1136
+ }) {
1137
+ const steps = useMemo(() => buildMissionSteps(mission, runtimeAgent, todos), [mission, runtimeAgent, todos]);
1138
+ return (
1139
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1140
+ <SectionLabel>Plan · Current Step</SectionLabel>
1141
+ <div className="mt-2 min-h-0 flex-1 space-y-1 overflow-auto pr-1">
1142
+ {steps.map((step, idx) => (
1143
+ <div
1144
+ key={step.id}
1145
+ className={`flex items-center gap-2 rounded border px-2 py-1.5 ${
1146
+ step.state === "active"
1147
+ ? "border-cyan-400/50 bg-cyan-400/10"
1148
+ : step.state === "done"
1149
+ ? "border-emerald-400/20 bg-emerald-400/[0.06]"
1150
+ : step.state === "blocked"
1151
+ ? "border-rose-400/40 bg-rose-400/10"
1152
+ : "border-slate-800 bg-slate-950/30"
1153
+ }`}
1154
+ >
1155
+ <span className={`grid h-5 w-5 shrink-0 place-items-center rounded font-mono text-[9px] ${
1156
+ step.state === "active"
1157
+ ? "bg-cyan-400 text-slate-950"
1158
+ : step.state === "done"
1159
+ ? "bg-emerald-500/70 text-slate-950"
1160
+ : step.state === "blocked"
1161
+ ? "bg-rose-500/80 text-white"
1162
+ : "bg-slate-800 text-slate-500"
1163
+ }`}>
1164
+ {idx + 1}
1165
+ </span>
1166
+ <div className="min-w-0 flex-1">
1167
+ <div className="truncate text-[11px] font-semibold text-slate-100">{step.label}</div>
1168
+ <div className="truncate font-mono text-[9px] text-slate-500">{step.detail}</div>
1169
+ </div>
1170
+ </div>
1171
+ ))}
1172
+ </div>
1173
+ </div>
1174
+ );
1175
+ }
1176
+
1177
+ function buildMissionSteps(
1178
+ mission: MissionDoc | null,
1179
+ runtimeAgent: string | null,
1180
+ todos: CxxTodo[]
1181
+ ) {
1182
+ const roleDone = (role: keyof Pick<MissionDoc, "ceo" | "coo" | "cdo" | "cto" | "cqo" | "ops">) => !!mission?.[role];
1183
+ const workerTotal = mission?.workers.length ?? 0;
1184
+ const workerDone = mission?.workers.filter((w) => w.status === "COMPLETE").length ?? 0;
1185
+ const blockedOwners = new Set(todos.filter((todo) => todo.status === "blocked").map((todo) => todo.owner));
1186
+ const terminal = mission ? isTerminalLifecycle(mission.lifecycle) || mission.lifecycle === "blocked" : false;
1187
+ const activeRole = (runtimeAgent ?? "").replace(/^harness-/, "");
1188
+ const rows = [
1189
+ { id: "owner", label: "Owner intake", done: !!mission, detail: mission?.type ?? "waiting for command" },
1190
+ { id: "ceo", label: "CEO routing/report", done: roleDone("ceo"), detail: mission?.ceo ? "ceo.md present" : "needs CEO record" },
1191
+ { id: "coo", label: "COO plan/research", done: roleDone("coo"), detail: mission?.coo ? "coo.md present" : "not routed yet" },
1192
+ { id: "cdo", label: "CDO design", done: roleDone("cdo"), detail: mission?.cdoPreview ? "preview available" : mission?.cdo ? "cdo.md present" : "not routed yet" },
1193
+ { id: "cto", label: "CTO implementation", done: roleDone("cto"), detail: mission?.cto ? "cto.md present" : "not routed yet" },
1194
+ { id: "cqo", label: "CQO verification", done: roleDone("cqo"), detail: mission?.cqo ? "cqo.md present" : "not routed yet" },
1195
+ { id: "ops", label: "OPS watch", done: roleDone("ops"), detail: mission?.ops ? "ops.md present" : "not routed yet" },
1196
+ { id: "workers", label: "Hired workers", done: workerTotal > 0 && workerDone >= workerTotal, detail: `${workerDone}/${workerTotal} reports complete` },
1197
+ { id: "terminal", label: "Runtime transition", done: terminal, detail: mission?.lifecycle ?? "unknown" },
1198
+ ];
1199
+ return rows.map((row) => ({
1200
+ ...row,
1201
+ state: blockedOwners.has(row.id)
1202
+ ? "blocked"
1203
+ : activeRole === row.id || (row.id === "workers" && workerTotal > workerDone)
1204
+ ? "active"
1205
+ : row.done
1206
+ ? "done"
1207
+ : "pending",
1208
+ }));
1209
+ }
1210
+
1211
+ function HarnessFileMapPanel({
1212
+ files,
1213
+ highlightedPaths,
1214
+ selectedPath,
1215
+ onSelect,
1216
+ }: {
1217
+ files: HarnessFileEntry[];
1218
+ highlightedPaths: Set<string>;
1219
+ selectedPath: string | null;
1220
+ onSelect: (entry: HarnessFileEntry) => void;
1221
+ }) {
1222
+ const visibleFiles = useMemo(() => {
1223
+ const priority = new Map<HarnessFileEntry["category"], number>([
1224
+ ["documents", 0],
1225
+ ["todos", 1],
1226
+ ["activity", 2],
1227
+ ["knowledge", 3],
1228
+ ["runtime", 4],
1229
+ ["config", 5],
1230
+ ["other", 6],
1231
+ ]);
1232
+ return [...files]
1233
+ .sort((a, b) => {
1234
+ const hotA = isHighlightedPath(a.path, highlightedPaths) ? -1 : 0;
1235
+ const hotB = isHighlightedPath(b.path, highlightedPaths) ? -1 : 0;
1236
+ if (hotA !== hotB) return hotA - hotB;
1237
+ const cat = (priority.get(a.category) ?? 9) - (priority.get(b.category) ?? 9);
1238
+ if (cat !== 0) return cat;
1239
+ return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "");
1240
+ })
1241
+ .slice(0, 90);
1242
+ }, [files, highlightedPaths]);
1243
+ return (
1244
+ <div className="panel-shell fade-in flex h-full min-h-0 flex-col overflow-hidden p-2">
1245
+ <div className="flex items-center justify-between">
1246
+ <SectionLabel>.harness · File Map</SectionLabel>
1247
+ <span className="font-mono text-[9px] text-slate-500">{files.length} indexed</span>
1248
+ </div>
1249
+ <div className="mt-2 min-h-0 flex-1 overflow-auto pr-1">
1250
+ {visibleFiles.length === 0 ? (
1251
+ <div className="text-[10px] text-slate-500">No .harness files indexed.</div>
1252
+ ) : (
1253
+ visibleFiles.map((entry) => {
1254
+ const active = isHighlightedPath(entry.path, highlightedPaths);
1255
+ const selected = selectedPath === entry.path;
1256
+ return (
1257
+ <button
1258
+ key={entry.path}
1259
+ type="button"
1260
+ onClick={() => onSelect(entry)}
1261
+ className={`mb-0.5 grid w-full grid-cols-[1fr_auto] items-center gap-2 rounded border px-1.5 py-1 text-left ${
1262
+ selected
1263
+ ? "border-cyan-300 bg-cyan-400/10"
1264
+ : active
1265
+ ? "border-emerald-400/40 bg-emerald-400/10"
1266
+ : "border-transparent hover:border-cyan-400/20 hover:bg-white/[0.03]"
1267
+ }`}
1268
+ title={entry.path}
1269
+ >
1270
+ <span className="min-w-0 truncate font-mono text-[10px] text-slate-200" style={{ paddingLeft: `${Math.max(0, entry.depth - 1) * 10}px` }}>
1271
+ <span className={entry.kind === "dir" ? "text-amber-300" : "text-slate-500"}>
1272
+ {entry.kind === "dir" ? "▸" : "•"}
1273
+ </span>{" "}
1274
+ {entry.name}
1275
+ </span>
1276
+ <span className={`rounded px-1 py-0.5 font-mono text-[8px] ${fileCategoryClass(entry.category)}`}>
1277
+ {active ? "focus" : entry.category}
1278
+ </span>
1279
+ <span className="col-span-2 truncate pl-4 font-mono text-[8px] text-slate-600">
1280
+ {entry.path} · {formatRelativeTime(entry.updatedAt)}
1281
+ </span>
1282
+ </button>
1283
+ );
1284
+ })
1285
+ )}
1286
+ </div>
1287
+ </div>
1288
+ );
1289
+ }
1290
+
1291
+ function isHighlightedPath(pathValue: string, highlightedPaths: Set<string>) {
1292
+ if (highlightedPaths.has(pathValue)) return true;
1293
+ for (const candidate of highlightedPaths) {
1294
+ if (candidate && pathValue.startsWith(`${candidate}/`)) return true;
1295
+ }
1296
+ return false;
1297
+ }
1298
+
1299
+ function fileCategoryClass(category: HarnessFileEntry["category"]) {
1300
+ return {
1301
+ documents: "bg-cyan-400/10 text-cyan-200",
1302
+ todos: "bg-orange-400/10 text-orange-200",
1303
+ activity: "bg-emerald-400/10 text-emerald-200",
1304
+ knowledge: "bg-violet-400/10 text-violet-200",
1305
+ runtime: "bg-slate-500/10 text-slate-300",
1306
+ config: "bg-amber-400/10 text-amber-200",
1307
+ other: "bg-slate-800 text-slate-400",
1308
+ }[category];
1309
+ }
1310
+
704
1311
  // ===== Knowledge base panel =====
705
1312
 
706
1313
  type KnowledgeKind = "Gotcha" | "Convention";
@@ -1542,7 +2149,7 @@ function WorkflowHeatmap({
1542
2149
  : selected
1543
2150
  ? "border-cyan-300"
1544
2151
  : "border-transparent"
1545
- } ${heatColor(cell.count)}`}
2152
+ } ${heatColor(cell.count, cell.laneId)}`}
1546
2153
  style={{ gridColumn: cell.x + 2, gridRow: cell.y + 2 }}
1547
2154
  title={
1548
2155
  interactive
@@ -1732,6 +2339,14 @@ function DocumentViewer({
1732
2339
  | undefined;
1733
2340
  const roleDoc = role ? mission?.[role] : null;
1734
2341
  const workerDoc = lane?.worker?.content ?? null;
2342
+ // CDO ships a self-contained HTML preview (cdo/preview.html). When the CDO
2343
+ // lane is selected and a preview exists, surface the visual deliverable in a
2344
+ // sandboxed iframe (the "click harness-cdo to view the Preview" contract),
2345
+ // with a toggle back to the cdo.md decision record.
2346
+ const cdoPreview = !customDoc && !workerDoc && role === "cdo" ? mission?.cdoPreview ?? null : null;
2347
+ const [cdoMode, setCdoMode] = useState<"preview" | "doc">("preview");
2348
+ const showPreview = !!cdoPreview && cdoMode === "preview";
2349
+
1735
2350
  const rawContent = customDoc
1736
2351
  ? customDoc.content
1737
2352
  : workerDoc ?? roleDoc ?? mission?.ceo ?? "_No document selected._";
@@ -1745,28 +2360,59 @@ function DocumentViewer({
1745
2360
  : role
1746
2361
  ? role.toUpperCase()
1747
2362
  : "CEO"
1748
- }`;
2363
+ }${cdoPreview ? (showPreview ? " · preview" : " · doc") : ""}`;
1749
2364
  return (
1750
2365
  <section className="panel-shell flex min-h-0 flex-1 flex-col p-2">
1751
2366
  <div className="flex items-center justify-between gap-2">
1752
2367
  <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>
2368
+ <div className="flex shrink-0 items-center gap-1">
2369
+ {cdoPreview && (
2370
+ <div className="mr-1 flex overflow-hidden rounded border border-slate-700 font-mono text-[9px]">
2371
+ <button
2372
+ type="button"
2373
+ onClick={() => setCdoMode("preview")}
2374
+ className={`px-1.5 py-0.5 ${showPreview ? "bg-violet-500/30 text-violet-200" : "text-slate-400 hover:text-violet-200"}`}
2375
+ >
2376
+ Preview
2377
+ </button>
2378
+ <button
2379
+ type="button"
2380
+ onClick={() => setCdoMode("doc")}
2381
+ className={`px-1.5 py-0.5 ${!showPreview ? "bg-violet-500/30 text-violet-200" : "text-slate-400 hover:text-violet-200"}`}
2382
+ >
2383
+ Doc
2384
+ </button>
2385
+ </div>
2386
+ )}
2387
+ <button
2388
+ type="button"
2389
+ onClick={onToggle}
2390
+ className="grid h-6 w-6 place-items-center rounded text-[11px] text-slate-400 hover:bg-white/10 hover:text-cyan-200"
2391
+ title={expanded ? "Minimize viewer" : "Expand to 50%"}
2392
+ aria-label={expanded ? "Minimize viewer" : "Expand viewer"}
2393
+ >
2394
+ <span className={`doc-toggle-icon ${expanded ? "rotate-180" : ""}`}>↗</span>
2395
+ </button>
2396
+ </div>
1769
2397
  </div>
2398
+ {showPreview ? (
2399
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-hidden p-1">
2400
+ <iframe
2401
+ title="CDO design preview"
2402
+ sandbox=""
2403
+ className="h-full w-full rounded bg-white"
2404
+ srcDoc={cdoPreview ?? ""}
2405
+ />
2406
+ </div>
2407
+ ) : (
2408
+ <div className="inset-shell mt-2 min-h-0 flex-1 overflow-auto p-3">
2409
+ <article className="markdown-doc text-[11px] leading-5 text-slate-300">
2410
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={MARKDOWN_COMPONENTS}>
2411
+ {content}
2412
+ </ReactMarkdown>
2413
+ </article>
2414
+ </div>
2415
+ )}
1770
2416
  </section>
1771
2417
  );
1772
2418
  }
@@ -1797,8 +2443,16 @@ function Arrow() {
1797
2443
  return <span className="text-slate-600">→</span>;
1798
2444
  }
1799
2445
 
1800
- function heatColor(count: number) {
1801
- // 3-state activity from sample.count: 0 idle / 1 standby / 2 in-progress.
2446
+ function heatColor(count: number, laneId?: string) {
2447
+ // 3-state activity from sample.count: 0 idle / 1 recent / 2 in-progress.
2448
+ // Render per-role so each lane's history shows in its own CXX colour instead of
2449
+ // collapsing into one dim emerald (which read as "only CEO is working"). The
2450
+ // base colour + "/30" variant mirror the patterns already used in LayerActivityPanel.
2451
+ const role = (laneId ?? "").split(":")[0] as CxxRole;
2452
+ if (role in CXX_COLOR) {
2453
+ if (count >= 2) return CXX_COLOR[role];
2454
+ if (count >= 1) return `${CXX_COLOR[role]}/30`;
2455
+ }
1802
2456
  if (count >= 2) return "bg-emerald-500/80";
1803
2457
  if (count >= 1) return "bg-emerald-500/30";
1804
2458
  return "bg-slate-900";