@walwal-harness/cli 7.1.8 → 7.1.10

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 (86) hide show
  1. package/HR-Resource/ceo/SKILL.md +4 -4
  2. package/HR-Resource/cqo/SKILL.md +3 -3
  3. package/HR-Resource/cto/SKILL.md +4 -4
  4. package/README.md +58 -5
  5. package/apps/harness-dashboard/app/api/env/route.ts +62 -0
  6. package/apps/harness-dashboard/app/api/log/route.ts +19 -0
  7. package/apps/harness-dashboard/app/api/missions/route.ts +15 -0
  8. package/apps/harness-dashboard/app/api/snapshot/route.ts +15 -0
  9. package/apps/harness-dashboard/app/api/stream/route.ts +71 -0
  10. package/apps/harness-dashboard/app/globals.css +17 -0
  11. package/apps/harness-dashboard/app/layout.tsx +29 -0
  12. package/apps/harness-dashboard/app/page.tsx +35 -0
  13. package/apps/harness-dashboard/components/Drawer.tsx +97 -0
  14. package/apps/harness-dashboard/components/Header.tsx +40 -0
  15. package/apps/harness-dashboard/components/MissionTimeline.tsx +214 -0
  16. package/apps/harness-dashboard/components/OrgTree.tsx +330 -0
  17. package/apps/harness-dashboard/components/Scene.tsx +563 -0
  18. package/apps/harness-dashboard/components/Wordmark.tsx +14 -0
  19. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +56 -0
  20. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +89 -0
  21. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +42 -0
  22. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +50 -0
  23. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +92 -0
  24. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +520 -0
  25. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +160 -0
  26. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +90 -0
  27. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +47 -0
  28. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +95 -0
  29. package/apps/harness-dashboard/components/three/DeptBoard.tsx +175 -0
  30. package/apps/harness-dashboard/components/three/Floor3D.tsx +404 -0
  31. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +123 -0
  32. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +71 -0
  33. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +98 -0
  34. package/apps/harness-dashboard/components/three/Minifig3D.tsx +391 -0
  35. package/apps/harness-dashboard/components/three/OpsSiren.tsx +99 -0
  36. package/apps/harness-dashboard/components/three/Stage3D.tsx +104 -0
  37. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +81 -0
  38. package/apps/harness-dashboard/hooks/useHarnessStream.ts +53 -0
  39. package/apps/harness-dashboard/lib/__tests__/company-flow.test.ts +270 -0
  40. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +194 -0
  41. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +16 -0
  42. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +42 -0
  43. package/apps/harness-dashboard/lib/__tests__/safe-json.test.ts +46 -0
  44. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +159 -0
  45. package/apps/harness-dashboard/lib/__tests__/watcher.test.ts +48 -0
  46. package/apps/harness-dashboard/lib/agent-roster.ts +42 -0
  47. package/apps/harness-dashboard/lib/furniture.ts +165 -0
  48. package/apps/harness-dashboard/lib/harness-root.ts +20 -0
  49. package/apps/harness-dashboard/lib/harness-state.ts +1193 -0
  50. package/apps/harness-dashboard/lib/i18n.ts +26 -0
  51. package/apps/harness-dashboard/lib/iso.ts +150 -0
  52. package/apps/harness-dashboard/lib/log-filter.ts +23 -0
  53. package/apps/harness-dashboard/lib/markdown.tsx +306 -0
  54. package/apps/harness-dashboard/lib/path-planning.ts +72 -0
  55. package/apps/harness-dashboard/lib/safe-json.ts +24 -0
  56. package/apps/harness-dashboard/lib/state-mapping.ts +126 -0
  57. package/apps/harness-dashboard/lib/types.ts +329 -0
  58. package/apps/harness-dashboard/lib/watcher.ts +64 -0
  59. package/apps/harness-dashboard/next-env.d.ts +6 -0
  60. package/apps/harness-dashboard/next.config.mjs +9 -0
  61. package/apps/harness-dashboard/package-lock.json +4280 -0
  62. package/apps/harness-dashboard/package.json +38 -0
  63. package/apps/harness-dashboard/playwright.config.ts +23 -0
  64. package/apps/harness-dashboard/postcss.config.js +6 -0
  65. package/apps/harness-dashboard/tailwind.config.ts +49 -0
  66. package/apps/harness-dashboard/tsconfig.json +24 -0
  67. package/apps/harness-dashboard/vitest.config.ts +15 -0
  68. package/assets/templates/AGENTS-ko.md.template +19 -6
  69. package/assets/templates/AGENTS.md.template +20 -7
  70. package/assets/templates/HARNESS.md +75 -199
  71. package/assets/templates/config.json +2 -2
  72. package/bin/init.js +11 -11
  73. package/commands/goal.md +6 -4
  74. package/commands/hot-fix.md +16 -13
  75. package/commands/submission.md +28 -0
  76. package/conventions/ceo.md +3 -3
  77. package/conventions/shared.md +4 -3
  78. package/package.json +11 -1
  79. package/scripts/harness-dashboard-up.sh +24 -4
  80. package/scripts/harness-session-start.sh +1 -1
  81. package/scripts/harness-stop.sh +1 -1
  82. package/scripts/harness-user-prompt-submit.sh +12 -4
  83. package/scripts/harness-worker-evidence-validate.sh +8 -3
  84. package/scripts/import-agency-agents.js +1 -1
  85. package/scripts/init-agents-md.sh +17 -2
  86. package/assets/templates/AGENTS-ko /341/204/207/341/205/251/341/206/250/341/204/211/341/205/241/341/204/207/341/205/251/341/206/253.md" +0 -114
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import type { HarnessSnapshot } from "@/lib/types";
4
+
5
+ export type ConnectionState = "connecting" | "open" | "stale" | "failed";
6
+
7
+ const FAILURE_THRESHOLD = 5;
8
+
9
+ export function useHarnessStream(initial: HarnessSnapshot): {
10
+ snapshot: HarnessSnapshot;
11
+ connectionState: ConnectionState;
12
+ } {
13
+ const [snapshot, setSnapshot] = useState<HarnessSnapshot>(initial);
14
+ const [connectionState, setConnectionState] = useState<ConnectionState>("connecting");
15
+ const failuresRef = useRef(0);
16
+
17
+ useEffect(() => {
18
+ if (typeof window === "undefined") return;
19
+ const es = new EventSource("/api/stream");
20
+
21
+ es.onopen = () => {
22
+ failuresRef.current = 0;
23
+ setConnectionState("open");
24
+ };
25
+
26
+ es.onmessage = (ev) => {
27
+ try {
28
+ const next = JSON.parse(ev.data) as HarnessSnapshot;
29
+ setSnapshot(next);
30
+ failuresRef.current = 0;
31
+ setConnectionState("open");
32
+ } catch {
33
+ // Malformed payload — leave snapshot unchanged.
34
+ }
35
+ };
36
+
37
+ es.onerror = () => {
38
+ failuresRef.current += 1;
39
+ if (failuresRef.current >= FAILURE_THRESHOLD) {
40
+ setConnectionState("failed");
41
+ es.close();
42
+ } else {
43
+ setConnectionState("stale");
44
+ }
45
+ };
46
+
47
+ return () => {
48
+ es.close();
49
+ };
50
+ }, []);
51
+
52
+ return { snapshot, connectionState };
53
+ }
@@ -0,0 +1,270 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { execFileSync } from "node:child_process";
3
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const repoRoot = path.resolve(__dirname, "../../../../");
11
+ const hasHarnessShellFixtures =
12
+ existsSync(path.join(repoRoot, "scripts", "lib", "harness-progress-migrate.sh")) &&
13
+ existsSync(path.join(repoRoot, "assets", "templates", "config.json"));
14
+ const describeHarnessShell = hasHarnessShellFixtures ? describe : describe.skip;
15
+
16
+ function runBash(script: string, projectRoot: string, args: string[] = []) {
17
+ return execFileSync("bash", [path.join(repoRoot, script), projectRoot, ...args], {
18
+ cwd: repoRoot,
19
+ encoding: "utf8",
20
+ });
21
+ }
22
+
23
+ function writeState(projectRoot: string, progress: unknown, configOverrides: Record<string, unknown> = {}) {
24
+ mkdirSync(path.join(projectRoot, ".harness", "actions", "meetings"), { recursive: true });
25
+
26
+ const config = JSON.parse(
27
+ readFileSync(path.join(repoRoot, "assets", "templates", "config.json"), "utf8")
28
+ ) as Record<string, unknown>;
29
+ Object.assign(config, configOverrides);
30
+
31
+ writeFileSync(path.join(projectRoot, ".harness", "config.json"), JSON.stringify(config, null, 2));
32
+ writeFileSync(path.join(projectRoot, ".harness", "progress.json"), JSON.stringify(progress, null, 2));
33
+ }
34
+
35
+ describeHarnessShell("company-loop shell flows", () => {
36
+ let dir: string;
37
+
38
+ beforeEach(() => {
39
+ dir = mkdtempSync(path.join(tmpdir(), "walwal-company-"));
40
+ });
41
+
42
+ afterEach(() => {
43
+ rmSync(dir, { recursive: true, force: true });
44
+ });
45
+
46
+ it("migrate_progress_schema normalizes legacy runtime mode while removing meeting decision mode", () => {
47
+ mkdirSync(path.join(dir, ".harness"), { recursive: true });
48
+ writeFileSync(
49
+ path.join(dir, ".harness", "progress.json"),
50
+ JSON.stringify(
51
+ {
52
+ mode: "team",
53
+ meetings: {
54
+ requested_mode: "hypothesis",
55
+ decision: {
56
+ mode: "legacy",
57
+ owner: "planner",
58
+ action_type: "hypothesis-validation",
59
+ tracks: [],
60
+ rendezvous: null,
61
+ },
62
+ },
63
+ },
64
+ null,
65
+ 2
66
+ )
67
+ );
68
+
69
+ runBash("scripts/lib/harness-progress-migrate.sh", path.join(dir, ".harness", "progress.json"));
70
+
71
+ const migrated = JSON.parse(readFileSync(path.join(dir, ".harness", "progress.json"), "utf8"));
72
+ expect(migrated.mode).toBe("company");
73
+ expect(migrated.meetings.requested_mode).toBeUndefined();
74
+ expect(migrated.meetings.decision.mode).toBeUndefined();
75
+ expect(Array.isArray(migrated.meetings.decision.tracks)).toBe(true);
76
+ });
77
+
78
+ it("read-tracks falls back to conductor.tracks when fork_context is missing", () => {
79
+ writeState(dir, {
80
+ current_agent: "meeting-manager",
81
+ agent_status: "completed",
82
+ next_agent: "meeting-manager",
83
+ mode: "auto",
84
+ pipeline: "FULLSTACK",
85
+ workflow: { stage: "followup-review" },
86
+ meetings: {
87
+ requested_type: "followup-review",
88
+ requested_reason: "rendezvous",
89
+ active: ["meeting-manager"],
90
+ decision: {
91
+ owner: "cto",
92
+ action_type: "bugfix",
93
+ rationale: "test",
94
+ evidence: [],
95
+ drift_classification: "implementation_drift",
96
+ source_path: null,
97
+ tracks: [],
98
+ rendezvous: null,
99
+ },
100
+ fork_context: null,
101
+ fork_meeting_id: null,
102
+ },
103
+ conductor: {
104
+ tracks: [
105
+ {
106
+ id: "track-1",
107
+ owner: "cto",
108
+ action_type: "bugfix",
109
+ deliverable: "hotfix-result",
110
+ deliverable_path: "/tmp/hotfix.md",
111
+ status: "completed",
112
+ },
113
+ {
114
+ id: "track-2",
115
+ owner: "planner",
116
+ action_type: "hypothesis-validation",
117
+ deliverable: "validation-report",
118
+ deliverable_path: "/tmp/report.md",
119
+ status: "completed",
120
+ },
121
+ ],
122
+ rendezvous: { type: "followup-review", when: "next_cadence" },
123
+ fork_meeting_id: "M-20260508T080000Z",
124
+ },
125
+ planner: { requested_mode: null, last_brief: "hypothesis:done" },
126
+ service_ops: { auto_retro: { last_report: null, open_recommendations: 0 }, monitor: { alerts_this_sprint: 0 }, incident: { open: [] }, drift_classification: null },
127
+ goals: { current_adherence: null },
128
+ cqo: { sprint_verdict: "pending", open_regressions: 0, axes_below_threshold: [] },
129
+ cto: { open_hotfixes: 0 },
130
+ artifacts: { "plan.md": { status: "pending" }, "feature-list.json": { status: "pending" }, "api-contract.json": { status: "pending" } },
131
+ });
132
+
133
+ const tracks = JSON.parse(runBash("scripts/harness-meeting-doc.sh", dir, ["read-tracks"]));
134
+ expect(tracks).toHaveLength(2);
135
+ expect(tracks[0].owner).toBe("cto");
136
+ expect(tracks[1].owner).toBe("planner");
137
+ });
138
+
139
+ it("followup-review prepare preserves fork context and persists the fallback context", () => {
140
+ writeState(dir, {
141
+ current_agent: "meeting-manager",
142
+ agent_status: "completed",
143
+ next_agent: "meeting-manager",
144
+ mode: "auto",
145
+ pipeline: "FULLSTACK",
146
+ workflow: { stage: "followup-review" },
147
+ meetings: {
148
+ requested_type: "followup-review",
149
+ requested_reason: "rendezvous",
150
+ active: ["meeting-manager"],
151
+ decision: {
152
+ owner: "cto",
153
+ action_type: "bugfix",
154
+ rationale: "test",
155
+ evidence: [],
156
+ drift_classification: "implementation_drift",
157
+ source_path: null,
158
+ tracks: [],
159
+ rendezvous: null,
160
+ },
161
+ requested_tracks: [],
162
+ requested_rendezvous: null,
163
+ fork_context: null,
164
+ fork_meeting_id: null,
165
+ },
166
+ conductor: {
167
+ tracks: [
168
+ {
169
+ id: "track-1",
170
+ owner: "cto",
171
+ action_type: "bugfix",
172
+ deliverable: "hotfix-result",
173
+ deliverable_path: "/tmp/hotfix.md",
174
+ status: "completed",
175
+ },
176
+ {
177
+ id: "track-2",
178
+ owner: "planner",
179
+ action_type: "hypothesis-validation",
180
+ deliverable: "validation-report",
181
+ deliverable_path: "/tmp/report.md",
182
+ status: "completed",
183
+ },
184
+ ],
185
+ rendezvous: { type: "followup-review", when: "next_cadence" },
186
+ fork_meeting_id: "M-20260508T080000Z",
187
+ },
188
+ planner: { requested_mode: null, last_brief: "hypothesis:done" },
189
+ service_ops: { auto_retro: { last_report: null, open_recommendations: 0 }, monitor: { alerts_this_sprint: 0 }, incident: { open: [] }, drift_classification: null },
190
+ goals: { current_adherence: null },
191
+ cqo: { sprint_verdict: "pending", open_regressions: 0, axes_below_threshold: [] },
192
+ cto: { open_hotfixes: 0 },
193
+ artifacts: { "plan.md": { status: "pending" }, "feature-list.json": { status: "pending" }, "api-contract.json": { status: "pending" } },
194
+ });
195
+
196
+ const recordPath = runBash("scripts/harness-meeting-doc.sh", dir, ["prepare"]).trim();
197
+ const record = readFileSync(path.join(dir, recordPath), "utf8");
198
+ expect(record).toContain("## Fork Context (v6.2)");
199
+ expect(record).toContain("hotfix-result");
200
+ expect(record).toContain("validation-report");
201
+
202
+ const progress = JSON.parse(readFileSync(path.join(dir, ".harness", "progress.json"), "utf8"));
203
+ expect(progress.meetings.fork_context.prior_tracks).toHaveLength(2);
204
+ expect(progress.meetings.fork_context.fork_meeting_id).toBe("M-20260508T080000Z");
205
+ });
206
+
207
+ it("planner hypothesis-verdict completes by returning to meeting-manager and clearing the stale request", () => {
208
+ writeState(dir, {
209
+ current_agent: "planner",
210
+ agent_status: "completed",
211
+ next_agent: "planner",
212
+ mode: "auto",
213
+ pipeline: "FULLSTACK",
214
+ workflow: { stage: "coo-hypothesis-verdict" },
215
+ meetings: {
216
+ requested_type: "followup-review",
217
+ requested_reason: "rendezvous",
218
+ active: ["meeting-manager"],
219
+ decision: {
220
+ owner: "cto",
221
+ action_type: "bugfix",
222
+ rationale: "test",
223
+ evidence: [],
224
+ drift_classification: "implementation_drift",
225
+ source_path: null,
226
+ tracks: [],
227
+ rendezvous: null,
228
+ },
229
+ fork_context: null,
230
+ fork_meeting_id: "M-20260508T080000Z",
231
+ },
232
+ conductor: {
233
+ tracks: [
234
+ {
235
+ id: "track-1",
236
+ owner: "cto",
237
+ action_type: "bugfix",
238
+ deliverable: "hotfix-result",
239
+ deliverable_path: "/tmp/hotfix.md",
240
+ status: "completed",
241
+ },
242
+ {
243
+ id: "track-2",
244
+ owner: "planner",
245
+ action_type: "hypothesis-validation",
246
+ deliverable: "validation-report",
247
+ deliverable_path: "/tmp/report.md",
248
+ status: "completed",
249
+ },
250
+ ],
251
+ rendezvous: { type: "followup-review", when: "next_cadence" },
252
+ fork_meeting_id: "M-20260508T080000Z",
253
+ },
254
+ planner: { requested_mode: "hypothesis-verdict", last_brief: "hypothesis:done" },
255
+ service_ops: { auto_retro: { last_report: null, open_recommendations: 0 }, monitor: { alerts_this_sprint: 0 }, incident: { open: [] }, drift_classification: null },
256
+ goals: { current_adherence: null },
257
+ cqo: { sprint_verdict: "pending", open_regressions: 0, axes_below_threshold: [] },
258
+ cto: { open_hotfixes: 0 },
259
+ artifacts: { "plan.md": { status: "pending" }, "feature-list.json": { status: "pending" }, "api-contract.json": { status: "pending" } },
260
+ });
261
+
262
+ runBash("scripts/conductor-tick.sh", dir);
263
+
264
+ const progress = JSON.parse(readFileSync(path.join(dir, ".harness", "progress.json"), "utf8"));
265
+ expect(progress.next_agent).toBe("meeting-manager");
266
+ expect(progress.workflow.stage).toBe("followup-review");
267
+ expect(progress.planner.requested_mode).toBeNull();
268
+ expect(progress.meetings.requested_type).toBe("followup-review");
269
+ });
270
+ });
@@ -0,0 +1,194 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { readHarnessState } from "../harness-state";
6
+ import { AGENT_ROSTER } from "../agent-roster";
7
+
8
+ describe("readHarnessState", () => {
9
+ let dir: string;
10
+
11
+ beforeEach(() => {
12
+ dir = mkdtempSync(path.join(tmpdir(), "harness-state-"));
13
+ });
14
+
15
+ afterEach(() => {
16
+ rmSync(dir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("valid progress.json: derives typing for current_agent, talking for meetings.active, idle for the rest", () => {
20
+ mkdirSync(path.join(dir, ".harness"), { recursive: true });
21
+ writeFileSync(
22
+ path.join(dir, ".harness", "progress.json"),
23
+ JSON.stringify({
24
+ sprint: { number: 1 },
25
+ current_agent: "generator-backend",
26
+ agent_status: "running",
27
+ meetings: { active: ["planner"], cadence: "normal", next_scheduled: null },
28
+ cto: { last_review: "2026-05-07", open_arch_risks: 0 },
29
+ cqo: { last_audit: null, sprint_verdict: "pending", open_regressions: 0 },
30
+ service_ops: { monitor: { last_check: "2026-05-07", alerts_this_sprint: 0 } },
31
+ goals: {
32
+ active_id: "g1",
33
+ list: [{ id: "g1", title: "Brick Office MVP", description: "Phase C launch" }],
34
+ current_adherence: 0.6,
35
+ },
36
+ })
37
+ );
38
+
39
+ const snap = readHarnessState(dir);
40
+ expect(snap.errorBanner).toBeNull();
41
+ expect(snap.agents.length).toBe(AGENT_ROSTER.length);
42
+ expect(snap.agents.map((a) => a.id).sort()).toEqual(
43
+ AGENT_ROSTER.map((a) => a.id).sort()
44
+ );
45
+ const stateOf = (id: string) => snap.agents.find((a) => a.id === id)!.minifigState;
46
+ expect(stateOf("generator-backend")).toBe("typing");
47
+ expect(stateOf("planner")).toBe("talking");
48
+ expect(stateOf("dispatcher")).toBe("idle");
49
+ expect(snap.rooms.length).toBe(7);
50
+
51
+ const planner = snap.agents.find((a) => a.id === "planner")!;
52
+ expect(planner.room).toBe("meeting");
53
+ expect(planner.homeRoom).toBe("coo");
54
+
55
+ const meetingRoom = snap.rooms.find((r) => r.id === "meeting")!;
56
+ expect(meetingRoom.seatLayout).toBeDefined();
57
+ expect(meetingRoom.seatLayout!.occupants).toContain("planner");
58
+
59
+ expect(snap.goal).not.toBeNull();
60
+ expect(snap.goal!.title).toBe("Brick Office MVP");
61
+ });
62
+
63
+ it("missing .harness: returns error banner, no crash", () => {
64
+ const snap = readHarnessState(dir);
65
+ expect(snap.errorBanner?.level).toBe("error");
66
+ expect(snap.agents.length).toBe(AGENT_ROSTER.length);
67
+ });
68
+
69
+ it("corrupt progress.json: returns error banner, no crash", () => {
70
+ mkdirSync(path.join(dir, ".harness"), { recursive: true });
71
+ writeFileSync(path.join(dir, ".harness", "progress.json"), "{ not json");
72
+ const snap = readHarnessState(dir);
73
+ expect(snap.errorBanner?.level).toBe("error");
74
+ expect(snap.errorBanner?.message_en).toContain("corrupt");
75
+ expect(snap.agents.length).toBe(AGENT_ROSTER.length);
76
+ });
77
+
78
+ it("v6 NEXUS: roster includes CTO, CQO, Brainstormer", () => {
79
+ const ids = AGENT_ROSTER.map((a) => a.id);
80
+ expect(ids).toContain("cto");
81
+ expect(ids).toContain("cqo");
82
+ expect(ids).toContain("brainstormer");
83
+ });
84
+
85
+ it("parallel tracks, hypothesis, escalations, incidents land in the snapshot", () => {
86
+ mkdirSync(path.join(dir, ".harness"), { recursive: true });
87
+ writeFileSync(
88
+ path.join(dir, ".harness", "progress.json"),
89
+ JSON.stringify({
90
+ sprint: { number: 3 },
91
+ pipeline: "FULLSTACK",
92
+ current_agent: "conductor",
93
+ agent_status: "running",
94
+ meetings: {
95
+ active: [],
96
+ cadence: "heavy",
97
+ next_scheduled: "2026-05-08T10:00Z",
98
+ current: { type: "incident-war-room", topic: "P0 prod outage" },
99
+ },
100
+ cto: {
101
+ last_review: "2026-05-07",
102
+ open_arch_risks: 1,
103
+ contract_signed: { be: true, fe: false },
104
+ },
105
+ cqo: {
106
+ last_audit: "2026-05-07",
107
+ sprint_verdict: "pending",
108
+ open_regressions: 2,
109
+ last_scores: {
110
+ functional: 2.9,
111
+ visual: 2.8,
112
+ code_quality: 2.5,
113
+ architecture: 2.95,
114
+ security: 3.0,
115
+ },
116
+ },
117
+ service_ops: {
118
+ monitor: { last_check: "2026-05-07", alerts_this_sprint: 4 },
119
+ incident: {
120
+ open: [
121
+ { id: "INC-1", dept: "CTO", severity: "critical", message: "DB down" },
122
+ { id: "INC-2", dept: "Operations", severity: "medium" },
123
+ ],
124
+ },
125
+ },
126
+ parallel_tracks: [
127
+ {
128
+ id: "T-1",
129
+ from_meeting: "spec-review",
130
+ to_dept: "CTO",
131
+ to_room: "cto-team",
132
+ status: "in_progress",
133
+ },
134
+ {
135
+ id: "T-2",
136
+ from_meeting: "spec-review",
137
+ to_dept: "Planner",
138
+ to_room: "coo",
139
+ status: "dispatched",
140
+ },
141
+ ],
142
+ hypothesis: {
143
+ active: [
144
+ { id: "H-1", brief: "Cache hit > 80%", verdict: "pending" },
145
+ { id: "H-2", brief: "p95 < 200ms", verdict: "valid" },
146
+ ],
147
+ },
148
+ escalations: {
149
+ open: [{ id: "E-1", reason: "three-fail", message: "regression x3" }],
150
+ },
151
+ contracts: {
152
+ api: { version: "v2.1.0" },
153
+ feature_list: { total: 16, passed: 12, failed: 1 },
154
+ },
155
+ })
156
+ );
157
+
158
+ const snap = readHarnessState(dir);
159
+ expect(snap.errorBanner).toBeNull();
160
+
161
+ expect(snap.tracks.map((t) => t.id)).toEqual(["T-1", "T-2"]);
162
+ expect(snap.incidents.length).toBe(2);
163
+ expect(snap.hypothesis.length).toBe(2);
164
+ expect(snap.escalations.length).toBe(1);
165
+ expect(snap.contract.pipeline).toBe("FULLSTACK");
166
+ expect(snap.contract.sprint_number).toBe(3);
167
+ expect(snap.contract.feature_total).toBe(16);
168
+ expect(snap.contract.contract_signed).toEqual({ be: true, fe: false });
169
+ expect(snap.evalScores?.code_quality).toBe(2.5);
170
+ expect(snap.meetings.cadence).toBe("heavy");
171
+ expect(snap.meetings.current?.type).toBe("incident-war-room");
172
+
173
+ // CTO has an open incident → CTO-dept agents go red-alert; Service-Ops
174
+ // is on-call and is *always* red when any incident is open.
175
+ const stateOf = (id: string) => snap.agents.find((a) => a.id === id)!.minifigState;
176
+ expect(stateOf("cto")).toBe("red-alert");
177
+ expect(stateOf("service-ops")).toBe("red-alert");
178
+ expect(stateOf("evaluator-visual")).toBe("idle"); // CQO dept untouched
179
+ });
180
+
181
+ it("buildArchive reads verdict.json when present", () => {
182
+ const sprintDir = path.join(dir, ".harness", "archive", "sprint-1");
183
+ mkdirSync(sprintDir, { recursive: true });
184
+ writeFileSync(path.join(sprintDir, "verdict.json"), JSON.stringify({ result: "PASS" }));
185
+ mkdirSync(path.join(dir, ".harness"), { recursive: true });
186
+ writeFileSync(
187
+ path.join(dir, ".harness", "progress.json"),
188
+ JSON.stringify({ sprint: { number: 1 } })
189
+ );
190
+
191
+ const snap = readHarnessState(dir);
192
+ expect(snap.archive.all.find((e) => e.dir === "sprint-1")?.result).toBe("PASS");
193
+ });
194
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { t } from "../i18n";
3
+
4
+ describe("i18n.t", () => {
5
+ it("returns ko by default", () => {
6
+ expect(t("drawer.tab.agent_log")).toBe("에이전트 로그");
7
+ });
8
+
9
+ it("returns en when lang=en", () => {
10
+ expect(t("drawer.tab.agent_log", "en")).toBe("Agent Log");
11
+ });
12
+
13
+ it("falls back to the key when missing", () => {
14
+ expect(t("nonexistent.key", "en")).toBe("nonexistent.key");
15
+ });
16
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ROOM_RECTS, CORRIDOR_RECTS, worldToScreen } from "../iso";
3
+
4
+ describe("iso projection", () => {
5
+ it("worldToScreen is a pure linear transform", () => {
6
+ const a = worldToScreen(0, 0);
7
+ expect(a).toEqual({ sx: 0, sy: 0 });
8
+ const b = worldToScreen(2, 0);
9
+ const c = worldToScreen(0, 2);
10
+ expect(b.sy).toBe(c.sy);
11
+ expect(b.sx).toBe(-c.sx);
12
+ });
13
+
14
+ it("rooms do not overlap corridors (corridors fill the gaps)", () => {
15
+ for (const r of ROOM_RECTS) {
16
+ for (const c of CORRIDOR_RECTS) {
17
+ const overlapX = r.wx < c.wx + c.ww && c.wx < r.wx + r.ww;
18
+ const overlapY = r.wy < c.wy + c.wh && c.wy < r.wy + r.wh;
19
+ expect(
20
+ overlapX && overlapY,
21
+ `room ${r.id} overlaps corridor at (${c.wx},${c.wy})`
22
+ ).toBe(false);
23
+ }
24
+ }
25
+ });
26
+
27
+ it("rooms have unique ids and non-overlapping world rectangles", () => {
28
+ const ids = new Set(ROOM_RECTS.map((r) => r.id));
29
+ expect(ids.size).toBe(ROOM_RECTS.length);
30
+ expect(ROOM_RECTS.length).toBe(7);
31
+
32
+ for (let i = 0; i < ROOM_RECTS.length; i++) {
33
+ for (let j = i + 1; j < ROOM_RECTS.length; j++) {
34
+ const a = ROOM_RECTS[i];
35
+ const b = ROOM_RECTS[j];
36
+ const overlapX = a.wx < b.wx + b.ww && b.wx < a.wx + a.ww;
37
+ const overlapY = a.wy < b.wy + b.wh && b.wy < a.wy + a.wh;
38
+ expect(overlapX && overlapY, `rooms overlap: ${a.id} vs ${b.id}`).toBe(false);
39
+ }
40
+ }
41
+ });
42
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { readJsonSafe } from "../safe-json";
6
+
7
+ describe("readJsonSafe", () => {
8
+ let dir: string;
9
+
10
+ beforeEach(() => {
11
+ dir = mkdtempSync(path.join(tmpdir(), "safe-json-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ rmSync(dir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("valid: returns ok with parsed value", () => {
19
+ const file = path.join(dir, "ok.json");
20
+ writeFileSync(file, JSON.stringify({ a: 1, b: "x" }));
21
+ const result = readJsonSafe<{ a: number; b: string }>(file);
22
+ expect(result.ok).toBe(true);
23
+ if (result.ok) {
24
+ expect(result.value).toEqual({ a: 1, b: "x" });
25
+ }
26
+ });
27
+
28
+ it("corrupt: returns ok=false with reason='corrupt' and does not throw", () => {
29
+ const file = path.join(dir, "broken.json");
30
+ writeFileSync(file, "{ not json");
31
+ const result = readJsonSafe(file);
32
+ expect(result.ok).toBe(false);
33
+ if (!result.ok) {
34
+ expect(result.reason).toBe("corrupt");
35
+ }
36
+ });
37
+
38
+ it("missing: returns ok=false with reason='missing' and does not throw", () => {
39
+ const file = path.join(dir, "absent.json");
40
+ const result = readJsonSafe(file);
41
+ expect(result.ok).toBe(false);
42
+ if (!result.ok) {
43
+ expect(result.reason).toBe("missing");
44
+ }
45
+ });
46
+ });