@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
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import type { MissionDoc, OwnerPromptEntry } from "@/lib/types";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
ownerHistory: OwnerPromptEntry[];
|
|
6
|
-
mission?: MissionDoc | null;
|
|
7
|
-
onEntryClick?: (entry: OwnerPromptEntry) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function extractSectionText(content: string, ...headings: string[]): string | null {
|
|
11
|
-
const body = content.replace(/^---[\s\S]*?---\n+/, "");
|
|
12
|
-
for (const heading of headings) {
|
|
13
|
-
const pattern = new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "m");
|
|
14
|
-
const match = pattern.exec(body);
|
|
15
|
-
if (!match) continue;
|
|
16
|
-
const start = match.index + match[0].length;
|
|
17
|
-
const rest = body.slice(start);
|
|
18
|
-
const lines = rest.split("\n");
|
|
19
|
-
const snippet: string[] = [];
|
|
20
|
-
for (const line of lines) {
|
|
21
|
-
if (line.startsWith("## ") || line.startsWith("# ")) break;
|
|
22
|
-
const trimmed = line.trim();
|
|
23
|
-
if (trimmed && !trimmed.startsWith("---")) snippet.push(trimmed);
|
|
24
|
-
if (snippet.length >= 6) break;
|
|
25
|
-
}
|
|
26
|
-
const result = snippet.join("\n").slice(0, 600);
|
|
27
|
-
if (result) return result;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function OwnerHistoryTab({ ownerHistory, mission, onEntryClick }: Props) {
|
|
33
|
-
// Mission-specific view
|
|
34
|
-
if (mission) {
|
|
35
|
-
const requestText = mission.ceo
|
|
36
|
-
? extractSectionText(mission.ceo, "Owner 요청 요약", "Owner Request")
|
|
37
|
-
: null;
|
|
38
|
-
|
|
39
|
-
// Find closest owner history entry by timestamp
|
|
40
|
-
let closestEntry: OwnerPromptEntry | null = null;
|
|
41
|
-
if (ownerHistory.length > 0) {
|
|
42
|
-
const missionTime = Date.parse(mission.ts);
|
|
43
|
-
let closestDiff = Infinity;
|
|
44
|
-
for (const entry of ownerHistory) {
|
|
45
|
-
const t = Date.parse(entry.ts);
|
|
46
|
-
if (!Number.isNaN(t)) {
|
|
47
|
-
const diff = Math.abs(t - missionTime);
|
|
48
|
-
if (diff < closestDiff) { closestDiff = diff; closestEntry = entry; }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className="space-y-3">
|
|
55
|
-
<div className="flex items-center gap-2 mb-3">
|
|
56
|
-
<span className="font-mono text-[9px] uppercase tracking-[0.2em] text-gray-500">from</span>
|
|
57
|
-
<span className="rounded bg-amber-500/10 border border-amber-500/30 px-2 py-0.5 text-[10px] text-amber-300 font-mono">
|
|
58
|
-
Owner
|
|
59
|
-
</span>
|
|
60
|
-
<span className="font-mono text-[9px] text-gray-600">→</span>
|
|
61
|
-
<span className="rounded bg-cyan-400/10 border border-cyan-400/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono">
|
|
62
|
-
CEO
|
|
63
|
-
</span>
|
|
64
|
-
<span className="ml-auto font-mono text-[9px] text-gray-600 truncate max-w-[160px]">
|
|
65
|
-
{mission.missionId}
|
|
66
|
-
</span>
|
|
67
|
-
</div>
|
|
68
|
-
|
|
69
|
-
{/* CEO's summary of owner request */}
|
|
70
|
-
{requestText && (
|
|
71
|
-
<div className="rounded border border-cyan-400/20 bg-cyan-400/5 p-2.5">
|
|
72
|
-
<div className="font-mono text-[9px] uppercase tracking-[0.15em] text-cyan-400/60 mb-1.5">
|
|
73
|
-
CEO가 정리한 Owner 요청
|
|
74
|
-
</div>
|
|
75
|
-
<p className="text-[11px] leading-relaxed text-gray-300 whitespace-pre-line">
|
|
76
|
-
{requestText}
|
|
77
|
-
</p>
|
|
78
|
-
</div>
|
|
79
|
-
)}
|
|
80
|
-
|
|
81
|
-
{/* Closest matching progress.log entry */}
|
|
82
|
-
{closestEntry && (
|
|
83
|
-
<div
|
|
84
|
-
className={`rounded border border-gray-700/60 bg-black/20 p-2 ${onEntryClick ? "cursor-pointer hover:border-amber-400/30 hover:bg-amber-400/5 transition-colors" : ""}`}
|
|
85
|
-
onClick={onEntryClick ? () => onEntryClick(closestEntry!) : undefined}
|
|
86
|
-
>
|
|
87
|
-
<div className="flex items-center justify-between gap-2 mb-1">
|
|
88
|
-
<span className="font-mono text-[9px] text-gray-500">progress.log</span>
|
|
89
|
-
<div className="flex items-center gap-1.5">
|
|
90
|
-
<span className="font-mono text-[9px] text-gray-600">{closestEntry.ts}</span>
|
|
91
|
-
<span
|
|
92
|
-
className={`font-mono text-[9px] px-1.5 py-0.5 rounded ${
|
|
93
|
-
closestEntry.type === "goal"
|
|
94
|
-
? "bg-emerald-500/15 text-emerald-300"
|
|
95
|
-
: closestEntry.type === "submission"
|
|
96
|
-
? "bg-sky-500/15 text-sky-300"
|
|
97
|
-
: closestEntry.type === "hot-fix"
|
|
98
|
-
? "bg-rose-500/15 text-rose-300"
|
|
99
|
-
: "bg-gray-500/15 text-gray-400"
|
|
100
|
-
}`}
|
|
101
|
-
>
|
|
102
|
-
{closestEntry.type}
|
|
103
|
-
</span>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
<p className="text-[11px] leading-relaxed text-gray-400 break-words">{closestEntry.content}</p>
|
|
107
|
-
</div>
|
|
108
|
-
)}
|
|
109
|
-
|
|
110
|
-
{!requestText && !closestEntry && (
|
|
111
|
-
<div className="text-gray-500 text-xs">No owner prompt found for this mission.</div>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// All-history fallback (no mission selected)
|
|
118
|
-
if (!ownerHistory || ownerHistory.length === 0) {
|
|
119
|
-
return <div className="text-gray-500">No prompt history found in progress.log.</div>;
|
|
120
|
-
}
|
|
121
|
-
return (
|
|
122
|
-
<div className="space-y-2">
|
|
123
|
-
<div className="mb-3 flex items-center gap-2">
|
|
124
|
-
<span className="font-mono text-[9px] uppercase tracking-[0.2em] text-gray-500">from</span>
|
|
125
|
-
<span className="rounded bg-amber-500/10 border border-amber-500/30 px-2 py-0.5 text-[10px] text-amber-300 font-mono">
|
|
126
|
-
Owner
|
|
127
|
-
</span>
|
|
128
|
-
<span className="font-mono text-[9px] text-gray-600">→</span>
|
|
129
|
-
<span className="rounded bg-cyan-400/10 border border-cyan-400/30 px-2 py-0.5 text-[10px] text-cyan-300 font-mono">
|
|
130
|
-
CEO
|
|
131
|
-
</span>
|
|
132
|
-
</div>
|
|
133
|
-
{ownerHistory.map((entry, i) => (
|
|
134
|
-
<div
|
|
135
|
-
key={i}
|
|
136
|
-
className={`rounded border border-gray-700/60 bg-black/20 p-2 ${onEntryClick ? "cursor-pointer hover:border-cyan-400/30 hover:bg-cyan-400/5 transition-colors" : ""}`}
|
|
137
|
-
onClick={onEntryClick ? () => onEntryClick(entry) : undefined}
|
|
138
|
-
>
|
|
139
|
-
<div className="flex items-center justify-between gap-2 mb-1">
|
|
140
|
-
<span className="font-mono text-[9px] text-gray-500">{entry.ts}</span>
|
|
141
|
-
<span
|
|
142
|
-
className={`font-mono text-[9px] px-1.5 py-0.5 rounded ${
|
|
143
|
-
entry.type === "goal"
|
|
144
|
-
? "bg-emerald-500/15 text-emerald-300"
|
|
145
|
-
: entry.type === "submission"
|
|
146
|
-
? "bg-sky-500/15 text-sky-300"
|
|
147
|
-
: entry.type === "hot-fix"
|
|
148
|
-
? "bg-rose-500/15 text-rose-300"
|
|
149
|
-
: "bg-gray-500/15 text-gray-400"
|
|
150
|
-
}`}
|
|
151
|
-
>
|
|
152
|
-
{entry.type}
|
|
153
|
-
</span>
|
|
154
|
-
</div>
|
|
155
|
-
<p className="text-[11px] leading-relaxed text-gray-300 break-words">{entry.content}</p>
|
|
156
|
-
</div>
|
|
157
|
-
))}
|
|
158
|
-
</div>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import type { RoomState } from "@/lib/types";
|
|
3
|
-
|
|
4
|
-
interface RoomMetricsTabProps {
|
|
5
|
-
room: RoomState;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const METRIC_LABEL: Record<string, string> = {
|
|
9
|
-
sprint_number: "Cycle",
|
|
10
|
-
last_review: "Last review",
|
|
11
|
-
last_audit: "Last audit",
|
|
12
|
-
last_check: "Last check",
|
|
13
|
-
sprint_verdict: "Verdict",
|
|
14
|
-
open_alerts: "Open alerts",
|
|
15
|
-
open_arch_risks: "Open arch risks",
|
|
16
|
-
open_regressions: "Open regressions",
|
|
17
|
-
cadence: "Cadence",
|
|
18
|
-
next_scheduled: "Next meeting",
|
|
19
|
-
active_tracks: "Active tracks",
|
|
20
|
-
active_hypothesis: "Active hypothesis",
|
|
21
|
-
active_workers: "Active workers",
|
|
22
|
-
open_incidents: "Open incidents",
|
|
23
|
-
pass_rate: "Pass rate",
|
|
24
|
-
contract_signed: "Contract signed",
|
|
25
|
-
eval_scores: "Eval scores",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
function fmt(key: string, value: unknown): string {
|
|
29
|
-
if (value === null || value === undefined || value === "") return "—";
|
|
30
|
-
if (key === "pass_rate" && typeof value === "number") {
|
|
31
|
-
return `${Math.round(value * 100)}%`;
|
|
32
|
-
}
|
|
33
|
-
if (key === "contract_signed" && typeof value === "object" && value) {
|
|
34
|
-
const v = value as { be?: boolean; fe?: boolean };
|
|
35
|
-
const parts = [v.be ? "BE" : null, v.fe ? "FE" : null].filter(Boolean);
|
|
36
|
-
return parts.length ? parts.join("·") : "—";
|
|
37
|
-
}
|
|
38
|
-
if (key === "eval_scores" && typeof value === "object" && value) {
|
|
39
|
-
const v = value as Record<string, number | null | undefined>;
|
|
40
|
-
const parts = Object.entries(v)
|
|
41
|
-
.filter(([, s]) => typeof s === "number")
|
|
42
|
-
.map(([k, s]) => `${k.slice(0, 3)} ${(s as number).toFixed(1)}`);
|
|
43
|
-
return parts.length ? parts.join(" · ") : "—";
|
|
44
|
-
}
|
|
45
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
46
|
-
return String(value);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function RoomMetricsTab({ room }: RoomMetricsTabProps) {
|
|
50
|
-
const metrics = room.metrics ?? {};
|
|
51
|
-
const entries = Object.entries(metrics);
|
|
52
|
-
return (
|
|
53
|
-
<div className="space-y-3">
|
|
54
|
-
<div className="text-gray-400 text-[11px]">
|
|
55
|
-
{room.label_ko} <span className="text-gray-600">({room.label_en})</span>
|
|
56
|
-
</div>
|
|
57
|
-
{entries.length === 0 ? (
|
|
58
|
-
<div className="text-gray-500">No metrics available for this room.</div>
|
|
59
|
-
) : (
|
|
60
|
-
<div className="grid grid-cols-2 gap-2">
|
|
61
|
-
{entries.map(([k, v]) => (
|
|
62
|
-
<div
|
|
63
|
-
key={k}
|
|
64
|
-
data-testid={`metric-${k}`}
|
|
65
|
-
className="rounded border border-brick-wall bg-brick-wall/30 px-2 py-1.5"
|
|
66
|
-
>
|
|
67
|
-
<div className="text-[10px] uppercase tracking-wider text-gray-500">
|
|
68
|
-
{METRIC_LABEL[k] ?? k}
|
|
69
|
-
</div>
|
|
70
|
-
<div className="text-[12px] text-gray-100">{fmt(k, v)}</div>
|
|
71
|
-
</div>
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
74
|
-
)}
|
|
75
|
-
{room.seatLayout && (
|
|
76
|
-
<div className="rounded border border-brick-wall bg-brick-wall/30 px-2 py-1.5">
|
|
77
|
-
<div className="text-[10px] uppercase tracking-wider text-gray-500">
|
|
78
|
-
Seats
|
|
79
|
-
</div>
|
|
80
|
-
<div className="text-[12px] text-gray-100">
|
|
81
|
-
{room.seatLayout.occupants.length} / {room.seatLayout.seats}
|
|
82
|
-
{room.seatLayout.overflow > 0 && (
|
|
83
|
-
<span className="text-aura-alert"> +{room.seatLayout.overflow}</span>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import type { ParallelTrack } from "@/lib/types";
|
|
3
|
-
|
|
4
|
-
const STATUS_TONE: Record<string, string> = {
|
|
5
|
-
dispatched: "border-aura-talking/40 bg-aura-talking/10 text-aura-talking",
|
|
6
|
-
in_progress: "border-aura-typing/40 bg-aura-typing/10 text-aura-typing",
|
|
7
|
-
joined: "border-aura-typing/60 bg-aura-typing/20 text-aura-typing",
|
|
8
|
-
blocked: "border-aura-alert/40 bg-aura-alert/10 text-aura-alert",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
interface TracksTabProps {
|
|
12
|
-
tracks: ParallelTrack[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function TracksTab({ tracks }: TracksTabProps) {
|
|
16
|
-
if (tracks.length === 0) {
|
|
17
|
-
return (
|
|
18
|
-
<div className="text-gray-500" data-testid="tracks-empty">
|
|
19
|
-
No parallel tracks active.
|
|
20
|
-
</div>
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
return (
|
|
24
|
-
<ul className="space-y-2" data-testid="tracks-list">
|
|
25
|
-
{tracks.map((t) => (
|
|
26
|
-
<li
|
|
27
|
-
key={t.id}
|
|
28
|
-
data-testid={`track-row-${t.id}`}
|
|
29
|
-
className={`rounded border px-2 py-1.5 ${
|
|
30
|
-
STATUS_TONE[t.status] ?? STATUS_TONE.dispatched
|
|
31
|
-
}`}
|
|
32
|
-
>
|
|
33
|
-
<div className="flex items-center justify-between text-[11px]">
|
|
34
|
-
<span className="font-semibold">{t.id}</span>
|
|
35
|
-
<span className="uppercase tracking-wider text-[9px]">{t.status}</span>
|
|
36
|
-
</div>
|
|
37
|
-
<div className="mt-1 text-[10px] text-gray-300">
|
|
38
|
-
{t.from_meeting} → {t.to_dept} ({t.to_room})
|
|
39
|
-
</div>
|
|
40
|
-
{t.label && (
|
|
41
|
-
<div className="mt-0.5 text-[11px] text-gray-100">{t.label}</div>
|
|
42
|
-
)}
|
|
43
|
-
</li>
|
|
44
|
-
))}
|
|
45
|
-
</ul>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { Html } from "@react-three/drei";
|
|
3
|
-
import { ROOM_RECTS } from "@/lib/iso";
|
|
4
|
-
import type { ArchiveStat } from "@/lib/types";
|
|
5
|
-
|
|
6
|
-
interface ArchiveBoxesProps {
|
|
7
|
-
archive: ArchiveStat;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Visualises completed sprints as cardboard boxes stacked inside the archive
|
|
11
|
-
// room. The most-recent three are labelled (drei <Html>); the rest are
|
|
12
|
-
// anonymous boxes plus a "+N more" label.
|
|
13
|
-
export function ArchiveBoxes({ archive }: ArchiveBoxesProps) {
|
|
14
|
-
const room = ROOM_RECTS.find((r) => r.id === "archive")!;
|
|
15
|
-
const cols = 6;
|
|
16
|
-
const rowSpacingX = (room.ww - 1.0) / cols; // leave 0.5 inset on each side
|
|
17
|
-
const rowSpacingZ = 0.45;
|
|
18
|
-
const baseX = room.wx + 0.5;
|
|
19
|
-
const baseZ = room.wy + 0.5;
|
|
20
|
-
|
|
21
|
-
// Use the full sorted list so each rendered box can carry its own verdict
|
|
22
|
-
// colour, not just the recent three.
|
|
23
|
-
const ordered = archive.all.length > 0 ? archive.all : archive.recent;
|
|
24
|
-
const boxes = Array.from({ length: archive.sprintCount }, (_, i) => {
|
|
25
|
-
const col = i % cols;
|
|
26
|
-
const row = Math.floor(i / cols);
|
|
27
|
-
const entry = i < ordered.length ? ordered[i] : null;
|
|
28
|
-
return {
|
|
29
|
-
x: baseX + col * rowSpacingX + rowSpacingX / 2,
|
|
30
|
-
z: baseZ + row * rowSpacingZ,
|
|
31
|
-
isRecent: i < 3,
|
|
32
|
-
entry,
|
|
33
|
-
result: entry?.result ?? "unknown",
|
|
34
|
-
};
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const overflowAfterThree = Math.max(0, archive.sprintCount - 3);
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<group data-testid="archive-boxes">
|
|
41
|
-
{boxes.map((b, i) => {
|
|
42
|
-
const baseColor = b.isRecent ? "#caa978" : "#8a7c5a";
|
|
43
|
-
const tapeColor =
|
|
44
|
-
b.result === "PASS"
|
|
45
|
-
? "#34d399"
|
|
46
|
-
: b.result === "FAIL"
|
|
47
|
-
? "#ef4444"
|
|
48
|
-
: "#6a5a3a";
|
|
49
|
-
return (
|
|
50
|
-
<group key={i} position={[b.x, 0.21, b.z]} data-testid={`archive-box-${b.result.toLowerCase()}`}>
|
|
51
|
-
<mesh castShadow position={[0, 0.16, 0]}>
|
|
52
|
-
<boxGeometry args={[0.34, 0.32, 0.28]} />
|
|
53
|
-
<meshStandardMaterial
|
|
54
|
-
color={baseColor}
|
|
55
|
-
roughness={0.85}
|
|
56
|
-
/>
|
|
57
|
-
</mesh>
|
|
58
|
-
{/* Tape line on top — verdict-coloured for at-a-glance archive scan. */}
|
|
59
|
-
<mesh position={[0, 0.33, 0]}>
|
|
60
|
-
<boxGeometry args={[0.36, 0.01, 0.06]} />
|
|
61
|
-
<meshStandardMaterial color={tapeColor} />
|
|
62
|
-
</mesh>
|
|
63
|
-
{b.isRecent && b.entry && (
|
|
64
|
-
<Html
|
|
65
|
-
position={[0, 0.45, 0]}
|
|
66
|
-
transform
|
|
67
|
-
occlude={false}
|
|
68
|
-
scale={0.08}
|
|
69
|
-
pointerEvents="none"
|
|
70
|
-
zIndexRange={[3, 0]}
|
|
71
|
-
>
|
|
72
|
-
<div className="rounded bg-black/70 px-1.5 py-0.5 text-[10px] font-mono text-gray-100 whitespace-nowrap">
|
|
73
|
-
{b.entry.label}
|
|
74
|
-
</div>
|
|
75
|
-
</Html>
|
|
76
|
-
)}
|
|
77
|
-
</group>
|
|
78
|
-
);
|
|
79
|
-
})}
|
|
80
|
-
{overflowAfterThree > 0 && (
|
|
81
|
-
<Html
|
|
82
|
-
position={[room.wx + room.ww / 2, 0.6, room.wy + room.wh - 0.3]}
|
|
83
|
-
transform
|
|
84
|
-
occlude={false}
|
|
85
|
-
scale={0.1}
|
|
86
|
-
pointerEvents="none"
|
|
87
|
-
>
|
|
88
|
-
<div className="rounded bg-aura-typing/40 px-2 py-0.5 text-[10px] font-mono text-aura-typing whitespace-nowrap">
|
|
89
|
-
+{overflowAfterThree} more
|
|
90
|
-
</div>
|
|
91
|
-
</Html>
|
|
92
|
-
)}
|
|
93
|
-
</group>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { Html } from "@react-three/drei";
|
|
3
|
-
import { ROOM_RECTS } from "@/lib/iso";
|
|
4
|
-
import type {
|
|
5
|
-
ContractSnapshot,
|
|
6
|
-
EvalScores,
|
|
7
|
-
RoomId,
|
|
8
|
-
} from "@/lib/types";
|
|
9
|
-
|
|
10
|
-
interface DeptBoardProps {
|
|
11
|
-
roomId: Extract<RoomId, "cto-team" | "cqo-team">;
|
|
12
|
-
contract: ContractSnapshot;
|
|
13
|
-
evalScores: EvalScores | null;
|
|
14
|
-
passRate: number | null;
|
|
15
|
-
openArchRisks?: number;
|
|
16
|
-
openRegressions?: number;
|
|
17
|
-
sprintVerdict?: "pending" | "PASS" | "FAIL";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function fmtRate(rate: number | null): string {
|
|
21
|
-
if (rate === null || !Number.isFinite(rate)) return "—";
|
|
22
|
-
return `${Math.round(rate * 100)}%`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function fmtScore(s: number | null | undefined): string {
|
|
26
|
-
if (s === null || s === undefined) return "—";
|
|
27
|
-
return s.toFixed(2);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Wall board for CTO/CQO rooms. CTO: contract sign-off + pass-rate. CQO:
|
|
31
|
-
// 5-axis eval scores + verdict + regressions.
|
|
32
|
-
export function DeptBoard({
|
|
33
|
-
roomId,
|
|
34
|
-
contract,
|
|
35
|
-
evalScores,
|
|
36
|
-
passRate,
|
|
37
|
-
openArchRisks,
|
|
38
|
-
openRegressions,
|
|
39
|
-
sprintVerdict,
|
|
40
|
-
}: DeptBoardProps) {
|
|
41
|
-
const room = ROOM_RECTS.find((r) => r.id === roomId)!;
|
|
42
|
-
const cx = room.wx + room.ww / 2;
|
|
43
|
-
// Mount on the back wall (wy is the north edge).
|
|
44
|
-
const cz = room.wy + 0.18;
|
|
45
|
-
const isCto = roomId === "cto-team";
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Html
|
|
49
|
-
position={[cx, 0.85, cz]}
|
|
50
|
-
transform
|
|
51
|
-
occlude={false}
|
|
52
|
-
scale={0.14}
|
|
53
|
-
pointerEvents="none"
|
|
54
|
-
zIndexRange={[5, 0]}
|
|
55
|
-
>
|
|
56
|
-
<div
|
|
57
|
-
data-testid={`dept-board-${roomId}`}
|
|
58
|
-
className="rounded-md border border-brick-wall bg-brick-bg/95 px-3 py-2 shadow-lg text-gray-100 font-mono"
|
|
59
|
-
style={{ minWidth: 240, maxWidth: 320 }}
|
|
60
|
-
>
|
|
61
|
-
<div className="text-[9px] uppercase tracking-widest text-gray-400">
|
|
62
|
-
{isCto ? "CTO BOARD" : "CQO BOARD"}
|
|
63
|
-
</div>
|
|
64
|
-
<div className="mt-1 grid grid-cols-2 gap-1 text-[10px]">
|
|
65
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5">
|
|
66
|
-
<span className="text-gray-500">sprint</span>{" "}
|
|
67
|
-
<span data-testid={`board-sprint-${roomId}`}>
|
|
68
|
-
{contract.sprint_number ?? "—"}
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5">
|
|
72
|
-
<span className="text-gray-500">pass</span>{" "}
|
|
73
|
-
<span data-testid={`board-pass-${roomId}`}>{fmtRate(passRate)}</span>
|
|
74
|
-
</div>
|
|
75
|
-
{isCto ? (
|
|
76
|
-
<>
|
|
77
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5">
|
|
78
|
-
<span className="text-gray-500">contract BE</span>{" "}
|
|
79
|
-
<span
|
|
80
|
-
className={
|
|
81
|
-
contract.contract_signed.be ? "text-aura-typing" : "text-gray-500"
|
|
82
|
-
}
|
|
83
|
-
>
|
|
84
|
-
{contract.contract_signed.be ? "signed" : "—"}
|
|
85
|
-
</span>
|
|
86
|
-
</div>
|
|
87
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5">
|
|
88
|
-
<span className="text-gray-500">contract FE</span>{" "}
|
|
89
|
-
<span
|
|
90
|
-
className={
|
|
91
|
-
contract.contract_signed.fe ? "text-aura-typing" : "text-gray-500"
|
|
92
|
-
}
|
|
93
|
-
>
|
|
94
|
-
{contract.contract_signed.fe ? "signed" : "—"}
|
|
95
|
-
</span>
|
|
96
|
-
</div>
|
|
97
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5 col-span-2">
|
|
98
|
-
<span className="text-gray-500">arch risks</span>{" "}
|
|
99
|
-
<span
|
|
100
|
-
className={
|
|
101
|
-
(openArchRisks ?? 0) > 0
|
|
102
|
-
? "text-aura-alert"
|
|
103
|
-
: "text-aura-typing"
|
|
104
|
-
}
|
|
105
|
-
>
|
|
106
|
-
{openArchRisks ?? 0}
|
|
107
|
-
</span>
|
|
108
|
-
</div>
|
|
109
|
-
</>
|
|
110
|
-
) : (
|
|
111
|
-
<>
|
|
112
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5 col-span-2">
|
|
113
|
-
<span className="text-gray-500">verdict</span>{" "}
|
|
114
|
-
<span
|
|
115
|
-
className={
|
|
116
|
-
sprintVerdict === "PASS"
|
|
117
|
-
? "text-aura-typing"
|
|
118
|
-
: sprintVerdict === "FAIL"
|
|
119
|
-
? "text-aura-alert"
|
|
120
|
-
: "text-gray-300"
|
|
121
|
-
}
|
|
122
|
-
>
|
|
123
|
-
{sprintVerdict ?? "pending"}
|
|
124
|
-
</span>
|
|
125
|
-
</div>
|
|
126
|
-
<div className="rounded bg-brick-wall/50 px-1.5 py-0.5 col-span-2">
|
|
127
|
-
<span className="text-gray-500">regressions</span>{" "}
|
|
128
|
-
<span
|
|
129
|
-
className={
|
|
130
|
-
(openRegressions ?? 0) > 0
|
|
131
|
-
? "text-aura-alert"
|
|
132
|
-
: "text-aura-typing"
|
|
133
|
-
}
|
|
134
|
-
>
|
|
135
|
-
{openRegressions ?? 0}
|
|
136
|
-
</span>
|
|
137
|
-
</div>
|
|
138
|
-
</>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{!isCto && evalScores && (
|
|
143
|
-
<div
|
|
144
|
-
data-testid="eval-scores"
|
|
145
|
-
className="mt-1.5 grid grid-cols-5 gap-1 text-[9px] uppercase"
|
|
146
|
-
>
|
|
147
|
-
{(
|
|
148
|
-
["functional", "visual", "code_quality", "architecture", "security"] as const
|
|
149
|
-
).map((axis) => {
|
|
150
|
-
const v = evalScores?.[axis] ?? null;
|
|
151
|
-
const fail = v !== null && typeof v === "number" && v < 2.8;
|
|
152
|
-
return (
|
|
153
|
-
<div
|
|
154
|
-
key={axis}
|
|
155
|
-
data-testid={`eval-axis-${axis}`}
|
|
156
|
-
className={`rounded px-1 py-0.5 text-center ${
|
|
157
|
-
fail
|
|
158
|
-
? "bg-aura-alert/30 text-aura-alert"
|
|
159
|
-
: "bg-aura-typing/20 text-aura-typing"
|
|
160
|
-
}`}
|
|
161
|
-
title={axis}
|
|
162
|
-
>
|
|
163
|
-
<div className="text-[8px] tracking-wider opacity-70">
|
|
164
|
-
{axis.slice(0, 3)}
|
|
165
|
-
</div>
|
|
166
|
-
<div>{fmtScore(v)}</div>
|
|
167
|
-
</div>
|
|
168
|
-
);
|
|
169
|
-
})}
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
</Html>
|
|
174
|
-
);
|
|
175
|
-
}
|