bmad-visual 0.1.0

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 (215) hide show
  1. package/bin/bmad-visual.js +255 -0
  2. package/package.json +31 -0
  3. package/templates/.agent/workflows/analyst.md +20 -0
  4. package/templates/.agent/workflows/architect.md +20 -0
  5. package/templates/.agent/workflows/bmad-advanced-elicitation.md +5 -0
  6. package/templates/.agent/workflows/bmad-brainstorming.md +5 -0
  7. package/templates/.agent/workflows/bmad-check-implementation-readiness.md +5 -0
  8. package/templates/.agent/workflows/bmad-checkpoint-preview.md +5 -0
  9. package/templates/.agent/workflows/bmad-code-review.md +5 -0
  10. package/templates/.agent/workflows/bmad-correct-course.md +5 -0
  11. package/templates/.agent/workflows/bmad-create-architecture.md +5 -0
  12. package/templates/.agent/workflows/bmad-create-epics-and-stories.md +5 -0
  13. package/templates/.agent/workflows/bmad-create-prd.md +5 -0
  14. package/templates/.agent/workflows/bmad-create-story.md +5 -0
  15. package/templates/.agent/workflows/bmad-create-ux-design.md +5 -0
  16. package/templates/.agent/workflows/bmad-dev-story.md +5 -0
  17. package/templates/.agent/workflows/bmad-distillator.md +5 -0
  18. package/templates/.agent/workflows/bmad-document-project.md +5 -0
  19. package/templates/.agent/workflows/bmad-domain-research.md +5 -0
  20. package/templates/.agent/workflows/bmad-edit-prd.md +5 -0
  21. package/templates/.agent/workflows/bmad-editorial-review-prose.md +5 -0
  22. package/templates/.agent/workflows/bmad-editorial-review-structure.md +5 -0
  23. package/templates/.agent/workflows/bmad-generate-project-context.md +5 -0
  24. package/templates/.agent/workflows/bmad-help-full.md +5 -0
  25. package/templates/.agent/workflows/bmad-help.md +5 -0
  26. package/templates/.agent/workflows/bmad-index-docs.md +5 -0
  27. package/templates/.agent/workflows/bmad-market-research.md +5 -0
  28. package/templates/.agent/workflows/bmad-party-mode.md +5 -0
  29. package/templates/.agent/workflows/bmad-prfaq.md +5 -0
  30. package/templates/.agent/workflows/bmad-product-brief.md +5 -0
  31. package/templates/.agent/workflows/bmad-qa-generate-e2e-tests.md +5 -0
  32. package/templates/.agent/workflows/bmad-quick-dev.md +5 -0
  33. package/templates/.agent/workflows/bmad-retrospective.md +5 -0
  34. package/templates/.agent/workflows/bmad-review-adversarial-general.md +5 -0
  35. package/templates/.agent/workflows/bmad-review-edge-case-hunter.md +5 -0
  36. package/templates/.agent/workflows/bmad-shard-doc.md +5 -0
  37. package/templates/.agent/workflows/bmad-sprint-planning.md +5 -0
  38. package/templates/.agent/workflows/bmad-sprint-status.md +5 -0
  39. package/templates/.agent/workflows/bmad-technical-research.md +5 -0
  40. package/templates/.agent/workflows/bmad-validate-prd.md +5 -0
  41. package/templates/.agent/workflows/dev.md +20 -0
  42. package/templates/.agent/workflows/pm.md +20 -0
  43. package/templates/.agent/workflows/qa.md +20 -0
  44. package/templates/.agent/workflows/sm.md +20 -0
  45. package/templates/.agent/workflows/solo-dev.md +20 -0
  46. package/templates/.agent/workflows/ux.md +20 -0
  47. package/templates/.claude/settings.local.json +50 -0
  48. package/templates/.claude/skills/analyst/SKILL.md +77 -0
  49. package/templates/.claude/skills/architect/SKILL.md +72 -0
  50. package/templates/.claude/skills/bmad-advanced-elicitation/SKILL.md +24 -0
  51. package/templates/.claude/skills/bmad-brainstorming/SKILL.md +26 -0
  52. package/templates/.claude/skills/bmad-check-implementation-readiness/SKILL.md +22 -0
  53. package/templates/.claude/skills/bmad-checkpoint-preview/SKILL.md +17 -0
  54. package/templates/.claude/skills/bmad-code-review/SKILL.md +22 -0
  55. package/templates/.claude/skills/bmad-correct-course/SKILL.md +17 -0
  56. package/templates/.claude/skills/bmad-create-architecture/SKILL.md +22 -0
  57. package/templates/.claude/skills/bmad-create-epics-and-stories/SKILL.md +22 -0
  58. package/templates/.claude/skills/bmad-create-prd/SKILL.md +30 -0
  59. package/templates/.claude/skills/bmad-create-story/SKILL.md +21 -0
  60. package/templates/.claude/skills/bmad-create-ux-design/SKILL.md +23 -0
  61. package/templates/.claude/skills/bmad-dev-story/SKILL.md +27 -0
  62. package/templates/.claude/skills/bmad-distillator/SKILL.md +22 -0
  63. package/templates/.claude/skills/bmad-document-project/SKILL.md +27 -0
  64. package/templates/.claude/skills/bmad-domain-research/SKILL.md +21 -0
  65. package/templates/.claude/skills/bmad-edit-prd/SKILL.md +18 -0
  66. package/templates/.claude/skills/bmad-editorial-review-prose/SKILL.md +21 -0
  67. package/templates/.claude/skills/bmad-editorial-review-structure/SKILL.md +25 -0
  68. package/templates/.claude/skills/bmad-generate-project-context/SKILL.md +20 -0
  69. package/templates/.claude/skills/bmad-help/SKILL.md +63 -0
  70. package/templates/.claude/skills/bmad-help-full/SKILL.md +23 -0
  71. package/templates/.claude/skills/bmad-index-docs/SKILL.md +16 -0
  72. package/templates/.claude/skills/bmad-market-research/SKILL.md +22 -0
  73. package/templates/.claude/skills/bmad-party-mode/SKILL.md +24 -0
  74. package/templates/.claude/skills/bmad-prfaq/SKILL.md +36 -0
  75. package/templates/.claude/skills/bmad-product-brief/SKILL.md +36 -0
  76. package/templates/.claude/skills/bmad-qa-generate-e2e-tests/SKILL.md +21 -0
  77. package/templates/.claude/skills/bmad-quick-dev/SKILL.md +23 -0
  78. package/templates/.claude/skills/bmad-retrospective/SKILL.md +17 -0
  79. package/templates/.claude/skills/bmad-review-adversarial-general/SKILL.md +21 -0
  80. package/templates/.claude/skills/bmad-review-edge-case-hunter/SKILL.md +22 -0
  81. package/templates/.claude/skills/bmad-shard-doc/SKILL.md +17 -0
  82. package/templates/.claude/skills/bmad-sprint-planning/SKILL.md +21 -0
  83. package/templates/.claude/skills/bmad-sprint-status/SKILL.md +17 -0
  84. package/templates/.claude/skills/bmad-technical-research/SKILL.md +21 -0
  85. package/templates/.claude/skills/bmad-validate-prd/SKILL.md +22 -0
  86. package/templates/.claude/skills/dev/SKILL.md +71 -0
  87. package/templates/.claude/skills/pm/SKILL.md +76 -0
  88. package/templates/.claude/skills/qa/SKILL.md +64 -0
  89. package/templates/.claude/skills/sm/SKILL.md +74 -0
  90. package/templates/.claude/skills/solo-dev/SKILL.md +64 -0
  91. package/templates/.claude/skills/ux/SKILL.md +71 -0
  92. package/templates/CLAUDE.md +60 -0
  93. package/templates/dashboard/index.html +15 -0
  94. package/templates/dashboard/package.json +30 -0
  95. package/templates/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
  96. package/templates/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
  97. package/templates/dashboard/public/assets/avatars/Female1_blink.png +0 -0
  98. package/templates/dashboard/public/assets/avatars/Female1_talk.png +0 -0
  99. package/templates/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
  100. package/templates/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
  101. package/templates/dashboard/public/assets/avatars/Female2_blink.png +0 -0
  102. package/templates/dashboard/public/assets/avatars/Female2_talk.png +0 -0
  103. package/templates/dashboard/public/assets/avatars/Female3_blink.png +0 -0
  104. package/templates/dashboard/public/assets/avatars/Female3_talk.png +0 -0
  105. package/templates/dashboard/public/assets/avatars/Female3_wave.png +0 -0
  106. package/templates/dashboard/public/assets/avatars/Female4_blink.png +0 -0
  107. package/templates/dashboard/public/assets/avatars/Female4_talk.png +0 -0
  108. package/templates/dashboard/public/assets/avatars/Female4_wave.png +0 -0
  109. package/templates/dashboard/public/assets/avatars/Female5_blink.png +0 -0
  110. package/templates/dashboard/public/assets/avatars/Female5_talk.png +0 -0
  111. package/templates/dashboard/public/assets/avatars/Female5_wave.png +0 -0
  112. package/templates/dashboard/public/assets/avatars/Female6_blink.png +0 -0
  113. package/templates/dashboard/public/assets/avatars/Female6_talk.png +0 -0
  114. package/templates/dashboard/public/assets/avatars/Female6_wave.png +0 -0
  115. package/templates/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
  116. package/templates/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
  117. package/templates/dashboard/public/assets/avatars/Male1_blink.png +0 -0
  118. package/templates/dashboard/public/assets/avatars/Male1_talk.png +0 -0
  119. package/templates/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
  120. package/templates/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
  121. package/templates/dashboard/public/assets/avatars/Male2_blink.png +0 -0
  122. package/templates/dashboard/public/assets/avatars/Male2_talk.png +0 -0
  123. package/templates/dashboard/public/assets/avatars/Male3_blink.png +0 -0
  124. package/templates/dashboard/public/assets/avatars/Male3_talk.png +0 -0
  125. package/templates/dashboard/public/assets/avatars/Male3_wave.png +0 -0
  126. package/templates/dashboard/public/assets/avatars/Male4_blink.png +0 -0
  127. package/templates/dashboard/public/assets/avatars/Male4_talk.png +0 -0
  128. package/templates/dashboard/public/assets/avatars/Male4_wave.png +0 -0
  129. package/templates/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
  130. package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
  131. package/templates/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
  132. package/templates/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
  133. package/templates/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
  134. package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
  135. package/templates/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
  136. package/templates/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
  137. package/templates/dashboard/public/assets/furniture/armchair_tan.png +0 -0
  138. package/templates/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
  139. package/templates/dashboard/public/assets/furniture/backpack_blue.png +0 -0
  140. package/templates/dashboard/public/assets/furniture/backpack_red.png +0 -0
  141. package/templates/dashboard/public/assets/furniture/blinds.png +0 -0
  142. package/templates/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
  143. package/templates/dashboard/public/assets/furniture/bookshelf.png +0 -0
  144. package/templates/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
  145. package/templates/dashboard/public/assets/furniture/bulletin_board.png +0 -0
  146. package/templates/dashboard/public/assets/furniture/clock.png +0 -0
  147. package/templates/dashboard/public/assets/furniture/coffee_mug.png +0 -0
  148. package/templates/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
  149. package/templates/dashboard/public/assets/furniture/coffee_table.png +0 -0
  150. package/templates/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
  151. package/templates/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
  152. package/templates/dashboard/public/assets/furniture/couch.png +0 -0
  153. package/templates/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
  154. package/templates/dashboard/public/assets/furniture/cushion_blue.png +0 -0
  155. package/templates/dashboard/public/assets/furniture/cushion_tan.png +0 -0
  156. package/templates/dashboard/public/assets/furniture/desk_wood.png +0 -0
  157. package/templates/dashboard/public/assets/furniture/fancy_rug.png +0 -0
  158. package/templates/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
  159. package/templates/dashboard/public/assets/furniture/flowers1.png +0 -0
  160. package/templates/dashboard/public/assets/furniture/flowers2.png +0 -0
  161. package/templates/dashboard/public/assets/furniture/lamp_tan.png +0 -0
  162. package/templates/dashboard/public/assets/furniture/lantern.png +0 -0
  163. package/templates/dashboard/public/assets/furniture/monstera.png +0 -0
  164. package/templates/dashboard/public/assets/furniture/monstera_small.png +0 -0
  165. package/templates/dashboard/public/assets/furniture/picture_frame.png +0 -0
  166. package/templates/dashboard/public/assets/furniture/plant1.png +0 -0
  167. package/templates/dashboard/public/assets/furniture/plant2.png +0 -0
  168. package/templates/dashboard/public/assets/furniture/plant3.png +0 -0
  169. package/templates/dashboard/public/assets/furniture/plant_poof.png +0 -0
  170. package/templates/dashboard/public/assets/furniture/plant_spindly.png +0 -0
  171. package/templates/dashboard/public/assets/furniture/poster_blue.png +0 -0
  172. package/templates/dashboard/public/assets/furniture/rug.png +0 -0
  173. package/templates/dashboard/public/assets/furniture/succulent_blue.png +0 -0
  174. package/templates/dashboard/public/assets/furniture/succulent_green.png +0 -0
  175. package/templates/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
  176. package/templates/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
  177. package/templates/dashboard/public/assets/furniture/whiteboard.png +0 -0
  178. package/templates/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
  179. package/templates/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
  180. package/templates/dashboard/public/assets/logo.png +0 -0
  181. package/templates/dashboard/src/App.tsx +119 -0
  182. package/templates/dashboard/src/components/SquadCard.tsx +47 -0
  183. package/templates/dashboard/src/components/SquadSelector.tsx +61 -0
  184. package/templates/dashboard/src/components/StatusBadge.tsx +32 -0
  185. package/templates/dashboard/src/components/StatusBar.tsx +97 -0
  186. package/templates/dashboard/src/hooks/useSquadSocket.ts +137 -0
  187. package/templates/dashboard/src/lib/formatTime.ts +16 -0
  188. package/templates/dashboard/src/lib/normalizeState.ts +25 -0
  189. package/templates/dashboard/src/main.tsx +14 -0
  190. package/templates/dashboard/src/office/AgentSprite.ts +249 -0
  191. package/templates/dashboard/src/office/OfficeScene.ts +577 -0
  192. package/templates/dashboard/src/office/PhaserGame.tsx +101 -0
  193. package/templates/dashboard/src/office/RoomBuilder.ts +190 -0
  194. package/templates/dashboard/src/office/assetKeys.ts +150 -0
  195. package/templates/dashboard/src/office/palette.ts +32 -0
  196. package/templates/dashboard/src/plugin/squadWatcher.ts +260 -0
  197. package/templates/dashboard/src/store/useSquadStore.ts +63 -0
  198. package/templates/dashboard/src/styles/globals.css +41 -0
  199. package/templates/dashboard/src/types/state.ts +69 -0
  200. package/templates/dashboard/src/vite-env.d.ts +1 -0
  201. package/templates/dashboard/tsconfig.json +24 -0
  202. package/templates/dashboard/tsconfig.tsbuildinfo +1 -0
  203. package/templates/dashboard/vite.config.ts +13 -0
  204. package/templates/squads/.gitkeep +0 -0
  205. package/templates/squads/bmad/agents/analyst.agent.md +87 -0
  206. package/templates/squads/bmad/agents/architect.agent.md +82 -0
  207. package/templates/squads/bmad/agents/developer.agent.md +91 -0
  208. package/templates/squads/bmad/agents/pm.agent.md +86 -0
  209. package/templates/squads/bmad/agents/qa-engineer.agent.md +84 -0
  210. package/templates/squads/bmad/agents/scrum-master.agent.md +84 -0
  211. package/templates/squads/bmad/agents/solo-dev.agent.md +81 -0
  212. package/templates/squads/bmad/agents/tech-writer.agent.md +86 -0
  213. package/templates/squads/bmad/agents/ux-designer.agent.md +81 -0
  214. package/templates/squads/bmad/squad.yaml +70 -0
  215. package/templates/squads/bmad/state.json +108 -0
@@ -0,0 +1,190 @@
1
+ import Phaser from 'phaser';
2
+ import { COLORS, TILE, MARGIN, WALL_H } from './palette';
3
+ import { FURNITURE_KEYS } from './assetKeys';
4
+
5
+ export class RoomBuilder {
6
+ private scene: Phaser.Scene;
7
+
8
+ constructor(scene: Phaser.Scene) {
9
+ this.scene = scene;
10
+ }
11
+
12
+ build(roomW: number, roomH: number): void {
13
+ this.drawFloor(roomW, roomH);
14
+ this.drawWalls(roomW);
15
+ this.drawRoomBorder(roomW, roomH);
16
+ this.placeFurniture(roomW, roomH);
17
+ }
18
+
19
+ private drawFloor(roomW: number, roomH: number): void {
20
+ const g = this.scene.add.graphics();
21
+ // Main floor fill
22
+ g.fillStyle(COLORS.floor, 1);
23
+ g.fillRect(0, WALL_H, roomW, roomH - WALL_H);
24
+ // Checkerboard texture
25
+ g.fillStyle(COLORS.floorAlt, 0.25);
26
+ for (let y = WALL_H; y < roomH; y += TILE) {
27
+ for (let x = 0; x < roomW; x += TILE) {
28
+ if ((x / TILE + y / TILE) % 2 === 0) {
29
+ g.fillRect(x, y, TILE, TILE);
30
+ }
31
+ }
32
+ }
33
+ // Shadow along the wall base for depth
34
+ g.fillStyle(0x000000, 0.1);
35
+ g.fillRect(0, WALL_H, roomW, 10);
36
+ g.fillStyle(0x000000, 0.05);
37
+ g.fillRect(0, WALL_H + 10, roomW, 6);
38
+ // Subtle shadow along left and right walls
39
+ g.fillStyle(0x000000, 0.04);
40
+ g.fillRect(0, WALL_H, 6, roomH - WALL_H);
41
+ g.fillRect(roomW - 6, WALL_H, 6, roomH - WALL_H);
42
+ // Subtle warm highlight on bottom edge
43
+ g.fillStyle(0xddc89e, 0.15);
44
+ g.fillRect(0, roomH - 4, roomW, 4);
45
+ g.setDepth(-2);
46
+ }
47
+
48
+ private drawWalls(roomW: number): void {
49
+ const g = this.scene.add.graphics();
50
+ // Wall background — slight gradient effect with two tones
51
+ g.fillStyle(COLORS.wall, 1);
52
+ g.fillRect(0, 0, roomW, WALL_H);
53
+ // Lighter upper band for depth
54
+ g.fillStyle(0xede2d6, 1);
55
+ g.fillRect(0, 0, roomW, WALL_H / 3);
56
+ // Baseboard trim
57
+ g.fillStyle(COLORS.wallTrim, 1);
58
+ g.fillRect(0, WALL_H - 5, roomW, 5);
59
+ // Thin highlight line above baseboard
60
+ g.fillStyle(0xc8b8a8, 1);
61
+ g.fillRect(0, WALL_H - 6, roomW, 1);
62
+ g.setDepth(-1);
63
+ }
64
+
65
+ private drawRoomBorder(roomW: number, roomH: number): void {
66
+ const g = this.scene.add.graphics();
67
+ const bg = 0x0a0e1a;
68
+ const pad = 200; // generous padding to cover any viewport overflow
69
+
70
+ // Dark overlay strips outside the room on all 4 sides
71
+ g.fillStyle(bg, 1);
72
+ g.fillRect(-pad, -pad, roomW + pad * 2, pad); // top
73
+ g.fillRect(-pad, roomH, roomW + pad * 2, pad); // bottom
74
+ g.fillRect(-pad, -pad, pad, roomH + pad * 2); // left
75
+ g.fillRect(roomW, -pad, pad, roomH + pad * 2); // right
76
+
77
+ // Clean 2px border around room edge
78
+ g.lineStyle(2, 0x1a2540, 0.8);
79
+ g.strokeRect(0, 0, roomW, roomH);
80
+ g.setDepth(1000);
81
+ }
82
+
83
+ placeFurniture(roomW: number, roomH: number): void {
84
+ const s = this.scene;
85
+ const centerX = roomW / 2;
86
+ const deskAreaBottom = roomH - MARGIN;
87
+ const loungeY = deskAreaBottom - TILE * 0.5;
88
+
89
+ // ================================================================
90
+ // WALL DECORATIONS — 5 evenly-spaced sections for breathing room
91
+ // ================================================================
92
+ const wallSections = 5;
93
+ const section = roomW / wallSections;
94
+
95
+ // Section 1: Window — far left
96
+ s.add.image(section * 0.6, WALL_H + 5, FURNITURE_KEYS.blindsLargeWhite)
97
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(0);
98
+
99
+ // Section 2: Bookshelf purple tall — left
100
+ s.add.image(section * 1.8, WALL_H + 40, FURNITURE_KEYS.bookshelfPurpleTall)
101
+ .setOrigin(0.5, 1).setScale(2).setDepth(0);
102
+
103
+ // Section 4: Whiteboard stand with graph — right of center
104
+ s.add.image(section * 3.2, WALL_H + 40, FURNITURE_KEYS.whiteboardStandGraph)
105
+ .setOrigin(0.5, 1).setScale(2.0).setDepth(0);
106
+
107
+ // Section 5: Window — far right
108
+ s.add.image(section * 4.4, WALL_H + 5, FURNITURE_KEYS.blindsLargeWhite)
109
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(0);
110
+
111
+ // ================================================================
112
+ // CORNER PLANTS — one plant anchored to each room corner
113
+ // ================================================================
114
+ // Top-left corner
115
+ s.add.image(MARGIN / 2 + 8, WALL_H + TILE * 2, FURNITURE_KEYS.monstera)
116
+ .setOrigin(0.5, 1).setScale(1.3).setDepth(WALL_H + TILE * 2);
117
+
118
+ // Top-right corner — water cooler against the right wall
119
+ s.add.image(roomW - MARGIN / 4, WALL_H + TILE * 2, FURNITURE_KEYS.waterCooler)
120
+ .setOrigin(0.5, 1).setScale(1.5).setDepth(WALL_H + TILE * 2 + 1);
121
+
122
+ // Bottom-left corner
123
+ s.add.image(MARGIN / 2, roomH - TILE * 0.5, FURNITURE_KEYS.plant3)
124
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(roomH - TILE * 0.5);
125
+
126
+ // Bottom-right corner
127
+ s.add.image(roomW - MARGIN / 2.5, roomH - TILE * 0.5, FURNITURE_KEYS.plantPoof)
128
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(roomH - TILE * 0.5);
129
+
130
+ // Bottom-right corner — treasure chest
131
+ s.add.image(roomW - MARGIN / 4, roomH - TILE * 2, FURNITURE_KEYS.treasurechestGold)
132
+ .setOrigin(0.5, 1).setScale(1.4).setDepth(1);
133
+
134
+
135
+
136
+
137
+ // Middle-left and middle-right (vertical center)
138
+ const midY = WALL_H + (roomH - WALL_H) / 2;
139
+ s.add.image(MARGIN / 2, midY, FURNITURE_KEYS.monsteraSmall)
140
+ .setOrigin(0.5, 1).setScale(1.5).setDepth(midY);
141
+ s.add.image(roomW - MARGIN / 2, midY, FURNITURE_KEYS.plant1)
142
+ .setOrigin(0.5, 1).setScale(1.5).setDepth(midY);
143
+
144
+ // ================================================================
145
+ // DESK-AREA ACCENTS — flowers near wall, anchored to desk tops
146
+ // ================================================================
147
+
148
+
149
+ // ================================================================
150
+ // LOUNGE ZONE — cozy social area at bottom
151
+ // ================================================================
152
+ // Rug — wide rectangular, covers full lounge seating group (448x256 sprite)
153
+ s.add.image(centerX, loungeY + TILE * 0.9, FURNITURE_KEYS.fancyRugWide)
154
+ .setOrigin(0.5, 0.5).setScale(0.7).setDepth(-0.5);
155
+
156
+ // Couch — centered, bigger
157
+ const couchY = loungeY + TILE * 0.3;
158
+ s.add.image(centerX, couchY, FURNITURE_KEYS.couchTanDown)
159
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(couchY);
160
+
161
+ // Cushions — sitting on the couch seat (slightly above couch bottom)
162
+ s.add.image(centerX - 26, couchY - 28, FURNITURE_KEYS.cushionBlue)
163
+ .setOrigin(0.5, 0.5).setScale(1.5).setDepth(couchY + 1);
164
+ s.add.image(centerX + 26, couchY - 28, FURNITURE_KEYS.cushionTan)
165
+ .setOrigin(0.5, 0.5).setScale(1.5).setDepth(couchY + 1);
166
+
167
+ // Armchairs — flanking the couch, same scale
168
+ s.add.image(centerX - 95, loungeY + TILE * 0.8, FURNITURE_KEYS.armchairTanDown)
169
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(loungeY + TILE * 0.8);
170
+ s.add.image(centerX + 95, loungeY + TILE * 0.8, FURNITURE_KEYS.armchairTanDown)
171
+ .setOrigin(0.5, 1).setScale(1.8).setDepth(loungeY + TILE * 0.8);
172
+
173
+ // Coffee table — in front of couch
174
+ s.add.image(centerX, loungeY + TILE * 1.5, FURNITURE_KEYS.coffeeTable)
175
+ .setOrigin(0.5, 1).setScale(1.4).setDepth(loungeY + TILE * 1.5);
176
+
177
+ // Coffee mug on table
178
+ s.add.image(centerX + 12, loungeY + TILE * 1.3, FURNITURE_KEYS.coffeeMugBlue)
179
+ .setOrigin(0.5, 1).setDepth(loungeY + TILE * 1.5 + 1);
180
+
181
+ // Coffee station — right side of lounge
182
+ const stationX = centerX + TILE * 6;
183
+ const stationY = loungeY + TILE * 1.9;
184
+ s.add.image(stationX, stationY, FURNITURE_KEYS.coffeetableBlackH)
185
+ .setOrigin(0.5, 1).setScale(1.5).setDepth(stationY);
186
+ s.add.image(stationX, stationY + 5, FURNITURE_KEYS.coffeepotRight)
187
+ .setOrigin(0.5, 1).setScale(1.2).setDepth(stationY + 1);
188
+
189
+ }
190
+ }
@@ -0,0 +1,150 @@
1
+ // Asset key constants for Phaser loader
2
+ // All paths relative to /assets/ in public/
3
+
4
+ // --- Characters ---
5
+ export const MALE_CHARACTERS = ['Male1', 'Male2', 'Male3', 'Male4'] as const;
6
+ export const FEMALE_CHARACTERS = ['Female1', 'Female2', 'Female3', 'Female4', 'Female5', 'Female6'] as const;
7
+
8
+ export type CharacterName =
9
+ | typeof MALE_CHARACTERS[number]
10
+ | typeof FEMALE_CHARACTERS[number];
11
+
12
+ // Combined array for preloading all sprites
13
+ export const CHARACTER_NAMES = [...MALE_CHARACTERS, ...FEMALE_CHARACTERS] as const;
14
+
15
+ // Characters with only a single wave frame (no _1wave/_2wave variants)
16
+ const SINGLE_WAVE = new Set<CharacterName>(['Male3', 'Male4', 'Female3', 'Female4', 'Female5', 'Female6']);
17
+
18
+ // Returns the asset keys for a character's animation frames
19
+ export function avatarKeys(name: CharacterName) {
20
+ return {
21
+ blink: `avatar_${name}_blink`,
22
+ talk: `avatar_${name}_talk`,
23
+ wave1: `avatar_${name}_wave1`,
24
+ wave2: `avatar_${name}_wave2`,
25
+ };
26
+ }
27
+
28
+ export function avatarPath(name: CharacterName, pose: string): string {
29
+ if (pose === 'wave1') return SINGLE_WAVE.has(name) ? `assets/avatars/${name}_wave.png` : `assets/avatars/${name}_1wave.png`;
30
+ if (pose === 'wave2') return SINGLE_WAVE.has(name) ? `assets/avatars/${name}_wave.png` : `assets/avatars/${name}_2wave.png`;
31
+ return `assets/avatars/${name}_${pose}.png`;
32
+ }
33
+
34
+ // --- Desks ---
35
+ export const DESK_KEYS = {
36
+ blackIdle: 'desk_black_idle',
37
+ blackCoding: 'desk_black_coding',
38
+ blackCodingAlt: 'desk_black_coding_alt',
39
+ whiteIdle: 'desk_white_idle',
40
+ whiteCoding: 'desk_white_coding',
41
+ whiteCodingAlt: 'desk_white_coding_alt',
42
+ blackUp: 'desk_black_up',
43
+ whiteUp: 'desk_white_up',
44
+ } as const;
45
+
46
+ export const DESK_PATHS: Record<string, string> = {
47
+ [DESK_KEYS.blackIdle]: 'assets/desks/desktop_set_black_down.png',
48
+ [DESK_KEYS.blackCoding]: 'assets/desks/desktop_set_black_down_coding.png',
49
+ [DESK_KEYS.blackCodingAlt]: 'assets/desks/desktop_set_black_down_coding-1.png',
50
+ [DESK_KEYS.whiteIdle]: 'assets/desks/desktop_set_white_down.png',
51
+ [DESK_KEYS.whiteCoding]: 'assets/desks/desktop_set_white_down_coding.png',
52
+ [DESK_KEYS.whiteCodingAlt]: 'assets/desks/desktop_set_white_down_coding-1.png',
53
+ [DESK_KEYS.blackUp]: 'assets/desks/desktop_set_black_up.png',
54
+ [DESK_KEYS.whiteUp]: 'assets/desks/desktop_set_white_up.png',
55
+ };
56
+
57
+ // --- Furniture ---
58
+ export const FURNITURE_KEYS = {
59
+ bookshelf: 'furniture_bookshelf',
60
+ whiteboard: 'furniture_whiteboard',
61
+ clock: 'furniture_clock',
62
+ plant1: 'furniture_plant1',
63
+ plant2: 'furniture_plant2',
64
+ plant3: 'furniture_plant3',
65
+ flowers1: 'furniture_flowers1',
66
+ flowers2: 'furniture_flowers2',
67
+ couch: 'furniture_couch',
68
+ rug: 'furniture_rug',
69
+ coffeeMug: 'furniture_coffee_mug',
70
+ blinds: 'furniture_blinds',
71
+ coffeeTable: 'furniture_coffee_table',
72
+ // New assets
73
+ lampTan: 'furniture_lamp_tan',
74
+ monstera: 'furniture_monstera',
75
+ monsteraSmall: 'furniture_monstera_small',
76
+ succulentGreen: 'furniture_succulent_green',
77
+ succulentBlue: 'furniture_succulent_blue',
78
+ posterBlue: 'furniture_poster_blue',
79
+ bulletinBoard: 'furniture_bulletin_board',
80
+ fancyRug: 'furniture_fancy_rug',
81
+ cushionBlue: 'furniture_cushion_blue',
82
+ cushionTan: 'furniture_cushion_tan',
83
+ armchairTan: 'furniture_armchair_tan',
84
+ backpackBlue: 'furniture_backpack_blue',
85
+ backpackRed: 'furniture_backpack_red',
86
+ plantPoof: 'furniture_plant_poof',
87
+ plantSpindly: 'furniture_plant_spindly',
88
+ coffeeMugBlue: 'furniture_coffee_mug_blue',
89
+ pictureFrame: 'furniture_picture_frame',
90
+ lantern: 'furniture_lantern',
91
+ windowBlindsOpen: 'furniture_window_blinds_open',
92
+ couchTanDown: 'furniture_couch_tan_down',
93
+ armchairTanDown: 'furniture_armchair_tan_down',
94
+ deskWood: 'furniture_desk_wood',
95
+ fancyRugWide: 'furniture_fancy_rug_wide',
96
+ waterCooler: 'furniture_water_cooler',
97
+ whiteboardStandGraph: 'furniture_whiteboard_stand_graph',
98
+ bookshelfPurpleTall: 'furniture_bookshelf_purple_tall',
99
+ coffeetableBlackH: 'furniture_coffeetable_black_h',
100
+ coffeepotRight: 'furniture_coffeepot_right',
101
+ blindsLargeWhite: 'furniture_blinds_large_white',
102
+ treasurechestGold: 'furniture_treasurechest_gold',
103
+ } as const;
104
+
105
+ export const FURNITURE_PATHS: Record<string, string> = {
106
+ [FURNITURE_KEYS.bookshelf]: 'assets/furniture/bookshelf.png',
107
+ [FURNITURE_KEYS.whiteboard]: 'assets/furniture/whiteboard.png',
108
+ [FURNITURE_KEYS.clock]: 'assets/furniture/clock.png',
109
+ [FURNITURE_KEYS.plant1]: 'assets/furniture/plant1.png',
110
+ [FURNITURE_KEYS.plant2]: 'assets/furniture/plant2.png',
111
+ [FURNITURE_KEYS.plant3]: 'assets/furniture/plant3.png',
112
+ [FURNITURE_KEYS.flowers1]: 'assets/furniture/flowers1.png',
113
+ [FURNITURE_KEYS.flowers2]: 'assets/furniture/flowers2.png',
114
+ [FURNITURE_KEYS.couch]: 'assets/furniture/couch.png',
115
+ [FURNITURE_KEYS.rug]: 'assets/furniture/rug.png',
116
+ [FURNITURE_KEYS.coffeeMug]: 'assets/furniture/coffee_mug.png',
117
+ [FURNITURE_KEYS.blinds]: 'assets/furniture/blinds.png',
118
+ [FURNITURE_KEYS.coffeeTable]: 'assets/furniture/coffee_table.png',
119
+ // New assets
120
+ [FURNITURE_KEYS.lampTan]: 'assets/furniture/lamp_tan.png',
121
+ [FURNITURE_KEYS.monstera]: 'assets/furniture/monstera.png',
122
+ [FURNITURE_KEYS.monsteraSmall]: 'assets/furniture/monstera_small.png',
123
+ [FURNITURE_KEYS.succulentGreen]: 'assets/furniture/succulent_green.png',
124
+ [FURNITURE_KEYS.succulentBlue]: 'assets/furniture/succulent_blue.png',
125
+ [FURNITURE_KEYS.posterBlue]: 'assets/furniture/poster_blue.png',
126
+ [FURNITURE_KEYS.bulletinBoard]: 'assets/furniture/bulletin_board.png',
127
+ [FURNITURE_KEYS.fancyRug]: 'assets/furniture/fancy_rug.png',
128
+ [FURNITURE_KEYS.cushionBlue]: 'assets/furniture/cushion_blue.png',
129
+ [FURNITURE_KEYS.cushionTan]: 'assets/furniture/cushion_tan.png',
130
+ [FURNITURE_KEYS.armchairTan]: 'assets/furniture/armchair_tan.png',
131
+ [FURNITURE_KEYS.backpackBlue]: 'assets/furniture/backpack_blue.png',
132
+ [FURNITURE_KEYS.backpackRed]: 'assets/furniture/backpack_red.png',
133
+ [FURNITURE_KEYS.plantPoof]: 'assets/furniture/plant_poof.png',
134
+ [FURNITURE_KEYS.plantSpindly]: 'assets/furniture/plant_spindly.png',
135
+ [FURNITURE_KEYS.coffeeMugBlue]: 'assets/furniture/coffee_mug_blue.png',
136
+ [FURNITURE_KEYS.pictureFrame]: 'assets/furniture/picture_frame.png',
137
+ [FURNITURE_KEYS.lantern]: 'assets/furniture/lantern.png',
138
+ [FURNITURE_KEYS.windowBlindsOpen]: 'assets/furniture/window_blinds_open.png',
139
+ [FURNITURE_KEYS.couchTanDown]: 'assets/furniture/couch_tan_down.png',
140
+ [FURNITURE_KEYS.armchairTanDown]: 'assets/furniture/armchair_tan_down.png',
141
+ [FURNITURE_KEYS.deskWood]: 'assets/furniture/desk_wood.png',
142
+ [FURNITURE_KEYS.fancyRugWide]: 'assets/furniture/fancy_rug_wide.png',
143
+ [FURNITURE_KEYS.waterCooler]: 'assets/furniture/water_cooler_better.png',
144
+ [FURNITURE_KEYS.whiteboardStandGraph]: 'assets/furniture/whiteboard_stand_graph.png',
145
+ [FURNITURE_KEYS.bookshelfPurpleTall]: 'assets/furniture/bookshelf_purple_tall.png',
146
+ [FURNITURE_KEYS.coffeetableBlackH]: 'assets/furniture/coffeetable_black_horizontal.png',
147
+ [FURNITURE_KEYS.coffeepotRight]: 'assets/furniture/coffeepot_right.png',
148
+ [FURNITURE_KEYS.blindsLargeWhite]: 'assets/furniture/blinds_large_closed_white.png',
149
+ [FURNITURE_KEYS.treasurechestGold]: 'assets/furniture/treasurechest_closed_gold.png',
150
+ };
@@ -0,0 +1,32 @@
1
+ // Slimmed palette — only badge colors and layout constants
2
+ // All visual rendering now uses sprite assets, not procedural colors
3
+
4
+ export const COLORS = {
5
+ // Status badge dots
6
+ statusIdle: 0x6b7894,
7
+ statusWorking: 0x3b82f6,
8
+ statusDone: 0x10b981,
9
+ statusCheckpoint: 0xf59e0b,
10
+
11
+ // Name badge
12
+ nameCardBg: 0x0f1629,
13
+ nameCardText: 0xf0f0f0,
14
+
15
+ // Background
16
+ background: 0x0a0e1a,
17
+
18
+ // Floor fill (warm wood)
19
+ floor: 0xc8ac86,
20
+ floorAlt: 0xbca07a,
21
+
22
+ // Wall fill
23
+ wall: 0xe6dace,
24
+ wallTrim: 0xa89888,
25
+ } as const;
26
+
27
+ // Layout constants
28
+ export const TILE = 32; // Base tile size in pixels
29
+ export const CELL_W = 3 * TILE; // 96px — desk cell width (tighter grid)
30
+ export const CELL_H = 3 * TILE; // 96px — desk cell height
31
+ export const MARGIN = 3 * TILE; // 96px — room edge margin (more breathing room)
32
+ export const WALL_H = 3 * TILE; // 96px — wall strip height (taller for decorations)
@@ -0,0 +1,260 @@
1
+ import type { Plugin, ViteDevServer } from "vite";
2
+ import { WebSocketServer, WebSocket } from "ws";
3
+ import type { Server, IncomingMessage } from "node:http";
4
+ import type { Duplex } from "node:stream";
5
+ import fs from "node:fs";
6
+ import fsp from "node:fs/promises";
7
+ import { watch as chokidarWatch } from "chokidar";
8
+ import path from "node:path";
9
+ import { parse as parseYaml } from "yaml";
10
+ import type { SquadInfo, SquadState, WsMessage } from "../types/state";
11
+
12
+ function resolveSquadsDir(): string {
13
+ const candidates = [
14
+ path.resolve(process.cwd(), "../squads"), // started from dashboard/
15
+ path.resolve(process.cwd(), "squads"), // started from project root
16
+ ];
17
+ for (const c of candidates) {
18
+ if (fs.existsSync(c)) return c;
19
+ }
20
+ return path.resolve(process.cwd(), "../squads"); // default (will be created on demand)
21
+ }
22
+
23
+ async function discoverSquads(squadsDir: string): Promise<SquadInfo[]> {
24
+ let entries;
25
+ try {
26
+ entries = await fsp.readdir(squadsDir, { withFileTypes: true });
27
+ } catch {
28
+ return [];
29
+ }
30
+
31
+ const squads: SquadInfo[] = [];
32
+
33
+ for (const entry of entries) {
34
+ if (!entry.isDirectory()) continue;
35
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
36
+
37
+ const yamlPath = path.join(squadsDir, entry.name, "squad.yaml");
38
+ try {
39
+ const raw = await fsp.readFile(yamlPath, "utf-8");
40
+ const parsed = parseYaml(raw);
41
+ const s = parsed?.squad;
42
+ if (s) {
43
+ squads.push({
44
+ code: typeof s.code === "string" ? s.code : entry.name,
45
+ name: typeof s.name === "string" ? s.name : entry.name,
46
+ description: typeof s.description === "string" ? s.description : "",
47
+ icon: typeof s.icon === "string" ? s.icon : "\u{1F4CB}",
48
+ agents: Array.isArray(s.agents) ? (s.agents as unknown[]).filter((a): a is string => typeof a === "string") : [],
49
+ });
50
+ continue;
51
+ }
52
+ } catch {
53
+ // No squad.yaml or invalid YAML — fall through to default
54
+ }
55
+
56
+ squads.push({
57
+ code: entry.name,
58
+ name: entry.name,
59
+ description: "",
60
+ icon: "\u{1F4CB}",
61
+ agents: [],
62
+ });
63
+ }
64
+
65
+ return squads;
66
+ }
67
+
68
+ function isValidState(data: unknown): data is SquadState {
69
+ if (!data || typeof data !== "object") return false;
70
+ const d = data as Record<string, unknown>;
71
+ return (
72
+ typeof d.status === "string" &&
73
+ d.step != null && typeof d.step === "object" &&
74
+ Array.isArray(d.agents)
75
+ );
76
+ }
77
+
78
+ async function readActiveStates(squadsDir: string): Promise<Record<string, SquadState>> {
79
+ const states: Record<string, SquadState> = {};
80
+
81
+ let entries;
82
+ try {
83
+ entries = await fsp.readdir(squadsDir, { withFileTypes: true });
84
+ } catch {
85
+ return states;
86
+ }
87
+
88
+ for (const entry of entries) {
89
+ if (!entry.isDirectory()) continue;
90
+ const statePath = path.join(squadsDir, entry.name, "state.json");
91
+
92
+ try {
93
+ const raw = await fsp.readFile(statePath, "utf-8");
94
+ const parsed = JSON.parse(raw);
95
+ if (isValidState(parsed)) {
96
+ states[entry.name] = parsed;
97
+ }
98
+ } catch {
99
+ // Skip missing or invalid JSON
100
+ }
101
+ }
102
+
103
+ return states;
104
+ }
105
+
106
+ async function buildSnapshot(squadsDir: string): Promise<WsMessage> {
107
+ return {
108
+ type: "SNAPSHOT",
109
+ squads: await discoverSquads(squadsDir),
110
+ activeStates: await readActiveStates(squadsDir),
111
+ };
112
+ }
113
+
114
+ function broadcast(wss: WebSocketServer, msg: WsMessage) {
115
+ const data = JSON.stringify(msg);
116
+ for (const client of wss.clients) {
117
+ if (client.readyState === WebSocket.OPEN) {
118
+ try {
119
+ client.send(data);
120
+ } catch {
121
+ // Client connection dying — ws library will clean it up
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ export function squadWatcherPlugin(): Plugin {
128
+ return {
129
+ name: "squad-watcher",
130
+ configureServer(server: ViteDevServer) {
131
+ if (!server.httpServer) {
132
+ server.config.logger.warn("[squad-watcher] no httpServer — skipping");
133
+ return;
134
+ }
135
+
136
+ const squadsDir = resolveSquadsDir();
137
+ server.config.logger.info(`[squad-watcher] squads dir: ${squadsDir}`);
138
+
139
+ // Create WebSocket server with noServer to avoid intercepting Vite's HMR
140
+ const wss = new WebSocketServer({ noServer: true });
141
+ (server.httpServer as Server).on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
142
+ if (req.url === "/__squads_ws") {
143
+ wss.handleUpgrade(req, socket, head, (ws) => {
144
+ wss.emit("connection", ws, req);
145
+ });
146
+ }
147
+ // Let Vite handle all other upgrade requests (HMR)
148
+ });
149
+
150
+ // Send snapshot on new connection
151
+ wss.on("connection", async (ws) => {
152
+ try {
153
+ const snap = await buildSnapshot(squadsDir);
154
+ ws.send(JSON.stringify(snap));
155
+ } catch {
156
+ // Connection may have closed before snapshot was ready
157
+ }
158
+ });
159
+
160
+ // Ensure squads directory exists
161
+ fsp.mkdir(squadsDir, { recursive: true }).catch((err) => {
162
+ server.config.logger.error(`[squad-watcher] failed to create squads dir: ${err.message}`);
163
+ });
164
+
165
+ // REST API fallback — serves snapshot over HTTP for polling clients
166
+ server.middlewares.use(async (req, res, next) => {
167
+ if (req.url !== "/api/snapshot") return next();
168
+ try {
169
+ const snapshot = await buildSnapshot(squadsDir);
170
+ res.setHeader("Content-Type", "application/json");
171
+ res.setHeader("Cache-Control", "no-cache");
172
+ res.end(JSON.stringify(snapshot));
173
+ } catch {
174
+ res.writeHead(500);
175
+ res.end("Internal Server Error");
176
+ }
177
+ });
178
+
179
+ // REST API — reset agent status to idle after delivery animation
180
+ server.middlewares.use(async (req, res, next) => {
181
+ if (req.url !== "/api/reset-agent" || req.method !== "POST") return next();
182
+ let body = "";
183
+ req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
184
+ req.on("end", async () => {
185
+ try {
186
+ const { squad, agentId } = JSON.parse(body);
187
+ const statePath = path.join(squadsDir, squad, "state.json");
188
+ const raw = await fsp.readFile(statePath, "utf-8");
189
+ const state = JSON.parse(raw);
190
+ if (!isValidState(state)) { res.writeHead(400); res.end("Invalid state"); return; }
191
+ const agent = state.agents.find((a: { id: string }) => a.id === agentId);
192
+ if (agent) {
193
+ agent.status = "idle";
194
+ state.updatedAt = new Date().toISOString();
195
+ await fsp.writeFile(statePath, JSON.stringify(state, null, 2));
196
+ }
197
+ res.setHeader("Content-Type", "application/json");
198
+ res.end(JSON.stringify({ ok: true }));
199
+ } catch {
200
+ res.writeHead(500);
201
+ res.end("Error");
202
+ }
203
+ });
204
+ });
205
+
206
+ // File watcher using chokidar — reliable cross-platform, handles partial writes
207
+ const watcher = chokidarWatch(squadsDir, {
208
+ ignoreInitial: true,
209
+ awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 50 },
210
+ ignored: [/(^|[/\\])\./, /node_modules/, /output[/\\]/],
211
+ depth: 2,
212
+ });
213
+
214
+ function handleFileChange(filePath: string) {
215
+ const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
216
+ const parts = relative.split("/");
217
+ if (parts.length < 2) return;
218
+
219
+ const squadName = parts[0];
220
+ const fileName = parts[1];
221
+
222
+ if (fileName === "state.json") {
223
+ fsp.readFile(filePath, "utf-8").then((raw) => {
224
+ const parsed = JSON.parse(raw);
225
+ if (!isValidState(parsed)) return;
226
+ broadcast(wss, { type: "SQUAD_UPDATE", squad: squadName, state: parsed });
227
+ }).catch(() => {
228
+ // Invalid JSON — next change event will retry
229
+ });
230
+ } else if (fileName === "squad.yaml") {
231
+ buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
232
+ }
233
+ }
234
+
235
+ function handleFileRemoval(filePath: string) {
236
+ const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
237
+ const parts = relative.split("/");
238
+ if (parts.length < 2) return;
239
+
240
+ const squadName = parts[0];
241
+ const fileName = parts[1];
242
+
243
+ if (fileName === "state.json") {
244
+ broadcast(wss, { type: "SQUAD_INACTIVE", squad: squadName });
245
+ } else if (fileName === "squad.yaml") {
246
+ buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
247
+ }
248
+ }
249
+
250
+ watcher.on("add", handleFileChange);
251
+ watcher.on("change", handleFileChange);
252
+ watcher.on("unlink", handleFileRemoval);
253
+
254
+ server.httpServer.on("close", () => {
255
+ watcher.close();
256
+ });
257
+ },
258
+ };
259
+ }
260
+