@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.
- 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 +20 -2
- package/HR-Resource/ops-prompt-inspector/SKILL.md +105 -0
- package/HR-Resource/ops-prompt-inspector/agents/installation-agent.json +7 -0
- package/HR-Resource/ops-prompt-inspector/assets/PromptInspector.tsx +599 -0
- package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
- package/HR-Resource/ops-prompt-inspector/references/handler-templates.md +255 -0
- package/HR-Resource/ops-prompt-inspector/scripts/discover_apis.py +509 -0
- package/HR-Resource/ops-prompt-inspector/scripts/install.sh +32 -0
- package/HR-Resource/ops-prompt-inspector/scripts/setup.py +381 -0
- package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
- package/apps/harness-dashboard/components/Scene.tsx +568 -35
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
- package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
- package/apps/harness-dashboard/lib/harness-state.ts +107 -796
- package/apps/harness-dashboard/lib/types.ts +22 -222
- package/apps/harness-dashboard/playwright.config.ts +26 -7
- package/assets/templates/AGENTS-ko.md.template +1 -1
- package/assets/templates/AGENTS.md.template +12 -2
- package/assets/templates/HARNESS.md +2 -0
- package/assets/templates/company-pipeline-manifest.json +5 -14
- package/assets/templates/worker-report.md.template +38 -0
- package/bin/init.js +4 -3
- package/commands/goal.md +4 -2
- package/commands/hot-fix.md +1 -1
- package/commands/submission.md +1 -1
- package/package.json +1 -1
- package/scripts/conductor-tick.sh +12 -0
- package/scripts/harness-agenda.sh +105 -0
- package/scripts/harness-company-block.sh +89 -0
- package/scripts/harness-company-complete.sh +17 -8
- package/scripts/harness-company-cycle.sh +54 -0
- package/scripts/harness-progress-set.sh +8 -2
- package/scripts/harness-stop.sh +76 -0
- package/scripts/harness-wake-control.sh +151 -0
- package/scripts/harness-wake.sh +5 -0
- package/apps/harness-dashboard/components/Drawer.tsx +0 -146
- package/apps/harness-dashboard/components/Header.tsx +0 -40
- package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
- package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
- package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
- package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
- package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
- package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
- package/apps/harness-dashboard/lib/furniture.ts +0 -165
- package/apps/harness-dashboard/lib/i18n.ts +0 -26
- package/apps/harness-dashboard/lib/iso.ts +0 -150
- package/apps/harness-dashboard/lib/markdown.tsx +0 -306
- package/apps/harness-dashboard/lib/path-planning.ts +0 -72
- package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
|
@@ -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:
|
|
265
|
-
<section className="grid shrink-0 grid-cols-
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
<
|
|
279
|
-
|
|
305
|
+
<StepProgressPanel
|
|
306
|
+
mission={selectedMission}
|
|
307
|
+
runtimeAgent={snapshot.runtime.currentAgent}
|
|
280
308
|
todos={snapshot.todos}
|
|
281
|
-
|
|
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
|
|
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.
|
|
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
|
-
<
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
}
|