agentshire 1.0.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 (406) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +437 -0
  4. package/README.zh-CN.md +441 -0
  5. package/ROADMAP.md +207 -0
  6. package/THIRD_PARTY_NOTICES.md +49 -0
  7. package/VISION.md +101 -0
  8. package/index.ts +386 -0
  9. package/openclaw.plugin.json +27 -0
  10. package/package.json +100 -0
  11. package/src/bridge/AGENTS.md +136 -0
  12. package/src/bridge/ActivityStream.ts +184 -0
  13. package/src/bridge/CitizenManager.ts +280 -0
  14. package/src/bridge/DirectorBridge.ts +1076 -0
  15. package/src/bridge/EventTranslator.ts +142 -0
  16. package/src/bridge/NpcEventQueue.ts +61 -0
  17. package/src/bridge/ReconnectManager.ts +42 -0
  18. package/src/bridge/RouteManager.ts +270 -0
  19. package/src/bridge/StateTracker.ts +100 -0
  20. package/src/bridge/ToolVfxMapper.ts +94 -0
  21. package/src/bridge/__tests__/ActivityStream.test.ts +156 -0
  22. package/src/bridge/__tests__/CitizenManager.test.ts +174 -0
  23. package/src/bridge/__tests__/EventTranslator.test.ts +292 -0
  24. package/src/bridge/__tests__/NpcEventQueue.test.ts +92 -0
  25. package/src/bridge/__tests__/RouteManager.test.ts +106 -0
  26. package/src/bridge/data/route-config.json +36 -0
  27. package/src/bridge/data/route-config.ts +19 -0
  28. package/src/bridge/implicit-chat.ts +192 -0
  29. package/src/bridge/index.ts +12 -0
  30. package/src/contracts/agent-state.ts +87 -0
  31. package/src/contracts/agui.ts +212 -0
  32. package/src/contracts/chat.ts +67 -0
  33. package/src/contracts/events.ts +170 -0
  34. package/src/contracts/index.ts +71 -0
  35. package/src/contracts/media.ts +77 -0
  36. package/src/contracts/registry.ts +32 -0
  37. package/src/plugin/AGENTS.md +192 -0
  38. package/src/plugin/__tests__/editor-serve-megapack.test.ts +150 -0
  39. package/src/plugin/__tests__/hook-translator.test.ts +197 -0
  40. package/src/plugin/auto-config.ts +131 -0
  41. package/src/plugin/channel.ts +399 -0
  42. package/src/plugin/chat-asset-resolver.ts +75 -0
  43. package/src/plugin/chat-session-watcher.ts +98 -0
  44. package/src/plugin/citizen-agent-manager.ts +178 -0
  45. package/src/plugin/citizen-chat-router.ts +83 -0
  46. package/src/plugin/citizen-workshop-manager.ts +96 -0
  47. package/src/plugin/custom-asset-manager.ts +250 -0
  48. package/src/plugin/editor-serve.ts +1368 -0
  49. package/src/plugin/group-discussion.ts +223 -0
  50. package/src/plugin/hook-translator.ts +202 -0
  51. package/src/plugin/llm-agent-proxy.ts +193 -0
  52. package/src/plugin/outbound-adapter.ts +187 -0
  53. package/src/plugin/paths.ts +33 -0
  54. package/src/plugin/plan-manager.ts +594 -0
  55. package/src/plugin/runtime.ts +9 -0
  56. package/src/plugin/session-history.ts +838 -0
  57. package/src/plugin/session-log-watcher.ts +221 -0
  58. package/src/plugin/soul-prompt-template.ts +290 -0
  59. package/src/plugin/subagent-tracker.ts +205 -0
  60. package/src/plugin/tools.ts +493 -0
  61. package/src/plugin/town-session.ts +40 -0
  62. package/src/plugin/ws-server.ts +680 -0
  63. package/src/town-souls.ts +134 -0
  64. package/town-frontend/dist/assets/CustomAssetStore-oi8aIurt.js +2 -0
  65. package/town-frontend/dist/assets/GLTFLoader-BA5RqSME.js +3804 -0
  66. package/town-frontend/dist/assets/OrbitControls-ZmySp9sQ.js +2 -0
  67. package/town-frontend/dist/assets/SettingsPanel-BO52reJe.js +2 -0
  68. package/town-frontend/dist/assets/SkeletonUtils-BCVmgslc.js +2 -0
  69. package/town-frontend/dist/assets/SkillLearnCard-Dk38iDpy.js +6 -0
  70. package/town-frontend/dist/assets/TopicSetupPanel-DLaLHB_Z.js +2 -0
  71. package/town-frontend/dist/assets/Trap-Bold-CT0JBE39.woff2 +0 -0
  72. package/town-frontend/dist/assets/Trap-SemiBold-R4_-Ld0j.woff2 +0 -0
  73. package/town-frontend/dist/assets/WeatherSystem-Cb3BvHes.js +550 -0
  74. package/town-frontend/dist/assets/avatars/char-female-a.webp +0 -0
  75. package/town-frontend/dist/assets/avatars/char-female-b.webp +0 -0
  76. package/town-frontend/dist/assets/avatars/char-female-c.webp +0 -0
  77. package/town-frontend/dist/assets/avatars/char-female-d.webp +0 -0
  78. package/town-frontend/dist/assets/avatars/char-female-e.webp +0 -0
  79. package/town-frontend/dist/assets/avatars/char-female-f.webp +0 -0
  80. package/town-frontend/dist/assets/avatars/char-male-a.webp +0 -0
  81. package/town-frontend/dist/assets/avatars/char-male-b.webp +0 -0
  82. package/town-frontend/dist/assets/avatars/char-male-c.webp +0 -0
  83. package/town-frontend/dist/assets/avatars/char-male-d.webp +0 -0
  84. package/town-frontend/dist/assets/avatars/char-male-e.webp +0 -0
  85. package/town-frontend/dist/assets/avatars/char-male-f.webp +0 -0
  86. package/town-frontend/dist/assets/avatars/char-pet-beaver.webp +0 -0
  87. package/town-frontend/dist/assets/avatars/char-pet-bee.webp +0 -0
  88. package/town-frontend/dist/assets/avatars/char-pet-bunny.webp +0 -0
  89. package/town-frontend/dist/assets/avatars/char-pet-cat.webp +0 -0
  90. package/town-frontend/dist/assets/avatars/char-pet-caterpillar.webp +0 -0
  91. package/town-frontend/dist/assets/avatars/char-pet-chick.webp +0 -0
  92. package/town-frontend/dist/assets/avatars/char-pet-cow.webp +0 -0
  93. package/town-frontend/dist/assets/avatars/char-pet-crab.webp +0 -0
  94. package/town-frontend/dist/assets/avatars/char-pet-deer.webp +0 -0
  95. package/town-frontend/dist/assets/avatars/char-pet-dog.webp +0 -0
  96. package/town-frontend/dist/assets/avatars/char-pet-elephant.webp +0 -0
  97. package/town-frontend/dist/assets/avatars/char-pet-fish.webp +0 -0
  98. package/town-frontend/dist/assets/avatars/char-pet-fox.webp +0 -0
  99. package/town-frontend/dist/assets/avatars/char-pet-giraffe.webp +0 -0
  100. package/town-frontend/dist/assets/avatars/char-pet-hog.webp +0 -0
  101. package/town-frontend/dist/assets/avatars/char-pet-koala.webp +0 -0
  102. package/town-frontend/dist/assets/avatars/char-pet-lion.webp +0 -0
  103. package/town-frontend/dist/assets/avatars/char-pet-monkey.webp +0 -0
  104. package/town-frontend/dist/assets/avatars/char-pet-panda.webp +0 -0
  105. package/town-frontend/dist/assets/avatars/char-pet-parrot.webp +0 -0
  106. package/town-frontend/dist/assets/avatars/char-pet-penguin.webp +0 -0
  107. package/town-frontend/dist/assets/avatars/char-pet-pig.webp +0 -0
  108. package/town-frontend/dist/assets/avatars/char-pet-polar.webp +0 -0
  109. package/town-frontend/dist/assets/avatars/char-pet-tiger.webp +0 -0
  110. package/town-frontend/dist/assets/character-catalog-DSmLtlNC.js +2 -0
  111. package/town-frontend/dist/assets/citizenEditor-DubGSJOQ.js +296 -0
  112. package/town-frontend/dist/assets/command-parser-BUd15Bmv.js +12 -0
  113. package/town-frontend/dist/assets/editor-B5QO0OtX.js +258 -0
  114. package/town-frontend/dist/assets/editor-Bk8g1NCD.css +1 -0
  115. package/town-frontend/dist/assets/index-BWfrufil.js +2 -0
  116. package/town-frontend/dist/assets/index-faS20RJk.js +6 -0
  117. package/town-frontend/dist/assets/logo-DJI6EtST.png +0 -0
  118. package/town-frontend/dist/assets/logo-title-AdKPZX5E.png +0 -0
  119. package/town-frontend/dist/assets/main-CqsN43aT.js +224 -0
  120. package/town-frontend/dist/assets/main-D7neuy3w.css +1 -0
  121. package/town-frontend/dist/assets/models/buildings/base.bin +0 -0
  122. package/town-frontend/dist/assets/models/buildings/base.gltf +136 -0
  123. package/town-frontend/dist/assets/models/buildings/bench.bin +0 -0
  124. package/town-frontend/dist/assets/models/buildings/bench.gltf +136 -0
  125. package/town-frontend/dist/assets/models/buildings/box_A.bin +0 -0
  126. package/town-frontend/dist/assets/models/buildings/box_A.gltf +136 -0
  127. package/town-frontend/dist/assets/models/buildings/box_B.bin +0 -0
  128. package/town-frontend/dist/assets/models/buildings/box_B.gltf +136 -0
  129. package/town-frontend/dist/assets/models/buildings/building_A.bin +0 -0
  130. package/town-frontend/dist/assets/models/buildings/building_A.gltf +136 -0
  131. package/town-frontend/dist/assets/models/buildings/building_A_withoutBase.bin +0 -0
  132. package/town-frontend/dist/assets/models/buildings/building_A_withoutBase.gltf +136 -0
  133. package/town-frontend/dist/assets/models/buildings/building_B.bin +0 -0
  134. package/town-frontend/dist/assets/models/buildings/building_B.gltf +136 -0
  135. package/town-frontend/dist/assets/models/buildings/building_B_withoutBase.bin +0 -0
  136. package/town-frontend/dist/assets/models/buildings/building_B_withoutBase.gltf +136 -0
  137. package/town-frontend/dist/assets/models/buildings/building_C.bin +0 -0
  138. package/town-frontend/dist/assets/models/buildings/building_C.gltf +136 -0
  139. package/town-frontend/dist/assets/models/buildings/building_C_withoutBase.bin +0 -0
  140. package/town-frontend/dist/assets/models/buildings/building_C_withoutBase.gltf +136 -0
  141. package/town-frontend/dist/assets/models/buildings/building_D.bin +0 -0
  142. package/town-frontend/dist/assets/models/buildings/building_D.gltf +136 -0
  143. package/town-frontend/dist/assets/models/buildings/building_D_withoutBase.bin +0 -0
  144. package/town-frontend/dist/assets/models/buildings/building_D_withoutBase.gltf +136 -0
  145. package/town-frontend/dist/assets/models/buildings/building_E.bin +0 -0
  146. package/town-frontend/dist/assets/models/buildings/building_E.gltf +136 -0
  147. package/town-frontend/dist/assets/models/buildings/building_E_withoutBase.bin +0 -0
  148. package/town-frontend/dist/assets/models/buildings/building_E_withoutBase.gltf +136 -0
  149. package/town-frontend/dist/assets/models/buildings/building_F.bin +0 -0
  150. package/town-frontend/dist/assets/models/buildings/building_F.gltf +136 -0
  151. package/town-frontend/dist/assets/models/buildings/building_F_withoutBase.bin +0 -0
  152. package/town-frontend/dist/assets/models/buildings/building_F_withoutBase.gltf +136 -0
  153. package/town-frontend/dist/assets/models/buildings/building_G.bin +0 -0
  154. package/town-frontend/dist/assets/models/buildings/building_G.gltf +136 -0
  155. package/town-frontend/dist/assets/models/buildings/building_G_withoutBase.bin +0 -0
  156. package/town-frontend/dist/assets/models/buildings/building_G_withoutBase.gltf +136 -0
  157. package/town-frontend/dist/assets/models/buildings/building_H.bin +0 -0
  158. package/town-frontend/dist/assets/models/buildings/building_H.gltf +136 -0
  159. package/town-frontend/dist/assets/models/buildings/building_H_withoutBase.bin +0 -0
  160. package/town-frontend/dist/assets/models/buildings/building_H_withoutBase.gltf +136 -0
  161. package/town-frontend/dist/assets/models/buildings/bush.bin +0 -0
  162. package/town-frontend/dist/assets/models/buildings/bush.gltf +136 -0
  163. package/town-frontend/dist/assets/models/buildings/car_hatchback.bin +0 -0
  164. package/town-frontend/dist/assets/models/buildings/car_hatchback.gltf +442 -0
  165. package/town-frontend/dist/assets/models/buildings/car_police.bin +0 -0
  166. package/town-frontend/dist/assets/models/buildings/car_police.gltf +442 -0
  167. package/town-frontend/dist/assets/models/buildings/car_sedan.bin +0 -0
  168. package/town-frontend/dist/assets/models/buildings/car_sedan.gltf +442 -0
  169. package/town-frontend/dist/assets/models/buildings/car_stationwagon.bin +0 -0
  170. package/town-frontend/dist/assets/models/buildings/car_stationwagon.gltf +442 -0
  171. package/town-frontend/dist/assets/models/buildings/car_taxi.bin +0 -0
  172. package/town-frontend/dist/assets/models/buildings/car_taxi.gltf +442 -0
  173. package/town-frontend/dist/assets/models/buildings/citybits_texture.png +0 -0
  174. package/town-frontend/dist/assets/models/buildings/dumpster.bin +0 -0
  175. package/town-frontend/dist/assets/models/buildings/dumpster.gltf +136 -0
  176. package/town-frontend/dist/assets/models/buildings/firehydrant.bin +0 -0
  177. package/town-frontend/dist/assets/models/buildings/firehydrant.gltf +136 -0
  178. package/town-frontend/dist/assets/models/buildings/road_corner.bin +0 -0
  179. package/town-frontend/dist/assets/models/buildings/road_corner.gltf +136 -0
  180. package/town-frontend/dist/assets/models/buildings/road_corner_curved.bin +0 -0
  181. package/town-frontend/dist/assets/models/buildings/road_corner_curved.gltf +136 -0
  182. package/town-frontend/dist/assets/models/buildings/road_junction.bin +0 -0
  183. package/town-frontend/dist/assets/models/buildings/road_junction.gltf +136 -0
  184. package/town-frontend/dist/assets/models/buildings/road_straight.bin +0 -0
  185. package/town-frontend/dist/assets/models/buildings/road_straight.gltf +136 -0
  186. package/town-frontend/dist/assets/models/buildings/road_straight_crossing.bin +0 -0
  187. package/town-frontend/dist/assets/models/buildings/road_straight_crossing.gltf +136 -0
  188. package/town-frontend/dist/assets/models/buildings/road_tsplit.bin +0 -0
  189. package/town-frontend/dist/assets/models/buildings/road_tsplit.gltf +136 -0
  190. package/town-frontend/dist/assets/models/buildings/streetlight.bin +0 -0
  191. package/town-frontend/dist/assets/models/buildings/streetlight.gltf +136 -0
  192. package/town-frontend/dist/assets/models/buildings/trafficlight_A.bin +0 -0
  193. package/town-frontend/dist/assets/models/buildings/trafficlight_A.gltf +136 -0
  194. package/town-frontend/dist/assets/models/buildings/trafficlight_B.bin +0 -0
  195. package/town-frontend/dist/assets/models/buildings/trafficlight_B.gltf +136 -0
  196. package/town-frontend/dist/assets/models/buildings/trafficlight_C.bin +0 -0
  197. package/town-frontend/dist/assets/models/buildings/trafficlight_C.gltf +136 -0
  198. package/town-frontend/dist/assets/models/buildings/trash_A.bin +0 -0
  199. package/town-frontend/dist/assets/models/buildings/trash_A.gltf +136 -0
  200. package/town-frontend/dist/assets/models/buildings/trash_B.bin +0 -0
  201. package/town-frontend/dist/assets/models/buildings/trash_B.gltf +136 -0
  202. package/town-frontend/dist/assets/models/buildings/watertower.bin +0 -0
  203. package/town-frontend/dist/assets/models/buildings/watertower.gltf +136 -0
  204. package/town-frontend/dist/assets/models/characters/Textures/colormap.png +0 -0
  205. package/town-frontend/dist/assets/models/characters/character-female-a.glb +0 -0
  206. package/town-frontend/dist/assets/models/characters/character-female-b.glb +0 -0
  207. package/town-frontend/dist/assets/models/characters/character-female-c.glb +0 -0
  208. package/town-frontend/dist/assets/models/characters/character-female-d.glb +0 -0
  209. package/town-frontend/dist/assets/models/characters/character-female-e.glb +0 -0
  210. package/town-frontend/dist/assets/models/characters/character-female-f.glb +0 -0
  211. package/town-frontend/dist/assets/models/characters/character-male-a.glb +0 -0
  212. package/town-frontend/dist/assets/models/characters/character-male-b.glb +0 -0
  213. package/town-frontend/dist/assets/models/characters/character-male-c.glb +0 -0
  214. package/town-frontend/dist/assets/models/characters/character-male-d.glb +0 -0
  215. package/town-frontend/dist/assets/models/characters/character-male-e.glb +0 -0
  216. package/town-frontend/dist/assets/models/characters/character-male-f.glb +0 -0
  217. package/town-frontend/dist/assets/models/characters/character-pet-beaver.glb +0 -0
  218. package/town-frontend/dist/assets/models/characters/character-pet-bee.glb +0 -0
  219. package/town-frontend/dist/assets/models/characters/character-pet-bunny.glb +0 -0
  220. package/town-frontend/dist/assets/models/characters/character-pet-cat.glb +0 -0
  221. package/town-frontend/dist/assets/models/characters/character-pet-caterpillar.glb +0 -0
  222. package/town-frontend/dist/assets/models/characters/character-pet-chick.glb +0 -0
  223. package/town-frontend/dist/assets/models/characters/character-pet-cow.glb +0 -0
  224. package/town-frontend/dist/assets/models/characters/character-pet-crab.glb +0 -0
  225. package/town-frontend/dist/assets/models/characters/character-pet-deer.glb +0 -0
  226. package/town-frontend/dist/assets/models/characters/character-pet-dog.glb +0 -0
  227. package/town-frontend/dist/assets/models/characters/character-pet-elephant.glb +0 -0
  228. package/town-frontend/dist/assets/models/characters/character-pet-fish.glb +0 -0
  229. package/town-frontend/dist/assets/models/characters/character-pet-fox.glb +0 -0
  230. package/town-frontend/dist/assets/models/characters/character-pet-giraffe.glb +0 -0
  231. package/town-frontend/dist/assets/models/characters/character-pet-hog.glb +0 -0
  232. package/town-frontend/dist/assets/models/characters/character-pet-koala.glb +0 -0
  233. package/town-frontend/dist/assets/models/characters/character-pet-lion.glb +0 -0
  234. package/town-frontend/dist/assets/models/characters/character-pet-monkey.glb +0 -0
  235. package/town-frontend/dist/assets/models/characters/character-pet-panda.glb +0 -0
  236. package/town-frontend/dist/assets/models/characters/character-pet-parrot.glb +0 -0
  237. package/town-frontend/dist/assets/models/characters/character-pet-penguin.glb +0 -0
  238. package/town-frontend/dist/assets/models/characters/character-pet-pig.glb +0 -0
  239. package/town-frontend/dist/assets/models/characters/character-pet-polar.glb +0 -0
  240. package/town-frontend/dist/assets/models/characters/character-pet-tiger.glb +0 -0
  241. package/town-frontend/dist/assets/models/characters/colormap.png +0 -0
  242. package/town-frontend/dist/assets/models/furniture/armchair.bin +0 -0
  243. package/town-frontend/dist/assets/models/furniture/armchair.gltf +136 -0
  244. package/town-frontend/dist/assets/models/furniture/armchair_pillows.bin +0 -0
  245. package/town-frontend/dist/assets/models/furniture/armchair_pillows.gltf +136 -0
  246. package/town-frontend/dist/assets/models/furniture/bed_double_A.bin +0 -0
  247. package/town-frontend/dist/assets/models/furniture/bed_double_A.gltf +136 -0
  248. package/town-frontend/dist/assets/models/furniture/bed_double_B.bin +0 -0
  249. package/town-frontend/dist/assets/models/furniture/bed_double_B.gltf +136 -0
  250. package/town-frontend/dist/assets/models/furniture/bed_single_A.bin +0 -0
  251. package/town-frontend/dist/assets/models/furniture/bed_single_A.gltf +136 -0
  252. package/town-frontend/dist/assets/models/furniture/bed_single_B.bin +0 -0
  253. package/town-frontend/dist/assets/models/furniture/bed_single_B.gltf +136 -0
  254. package/town-frontend/dist/assets/models/furniture/book_set.bin +0 -0
  255. package/town-frontend/dist/assets/models/furniture/book_set.gltf +136 -0
  256. package/town-frontend/dist/assets/models/furniture/book_single.bin +0 -0
  257. package/town-frontend/dist/assets/models/furniture/book_single.gltf +136 -0
  258. package/town-frontend/dist/assets/models/furniture/cabinet_medium.bin +0 -0
  259. package/town-frontend/dist/assets/models/furniture/cabinet_medium.gltf +136 -0
  260. package/town-frontend/dist/assets/models/furniture/cabinet_medium_decorated.bin +0 -0
  261. package/town-frontend/dist/assets/models/furniture/cabinet_medium_decorated.gltf +136 -0
  262. package/town-frontend/dist/assets/models/furniture/cabinet_small.bin +0 -0
  263. package/town-frontend/dist/assets/models/furniture/cabinet_small.gltf +136 -0
  264. package/town-frontend/dist/assets/models/furniture/cabinet_small_decorated.bin +0 -0
  265. package/town-frontend/dist/assets/models/furniture/cabinet_small_decorated.gltf +136 -0
  266. package/town-frontend/dist/assets/models/furniture/cactus_medium_A.bin +0 -0
  267. package/town-frontend/dist/assets/models/furniture/cactus_medium_A.gltf +136 -0
  268. package/town-frontend/dist/assets/models/furniture/cactus_medium_B.bin +0 -0
  269. package/town-frontend/dist/assets/models/furniture/cactus_medium_B.gltf +136 -0
  270. package/town-frontend/dist/assets/models/furniture/cactus_small_A.bin +0 -0
  271. package/town-frontend/dist/assets/models/furniture/cactus_small_A.gltf +136 -0
  272. package/town-frontend/dist/assets/models/furniture/cactus_small_B.bin +0 -0
  273. package/town-frontend/dist/assets/models/furniture/cactus_small_B.gltf +136 -0
  274. package/town-frontend/dist/assets/models/furniture/chair_A.bin +0 -0
  275. package/town-frontend/dist/assets/models/furniture/chair_A.gltf +136 -0
  276. package/town-frontend/dist/assets/models/furniture/chair_A_wood.bin +0 -0
  277. package/town-frontend/dist/assets/models/furniture/chair_A_wood.gltf +136 -0
  278. package/town-frontend/dist/assets/models/furniture/chair_B.bin +0 -0
  279. package/town-frontend/dist/assets/models/furniture/chair_B.gltf +136 -0
  280. package/town-frontend/dist/assets/models/furniture/chair_B_wood.bin +0 -0
  281. package/town-frontend/dist/assets/models/furniture/chair_B_wood.gltf +136 -0
  282. package/town-frontend/dist/assets/models/furniture/chair_C.bin +0 -0
  283. package/town-frontend/dist/assets/models/furniture/chair_C.gltf +136 -0
  284. package/town-frontend/dist/assets/models/furniture/chair_stool.bin +0 -0
  285. package/town-frontend/dist/assets/models/furniture/chair_stool.gltf +136 -0
  286. package/town-frontend/dist/assets/models/furniture/chair_stool_wood.bin +0 -0
  287. package/town-frontend/dist/assets/models/furniture/chair_stool_wood.gltf +136 -0
  288. package/town-frontend/dist/assets/models/furniture/couch.bin +0 -0
  289. package/town-frontend/dist/assets/models/furniture/couch.gltf +136 -0
  290. package/town-frontend/dist/assets/models/furniture/couch_pillows.bin +0 -0
  291. package/town-frontend/dist/assets/models/furniture/couch_pillows.gltf +136 -0
  292. package/town-frontend/dist/assets/models/furniture/furniturebits_texture.png +0 -0
  293. package/town-frontend/dist/assets/models/furniture/lamp_standing.bin +0 -0
  294. package/town-frontend/dist/assets/models/furniture/lamp_standing.gltf +136 -0
  295. package/town-frontend/dist/assets/models/furniture/lamp_table.bin +0 -0
  296. package/town-frontend/dist/assets/models/furniture/lamp_table.gltf +136 -0
  297. package/town-frontend/dist/assets/models/furniture/pictureframe_large_A.bin +0 -0
  298. package/town-frontend/dist/assets/models/furniture/pictureframe_large_A.gltf +136 -0
  299. package/town-frontend/dist/assets/models/furniture/pictureframe_large_B.bin +0 -0
  300. package/town-frontend/dist/assets/models/furniture/pictureframe_large_B.gltf +136 -0
  301. package/town-frontend/dist/assets/models/furniture/pictureframe_medium.bin +0 -0
  302. package/town-frontend/dist/assets/models/furniture/pictureframe_medium.gltf +136 -0
  303. package/town-frontend/dist/assets/models/furniture/pictureframe_small_A.bin +0 -0
  304. package/town-frontend/dist/assets/models/furniture/pictureframe_small_A.gltf +136 -0
  305. package/town-frontend/dist/assets/models/furniture/pictureframe_small_B.bin +0 -0
  306. package/town-frontend/dist/assets/models/furniture/pictureframe_small_B.gltf +136 -0
  307. package/town-frontend/dist/assets/models/furniture/pictureframe_small_C.bin +0 -0
  308. package/town-frontend/dist/assets/models/furniture/pictureframe_small_C.gltf +136 -0
  309. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_A.bin +0 -0
  310. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_A.gltf +136 -0
  311. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_B.bin +0 -0
  312. package/town-frontend/dist/assets/models/furniture/pictureframe_standing_B.gltf +136 -0
  313. package/town-frontend/dist/assets/models/furniture/pillow_A.bin +0 -0
  314. package/town-frontend/dist/assets/models/furniture/pillow_A.gltf +136 -0
  315. package/town-frontend/dist/assets/models/furniture/pillow_B.bin +0 -0
  316. package/town-frontend/dist/assets/models/furniture/pillow_B.gltf +136 -0
  317. package/town-frontend/dist/assets/models/furniture/rug_oval_A.bin +0 -0
  318. package/town-frontend/dist/assets/models/furniture/rug_oval_A.gltf +136 -0
  319. package/town-frontend/dist/assets/models/furniture/rug_oval_B.bin +0 -0
  320. package/town-frontend/dist/assets/models/furniture/rug_oval_B.gltf +136 -0
  321. package/town-frontend/dist/assets/models/furniture/rug_rectangle_A.bin +0 -0
  322. package/town-frontend/dist/assets/models/furniture/rug_rectangle_A.gltf +136 -0
  323. package/town-frontend/dist/assets/models/furniture/rug_rectangle_B.bin +0 -0
  324. package/town-frontend/dist/assets/models/furniture/rug_rectangle_B.gltf +136 -0
  325. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_A.bin +0 -0
  326. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_A.gltf +136 -0
  327. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_B.bin +0 -0
  328. package/town-frontend/dist/assets/models/furniture/rug_rectangle_stripes_B.gltf +136 -0
  329. package/town-frontend/dist/assets/models/furniture/shelf_A_big.bin +0 -0
  330. package/town-frontend/dist/assets/models/furniture/shelf_A_big.gltf +136 -0
  331. package/town-frontend/dist/assets/models/furniture/shelf_A_small.bin +0 -0
  332. package/town-frontend/dist/assets/models/furniture/shelf_A_small.gltf +136 -0
  333. package/town-frontend/dist/assets/models/furniture/shelf_B_large.bin +0 -0
  334. package/town-frontend/dist/assets/models/furniture/shelf_B_large.gltf +136 -0
  335. package/town-frontend/dist/assets/models/furniture/shelf_B_large_decorated.bin +0 -0
  336. package/town-frontend/dist/assets/models/furniture/shelf_B_large_decorated.gltf +136 -0
  337. package/town-frontend/dist/assets/models/furniture/shelf_B_small.bin +0 -0
  338. package/town-frontend/dist/assets/models/furniture/shelf_B_small.gltf +136 -0
  339. package/town-frontend/dist/assets/models/furniture/shelf_B_small_decorated.bin +0 -0
  340. package/town-frontend/dist/assets/models/furniture/shelf_B_small_decorated.gltf +136 -0
  341. package/town-frontend/dist/assets/models/furniture/table_low.bin +0 -0
  342. package/town-frontend/dist/assets/models/furniture/table_low.gltf +136 -0
  343. package/town-frontend/dist/assets/models/furniture/table_medium.bin +0 -0
  344. package/town-frontend/dist/assets/models/furniture/table_medium.gltf +136 -0
  345. package/town-frontend/dist/assets/models/furniture/table_medium_long.bin +0 -0
  346. package/town-frontend/dist/assets/models/furniture/table_medium_long.gltf +136 -0
  347. package/town-frontend/dist/assets/models/furniture/table_small.bin +0 -0
  348. package/town-frontend/dist/assets/models/furniture/table_small.gltf +136 -0
  349. package/town-frontend/dist/assets/models/props/bench.bin +0 -0
  350. package/town-frontend/dist/assets/models/props/bench.gltf +136 -0
  351. package/town-frontend/dist/assets/models/props/bush.bin +0 -0
  352. package/town-frontend/dist/assets/models/props/bush.gltf +136 -0
  353. package/town-frontend/dist/assets/models/props/capybara.glb +0 -0
  354. package/town-frontend/dist/assets/models/props/car_hatchback.bin +0 -0
  355. package/town-frontend/dist/assets/models/props/car_hatchback.gltf +442 -0
  356. package/town-frontend/dist/assets/models/props/car_sedan.bin +0 -0
  357. package/town-frontend/dist/assets/models/props/car_sedan.gltf +442 -0
  358. package/town-frontend/dist/assets/models/props/car_taxi.bin +0 -0
  359. package/town-frontend/dist/assets/models/props/car_taxi.gltf +442 -0
  360. package/town-frontend/dist/assets/models/props/citybits_texture.png +0 -0
  361. package/town-frontend/dist/assets/models/props/dumpster.bin +0 -0
  362. package/town-frontend/dist/assets/models/props/dumpster.gltf +136 -0
  363. package/town-frontend/dist/assets/models/props/firehydrant.bin +0 -0
  364. package/town-frontend/dist/assets/models/props/firehydrant.gltf +136 -0
  365. package/town-frontend/dist/assets/models/props/streetlight.bin +0 -0
  366. package/town-frontend/dist/assets/models/props/streetlight.gltf +136 -0
  367. package/town-frontend/dist/assets/models/props/trafficlight_A.bin +0 -0
  368. package/town-frontend/dist/assets/models/props/trafficlight_A.gltf +136 -0
  369. package/town-frontend/dist/assets/models/props/trash_A.bin +0 -0
  370. package/town-frontend/dist/assets/models/props/trash_A.gltf +136 -0
  371. package/town-frontend/dist/assets/models/props/watertower.bin +0 -0
  372. package/town-frontend/dist/assets/models/props/watertower.gltf +136 -0
  373. package/town-frontend/dist/assets/models/stage-deco/Flowers_1_D.glb +0 -0
  374. package/town-frontend/dist/assets/models/stage-deco/Flowers_2_B.glb +0 -0
  375. package/town-frontend/dist/assets/models/stage-deco/Grass_A_1.glb +0 -0
  376. package/town-frontend/dist/assets/models/stage-deco/Park_GrassHill_A.glb +0 -0
  377. package/town-frontend/dist/assets/models/stage-deco/Pebles_1_A_2.glb +0 -0
  378. package/town-frontend/dist/assets/music/bgm_day.mp3 +0 -0
  379. package/town-frontend/dist/assets/music/bgm_dusk.mp3 +0 -0
  380. package/town-frontend/dist/assets/music/bgm_night.mp3 +0 -0
  381. package/town-frontend/dist/assets/music/bgm_work.mp3 +0 -0
  382. package/town-frontend/dist/assets/office-whiteboard-idle-CyEwBrq_.webp +0 -0
  383. package/town-frontend/dist/assets/preview-B6hYEQij.js +2 -0
  384. package/town-frontend/dist/assets/town-BKesnERP.js +8888 -0
  385. package/town-frontend/dist/assets/town-BnswKsjF.css +1 -0
  386. package/town-frontend/dist/citizen-editor.html +166 -0
  387. package/town-frontend/dist/editor.html +227 -0
  388. package/town-frontend/dist/index.html +22 -0
  389. package/town-frontend/dist/preview.html +56 -0
  390. package/town-frontend/dist/town.html +165 -0
  391. package/town-frontend/dist/viewer.html +91 -0
  392. package/town-souls/CHEN.md +130 -0
  393. package/town-souls/CHENGZI.md +92 -0
  394. package/town-souls/CITIZEN_tpl.md +16 -0
  395. package/town-souls/DIANDIAN.md +92 -0
  396. package/town-souls/HAITANG.md +94 -0
  397. package/town-souls/QIQI.md +119 -0
  398. package/town-souls/SOUL.md +141 -0
  399. package/town-souls/SOUL_tpl.md +135 -0
  400. package/town-souls/XIAOLIE.md +107 -0
  401. package/town-souls/YAN.md +107 -0
  402. package/town-workspace/IDENTITY.md +5 -0
  403. package/town-workspace/SOUL.md +141 -0
  404. package/town-workspace/project-workflow.md +81 -0
  405. package/town-workspace/town-defaults.json +92 -0
  406. package/town-workspace/town-guide.md +45 -0
@@ -0,0 +1,94 @@
1
+ // @desc Pure functions mapping tool names to VFX events, emoji, and NPC animation phases
2
+ import type { GameEvent } from '../../town-frontend/src/data/GameProtocol.js'
3
+
4
+ const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'])
5
+ const VIDEO_EXTENSIONS = new Set(['mp4', 'webm', 'mov', 'm4v'])
6
+ const AUDIO_EXTENSIONS = new Set(['mp3', 'wav', 'ogg', 'm4a', 'aac', 'flac'])
7
+
8
+ const CARD_TYPES: Record<string, string> = {
9
+ game: 'game', app: 'app', website: 'website',
10
+ }
11
+
12
+ /** Extract a file path from tool input args (supports read/write/edit_file and bash commands) */
13
+ export function extractFilePath(toolName: string, input: Record<string, unknown>): string | null {
14
+ const FILE_TOOLS = new Set(['read_file', 'write_file', 'edit_file'])
15
+ if (FILE_TOOLS.has(toolName)) return (input.path ?? input.file) as string | null
16
+ if (toolName === 'bash') {
17
+ const cmd = String(input.command ?? '')
18
+ const m = cmd.match(/(?:cat|head|tail|vi|vim|nano|code)\s+["']?([^\s"']+)/)
19
+ return m ? m[1] : null
20
+ }
21
+ return null
22
+ }
23
+
24
+ /** Check if a file path refers to a persona directory */
25
+ export function isPersonaPath(filePath: string | null): boolean {
26
+ if (!filePath) return false
27
+ return /\bpersonas?\b/i.test(filePath)
28
+ }
29
+
30
+ /** Return the emoji icon for a given tool name */
31
+ export function toolEmoji(toolName: string): string {
32
+ if (['write_file', 'edit_file'].includes(toolName)) return '💻'
33
+ if (toolName === 'bash') return '⚡'
34
+ if (['read_file', 'grep', 'glob'].includes(toolName)) return '🔍'
35
+ if (toolName === 'web_search' || toolName === 'web_fetch') return '🌐'
36
+ if (toolName === 'skill') return '⚡'
37
+ return '🔧'
38
+ }
39
+
40
+ const MANAGEMENT_TOOLS = new Set([
41
+ 'sessions_spawn', 'sessions_yield', 'next_step', 'create_plan',
42
+ 'project_complete', 'register_project', 'spawn_agent',
43
+ ])
44
+
45
+ /** Generate GameEvents for NPC animation, emoji, and VFX based on which tool is being used */
46
+ export function toolToVfxEvents(toolName: string, npcId: string, input?: Record<string, unknown>): { events: GameEvent[], phase: string } {
47
+ const events: GameEvent[] = []
48
+ const filePath = extractFilePath(toolName, input ?? {})
49
+ const fileName = filePath?.split('/').pop() ?? ''
50
+ const isPersona = isPersonaPath(filePath)
51
+
52
+ if (MANAGEMENT_TOOLS.has(toolName)) {
53
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
54
+ return { events, phase: 'thinking' }
55
+ }
56
+
57
+ if (['read_file', 'grep', 'glob'].includes(toolName)) {
58
+ if (isPersona) {
59
+ return { events, phase: 'documenting' }
60
+ }
61
+ events.push({ type: 'npc_anim', npcId, anim: 'reading' })
62
+ if (fileName) events.push({ type: 'fx', effect: 'fileIcon', params: { npcId, fileName } })
63
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
64
+ } else if (['write_file', 'edit_file'].includes(toolName)) {
65
+ if (isPersona) {
66
+ return { events, phase: 'documenting' }
67
+ }
68
+ events.push({ type: 'npc_anim', npcId, anim: 'typing' })
69
+ if (fileName) events.push({ type: 'fx', effect: 'fileIcon', params: { npcId, fileName } })
70
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
71
+ } else if (toolName === 'bash') {
72
+ events.push({ type: 'npc_anim', npcId, anim: 'typing' })
73
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
74
+ } else if (toolName === 'web_search' || toolName === 'web_fetch') {
75
+ events.push({ type: 'npc_anim', npcId, anim: 'reading' })
76
+ events.push({ type: 'fx', effect: 'searchRadar', params: { npcId } })
77
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
78
+ } else {
79
+ events.push({ type: 'npc_emoji', npcId, emoji: toolEmoji(toolName) })
80
+ }
81
+ return { events, phase: 'working' }
82
+ }
83
+
84
+ /** Infer the deliverable card type (image/video/audio/file) from a file extension */
85
+ export function inferDeliverableCardType(filePath: string): 'image' | 'video' | 'audio' | 'file' {
86
+ const normalized = filePath.toLowerCase()
87
+ const extension = normalized.split('.').pop() ?? ''
88
+ if (IMAGE_EXTENSIONS.has(extension)) return 'image'
89
+ if (VIDEO_EXTENSIONS.has(extension)) return 'video'
90
+ if (AUDIO_EXTENSIONS.has(extension)) return 'audio'
91
+ return 'file'
92
+ }
93
+
94
+ export { CARD_TYPES }
@@ -0,0 +1,156 @@
1
+ // @desc Tests for ActivityStream: thinking buffer and activity log
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
3
+ import { ActivityStream } from '../ActivityStream.js'
4
+
5
+ describe('ActivityStream', () => {
6
+ let emitFn: ReturnType<typeof vi.fn>
7
+ let stream: ActivityStream
8
+
9
+ beforeEach(() => {
10
+ vi.useFakeTimers()
11
+ emitFn = vi.fn()
12
+ stream = new ActivityStream(emitFn)
13
+ })
14
+
15
+ afterEach(() => {
16
+ vi.useRealTimers()
17
+ })
18
+
19
+ describe('emitActivity', () => {
20
+ it('emits npc_activity event with correct fields', () => {
21
+ stream.emitActivity('npc1', 'wrench', '执行命令')
22
+ expect(emitFn).toHaveBeenCalledWith([
23
+ expect.objectContaining({
24
+ type: 'npc_activity',
25
+ npcId: 'npc1',
26
+ icon: 'wrench',
27
+ message: '执行命令',
28
+ time: expect.any(String),
29
+ }),
30
+ ])
31
+ })
32
+ })
33
+
34
+ describe('emitActivityStatus', () => {
35
+ it('marks last activity entry as success', () => {
36
+ stream.emitActivity('npc1', 'wrench', 'task')
37
+ emitFn.mockClear()
38
+
39
+ stream.emitActivityStatus('npc1', true)
40
+ expect(emitFn).toHaveBeenCalledWith([
41
+ expect.objectContaining({
42
+ type: 'npc_activity_status',
43
+ npcId: 'npc1',
44
+ success: true,
45
+ }),
46
+ ])
47
+
48
+ const replay = stream.getActivityReplayEvents()
49
+ const activityEntry = replay.find(
50
+ (e: any) => e.type === 'npc_activity' && e.npcId === 'npc1',
51
+ )
52
+ expect(activityEntry.status).toBe(true)
53
+ })
54
+
55
+ it('marks last activity entry as failure', () => {
56
+ stream.emitActivity('npc1', 'wrench', 'task')
57
+ stream.emitActivityStatus('npc1', false)
58
+
59
+ const replay = stream.getActivityReplayEvents()
60
+ const activityEntry = replay.find(
61
+ (e: any) => e.type === 'npc_activity' && e.npcId === 'npc1',
62
+ )
63
+ expect(activityEntry.status).toBe(false)
64
+ })
65
+ })
66
+
67
+ describe('appendThinkingDelta + flushThinking', () => {
68
+ it('buffers text and flushes on interval', () => {
69
+ stream.appendThinkingDelta('npc1', 'hello ')
70
+ stream.appendThinkingDelta('npc1', 'world')
71
+
72
+ // startThinkingStream emits initial activity
73
+ expect(emitFn).toHaveBeenCalledWith([
74
+ expect.objectContaining({ type: 'npc_activity', npcId: 'npc1', icon: 'brain' }),
75
+ ])
76
+
77
+ emitFn.mockClear()
78
+ vi.advanceTimersByTime(500)
79
+
80
+ expect(emitFn).toHaveBeenCalledWith([
81
+ expect.objectContaining({
82
+ type: 'npc_activity_stream',
83
+ npcId: 'npc1',
84
+ delta: 'hello world',
85
+ }),
86
+ ])
87
+ })
88
+
89
+ it('flushThinking emits remaining buffer and stream_end', () => {
90
+ stream.appendThinkingDelta('npc1', 'remaining')
91
+ emitFn.mockClear()
92
+
93
+ stream.flushThinking('npc1')
94
+
95
+ expect(emitFn).toHaveBeenCalledWith([
96
+ expect.objectContaining({ type: 'npc_activity_stream', delta: 'remaining' }),
97
+ ])
98
+ expect(emitFn).toHaveBeenCalledWith([
99
+ expect.objectContaining({ type: 'npc_activity_stream_end', npcId: 'npc1' }),
100
+ ])
101
+ })
102
+ })
103
+
104
+ describe('getActivityReplayEvents', () => {
105
+ it('returns cached events as a copy', () => {
106
+ stream.emitActivity('npc1', 'wrench', 'a')
107
+ stream.emitActivity('npc1', 'wrench', 'b')
108
+
109
+ const replay = stream.getActivityReplayEvents()
110
+ expect(replay).toHaveLength(2)
111
+ expect(replay[0].message).toBe('a')
112
+ expect(replay[1].message).toBe('b')
113
+
114
+ replay.push({ type: 'fake' })
115
+ expect(stream.getActivityReplayEvents()).toHaveLength(2)
116
+ })
117
+ })
118
+
119
+ describe('activity log cap', () => {
120
+ it('does not exceed MAX_ACTIVITY_LOG (500)', () => {
121
+ for (let i = 0; i < 520; i++) {
122
+ stream.emitActivity('npc1', 'wrench', `msg-${i}`)
123
+ }
124
+ const replay = stream.getActivityReplayEvents()
125
+ expect(replay.length).toBeLessThanOrEqual(500)
126
+ })
127
+ })
128
+
129
+ describe('emitTodoActivity', () => {
130
+ it('emits npc_activity_todo event with todos', () => {
131
+ stream.emitTodoActivity('npc1', {
132
+ todos: [
133
+ { id: 1, content: 'task one', status: 'pending' },
134
+ { id: 2, content: 'task two', status: 'completed' },
135
+ ],
136
+ })
137
+
138
+ expect(emitFn).toHaveBeenCalledWith([
139
+ expect.objectContaining({
140
+ type: 'npc_activity_todo',
141
+ npcId: 'npc1',
142
+ todos: [
143
+ { id: 1, content: 'task one', status: 'pending' },
144
+ { id: 2, content: 'task two', status: 'completed' },
145
+ ],
146
+ }),
147
+ ])
148
+ })
149
+
150
+ it('does not emit when todos is missing or not an array', () => {
151
+ stream.emitTodoActivity('npc1', {})
152
+ stream.emitTodoActivity('npc1', { todos: 'invalid' })
153
+ expect(emitFn).not.toHaveBeenCalled()
154
+ })
155
+ })
156
+ })
@@ -0,0 +1,174 @@
1
+ // @desc Tests for CitizenManager: citizen detection, name matching, lifecycle
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
3
+ import { CitizenManager, type CitizenManagerDeps } from '../CitizenManager.js'
4
+
5
+ vi.mock('../../town-frontend/src/data/CharacterRoster.js', () => ({
6
+ getCharacterKeyForNpc: (id: string) => `char_${id}`,
7
+ }))
8
+
9
+ function createMockDeps(overrides: Partial<CitizenManagerDeps> = {}): CitizenManagerDeps {
10
+ return {
11
+ emit: vi.fn(),
12
+ routes: {
13
+ getGreetPoint: vi.fn(() => ({ x: 17, z: 15 })),
14
+ moveNpcAndWait: vi.fn(async () => 'arrived' as const),
15
+ chooseCitizenDestination: vi.fn(() => ({ id: 'dest_a', x: 30, z: 20, score: 5 })),
16
+ claimDestinationForNpc: vi.fn(),
17
+ releaseDestinationClaim: vi.fn(),
18
+ releaseDestinationClaimLater: vi.fn(),
19
+ distance2D: vi.fn(() => 10),
20
+ } as any,
21
+ getConfig: vi.fn(() => ({
22
+ citizens: [
23
+ { id: 'citizen_alice', name: 'Alice' },
24
+ { id: 'citizen_bob', name: 'Bob' },
25
+ ],
26
+ })),
27
+ getCharacterAssignments: vi.fn(() => new Map()),
28
+ isStewardConfirmed: vi.fn(() => true),
29
+ getPersonaName: vi.fn(() => 'Steward'),
30
+ getStewardName: vi.fn(() => 'steward'),
31
+ onPersonaChanged: vi.fn(),
32
+ delayMs: vi.fn(async () => {}),
33
+ scheduleDelayedEmit: vi.fn(),
34
+ getLastToolInput: vi.fn(() => ({})),
35
+ ...overrides,
36
+ }
37
+ }
38
+
39
+ describe('CitizenManager', () => {
40
+ let deps: CitizenManagerDeps
41
+ let cm: CitizenManager
42
+
43
+ beforeEach(() => {
44
+ deps = createMockDeps()
45
+ cm = new CitizenManager(deps)
46
+ })
47
+
48
+ describe('findCitizenNpcId', () => {
49
+ it('returns exact match by name from config', () => {
50
+ expect(cm.findCitizenNpcId('Alice')).toBe('citizen_alice')
51
+ expect(cm.findCitizenNpcId('Bob')).toBe('citizen_bob')
52
+ })
53
+
54
+ it('returns null for unknown name', () => {
55
+ expect(cm.findCitizenNpcId('Charlie')).toBeNull()
56
+ })
57
+
58
+ it('matches by spawned IDs when not in config', () => {
59
+ cm.spawnedCitizenIds.add('citizen_charlie')
60
+ expect(cm.findCitizenNpcId('Charlie')).toBe('citizen_charlie')
61
+ })
62
+ })
63
+
64
+ describe('fuzzyMatchCitizen', () => {
65
+ it('matches partial name against config', () => {
66
+ expect(cm.fuzzyMatchCitizen('ali')).toBe('citizen_alice')
67
+ })
68
+
69
+ it('returns null for empty string', () => {
70
+ expect(cm.fuzzyMatchCitizen('')).toBeNull()
71
+ })
72
+
73
+ it('matches against spawned citizen IDs', () => {
74
+ cm.spawnedCitizenIds.add('citizen_diana_prince')
75
+ expect(cm.fuzzyMatchCitizen('diana')).toBe('citizen_diana_prince')
76
+ })
77
+ })
78
+
79
+ describe('looksLikeIdFragment', () => {
80
+ it('returns true for short ASCII strings', () => {
81
+ expect(cm.looksLikeIdFragment('abc123')).toBe(true)
82
+ expect(cm.looksLikeIdFragment('npc-1')).toBe(true)
83
+ expect(cm.looksLikeIdFragment('a')).toBe(true)
84
+ })
85
+
86
+ it('returns false for strings with Chinese characters', () => {
87
+ expect(cm.looksLikeIdFragment('小明')).toBe(false)
88
+ })
89
+
90
+ it('returns false for strings longer than 12 characters', () => {
91
+ expect(cm.looksLikeIdFragment('abcdefghijklm')).toBe(false)
92
+ })
93
+ })
94
+
95
+ describe('isStewardName', () => {
96
+ it('matches persona name', () => {
97
+ expect(cm.isStewardName('Steward')).toBe(true)
98
+ })
99
+
100
+ it('does not match random name', () => {
101
+ expect(cm.isStewardName('RandomPerson')).toBe(false)
102
+ })
103
+
104
+ it('matches when getStewardName returns a custom name', () => {
105
+ deps = createMockDeps({
106
+ getPersonaName: vi.fn(() => null),
107
+ getStewardName: vi.fn(() => 'CustomBot'),
108
+ })
109
+ cm = new CitizenManager(deps)
110
+ expect(cm.isStewardName('CustomBot')).toBe(true)
111
+ })
112
+ })
113
+
114
+ describe('isPersonaPath', () => {
115
+ it('returns true for persona directory paths', () => {
116
+ expect(cm.isPersonaPath('/workspace/personas/alice.md')).toBe(true)
117
+ expect(cm.isPersonaPath('/workspace/persona/bob.md')).toBe(true)
118
+ })
119
+
120
+ it('returns false for non-persona paths', () => {
121
+ expect(cm.isPersonaPath('/workspace/src/main.ts')).toBe(false)
122
+ })
123
+
124
+ it('returns false for null', () => {
125
+ expect(cm.isPersonaPath(null)).toBe(false)
126
+ })
127
+ })
128
+
129
+ describe('detectCitizenCreated', () => {
130
+ it('queues citizen for spawn on persona write_file event', () => {
131
+ cm.detectCitizenCreated({
132
+ name: 'write_file',
133
+ meta: { filePath: '/workspace/personas/Charlie.md' },
134
+ })
135
+
136
+ expect(cm.spawnedCitizenIds.has('citizen_charlie')).toBe(true)
137
+ })
138
+
139
+ it('ignores non-write_file events', () => {
140
+ cm.detectCitizenCreated({ name: 'read_file', meta: {} })
141
+ expect(cm.spawnedCitizenIds.size).toBe(0)
142
+ })
143
+
144
+ it('ignores non-persona paths', () => {
145
+ cm.detectCitizenCreated({
146
+ name: 'write_file',
147
+ meta: { filePath: '/workspace/src/main.ts' },
148
+ })
149
+ expect(cm.spawnedCitizenIds.size).toBe(0)
150
+ })
151
+
152
+ it('skips steward persona file', () => {
153
+ cm.detectCitizenCreated({
154
+ name: 'write_file',
155
+ meta: { filePath: '/workspace/personas/Steward.md' },
156
+ })
157
+ expect(cm.spawnedCitizenIds.has('citizen_steward')).toBe(true)
158
+ expect(cm.citizenSpawnQueue).toHaveLength(0)
159
+ })
160
+
161
+ it('does not duplicate already-spawned citizen', () => {
162
+ cm.detectCitizenCreated({
163
+ name: 'write_file',
164
+ meta: { filePath: '/workspace/personas/Charlie.md' },
165
+ })
166
+ const queueLen = cm.citizenSpawnQueue.length
167
+ cm.detectCitizenCreated({
168
+ name: 'write_file',
169
+ meta: { filePath: '/workspace/personas/Charlie.md' },
170
+ })
171
+ expect(cm.citizenSpawnQueue.length).toBeLessThanOrEqual(queueLen)
172
+ })
173
+ })
174
+ })