@walwal-harness/cli 7.1.40 → 7.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  11. package/apps/harness-dashboard/components/Scene.tsx +568 -35
  12. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  13. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  14. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  15. package/apps/harness-dashboard/lib/types.ts +22 -222
  16. package/apps/harness-dashboard/playwright.config.ts +26 -7
  17. package/assets/templates/AGENTS-ko.md.template +1 -1
  18. package/assets/templates/AGENTS.md.template +12 -2
  19. package/assets/templates/HARNESS.md +2 -0
  20. package/assets/templates/company-pipeline-manifest.json +5 -14
  21. package/assets/templates/worker-report.md.template +38 -0
  22. package/bin/init.js +2 -1
  23. package/commands/goal.md +4 -2
  24. package/commands/hot-fix.md +1 -1
  25. package/commands/submission.md +1 -1
  26. package/package.json +1 -1
  27. package/scripts/conductor-tick.sh +12 -0
  28. package/scripts/harness-agenda.sh +105 -0
  29. package/scripts/harness-company-block.sh +89 -0
  30. package/scripts/harness-company-complete.sh +17 -8
  31. package/scripts/harness-company-cycle.sh +54 -0
  32. package/scripts/harness-progress-set.sh +8 -2
  33. package/scripts/harness-stop.sh +76 -0
  34. package/scripts/harness-wake-control.sh +151 -0
  35. package/scripts/harness-wake.sh +5 -0
  36. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  37. package/apps/harness-dashboard/components/Header.tsx +0 -40
  38. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  39. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  40. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  41. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  42. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  43. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  44. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  45. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  46. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  47. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  48. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  49. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  50. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  51. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  52. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  53. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  54. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  55. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  56. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  57. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  58. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  59. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  60. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  61. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  62. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  63. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  64. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  65. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  66. package/apps/harness-dashboard/lib/iso.ts +0 -150
  67. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  68. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  69. 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
- }