@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.
- package/HR-Resource/cdo/SKILL.md +9 -1
- package/HR-Resource/ceo/SKILL.md +33 -2
- package/HR-Resource/coo/SKILL.md +9 -1
- package/HR-Resource/cqo/SKILL.md +9 -1
- package/HR-Resource/cto/SKILL.md +10 -2
- package/HR-Resource/ops/SKILL.md +12 -4
- package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
- package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/app/brick.css +482 -0
- package/apps/harness-dashboard/app/layout.tsx +13 -0
- package/apps/harness-dashboard/app/page.tsx +2 -2
- package/apps/harness-dashboard/components/Scene.tsx +690 -36
- package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
- package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
- package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
- package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
- package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
- package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
- package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
- package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +2 -1
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/scripts/harness-worker-dispatch.sh +31 -7
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -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:
|
|
265
|
-
|
|
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={
|
|
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
|
|
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.
|
|
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
|
-
<
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
|
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";
|