@walwal-harness/cli 7.1.8 → 7.1.9
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/README.md +40 -0
- package/apps/harness-dashboard/app/api/env/route.ts +62 -0
- package/apps/harness-dashboard/app/api/log/route.ts +19 -0
- package/apps/harness-dashboard/app/api/missions/route.ts +15 -0
- package/apps/harness-dashboard/app/api/snapshot/route.ts +15 -0
- package/apps/harness-dashboard/app/api/stream/route.ts +71 -0
- package/apps/harness-dashboard/app/globals.css +17 -0
- package/apps/harness-dashboard/app/layout.tsx +29 -0
- package/apps/harness-dashboard/app/page.tsx +35 -0
- package/apps/harness-dashboard/components/Drawer.tsx +97 -0
- package/apps/harness-dashboard/components/Header.tsx +40 -0
- package/apps/harness-dashboard/components/MissionTimeline.tsx +210 -0
- package/apps/harness-dashboard/components/OrgTree.tsx +330 -0
- package/apps/harness-dashboard/components/Scene.tsx +563 -0
- package/apps/harness-dashboard/components/Wordmark.tsx +14 -0
- package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +56 -0
- package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +89 -0
- package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +42 -0
- package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +50 -0
- package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +92 -0
- package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +517 -0
- package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +156 -0
- package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +90 -0
- package/apps/harness-dashboard/components/drawer/TracksTab.tsx +47 -0
- package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +95 -0
- package/apps/harness-dashboard/components/three/DeptBoard.tsx +175 -0
- package/apps/harness-dashboard/components/three/Floor3D.tsx +404 -0
- package/apps/harness-dashboard/components/three/GoalCard3D.tsx +123 -0
- package/apps/harness-dashboard/components/three/HypothesisCards.tsx +71 -0
- package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +98 -0
- package/apps/harness-dashboard/components/three/Minifig3D.tsx +391 -0
- package/apps/harness-dashboard/components/three/OpsSiren.tsx +99 -0
- package/apps/harness-dashboard/components/three/Stage3D.tsx +104 -0
- package/apps/harness-dashboard/components/three/TrackRibbons.tsx +81 -0
- package/apps/harness-dashboard/hooks/useHarnessStream.ts +53 -0
- package/apps/harness-dashboard/lib/__tests__/company-flow.test.ts +270 -0
- package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +194 -0
- package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +16 -0
- package/apps/harness-dashboard/lib/__tests__/iso.test.ts +42 -0
- package/apps/harness-dashboard/lib/__tests__/safe-json.test.ts +46 -0
- package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +159 -0
- package/apps/harness-dashboard/lib/__tests__/watcher.test.ts +48 -0
- package/apps/harness-dashboard/lib/agent-roster.ts +42 -0
- package/apps/harness-dashboard/lib/furniture.ts +165 -0
- package/apps/harness-dashboard/lib/harness-root.ts +20 -0
- package/apps/harness-dashboard/lib/harness-state.ts +1162 -0
- package/apps/harness-dashboard/lib/i18n.ts +26 -0
- package/apps/harness-dashboard/lib/iso.ts +150 -0
- package/apps/harness-dashboard/lib/log-filter.ts +23 -0
- package/apps/harness-dashboard/lib/markdown.tsx +306 -0
- package/apps/harness-dashboard/lib/path-planning.ts +72 -0
- package/apps/harness-dashboard/lib/safe-json.ts +24 -0
- package/apps/harness-dashboard/lib/state-mapping.ts +126 -0
- package/apps/harness-dashboard/lib/types.ts +329 -0
- package/apps/harness-dashboard/lib/watcher.ts +64 -0
- package/apps/harness-dashboard/next-env.d.ts +6 -0
- package/apps/harness-dashboard/next.config.mjs +9 -0
- package/apps/harness-dashboard/package-lock.json +4280 -0
- package/apps/harness-dashboard/package.json +38 -0
- package/apps/harness-dashboard/playwright.config.ts +23 -0
- package/apps/harness-dashboard/postcss.config.js +6 -0
- package/apps/harness-dashboard/tailwind.config.ts +49 -0
- package/apps/harness-dashboard/tsconfig.json +24 -0
- package/apps/harness-dashboard/vitest.config.ts +15 -0
- package/package.json +11 -1
- package/scripts/harness-dashboard-up.sh +24 -4
|
@@ -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
|
+
});
|