@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,42 +0,0 @@
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
- });
@@ -1,183 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { deriveMinifigState } from "../state-mapping";
3
-
4
- describe("deriveMinifigState", () => {
5
- it("returns idle when there is no progress data", () => {
6
- expect(deriveMinifigState("planner", null)).toBe("idle");
7
- });
8
-
9
- it("returns typing when agent is current and running", () => {
10
- expect(
11
- deriveMinifigState("generator-backend", {
12
- current_agent: "generator-backend",
13
- agent_status: "running",
14
- })
15
- ).toBe("typing");
16
- });
17
-
18
- it("maps v7 CEO alias to dispatcher and brainstormer", () => {
19
- const progress = {
20
- current_agent: "ceo",
21
- agent_status: "running",
22
- };
23
- expect(deriveMinifigState("dispatcher", progress)).toBe("typing");
24
- expect(deriveMinifigState("brainstormer", progress)).toBe("typing");
25
- });
26
-
27
- it("maps v7 CXX aliases to their dashboard departments", () => {
28
- expect(
29
- deriveMinifigState("generator-designer", {
30
- current_agent: "cdo",
31
- agent_status: "running",
32
- })
33
- ).toBe("typing");
34
- expect(
35
- deriveMinifigState("service-ops", {
36
- next_agent: "ops",
37
- agent_status: "pending",
38
- })
39
- ).toBe("queued");
40
- });
41
-
42
- it("does not return typing when agent_status is not running", () => {
43
- expect(
44
- deriveMinifigState("generator-backend", {
45
- current_agent: "generator-backend",
46
- agent_status: "completed",
47
- })
48
- ).toBe("idle");
49
- });
50
-
51
- it("returns queued when agent is assigned as next_agent", () => {
52
- expect(
53
- deriveMinifigState("cto", {
54
- current_agent: "planner",
55
- next_agent: "cto",
56
- agent_status: "completed",
57
- })
58
- ).toBe("queued");
59
- });
60
-
61
- it("returns talking when agent is in meetings.active", () => {
62
- expect(
63
- deriveMinifigState("planner", {
64
- meetings: { active: ["planner", "dispatcher"] },
65
- })
66
- ).toBe("talking");
67
- });
68
-
69
- it("talking wins over typing when both apply", () => {
70
- expect(
71
- deriveMinifigState("planner", {
72
- current_agent: "planner",
73
- agent_status: "running",
74
- meetings: { active: ["planner"] },
75
- })
76
- ).toBe("talking");
77
- });
78
-
79
- it("red-alert wins over everything", () => {
80
- expect(
81
- deriveMinifigState("evaluator-functional", {
82
- current_agent: "evaluator-functional",
83
- agent_status: "running",
84
- meetings: { active: ["evaluator-functional"] },
85
- failure: { agent: "evaluator-functional", message: "regression" },
86
- })
87
- ).toBe("red-alert");
88
- });
89
-
90
- it("Service-Ops always goes red-alert when any incident is open", () => {
91
- // Even if the incident's dept is unknown, Service-Ops is the on-call
92
- // department and should reflect the alert state.
93
- expect(
94
- deriveMinifigState(
95
- "service-ops",
96
- { current_agent: null, agent_status: "idle" },
97
- { dept: "Operations", incidentDepts: new Set(["CTO"]) }
98
- )
99
- ).toBe("red-alert");
100
- });
101
-
102
- it("dept-matching incident lights up the dept's agents", () => {
103
- expect(
104
- deriveMinifigState(
105
- "generator-backend",
106
- { current_agent: null },
107
- { dept: "CTO", incidentDepts: new Set(["CTO"]) }
108
- )
109
- ).toBe("red-alert");
110
- });
111
-
112
- it("agents in unrelated dept stay idle when incident is elsewhere", () => {
113
- expect(
114
- deriveMinifigState(
115
- "evaluator-visual",
116
- { current_agent: null },
117
- { dept: "CQO", incidentDepts: new Set(["CTO"]) }
118
- )
119
- ).toBe("idle");
120
- });
121
-
122
- it("v6 parallel: conductor.tracks running owners are typing", () => {
123
- expect(
124
- deriveMinifigState("planner", {
125
- current_agent: "coo-developer",
126
- agent_status: "running",
127
- conductor: {
128
- tracks: [
129
- { owner: "planner", status: "running" },
130
- { owner: "documentationer", status: "completed" },
131
- ],
132
- },
133
- })
134
- ).toBe("typing");
135
- });
136
-
137
- it("v6 parallel: conductor.tracks pending owners are queued", () => {
138
- expect(
139
- deriveMinifigState("documentationer", {
140
- current_agent: "planner",
141
- agent_status: "running",
142
- conductor: {
143
- tracks: [
144
- { owner: "planner", status: "running" },
145
- { owner: "documentationer", status: "pending" },
146
- ],
147
- },
148
- })
149
- ).toBe("queued");
150
- });
151
-
152
- it("v6 parallel: conductor.current_action spawn target is typing", () => {
153
- // current_agent has finished the previous tick; the next agent appears in
154
- // current_action and must already light up so the office never goes dark.
155
- expect(
156
- deriveMinifigState("generator-backend", {
157
- current_agent: "cto",
158
- agent_status: "completed",
159
- conductor: { current_action: "spawn:generator-backend (foundation F-101)" },
160
- })
161
- ).toBe("typing");
162
- });
163
-
164
- it("G-006: service-ops with stream_active=true is typing", () => {
165
- expect(
166
- deriveMinifigState("service-ops", {
167
- current_agent: "generator-frontend",
168
- agent_status: "running",
169
- service_ops: { monitor: { stream_active: true, stream_target: "generator-frontend" } },
170
- })
171
- ).toBe("typing");
172
- });
173
-
174
- it("agents not involved stay idle (default-to-desk handled in 3D)", () => {
175
- expect(
176
- deriveMinifigState("evaluator-architecture", {
177
- current_agent: "generator-backend",
178
- agent_status: "running",
179
- conductor: { tracks: [{ owner: "planner", status: "running" }] },
180
- })
181
- ).toBe("idle");
182
- });
183
- });
@@ -1,42 +0,0 @@
1
- import type { AgentId, Dept, RoomId } from "./types";
2
-
3
- export interface RosterEntry {
4
- id: AgentId;
5
- name: string;
6
- dept: Dept;
7
- room: RoomId;
8
- }
9
-
10
- // SoT for the office roster. Mirrors v6 NEXUS organization in CLAUDE.md.
11
- // Order within a room dictates desk assignment (see lib/furniture.ts).
12
- export const AGENT_ROSTER: RosterEntry[] = [
13
- { id: "dispatcher", name: "CEO (Dispatcher)", dept: "CEO", room: "ceo" },
14
- { id: "brainstormer", name: "Brainstormer", dept: "CEO", room: "ceo" },
15
- { id: "meeting-manager", name: "Meeting Manager", dept: "Meeting", room: "meeting" },
16
- { id: "planner", name: "COO (Planner)", dept: "Planner", room: "coo" },
17
- { id: "coo-developer", name: "Developer 1 (Hypothesis Cell)", dept: "Planner", room: "coo" },
18
- { id: "documentationer", name: "Documentationer 1 (Hypothesis Cell)", dept: "Planner", room: "coo" },
19
- { id: "cto", name: "CTO (Gen 총괄)", dept: "CTO", room: "cto-team" },
20
- { id: "conductor", name: "Conductor", dept: "CTO", room: "cto-team" },
21
- { id: "generator-backend", name: "Generator — Backend", dept: "CTO", room: "cto-team" },
22
- { id: "generator-frontend", name: "Generator — Frontend", dept: "CTO", room: "cto-team" },
23
- { id: "generator-designer", name: "Designer", dept: "CTO", room: "cto-team" },
24
- { id: "generator-devops", name: "DevOps", dept: "CTO", room: "cto-team" },
25
- { id: "cqo", name: "CQO (Eval 총괄)", dept: "CQO", room: "cqo-team" },
26
- { id: "evaluator-functional", name: "Eval — Functional", dept: "CQO", room: "cqo-team" },
27
- { id: "evaluator-visual", name: "Eval — Visual", dept: "CQO", room: "cqo-team" },
28
- { id: "evaluator-code-quality", name: "Eval — Code Quality", dept: "CQO", room: "cqo-team" },
29
- { id: "evaluator-architecture", name: "Eval — Architecture", dept: "CQO", room: "cqo-team" },
30
- { id: "evaluator-security", name: "Eval — Security", dept: "CQO", room: "cqo-team" },
31
- { id: "service-ops", name: "Service Ops", dept: "Operations", room: "service-ops" },
32
- ];
33
-
34
- export const ROOM_LABELS: Record<RoomId, { ko: string; en: string; dept: Dept | "Archive" }> = {
35
- ceo: { ko: "CEO실", en: "CEO Office", dept: "CEO" },
36
- meeting: { ko: "회의실", en: "Meeting Room", dept: "Meeting" },
37
- coo: { ko: "COO실", en: "COO Office", dept: "Planner" },
38
- "cto-team": { ko: "CTO팀", en: "CTO Team", dept: "CTO" },
39
- "cqo-team": { ko: "CQO팀", en: "CQO Team", dept: "CQO" },
40
- "service-ops": { ko: "Service-Ops", en: "Service Ops", dept: "Operations" },
41
- archive: { ko: "아카이브", en: "Archive", dept: "Archive" },
42
- };
@@ -1,165 +0,0 @@
1
- import { ROOM_RECTS, type RoomRect } from "./iso";
2
- import type { RoomId } from "./types";
3
- import { AGENT_ROSTER } from "./agent-roster";
4
-
5
- // How many desks each room should physically contain. The meeting room is
6
- // over-provisioned because anyone can drop in. Room desks must accommodate the
7
- // full roster in agent-roster.ts (CEO+Brainstormer; CTO+Conductor+4 Generators;
8
- // CQO+5 Evaluators).
9
- export const ROOM_CAPACITY: Record<RoomId, number> = {
10
- ceo: 2,
11
- meeting: 7,
12
- coo: 3,
13
- "cto-team": 6,
14
- "cqo-team": 6,
15
- "service-ops": 1,
16
- archive: 0,
17
- };
18
-
19
- export interface DeskSlot {
20
- index: number;
21
- // Desk position (slab top center) in world coords.
22
- desk: [number, number, number];
23
- // Where the seated character stands (chair position).
24
- chair: [number, number, number];
25
- // Facing direction (yaw, radians) — character faces the desk.
26
- facing: number;
27
- }
28
-
29
- const DESK_INSET = 0.55; // distance from room edge to desk
30
- const DESK_SPACING_MIN = 0.85;
31
-
32
- // Layout desks in two rows along the room's long axis, each row hugging one
33
- // of the long walls. Chairs sit on the inside of each row.
34
- export function deskSlotsForRoom(rect: RoomRect): DeskSlot[] {
35
- const cap = ROOM_CAPACITY[rect.id];
36
- if (cap === 0) return [];
37
-
38
- const slots: DeskSlot[] = [];
39
- const longAxisIsX = rect.ww >= rect.wh;
40
- const longLen = longAxisIsX ? rect.ww : rect.wh;
41
- const shortLen = longAxisIsX ? rect.wh : rect.ww;
42
-
43
- // 2 rows if room is deep enough, else 1 row.
44
- const rows = shortLen >= 2.0 && cap >= 3 ? 2 : 1;
45
- const perRow = Math.ceil(cap / rows);
46
- const usable = longLen - DESK_INSET * 2;
47
- const spacing = perRow > 1 ? Math.max(DESK_SPACING_MIN, usable / perRow) : 0;
48
- const startLong = rect.wx + (longAxisIsX ? DESK_INSET + spacing / 2 : 0);
49
- const startLongZ = rect.wy + (!longAxisIsX ? DESK_INSET + spacing / 2 : 0);
50
-
51
- for (let i = 0; i < cap; i++) {
52
- const row = i % rows;
53
- const col = Math.floor(i / rows);
54
- const longOffset = perRow > 1 ? col * spacing : longLen / 2;
55
-
56
- let dx: number;
57
- let dz: number;
58
- let cx: number;
59
- let cz: number;
60
- let facing: number;
61
-
62
- if (longAxisIsX) {
63
- dx = (startLong as number) + longOffset;
64
- // Row 0 hugs the +z wall, row 1 hugs the -z wall.
65
- const wallZ =
66
- row === 0 ? rect.wy + rect.wh - DESK_INSET : rect.wy + DESK_INSET;
67
- dz = wallZ;
68
- cx = dx;
69
- cz = row === 0 ? wallZ - 0.55 : wallZ + 0.55;
70
- facing = row === 0 ? Math.PI : 0; // face the wall (Roblox cubicle vibe)
71
- } else {
72
- dz = (startLongZ as number) + longOffset;
73
- const wallX =
74
- row === 0 ? rect.wx + rect.ww - DESK_INSET : rect.wx + DESK_INSET;
75
- dx = wallX;
76
- cx = row === 0 ? wallX - 0.55 : wallX + 0.55;
77
- cz = dz;
78
- facing = row === 0 ? -Math.PI / 2 : Math.PI / 2;
79
- }
80
-
81
- slots.push({
82
- index: i,
83
- desk: [dx, 0.2, dz],
84
- chair: [cx, 0.2, cz],
85
- facing,
86
- });
87
- }
88
-
89
- return slots;
90
- }
91
-
92
- // Stable mapping: which desk slot each agent owns in their *home* room.
93
- // Built once at module load from AGENT_ROSTER.
94
- const agentHomeDesk: Record<string, DeskSlot> = (() => {
95
- const out: Record<string, DeskSlot> = {};
96
- const counters = new Map<RoomId, number>();
97
- for (const entry of AGENT_ROSTER) {
98
- const slots = deskSlotsForRoom(
99
- ROOM_RECTS.find((r) => r.id === entry.room)!
100
- );
101
- if (slots.length === 0) continue;
102
- const idx = counters.get(entry.room) ?? 0;
103
- out[entry.id] = slots[idx % slots.length];
104
- counters.set(entry.room, idx + 1);
105
- }
106
- return out;
107
- })();
108
-
109
- export function homeDeskFor(agentId: string): DeskSlot | undefined {
110
- return agentHomeDesk[agentId];
111
- }
112
-
113
- // Door positions per room — used by Floor3D to leave gaps in the walls and by
114
- // the meeting-room teleport logic. We pick the side that opens onto the
115
- // largest neighbor footprint so the office reads as "all connected".
116
- export interface DoorSpec {
117
- side: "north" | "south" | "east" | "west";
118
- // World position of the door center.
119
- position: [number, number];
120
- width: number;
121
- }
122
-
123
- export function doorsForRoom(rect: RoomRect): DoorSpec[] {
124
- const w = 1.0;
125
- const cx = rect.wx + rect.ww / 2;
126
- const cz = rect.wy + rect.wh / 2;
127
- const doors: DoorSpec[] = [];
128
-
129
- // Every room has a door on every side that opens onto a corridor — the
130
- // corridor mesh is drawn around all rooms in Floor3D.
131
- switch (rect.id) {
132
- case "ceo":
133
- doors.push({ side: "east", position: [rect.wx + rect.ww, cz], width: w });
134
- doors.push({ side: "south", position: [cx, rect.wy + rect.wh], width: w });
135
- break;
136
- case "meeting":
137
- doors.push({ side: "west", position: [rect.wx, cz], width: w });
138
- doors.push({ side: "east", position: [rect.wx + rect.ww, cz], width: w });
139
- doors.push({ side: "south", position: [cx, rect.wy + rect.wh], width: w });
140
- break;
141
- case "coo":
142
- doors.push({ side: "west", position: [rect.wx, cz], width: w });
143
- doors.push({ side: "south", position: [cx, rect.wy + rect.wh], width: w });
144
- break;
145
- case "cto-team":
146
- doors.push({ side: "north", position: [cx, rect.wy], width: w });
147
- doors.push({ side: "east", position: [rect.wx + rect.ww, cz], width: w });
148
- doors.push({ side: "south", position: [cx, rect.wy + rect.wh], width: w });
149
- break;
150
- case "cqo-team":
151
- doors.push({ side: "north", position: [cx, rect.wy], width: w });
152
- doors.push({ side: "west", position: [rect.wx, cz], width: w });
153
- doors.push({ side: "south", position: [cx, rect.wy + rect.wh], width: w });
154
- break;
155
- case "service-ops":
156
- doors.push({ side: "north", position: [cx, rect.wy], width: w });
157
- doors.push({ side: "east", position: [rect.wx + rect.ww, cz], width: w });
158
- break;
159
- case "archive":
160
- doors.push({ side: "north", position: [cx, rect.wy], width: w });
161
- doors.push({ side: "west", position: [rect.wx, cz], width: w });
162
- break;
163
- }
164
- return doors;
165
- }
@@ -1,26 +0,0 @@
1
- export type Lang = "ko" | "en";
2
-
3
- const DICT: Record<string, { ko: string; en: string }> = {
4
- "banner.run_dispatcher_first.ko": {
5
- ko: "Dispatcher를 먼저 실행하세요.",
6
- en: "Run Dispatcher first.",
7
- },
8
- "banner.progress_corrupt": {
9
- ko: "progress.json이 손상되었습니다.",
10
- en: "progress.json is corrupt.",
11
- },
12
- "drawer.tab.agent_log": { ko: "에이전트 로그", en: "Agent Log" },
13
- "drawer.tab.room_metrics": { ko: "룸 메트릭", en: "Room Metrics" },
14
- "drawer.tab.archive_list": { ko: "아카이브", en: "Archive" },
15
- "header.subtitle": {
16
- ko: "walwal-harness 라이브 운영 대시보드",
17
- en: "walwal-harness live operations dashboard",
18
- },
19
- "goal.empty": { ko: "활성 GOAL 없음", en: "No active goal" },
20
- };
21
-
22
- export function t(key: string, lang: Lang = "ko"): string {
23
- const entry = DICT[key];
24
- if (!entry) return key;
25
- return entry[lang] ?? entry.ko ?? key;
26
- }
@@ -1,150 +0,0 @@
1
- import type { RoomId } from "./types";
2
-
3
- // Isometric projection (2:1 dimetric). Tile basis vectors:
4
- // X axis (world right) → screen ( +TILE_W/2, +TILE_H/2 )
5
- // Y axis (world down) → screen ( -TILE_W/2, +TILE_H/2 )
6
- // Caller multiplies by world coordinates and adds an origin offset.
7
- export const TILE_W = 64;
8
- export const TILE_H = 32;
9
-
10
- export interface ScreenPoint {
11
- sx: number;
12
- sy: number;
13
- }
14
-
15
- export function worldToScreen(
16
- wx: number,
17
- wy: number,
18
- origin: ScreenPoint = { sx: 0, sy: 0 }
19
- ): ScreenPoint {
20
- return {
21
- sx: origin.sx + (wx - wy) * (TILE_W / 2),
22
- sy: origin.sy + (wx + wy) * (TILE_H / 2),
23
- };
24
- }
25
-
26
- // Each room is a world-coordinate axis-aligned rectangle. Floor plan is laid out
27
- // in a 12x8 world grid. Rectangles do not overlap interior cells.
28
- export interface RoomRect {
29
- id: RoomId;
30
- wx: number;
31
- wy: number;
32
- ww: number;
33
- wh: number;
34
- }
35
-
36
- // Rooms are now spaced apart so the gaps between them form a real corridor
37
- // network (1 unit wide). The corridor mesh is rendered in Floor3D.
38
- export const ROOM_RECTS: RoomRect[] = [
39
- { id: "ceo", wx: 0, wy: 0, ww: 3, wh: 2 },
40
- { id: "meeting", wx: 4, wy: 0, ww: 3, wh: 2 },
41
- { id: "coo", wx: 8, wy: 0, ww: 3, wh: 2 },
42
- { id: "cto-team", wx: 0, wy: 3, ww: 5, wh: 3 },
43
- { id: "cqo-team", wx: 6, wy: 3, ww: 5, wh: 3 },
44
- { id: "service-ops", wx: 0, wy: 7, ww: 4, wh: 2 },
45
- { id: "archive", wx: 5, wy: 7, ww: 6, wh: 2 },
46
- ];
47
-
48
- export interface CorridorRect {
49
- wx: number;
50
- wy: number;
51
- ww: number;
52
- wh: number;
53
- }
54
-
55
- // Corridor segments — vertical gaps between rooms in the same row, plus the
56
- // horizontal corridors that run between row blocks.
57
- export const CORRIDOR_RECTS: CorridorRect[] = [
58
- { wx: 3, wy: 0, ww: 1, wh: 2 }, // ceo ↔ meeting
59
- { wx: 7, wy: 0, ww: 1, wh: 2 }, // meeting ↔ coo
60
- { wx: 5, wy: 3, ww: 1, wh: 3 }, // cto-team ↔ cqo-team
61
- { wx: 4, wy: 7, ww: 1, wh: 2 }, // service-ops ↔ archive
62
- { wx: 0, wy: 2, ww: 11, wh: 1 }, // horizontal corridor (top ↔ middle row)
63
- { wx: 0, wy: 6, ww: 11, wh: 1 }, // horizontal corridor (middle ↔ bottom row)
64
- ];
65
-
66
- // Free-roam waypoint pool: union of room interiors + corridor cells + a few
67
- // outdoor strolling spots. Coordinates are in world space. The minifig roam
68
- // loop picks a random target from this list when idle, so the office reads as
69
- // "people moving naturally" instead of marching a rectangular track.
70
- export const ROAM_POOL: Array<[number, number]> = [
71
- // room interiors (center + a corner each)
72
- [1.5, 1], [5.5, 1], [9.5, 1],
73
- [2.5, 4.5], [8.5, 4.5],
74
- [2, 8], [8, 8],
75
- // corridor — top row gaps
76
- [3.5, 1], [3.5, 1.5], [7.5, 1], [7.5, 1.5],
77
- // corridor — between top and middle row (runs full width)
78
- [1, 2.5], [3, 2.5], [5.5, 2.5], [8, 2.5], [10, 2.5],
79
- // corridor — between middle and bottom row
80
- [1, 6.5], [3, 6.5], [5.5, 6.5], [8, 6.5], [10, 6.5],
81
- // corridor — between cto/cqo teams
82
- [5.5, 4], [5.5, 5],
83
- // corridor — between service-ops/archive
84
- [4.5, 7.5], [4.5, 8.5],
85
- // outdoor strolling spots
86
- [-1, -0.6], [12, -0.6], [-1, 4], [12, 4], [-1, 9.5], [12, 9.5],
87
- [3, 10], [8, 10],
88
- ];
89
-
90
- // Canvas origin chosen so every projected room point falls inside viewBox 0..1200 x 0..720.
91
- // World grid spans x:[0..9], y:[0..7]. Projected x range = (0-7..9-0) = -7..+9 cells →
92
- // width (-7..+9)*TILE_W/2 = (-224..+288). Shift x so min becomes ~80.
93
- // Projected y range = (0+0..9+7) = 0..16 cells → height 16*TILE_H/2 = 256. Shift y so top
94
- // is at ~140 (leaves room for header inside layout).
95
- export const ISO_ORIGIN: ScreenPoint = { sx: 80 + 224, sy: 140 };
96
-
97
- export function roomPolygon(rect: RoomRect): string {
98
- const o = ISO_ORIGIN;
99
- const a = worldToScreen(rect.wx, rect.wy, o);
100
- const b = worldToScreen(rect.wx + rect.ww, rect.wy, o);
101
- const c = worldToScreen(rect.wx + rect.ww, rect.wy + rect.wh, o);
102
- const d = worldToScreen(rect.wx, rect.wy + rect.wh, o);
103
- return `${a.sx},${a.sy} ${b.sx},${b.sy} ${c.sx},${c.sy} ${d.sx},${d.sy}`;
104
- }
105
-
106
- export function roomCenter(rect: RoomRect): ScreenPoint {
107
- return worldToScreen(rect.wx + rect.ww / 2, rect.wy + rect.wh / 2, ISO_ORIGIN);
108
- }
109
-
110
- // Returns N seat positions inside a room rectangle, arranged in a grid that fits
111
- // gracefully even when the room is small. Used by Minifig placement.
112
- export function seatPositions(rect: RoomRect, count: number): ScreenPoint[] {
113
- if (count <= 0) return [];
114
- const cols = Math.min(count, Math.max(2, Math.floor(rect.ww)));
115
- const rows = Math.ceil(count / cols);
116
- const padX = rect.ww / (cols + 1);
117
- const padY = rect.wh / (rows + 1);
118
- const out: ScreenPoint[] = [];
119
- for (let i = 0; i < count; i++) {
120
- const col = i % cols;
121
- const row = Math.floor(i / cols);
122
- const wx = rect.wx + padX * (col + 1);
123
- const wy = rect.wy + padY * (row + 1);
124
- out.push(worldToScreen(wx, wy, ISO_ORIGIN));
125
- }
126
- return out;
127
- }
128
-
129
- // Given a world rectangle, returns the smallest screen-space bounding box.
130
- // Used by tests to assert non-overlap.
131
- export function rectBounds(rect: RoomRect): {
132
- minX: number;
133
- maxX: number;
134
- minY: number;
135
- maxY: number;
136
- } {
137
- const o = ISO_ORIGIN;
138
- const corners = [
139
- worldToScreen(rect.wx, rect.wy, o),
140
- worldToScreen(rect.wx + rect.ww, rect.wy, o),
141
- worldToScreen(rect.wx + rect.ww, rect.wy + rect.wh, o),
142
- worldToScreen(rect.wx, rect.wy + rect.wh, o),
143
- ];
144
- return {
145
- minX: Math.min(...corners.map((c) => c.sx)),
146
- maxX: Math.max(...corners.map((c) => c.sx)),
147
- minY: Math.min(...corners.map((c) => c.sy)),
148
- maxY: Math.max(...corners.map((c) => c.sy)),
149
- };
150
- }