@holoscript/framework 6.0.3

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 (329) hide show
  1. package/ALL-test-results.json +1 -0
  2. package/CHANGELOG.md +8 -0
  3. package/LICENSE +21 -0
  4. package/ROADMAP.md +175 -0
  5. package/dist/AgentManifest-CB4xM-Ma.d.cts +704 -0
  6. package/dist/AgentManifest-CB4xM-Ma.d.ts +704 -0
  7. package/dist/BehaviorTree-BrBFECv5.d.cts +103 -0
  8. package/dist/BehaviorTree-BrBFECv5.d.ts +103 -0
  9. package/dist/InvisibleWallet-BB6tFvRA.d.cts +1732 -0
  10. package/dist/InvisibleWallet-rtRrBOA8.d.ts +1732 -0
  11. package/dist/OrchestratorAgent-BvWgf9uw.d.cts +798 -0
  12. package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +798 -0
  13. package/dist/agents/index.cjs +4790 -0
  14. package/dist/agents/index.d.cts +1788 -0
  15. package/dist/agents/index.d.ts +1788 -0
  16. package/dist/agents/index.js +4695 -0
  17. package/dist/ai/index.cjs +5347 -0
  18. package/dist/ai/index.d.cts +1753 -0
  19. package/dist/ai/index.d.ts +1753 -0
  20. package/dist/ai/index.js +5244 -0
  21. package/dist/behavior.cjs +449 -0
  22. package/dist/behavior.d.cts +130 -0
  23. package/dist/behavior.d.ts +130 -0
  24. package/dist/behavior.js +407 -0
  25. package/dist/economy/index.cjs +3659 -0
  26. package/dist/economy/index.d.cts +747 -0
  27. package/dist/economy/index.d.ts +747 -0
  28. package/dist/economy/index.js +3617 -0
  29. package/dist/implementations-D9T3un9D.d.cts +236 -0
  30. package/dist/implementations-D9T3un9D.d.ts +236 -0
  31. package/dist/index.cjs +24550 -0
  32. package/dist/index.d.cts +1729 -0
  33. package/dist/index.d.ts +1729 -0
  34. package/dist/index.js +24277 -0
  35. package/dist/learning/index.cjs +219 -0
  36. package/dist/learning/index.d.cts +104 -0
  37. package/dist/learning/index.d.ts +104 -0
  38. package/dist/learning/index.js +189 -0
  39. package/dist/negotiation/index.cjs +970 -0
  40. package/dist/negotiation/index.d.cts +610 -0
  41. package/dist/negotiation/index.d.ts +610 -0
  42. package/dist/negotiation/index.js +931 -0
  43. package/dist/skills/index.cjs +1118 -0
  44. package/dist/skills/index.d.cts +289 -0
  45. package/dist/skills/index.d.ts +289 -0
  46. package/dist/skills/index.js +1079 -0
  47. package/dist/swarm/index.cjs +5268 -0
  48. package/dist/swarm/index.d.cts +2433 -0
  49. package/dist/swarm/index.d.ts +2433 -0
  50. package/dist/swarm/index.js +5221 -0
  51. package/dist/training/index.cjs +2745 -0
  52. package/dist/training/index.d.cts +1734 -0
  53. package/dist/training/index.d.ts +1734 -0
  54. package/dist/training/index.js +2687 -0
  55. package/extract-failures.js +10 -0
  56. package/package.json +82 -0
  57. package/src/__tests__/bounty-marketplace.test.ts +374 -0
  58. package/src/__tests__/delegation.test.ts +144 -0
  59. package/src/__tests__/distributed-claimer.test.ts +147 -0
  60. package/src/__tests__/done-log-audit.test.ts +342 -0
  61. package/src/__tests__/framework.test.ts +865 -0
  62. package/src/__tests__/goal-synthesizer.test.ts +236 -0
  63. package/src/__tests__/presence.test.ts +223 -0
  64. package/src/__tests__/protocol-agent.test.ts +254 -0
  65. package/src/__tests__/revenue-splitter.test.ts +114 -0
  66. package/src/__tests__/scenario-driven-todo.test.ts +197 -0
  67. package/src/__tests__/self-improve.test.ts +349 -0
  68. package/src/__tests__/service-lifecycle.test.ts +237 -0
  69. package/src/__tests__/skill-router.test.ts +121 -0
  70. package/src/agents/AgentManifest.ts +493 -0
  71. package/src/agents/AgentRegistry.ts +475 -0
  72. package/src/agents/AgentTypes.ts +585 -0
  73. package/src/agents/AgentWalletRegistry.ts +83 -0
  74. package/src/agents/AuthenticatedCRDT.ts +388 -0
  75. package/src/agents/CapabilityMatcher.ts +453 -0
  76. package/src/agents/CrossRealityHandoff.ts +305 -0
  77. package/src/agents/CulturalMemory.ts +454 -0
  78. package/src/agents/FederatedRegistryAdapter.ts +429 -0
  79. package/src/agents/NormEngine.ts +450 -0
  80. package/src/agents/OrchestratorAgent.ts +414 -0
  81. package/src/agents/SkillWorkflowEngine.ts +472 -0
  82. package/src/agents/TaskDelegationService.ts +551 -0
  83. package/src/agents/__tests__/AgentManifest.prod.test.ts +134 -0
  84. package/src/agents/__tests__/AgentManifest.test.ts +182 -0
  85. package/src/agents/__tests__/AgentModule.test.ts +864 -0
  86. package/src/agents/__tests__/AgentRegistry.prod.test.ts +125 -0
  87. package/src/agents/__tests__/AgentRegistry.test.ts +148 -0
  88. package/src/agents/__tests__/AgentTypes.test.ts +534 -0
  89. package/src/agents/__tests__/AgentWalletRegistry.test.ts +152 -0
  90. package/src/agents/__tests__/AuthenticatedCRDT.test.ts +558 -0
  91. package/src/agents/__tests__/CapabilityMatcher.prod.test.ts +117 -0
  92. package/src/agents/__tests__/CapabilityMatcher.test.ts +178 -0
  93. package/src/agents/__tests__/CrossRealityHandoff.test.ts +402 -0
  94. package/src/agents/__tests__/CulturalMemory.test.ts +200 -0
  95. package/src/agents/__tests__/FederatedRegistryAdapter.test.ts +409 -0
  96. package/src/agents/__tests__/NormEngine.test.ts +276 -0
  97. package/src/agents/__tests__/OrchestratorAgent.test.ts +182 -0
  98. package/src/agents/__tests__/SkillWorkflowEngine.test.ts +357 -0
  99. package/src/agents/__tests__/TaskDelegationService.test.ts +446 -0
  100. package/src/agents/index.ts +107 -0
  101. package/src/agents/spatial-comms/Layer1RealTime.ts +621 -0
  102. package/src/agents/spatial-comms/Layer2A2A.ts +661 -0
  103. package/src/agents/spatial-comms/Layer3MCP.ts +651 -0
  104. package/src/agents/spatial-comms/ProtocolTypes.ts +543 -0
  105. package/src/agents/spatial-comms/SpatialCommClient.ts +483 -0
  106. package/src/agents/spatial-comms/__tests__/performance-benchmark.test.ts +465 -0
  107. package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +409 -0
  108. package/src/agents/spatial-comms/index.ts +66 -0
  109. package/src/ai/AIAdapter.ts +313 -0
  110. package/src/ai/AICopilot.ts +331 -0
  111. package/src/ai/AIOutputValidator.ts +203 -0
  112. package/src/ai/BTNodes.ts +239 -0
  113. package/src/ai/BehaviorSelector.ts +135 -0
  114. package/src/ai/BehaviorTree.ts +153 -0
  115. package/src/ai/Blackboard.ts +165 -0
  116. package/src/ai/GenerationAnalytics.ts +461 -0
  117. package/src/ai/GenerationCache.ts +265 -0
  118. package/src/ai/GoalPlanner.ts +165 -0
  119. package/src/ai/HoloScriptGenerator.ts +580 -0
  120. package/src/ai/InfluenceMap.ts +180 -0
  121. package/src/ai/NavMesh.ts +168 -0
  122. package/src/ai/PerceptionSystem.ts +178 -0
  123. package/src/ai/PromptTemplates.ts +453 -0
  124. package/src/ai/SemanticSearchService.ts +80 -0
  125. package/src/ai/StateMachine.ts +196 -0
  126. package/src/ai/SteeringBehavior.ts +150 -0
  127. package/src/ai/SteeringBehaviors.ts +244 -0
  128. package/src/ai/TrainingDataGenerator.ts +1082 -0
  129. package/src/ai/UtilityAI.ts +145 -0
  130. package/src/ai/__tests__/AIAdapter.prod.test.ts +259 -0
  131. package/src/ai/__tests__/AIAdapter.test.ts +109 -0
  132. package/src/ai/__tests__/AICopilot.prod.test.ts +341 -0
  133. package/src/ai/__tests__/AICopilot.test.ts +178 -0
  134. package/src/ai/__tests__/AIOutputValidator.prod.test.ts +226 -0
  135. package/src/ai/__tests__/AIOutputValidator.test.ts +138 -0
  136. package/src/ai/__tests__/BTNodes.prod.test.ts +391 -0
  137. package/src/ai/__tests__/BTNodes.test.ts +263 -0
  138. package/src/ai/__tests__/BehaviorSelector.prod.test.ts +129 -0
  139. package/src/ai/__tests__/BehaviorSelector.test.ts +132 -0
  140. package/src/ai/__tests__/BehaviorTree.prod.test.ts +266 -0
  141. package/src/ai/__tests__/BehaviorTree.test.ts +216 -0
  142. package/src/ai/__tests__/Blackboard.prod.test.ts +339 -0
  143. package/src/ai/__tests__/Blackboard.test.ts +183 -0
  144. package/src/ai/__tests__/GenerationAnalytics.prod.test.ts +141 -0
  145. package/src/ai/__tests__/GenerationAnalytics.test.ts +165 -0
  146. package/src/ai/__tests__/GenerationCache.prod.test.ts +144 -0
  147. package/src/ai/__tests__/GenerationCache.test.ts +171 -0
  148. package/src/ai/__tests__/GoalPlanner.prod.test.ts +189 -0
  149. package/src/ai/__tests__/GoalPlanner.test.ts +137 -0
  150. package/src/ai/__tests__/GoalPlannerDepth.prod.test.ts +217 -0
  151. package/src/ai/__tests__/HoloScriptGenerator.test.ts +125 -0
  152. package/src/ai/__tests__/InfluenceMap.prod.test.ts +146 -0
  153. package/src/ai/__tests__/InfluenceMap.test.ts +149 -0
  154. package/src/ai/__tests__/NavMesh.prod.test.ts +141 -0
  155. package/src/ai/__tests__/NavMesh.test.ts +159 -0
  156. package/src/ai/__tests__/PerceptionSystem.prod.test.ts +135 -0
  157. package/src/ai/__tests__/PerceptionSystem.test.ts +250 -0
  158. package/src/ai/__tests__/PromptTemplates.prod.test.ts +313 -0
  159. package/src/ai/__tests__/PromptTemplates.test.ts +146 -0
  160. package/src/ai/__tests__/SemanticSearch.test.ts +37 -0
  161. package/src/ai/__tests__/StateMachine.prod.test.ts +162 -0
  162. package/src/ai/__tests__/StateMachine.test.ts +163 -0
  163. package/src/ai/__tests__/SteeringBehavior.prod.test.ts +251 -0
  164. package/src/ai/__tests__/SteeringBehavior.test.ts +135 -0
  165. package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +133 -0
  166. package/src/ai/__tests__/SteeringBehaviors.test.ts +151 -0
  167. package/src/ai/__tests__/TrainingDataGenerator.prod.test.ts +286 -0
  168. package/src/ai/__tests__/TrainingDataGenerator.test.ts +286 -0
  169. package/src/ai/__tests__/UtilityAI.prod.test.ts +207 -0
  170. package/src/ai/__tests__/UtilityAI.test.ts +155 -0
  171. package/src/ai/__tests__/adapters.prod.test.ts +263 -0
  172. package/src/ai/__tests__/adapters.test.ts +320 -0
  173. package/src/ai/adapters.ts +1585 -0
  174. package/src/ai/index.ts +130 -0
  175. package/src/behavior/BehaviorPresets.ts +140 -0
  176. package/src/behavior/BehaviorTree.ts +236 -0
  177. package/src/behavior/StateMachine.ts +176 -0
  178. package/src/behavior/StateTrait.ts +67 -0
  179. package/src/behavior/index.ts +8 -0
  180. package/src/behavior.ts +8 -0
  181. package/src/board/audit.ts +284 -0
  182. package/src/board/board-ops.ts +336 -0
  183. package/src/board/board-types.ts +302 -0
  184. package/src/board/index.ts +69 -0
  185. package/src/define-agent.ts +46 -0
  186. package/src/define-team.ts +33 -0
  187. package/src/delegation.ts +265 -0
  188. package/src/distributed-claimer.ts +228 -0
  189. package/src/economy/AgentBudgetEnforcer.ts +464 -0
  190. package/src/economy/BountyManager.ts +185 -0
  191. package/src/economy/CreatorRevenueAggregator.ts +460 -0
  192. package/src/economy/InvisibleWallet.ts +82 -0
  193. package/src/economy/KnowledgeMarketplace.ts +193 -0
  194. package/src/economy/PaymentWebhookService.ts +512 -0
  195. package/src/economy/RevenueSplitter.ts +156 -0
  196. package/src/economy/SubscriptionManager.ts +546 -0
  197. package/src/economy/UnifiedBudgetOptimizer.ts +635 -0
  198. package/src/economy/UsageMeter.ts +440 -0
  199. package/src/economy/_core-stubs.ts +219 -0
  200. package/src/economy/index.ts +100 -0
  201. package/src/economy/x402-facilitator.ts +1978 -0
  202. package/src/index.ts +348 -0
  203. package/src/knowledge/__tests__/knowledge-consolidator.test.ts +444 -0
  204. package/src/knowledge/__tests__/knowledge-store-vector.test.ts +291 -0
  205. package/src/knowledge/brain.ts +167 -0
  206. package/src/knowledge/consolidation.ts +581 -0
  207. package/src/knowledge/knowledge-consolidator.ts +510 -0
  208. package/src/knowledge/knowledge-store.ts +616 -0
  209. package/src/learning/MemoryConsolidator.ts +102 -0
  210. package/src/learning/MemoryScorer.ts +69 -0
  211. package/src/learning/ProceduralCompiler.ts +45 -0
  212. package/src/learning/SemanticClusterer.ts +66 -0
  213. package/src/learning/index.ts +8 -0
  214. package/src/llm/llm-adapter.ts +159 -0
  215. package/src/mesh/index.ts +309 -0
  216. package/src/negotiation/NegotiationProtocol.ts +694 -0
  217. package/src/negotiation/NegotiationTypes.ts +473 -0
  218. package/src/negotiation/VotingMechanisms.ts +691 -0
  219. package/src/negotiation/index.ts +49 -0
  220. package/src/protocol/goal-synthesizer.ts +317 -0
  221. package/src/protocol/implementations.ts +474 -0
  222. package/src/protocol/micro-phase-decomposer.ts +299 -0
  223. package/src/protocol/micro-step-decomposer.test.ts +306 -0
  224. package/src/protocol-agent.test.ts +353 -0
  225. package/src/protocol-agent.ts +670 -0
  226. package/src/self-improve/absorb-scanner.ts +252 -0
  227. package/src/self-improve/evolution-engine.ts +149 -0
  228. package/src/self-improve/framework-absorber.ts +214 -0
  229. package/src/self-improve/index.ts +50 -0
  230. package/src/self-improve/prompt-optimizer.ts +212 -0
  231. package/src/self-improve/test-generator.ts +175 -0
  232. package/src/skill-router.ts +186 -0
  233. package/src/skills/index.ts +5 -0
  234. package/src/skills/skill-md-bridge.ts +1699 -0
  235. package/src/swarm/ACOEngine.ts +261 -0
  236. package/src/swarm/CollectiveIntelligence.ts +383 -0
  237. package/src/swarm/ContributionSynthesizer.ts +481 -0
  238. package/src/swarm/LeaderElection.ts +393 -0
  239. package/src/swarm/PSOEngine.ts +206 -0
  240. package/src/swarm/QuorumPolicy.ts +173 -0
  241. package/src/swarm/SwarmCoordinator.ts +335 -0
  242. package/src/swarm/SwarmManager.ts +442 -0
  243. package/src/swarm/SwarmMembership.ts +456 -0
  244. package/src/swarm/VotingRound.ts +255 -0
  245. package/src/swarm/__tests__/ACOEngine.prod.test.ts +164 -0
  246. package/src/swarm/__tests__/ACOEngine.test.ts +117 -0
  247. package/src/swarm/__tests__/CollectiveIntelligence.prod.test.ts +296 -0
  248. package/src/swarm/__tests__/CollectiveIntelligence.test.ts +457 -0
  249. package/src/swarm/__tests__/ContributionSynthesizer.prod.test.ts +269 -0
  250. package/src/swarm/__tests__/ContributionSynthesizer.test.ts +254 -0
  251. package/src/swarm/__tests__/LeaderElection.prod.test.ts +196 -0
  252. package/src/swarm/__tests__/LeaderElection.test.ts +151 -0
  253. package/src/swarm/__tests__/PSOEngine.prod.test.ts +162 -0
  254. package/src/swarm/__tests__/PSOEngine.test.ts +106 -0
  255. package/src/swarm/__tests__/QuorumPolicy.prod.test.ts +216 -0
  256. package/src/swarm/__tests__/QuorumPolicy.test.ts +177 -0
  257. package/src/swarm/__tests__/SwarmCoordinator.prod.test.ts +186 -0
  258. package/src/swarm/__tests__/SwarmCoordinator.test.ts +167 -0
  259. package/src/swarm/__tests__/SwarmManager.prod.test.ts +308 -0
  260. package/src/swarm/__tests__/SwarmManager.test.ts +373 -0
  261. package/src/swarm/__tests__/SwarmMembership.prod.test.ts +273 -0
  262. package/src/swarm/__tests__/SwarmMembership.test.ts +264 -0
  263. package/src/swarm/__tests__/VotingRound.prod.test.ts +233 -0
  264. package/src/swarm/__tests__/VotingRound.test.ts +174 -0
  265. package/src/swarm/analytics/SwarmInspector.ts +476 -0
  266. package/src/swarm/analytics/SwarmMetrics.ts +449 -0
  267. package/src/swarm/analytics/__tests__/SwarmInspector.prod.test.ts +366 -0
  268. package/src/swarm/analytics/__tests__/SwarmInspector.test.ts +454 -0
  269. package/src/swarm/analytics/__tests__/SwarmMetrics.prod.test.ts +254 -0
  270. package/src/swarm/analytics/__tests__/SwarmMetrics.test.ts +370 -0
  271. package/src/swarm/analytics/index.ts +7 -0
  272. package/src/swarm/index.ts +69 -0
  273. package/src/swarm/messaging/BroadcastChannel.ts +509 -0
  274. package/src/swarm/messaging/GossipProtocol.ts +565 -0
  275. package/src/swarm/messaging/SwarmEventBus.ts +443 -0
  276. package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +331 -0
  277. package/src/swarm/messaging/__tests__/BroadcastChannel.test.ts +333 -0
  278. package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +356 -0
  279. package/src/swarm/messaging/__tests__/GossipProtocol.test.ts +437 -0
  280. package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +191 -0
  281. package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +247 -0
  282. package/src/swarm/messaging/index.ts +8 -0
  283. package/src/swarm/spatial/FlockingBehavior.ts +462 -0
  284. package/src/swarm/spatial/FormationController.ts +500 -0
  285. package/src/swarm/spatial/Vector3.ts +170 -0
  286. package/src/swarm/spatial/ZoneClaiming.ts +509 -0
  287. package/src/swarm/spatial/__tests__/FlockingBehavior.prod.test.ts +239 -0
  288. package/src/swarm/spatial/__tests__/FlockingBehavior.test.ts +298 -0
  289. package/src/swarm/spatial/__tests__/FormationController.prod.test.ts +240 -0
  290. package/src/swarm/spatial/__tests__/FormationController.test.ts +297 -0
  291. package/src/swarm/spatial/__tests__/Vector3.prod.test.ts +283 -0
  292. package/src/swarm/spatial/__tests__/Vector3.test.ts +224 -0
  293. package/src/swarm/spatial/__tests__/ZoneClaiming.prod.test.ts +246 -0
  294. package/src/swarm/spatial/__tests__/ZoneClaiming.test.ts +374 -0
  295. package/src/swarm/spatial/index.ts +28 -0
  296. package/src/team.ts +1245 -0
  297. package/src/training/LRScheduler.ts +377 -0
  298. package/src/training/QualityScoringPipeline.ts +139 -0
  299. package/src/training/SoftDedup.ts +461 -0
  300. package/src/training/SparsityMonitor.ts +685 -0
  301. package/src/training/SparsityMonitorTypes.ts +209 -0
  302. package/src/training/SpatialTrainingDataGenerator.ts +1526 -0
  303. package/src/training/SpatialTrainingDataTypes.ts +216 -0
  304. package/src/training/TrainingPipelineConfig.ts +215 -0
  305. package/src/training/constants.ts +94 -0
  306. package/src/training/index.ts +138 -0
  307. package/src/training/schema.ts +147 -0
  308. package/src/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
  309. package/src/training/scripts/generate-spatial-dataset.ts +521 -0
  310. package/src/training/training/data/novel-use-cases.jsonl +153 -0
  311. package/src/training/training/data/spatial-reasoning-10k.jsonl +9354 -0
  312. package/src/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
  313. package/src/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
  314. package/src/training/trainingmonkey/index.ts +26 -0
  315. package/src/training/trait-mappings.ts +157 -0
  316. package/src/types/core-stubs.d.ts +113 -0
  317. package/src/types.ts +304 -0
  318. package/test-output.txt +0 -0
  319. package/test-result.json +1 -0
  320. package/tsc-errors.txt +4 -0
  321. package/tsc_output.txt +0 -0
  322. package/tsconfig.json +14 -0
  323. package/tsup-learning-esm.config.ts +12 -0
  324. package/tsup.config.ts +21 -0
  325. package/typescript-errors-2.txt +0 -0
  326. package/typescript-errors.txt +22 -0
  327. package/vitest-log-utf8.txt +268 -0
  328. package/vitest-log.txt +0 -0
  329. package/vitest.config.ts +8 -0
@@ -0,0 +1,865 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { defineAgent } from '../define-agent';
3
+ import { defineTeam } from '../define-team';
4
+ import { KnowledgeStore } from '../knowledge/knowledge-store';
5
+ import { SequenceNode, SelectorNode, ActionNode, ConditionNode, BehaviorTree } from '../behavior';
6
+ import type { AgentConfig } from '../types';
7
+ // ── defineAgent ──
8
+
9
+ describe('defineAgent', () => {
10
+ const validAgent: AgentConfig = {
11
+ name: 'TestCoder',
12
+ role: 'coder',
13
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
14
+ capabilities: ['code-generation'],
15
+ claimFilter: { roles: ['coder'], maxPriority: 8 },
16
+ };
17
+
18
+ it('returns a valid agent config', () => {
19
+ const agent = defineAgent(validAgent);
20
+ expect(agent.name).toBe('TestCoder');
21
+ expect(agent.role).toBe('coder');
22
+ expect(agent.knowledgeDomains).toEqual(['general']); // default
23
+ });
24
+
25
+ it('throws on empty name', () => {
26
+ expect(() => defineAgent({ ...validAgent, name: '' })).toThrow('name is required');
27
+ });
28
+
29
+ it('throws on invalid role', () => {
30
+ expect(() => defineAgent({ ...validAgent, role: 'wizard' as unknown as AgentConfig['role'] })).toThrow('Invalid role');
31
+ });
32
+
33
+ it('throws on missing model', () => {
34
+ expect(() => defineAgent({ ...validAgent, model: { provider: 'anthropic', model: '' } })).toThrow('model');
35
+ });
36
+
37
+ it('throws on empty capabilities', () => {
38
+ expect(() => defineAgent({ ...validAgent, capabilities: [] })).toThrow('capability');
39
+ });
40
+ });
41
+
42
+ // ── defineTeam ──
43
+
44
+ describe('defineTeam', () => {
45
+ const coder = defineAgent({
46
+ name: 'Coder',
47
+ role: 'coder',
48
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
49
+ capabilities: ['code-generation'],
50
+ claimFilter: { roles: ['coder'], maxPriority: 8 },
51
+ });
52
+
53
+ const reviewer = defineAgent({
54
+ name: 'Reviewer',
55
+ role: 'reviewer',
56
+ model: { provider: 'openai', model: 'gpt-4o' },
57
+ capabilities: ['code-review'],
58
+ claimFilter: { roles: ['reviewer'], maxPriority: 5 },
59
+ });
60
+
61
+ it('creates a team with agents', () => {
62
+ const team = defineTeam({
63
+ name: 'test-team',
64
+ agents: [coder, reviewer],
65
+ });
66
+ expect(team.name).toBe('test-team');
67
+ expect(team.openTasks).toHaveLength(0);
68
+ });
69
+
70
+ it('throws on empty name', () => {
71
+ expect(() => defineTeam({ name: '', agents: [coder] })).toThrow('name');
72
+ });
73
+
74
+ it('throws on no agents', () => {
75
+ expect(() => defineTeam({ name: 'test', agents: [] })).toThrow('at least one');
76
+ });
77
+
78
+ it('throws on duplicate agent names', () => {
79
+ expect(() => defineTeam({ name: 'test', agents: [coder, coder] })).toThrow('Duplicate');
80
+ });
81
+
82
+ it('throws when agents exceed max slots', () => {
83
+ expect(() => defineTeam({ name: 'test', agents: [coder, reviewer], maxSlots: 1 })).toThrow('slots');
84
+ });
85
+ });
86
+
87
+ // ── Task Board ──
88
+
89
+ describe('Team.addTasks', () => {
90
+ it('adds tasks and deduplicates', async () => {
91
+ const team = defineTeam({
92
+ name: 'board-test',
93
+ agents: [defineAgent({
94
+ name: 'A', role: 'coder',
95
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
96
+ capabilities: ['c'], claimFilter: { roles: ['coder'], maxPriority: 10 },
97
+ })],
98
+ });
99
+
100
+ const added1 = await team.addTasks([
101
+ { title: 'Fix auth bug', description: 'JWT expired', priority: 1 },
102
+ { title: 'Add tests', description: 'Coverage gap', priority: 3 },
103
+ ]);
104
+ expect(added1).toHaveLength(2);
105
+ expect(team.openTasks).toHaveLength(2);
106
+
107
+ // Dedup: same title shouldn't add again
108
+ const added2 = await team.addTasks([
109
+ { title: 'Fix auth bug', description: 'duplicate', priority: 1 },
110
+ { title: 'New task', description: 'fresh', priority: 5 },
111
+ ]);
112
+ expect(added2).toHaveLength(1);
113
+ expect(team.openTasks).toHaveLength(3);
114
+ });
115
+
116
+ it('sorts open tasks by priority', async () => {
117
+ const team = defineTeam({
118
+ name: 'priority-test',
119
+ agents: [defineAgent({
120
+ name: 'A', role: 'coder',
121
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
122
+ capabilities: ['c'], claimFilter: { roles: ['coder'], maxPriority: 10 },
123
+ })],
124
+ });
125
+
126
+ await team.addTasks([
127
+ { title: 'Low', description: '', priority: 5 },
128
+ { title: 'Critical', description: '', priority: 1 },
129
+ { title: 'Medium', description: '', priority: 3 },
130
+ ]);
131
+
132
+ const tasks = team.openTasks;
133
+ expect(tasks[0].title).toBe('Critical');
134
+ expect(tasks[1].title).toBe('Medium');
135
+ expect(tasks[2].title).toBe('Low');
136
+ });
137
+ });
138
+
139
+ // ── Scout ──
140
+
141
+ describe('Team.scoutFromTodos', () => {
142
+ it('parses grep output into tasks', async () => {
143
+ const team = defineTeam({
144
+ name: 'scout-test',
145
+ agents: [defineAgent({
146
+ name: 'A', role: 'coder',
147
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
148
+ capabilities: ['c'], claimFilter: { roles: ['coder'], maxPriority: 10 },
149
+ })],
150
+ });
151
+
152
+ const grepOutput = [
153
+ 'src/auth.ts:42: // TODO: add rate limiting',
154
+ 'src/db.ts:100: // FIXME: connection pool leak',
155
+ 'src/api.ts:200: // HACK: temporary workaround for CORS',
156
+ ].join('\n');
157
+
158
+ const tasks = await team.scoutFromTodos(grepOutput);
159
+ expect(tasks).toHaveLength(3);
160
+
161
+ // addTasks returns in insertion order; openTasks sorts by priority
162
+ const sorted = team.openTasks;
163
+ expect(sorted[0].title).toContain('FIXME'); // highest priority (2) sorts first
164
+ expect(sorted[0].priority).toBe(2); // FIXME = priority 2
165
+ expect(sorted[1].priority).toBe(3); // TODO = priority 3
166
+ expect(sorted[2].priority).toBe(3); // HACK = priority 3
167
+ });
168
+ });
169
+
170
+ // ── KnowledgeStore ──
171
+
172
+ describe('KnowledgeStore', () => {
173
+ it('publishes and searches entries', () => {
174
+ const store = new KnowledgeStore({ persist: false });
175
+
176
+ store.publish({ type: 'pattern', content: 'Use JWT for stateless auth', domain: 'security', confidence: 0.9, source: 'Coder' }, 'Coder');
177
+ store.publish({ type: 'gotcha', content: 'Never store tokens in localStorage', domain: 'security', confidence: 0.95, source: 'Reviewer' }, 'Reviewer');
178
+ store.publish({ type: 'wisdom', content: 'GraphQL reduces over-fetching', domain: 'api', confidence: 0.7, source: 'Researcher' }, 'Researcher');
179
+
180
+ expect(store.size).toBe(3);
181
+
182
+ const authResults = store.search('auth token');
183
+ expect(authResults.length).toBeGreaterThan(0);
184
+ expect(authResults[0].domain).toBe('security');
185
+
186
+ const apiResults = store.byDomain('api');
187
+ expect(apiResults).toHaveLength(1);
188
+ });
189
+
190
+ it('deduplicates identical content', () => {
191
+ const store = new KnowledgeStore({ persist: false });
192
+
193
+ const e1 = store.publish({ type: 'wisdom', content: 'Test your code', domain: 'general', confidence: 0.8, source: 'A' }, 'A');
194
+ const e2 = store.publish({ type: 'wisdom', content: 'Test your code', domain: 'general', confidence: 0.8, source: 'B' }, 'B');
195
+
196
+ expect(e1.id).toBe(e2.id); // same entry returned
197
+ expect(store.size).toBe(1);
198
+ });
199
+
200
+ it('compounds cross-domain insights', () => {
201
+ const store = new KnowledgeStore({ persist: false });
202
+
203
+ store.publish({ type: 'pattern', content: 'Rate limiting prevents abuse', domain: 'security', confidence: 0.9, source: 'A' }, 'A');
204
+ store.publish({ type: 'pattern', content: 'Cache invalidation is hard', domain: 'performance', confidence: 0.8, source: 'B' }, 'B');
205
+
206
+ const crossRefs = store.compound([
207
+ { type: 'wisdom', content: 'Rate limiting and caching need coordination', domain: 'architecture', confidence: 0.7, source: 'C' },
208
+ ]);
209
+
210
+ expect(crossRefs).toBeGreaterThan(0);
211
+ });
212
+ });
213
+
214
+ // ── Behavior Tree (composed from @holoscript/core) ──
215
+
216
+ describe('Behavior Tree', () => {
217
+ it('Sequence succeeds when all children succeed', () => {
218
+ const tree = new BehaviorTree(
219
+ new SequenceNode([
220
+ new ActionNode('a', () => 'success'),
221
+ new ActionNode('b', () => 'success'),
222
+ ])
223
+ );
224
+ expect(tree.tick(0)).toBe('success');
225
+ });
226
+
227
+ it('Sequence fails on first failure', () => {
228
+ let bRan = false;
229
+ const tree = new BehaviorTree(
230
+ new SequenceNode([
231
+ new ActionNode('a', () => 'failure'),
232
+ new ActionNode('b', () => { bRan = true; return 'success'; }),
233
+ ])
234
+ );
235
+ expect(tree.tick(0)).toBe('failure');
236
+ expect(bRan).toBe(false);
237
+ });
238
+
239
+ it('Selector succeeds on first success', () => {
240
+ const tree = new BehaviorTree(
241
+ new SelectorNode([
242
+ new ActionNode('a', () => 'failure'),
243
+ new ActionNode('b', () => 'success'),
244
+ new ActionNode('c', () => 'failure'),
245
+ ])
246
+ );
247
+ expect(tree.tick(0)).toBe('success');
248
+ });
249
+
250
+ it('Condition + Action composes', () => {
251
+ let executed = false;
252
+ const tree = new BehaviorTree(
253
+ new SequenceNode([
254
+ new ConditionNode('check', () => true),
255
+ new ActionNode('do', () => { executed = true; return 'success'; }),
256
+ ])
257
+ );
258
+ tree.tick(0);
259
+ expect(executed).toBe(true);
260
+ });
261
+
262
+ it('convenience builders produce core node types', () => {
263
+ const seq = new SequenceNode([new ActionNode('test', () => 'success')]);
264
+ expect(seq.type).toBe('sequence');
265
+ const sel = new SelectorNode([new ActionNode('test', () => 'success')]);
266
+ expect(sel.type).toBe('selector');
267
+ });
268
+ });
269
+
270
+ // ── Goal Synthesis ──
271
+
272
+ vi.mock('../protocol-agent', () => ({
273
+ runProtocolCycle: vi.fn().mockResolvedValue({
274
+ summary: 'Completed synthesized task',
275
+ insights: [
276
+ { type: 'wisdom', content: 'Autonomous goals keep agents productive', domain: 'security', confidence: 0.7, source: 'Coder' },
277
+ ],
278
+ }),
279
+ }));
280
+
281
+ describe('Goal Synthesis (empty board)', () => {
282
+ const makeTeam = () =>
283
+ defineTeam({
284
+ name: 'synth-test',
285
+ agents: [
286
+ defineAgent({
287
+ name: 'Coder',
288
+ role: 'coder',
289
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
290
+ capabilities: ['code-generation'],
291
+ claimFilter: { roles: ['coder'], maxPriority: 10 },
292
+ knowledgeDomains: ['security'],
293
+ }),
294
+ ],
295
+ });
296
+
297
+ it('synthesizes a goal when board is empty instead of skipping', async () => {
298
+ const team = makeTeam();
299
+ // Board is empty — agent should synthesize
300
+ const result = await team.runCycle();
301
+ expect(result.agentResults).toHaveLength(1);
302
+ const agentResult = result.agentResults[0];
303
+ expect(agentResult.action).toBe('synthesized');
304
+ expect(agentResult.taskId).toBeTruthy();
305
+ expect(agentResult.taskTitle).toBeTruthy();
306
+ expect(agentResult.summary).toBe('Completed synthesized task');
307
+ // The synthesized task should be completed and removed from the board
308
+ expect(team.openTasks).toHaveLength(0);
309
+ expect(team.completedCount).toBe(1);
310
+ });
311
+
312
+ it('claims existing tasks normally when board has tasks', async () => {
313
+ const team = makeTeam();
314
+ await team.addTasks([
315
+ { title: 'Fix auth bug', description: 'JWT issue', priority: 1, role: 'coder' },
316
+ { title: 'Add tests', description: 'Coverage', priority: 2, role: 'coder' },
317
+ { title: 'Refactor DB', description: 'Cleanup', priority: 3, role: 'coder' },
318
+ ]);
319
+ expect(team.openTasks).toHaveLength(3);
320
+
321
+ const result = await team.runCycle();
322
+ const agentResult = result.agentResults[0];
323
+ // Should claim a real task, not synthesize
324
+ expect(agentResult.action).toBe('completed');
325
+ expect(agentResult.taskTitle).toBe('Fix auth bug');
326
+ expect(team.openTasks).toHaveLength(2);
327
+ });
328
+
329
+ it('synthesized task has source prefix synthesizer:', async () => {
330
+ const team = makeTeam();
331
+ const result = await team.runCycle();
332
+ // The task was completed and moved to doneLog, but we can verify via the cycle result
333
+ expect(result.agentResults[0].taskId).toMatch(/^task_synth_/);
334
+ });
335
+
336
+ it('publishes knowledge from synthesized task execution', async () => {
337
+ const team = makeTeam();
338
+ const result = await team.runCycle();
339
+ expect(result.knowledgeProduced).toHaveLength(1);
340
+ expect(result.knowledgeProduced[0].type).toBe('wisdom');
341
+ expect(result.knowledgeProduced[0].content).toContain('Autonomous goals');
342
+ });
343
+ });
344
+
345
+ // ── Remote facade methods ──
346
+
347
+ describe('Team remote facade methods', () => {
348
+ const agent = defineAgent({
349
+ name: 'A', role: 'coder',
350
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
351
+ capabilities: ['c'], claimFilter: { roles: ['coder'], maxPriority: 10 },
352
+ });
353
+
354
+ // Helper: create a local-only team (no boardUrl)
355
+ function localTeam() {
356
+ return defineTeam({ name: 'local-team', agents: [agent] });
357
+ }
358
+
359
+ // Helper: create a remote team with mocked fetch
360
+ function remoteTeam(mockResponse: Record<string, unknown>) {
361
+ const team = defineTeam({
362
+ name: 'remote-team',
363
+ agents: [agent],
364
+ boardUrl: 'https://example.com',
365
+ boardApiKey: 'test-key',
366
+ });
367
+ // Mock global fetch
368
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
369
+ json: async () => mockResponse,
370
+ } as Response);
371
+ return { team, fetchSpy };
372
+ }
373
+
374
+ // ── suggest() (remote) ──
375
+
376
+ describe('suggest() remote', () => {
377
+ it('calls POST /suggestions with correct body', async () => {
378
+ const { team, fetchSpy } = remoteTeam({ suggestion: { id: 's1', title: 'idea', status: 'open', votes: 0, createdAt: '2026-01-01' } });
379
+ const result = await team.suggest('idea', { description: 'desc', category: 'ux', evidence: 'data' });
380
+ expect(result.suggestion.id).toBe('s1');
381
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
382
+ const [url, opts] = fetchSpy.mock.calls[0];
383
+ expect(url).toContain('/api/holomesh/team/remote-team/suggestions');
384
+ expect(opts?.method).toBe('POST');
385
+ const body = JSON.parse(opts?.body as string);
386
+ expect(body.title).toBe('idea');
387
+ expect(body.description).toBe('desc');
388
+ expect(body.category).toBe('ux');
389
+ expect(body.evidence).toBe('data');
390
+ fetchSpy.mockRestore();
391
+ });
392
+
393
+ it('throws on error response', async () => {
394
+ const { team, fetchSpy } = remoteTeam({ error: 'bad request' });
395
+ await expect(team.suggest('x')).rejects.toThrow('bad request');
396
+ fetchSpy.mockRestore();
397
+ });
398
+ });
399
+
400
+ // ── vote() (remote) ──
401
+
402
+ describe('vote() remote', () => {
403
+ it('calls PATCH /suggestions/:id with vote action', async () => {
404
+ const { team, fetchSpy } = remoteTeam({ suggestion: { id: 's1', title: 'idea', status: 'open', votes: 1, createdAt: '2026-01-01' } });
405
+ const result = await team.vote('s1', 1, 'good idea');
406
+ expect(result.suggestion.votes).toBe(1);
407
+ const [url, opts] = fetchSpy.mock.calls[0];
408
+ expect(url).toContain('/api/holomesh/team/remote-team/suggestions/s1');
409
+ expect(opts?.method).toBe('PATCH');
410
+ const body = JSON.parse(opts?.body as string);
411
+ expect(body.action).toBe('vote');
412
+ expect(body.value).toBe(1);
413
+ expect(body.reason).toBe('good idea');
414
+ fetchSpy.mockRestore();
415
+ });
416
+ });
417
+
418
+ // ── suggestions() (remote) ──
419
+
420
+ describe('suggestions() remote', () => {
421
+ it('calls GET /suggestions without filter', async () => {
422
+ const { team, fetchSpy } = remoteTeam({ suggestions: [] });
423
+ const result = await team.suggestions();
424
+ expect(result.suggestions).toEqual([]);
425
+ const [url] = fetchSpy.mock.calls[0];
426
+ expect(url).toContain('/api/holomesh/team/remote-team/suggestions');
427
+ expect(url).not.toContain('?status=');
428
+ fetchSpy.mockRestore();
429
+ });
430
+
431
+ it('calls GET /suggestions?status=open with filter', async () => {
432
+ const { team, fetchSpy } = remoteTeam({ suggestions: [{ id: 's1' }] });
433
+ await team.suggestions('open');
434
+ const [url] = fetchSpy.mock.calls[0];
435
+ expect(url).toContain('?status=open');
436
+ fetchSpy.mockRestore();
437
+ });
438
+ });
439
+
440
+ // ── setMode() ──
441
+
442
+ describe('setMode()', () => {
443
+ it('changes mode locally on local-only team', async () => {
444
+ const team = localTeam();
445
+ const result = await team.setMode('audit');
446
+ expect(result.mode).toBe('audit');
447
+ expect(team.mode).toBe('audit');
448
+ });
449
+
450
+ it('calls POST /mode with mode body', async () => {
451
+ const { team, fetchSpy } = remoteTeam({ mode: 'audit', previousMode: 'build' });
452
+ const result = await team.setMode('audit');
453
+ expect(result.mode).toBe('audit');
454
+ expect(result.previousMode).toBe('build');
455
+ const [url, opts] = fetchSpy.mock.calls[0];
456
+ expect(url).toContain('/api/holomesh/team/remote-team/mode');
457
+ expect(opts?.method).toBe('POST');
458
+ const body = JSON.parse(opts?.body as string);
459
+ expect(body.mode).toBe('audit');
460
+ fetchSpy.mockRestore();
461
+ });
462
+ });
463
+
464
+ // ── derive() ──
465
+
466
+ describe('derive()', () => {
467
+ it('works on local-only team (local-first)', async () => {
468
+ const team = localTeam();
469
+ const result = await team.derive('audit', '# Findings\n- [ ] Fix Y');
470
+ expect(Array.isArray(result.tasks)).toBe(true);
471
+ expect(result.tasks.length).toBeGreaterThan(0);
472
+ });
473
+
474
+ it('calls POST /board/derive with source and content', async () => {
475
+ const { team, fetchSpy } = remoteTeam({ tasks: [{ id: 't1', title: 'Fix X' }] });
476
+ const result = await team.derive('audit-report', '# Findings\n- Fix X');
477
+ expect(result.tasks).toHaveLength(1);
478
+ const [url, opts] = fetchSpy.mock.calls[0];
479
+ expect(url).toContain('/api/holomesh/team/remote-team/board/derive');
480
+ expect(opts?.method).toBe('POST');
481
+ const body = JSON.parse(opts?.body as string);
482
+ expect(body.source).toBe('audit-report');
483
+ expect(body.content).toBe('# Findings\n- Fix X');
484
+ fetchSpy.mockRestore();
485
+ });
486
+ });
487
+
488
+ // ── presence() ──
489
+
490
+ describe('presence()', () => {
491
+ it('throws on local-only team', async () => {
492
+ const team = localTeam();
493
+ await expect(team.presence()).rejects.toThrow('requires a remote board');
494
+ });
495
+
496
+ it('calls GET /slots', async () => {
497
+ const { team, fetchSpy } = remoteTeam({ slots: [{ agentName: 'A', role: 'coder', status: 'active' }] });
498
+ const result = await team.presence();
499
+ expect(result.slots).toHaveLength(1);
500
+ expect(result.slots[0].agentName).toBe('A');
501
+ const [url, opts] = fetchSpy.mock.calls[0];
502
+ expect(url).toContain('/api/holomesh/team/remote-team/slots');
503
+ expect(opts?.method).toBe('GET');
504
+ fetchSpy.mockRestore();
505
+ });
506
+ });
507
+
508
+ // ── heartbeat() ──
509
+
510
+ describe('heartbeat()', () => {
511
+ it('throws on local-only team', async () => {
512
+ const team = localTeam();
513
+ await expect(team.heartbeat()).rejects.toThrow('requires a remote board');
514
+ });
515
+
516
+ it('calls POST /presence with ide_type and status', async () => {
517
+ const { team, fetchSpy } = remoteTeam({ ok: true });
518
+ const result = await team.heartbeat('vscode');
519
+ expect(result.ok).toBe(true);
520
+ const [url, opts] = fetchSpy.mock.calls[0];
521
+ expect(url).toContain('/api/holomesh/team/remote-team/presence');
522
+ expect(opts?.method).toBe('POST');
523
+ const body = JSON.parse(opts?.body as string);
524
+ expect(body.ide_type).toBe('vscode');
525
+ expect(body.status).toBe('active');
526
+ fetchSpy.mockRestore();
527
+ });
528
+
529
+ it('defaults ide_type to unknown', async () => {
530
+ const { team, fetchSpy } = remoteTeam({ ok: true });
531
+ await team.heartbeat();
532
+ const [, opts] = fetchSpy.mock.calls[0];
533
+ const body = JSON.parse(opts?.body as string);
534
+ expect(body.ide_type).toBe('unknown');
535
+ fetchSpy.mockRestore();
536
+ });
537
+ });
538
+ });
539
+
540
+ // ── Local Suggestions (FW-0.3) ──
541
+
542
+ describe('Team local suggestions', () => {
543
+ const agent1 = defineAgent({
544
+ name: 'Alice', role: 'coder',
545
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
546
+ capabilities: ['code-generation'], claimFilter: { roles: ['coder'], maxPriority: 10 },
547
+ });
548
+ const agent2 = defineAgent({
549
+ name: 'Bob', role: 'researcher',
550
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
551
+ capabilities: ['research'], claimFilter: { roles: ['researcher'], maxPriority: 10 },
552
+ });
553
+
554
+ function makeTeam() {
555
+ return defineTeam({ name: 'test-team', agents: [agent1, agent2] });
556
+ }
557
+
558
+ describe('suggest()', () => {
559
+ it('creates a local suggestion with defaults', async () => {
560
+ const team = makeTeam();
561
+ const result = await team.suggest('Add caching layer');
562
+ expect(result.suggestion.id).toMatch(/^sug_/);
563
+ expect(result.suggestion.title).toBe('Add caching layer');
564
+ expect(result.suggestion.status).toBe('open');
565
+ expect(result.suggestion.proposedBy).toBe('anonymous');
566
+ expect(result.suggestion.votes).toEqual([]);
567
+ expect(result.suggestion.score).toBe(0);
568
+ });
569
+
570
+ it('creates a suggestion with all options', async () => {
571
+ const team = makeTeam();
572
+ const result = await team.suggest('Refactor parser', {
573
+ description: 'The parser is too complex',
574
+ category: 'architecture',
575
+ evidence: 'Cyclomatic complexity > 20',
576
+ proposedBy: 'Alice',
577
+ autoPromoteThreshold: 3,
578
+ autoDismissThreshold: 2,
579
+ });
580
+ const s = result.suggestion;
581
+ expect(s.description).toBe('The parser is too complex');
582
+ expect(s.category).toBe('architecture');
583
+ expect(s.evidence).toBe('Cyclomatic complexity > 20');
584
+ expect(s.proposedBy).toBe('Alice');
585
+ expect(s.autoPromoteThreshold).toBe(3);
586
+ expect(s.autoDismissThreshold).toBe(2);
587
+ });
588
+
589
+ it('throws on empty title', async () => {
590
+ const team = makeTeam();
591
+ await expect(team.suggest(' ')).rejects.toThrow('title is required');
592
+ });
593
+
594
+ it('deduplicates against open suggestions', async () => {
595
+ const team = makeTeam();
596
+ await team.suggest('Add caching');
597
+ await expect(team.suggest('add CACHING')).rejects.toThrow('similar open suggestion');
598
+ });
599
+
600
+ it('allows resubmission after dismiss', async () => {
601
+ const team = makeTeam();
602
+ const { suggestion } = (await team.suggest('Add caching'));
603
+ team.dismissSuggestion(suggestion.id);
604
+ // Should not throw — dismissed suggestions don't block new ones
605
+ const result = await team.suggest('Add caching');
606
+ expect(result.suggestion.status).toBe('open');
607
+ });
608
+ });
609
+
610
+ describe('vote()', () => {
611
+ it('records an upvote', async () => {
612
+ const team = makeTeam();
613
+ const { suggestion } = await team.suggest('Idea');
614
+ const result = await team.vote(suggestion.id, 'Alice', 'up');
615
+ expect(result.suggestion.votes).toHaveLength(1);
616
+ expect(result.suggestion.votes[0].agent).toBe('Alice');
617
+ expect(result.suggestion.votes[0].vote).toBe('up');
618
+ expect(result.suggestion.score).toBe(1);
619
+ });
620
+
621
+ it('records a downvote', async () => {
622
+ const team = makeTeam();
623
+ const { suggestion } = await team.suggest('Bad idea');
624
+ const result = await team.vote(suggestion.id, 'Bob', 'down', 'not useful');
625
+ expect(result.suggestion.votes[0].vote).toBe('down');
626
+ expect(result.suggestion.votes[0].reason).toBe('not useful');
627
+ expect(result.suggestion.score).toBe(-1);
628
+ });
629
+
630
+ it('replaces previous vote from same agent', async () => {
631
+ const team = makeTeam();
632
+ // High threshold so first vote doesn't auto-promote
633
+ const { suggestion } = await team.suggest('Idea', { autoPromoteThreshold: 10 });
634
+ await team.vote(suggestion.id, 'Alice', 'up');
635
+ const result = await team.vote(suggestion.id, 'Alice', 'down');
636
+ expect(result.suggestion.votes).toHaveLength(1);
637
+ expect(result.suggestion.votes[0].vote).toBe('down');
638
+ expect(result.suggestion.score).toBe(-1);
639
+ });
640
+
641
+ it('throws on unknown suggestion', async () => {
642
+ const team = makeTeam();
643
+ await expect(team.vote('nonexistent', 'Alice', 'up')).rejects.toThrow('Suggestion not found');
644
+ });
645
+
646
+ it('throws on closed suggestion', async () => {
647
+ const team = makeTeam();
648
+ const { suggestion } = await team.suggest('Idea');
649
+ team.dismissSuggestion(suggestion.id);
650
+ await expect(team.vote(suggestion.id, 'Alice', 'up')).rejects.toThrow('voting closed');
651
+ });
652
+
653
+ it('auto-promotes when upvotes reach threshold', async () => {
654
+ const team = makeTeam();
655
+ // Default threshold = ceil(2 agents / 2) = 1
656
+ const { suggestion } = await team.suggest('Ship it', { proposedBy: 'Alice' });
657
+ const result = await team.vote(suggestion.id, 'Alice', 'up');
658
+ expect(result.suggestion.status).toBe('promoted');
659
+ expect(result.promotedTaskId).toBeDefined();
660
+ expect(result.promotedTaskId).toMatch(/^task_/);
661
+ // Verify task was added to board
662
+ expect(team.openTasks.some(t => t.source === `suggestion:${suggestion.id}`)).toBe(true);
663
+ });
664
+
665
+ it('auto-dismisses when downvotes reach threshold', async () => {
666
+ const team = makeTeam();
667
+ const { suggestion } = await team.suggest('Bad plan');
668
+ const result = await team.vote(suggestion.id, 'Bob', 'down');
669
+ expect(result.suggestion.status).toBe('dismissed');
670
+ expect(result.suggestion.resolvedAt).toBeDefined();
671
+ });
672
+
673
+ it('respects custom autoPromoteThreshold', async () => {
674
+ const team = makeTeam();
675
+ const { suggestion } = await team.suggest('Needs consensus', { autoPromoteThreshold: 2 });
676
+ // First vote: not enough
677
+ const r1 = await team.vote(suggestion.id, 'Alice', 'up');
678
+ expect(r1.suggestion.status).toBe('open');
679
+ // Second vote: reaches threshold
680
+ const r2 = await team.vote(suggestion.id, 'Bob', 'up');
681
+ expect(r2.suggestion.status).toBe('promoted');
682
+ expect(r2.promotedTaskId).toBeDefined();
683
+ });
684
+
685
+ it('respects custom autoDismissThreshold', async () => {
686
+ const team = makeTeam();
687
+ const { suggestion } = await team.suggest('Maybe bad', { autoDismissThreshold: 2 });
688
+ const r1 = await team.vote(suggestion.id, 'Alice', 'down');
689
+ expect(r1.suggestion.status).toBe('open');
690
+ const r2 = await team.vote(suggestion.id, 'Bob', 'down');
691
+ expect(r2.suggestion.status).toBe('dismissed');
692
+ });
693
+ });
694
+
695
+ describe('suggestions()', () => {
696
+ it('returns empty list initially', async () => {
697
+ const team = makeTeam();
698
+ const result = await team.suggestions();
699
+ expect(result.suggestions).toEqual([]);
700
+ });
701
+
702
+ it('returns all suggestions', async () => {
703
+ const team = makeTeam();
704
+ await team.suggest('A');
705
+ await team.suggest('B');
706
+ const result = await team.suggestions();
707
+ expect(result.suggestions).toHaveLength(2);
708
+ });
709
+
710
+ it('filters by status', async () => {
711
+ const team = makeTeam();
712
+ const { suggestion: s1 } = await team.suggest('Keep');
713
+ await team.suggest('Dismiss me');
714
+ const list = await team.suggestions();
715
+ team.dismissSuggestion(list.suggestions[1].id);
716
+
717
+ const open = await team.suggestions('open');
718
+ expect(open.suggestions).toHaveLength(1);
719
+ expect(open.suggestions[0].id).toBe(s1.id);
720
+
721
+ const dismissed = await team.suggestions('dismissed');
722
+ expect(dismissed.suggestions).toHaveLength(1);
723
+ });
724
+ });
725
+
726
+ describe('promoteSuggestion()', () => {
727
+ it('promotes and creates a board task', async () => {
728
+ const team = makeTeam();
729
+ const { suggestion } = await team.suggest('Build widget', {
730
+ description: 'We need a widget',
731
+ proposedBy: 'Alice',
732
+ });
733
+ const result = await team.promoteSuggestion(suggestion.id, 'Bob');
734
+ expect(result.suggestion.status).toBe('promoted');
735
+ expect(result.promotedTaskId).toBeDefined();
736
+ const task = team.openTasks.find(t => t.id === result.promotedTaskId);
737
+ expect(task).toBeDefined();
738
+ expect(task!.title).toBe('Build widget');
739
+ expect(task!.description).toContain('Promoted by Bob');
740
+ expect(task!.source).toBe(`suggestion:${suggestion.id}`);
741
+ });
742
+
743
+ it('throws on already promoted', async () => {
744
+ const team = makeTeam();
745
+ const { suggestion } = await team.suggest('X');
746
+ await team.promoteSuggestion(suggestion.id);
747
+ await expect(team.promoteSuggestion(suggestion.id)).rejects.toThrow('already promoted');
748
+ });
749
+
750
+ it('throws on not found', async () => {
751
+ const team = makeTeam();
752
+ await expect(team.promoteSuggestion('nope')).rejects.toThrow('not found');
753
+ });
754
+
755
+ it('assigns priority 2 for architecture category', async () => {
756
+ const team = makeTeam();
757
+ const { suggestion } = await team.suggest('Restructure', { category: 'architecture' });
758
+ const result = await team.promoteSuggestion(suggestion.id);
759
+ const task = team.openTasks.find(t => t.id === result.promotedTaskId);
760
+ expect(task!.priority).toBe(2);
761
+ });
762
+
763
+ it('assigns priority 3 for testing category', async () => {
764
+ const team = makeTeam();
765
+ const { suggestion } = await team.suggest('Add tests', { category: 'testing' });
766
+ const result = await team.promoteSuggestion(suggestion.id);
767
+ const task = team.openTasks.find(t => t.id === result.promotedTaskId);
768
+ expect(task!.priority).toBe(3);
769
+ });
770
+ });
771
+
772
+ describe('dismissSuggestion()', () => {
773
+ it('dismisses an open suggestion', async () => {
774
+ const team = makeTeam();
775
+ const { suggestion } = await team.suggest('Nah');
776
+ const result = team.dismissSuggestion(suggestion.id);
777
+ expect(result.suggestion.status).toBe('dismissed');
778
+ expect(result.suggestion.resolvedAt).toBeDefined();
779
+ });
780
+
781
+ it('throws on already dismissed', async () => {
782
+ const team = makeTeam();
783
+ const { suggestion } = await team.suggest('Gone');
784
+ team.dismissSuggestion(suggestion.id);
785
+ expect(() => team.dismissSuggestion(suggestion.id)).toThrow('already dismissed');
786
+ });
787
+
788
+ it('throws on remote team', async () => {
789
+ const team = defineTeam({
790
+ name: 'remote', agents: [agent1],
791
+ boardUrl: 'https://example.com', boardApiKey: 'key',
792
+ });
793
+ expect(() => team.dismissSuggestion('s1')).toThrow('not supported in remote mode');
794
+ });
795
+ });
796
+ });
797
+
798
+ // ── Mesh Integration (FW-0.4) ──
799
+
800
+ describe('Team mesh integration', () => {
801
+ const agent1: AgentConfig = {
802
+ name: 'Coder1', role: 'coder',
803
+ model: { provider: 'anthropic', model: 'claude-sonnet-4' },
804
+ capabilities: ['code-gen', 'testing'],
805
+ claimFilter: { roles: ['coder'], maxPriority: 8 },
806
+ };
807
+
808
+ const mkTeam = () => defineTeam({ name: 'mesh-team', agents: [agent1] });
809
+
810
+ it('initializes mesh, signals, and gossip', () => {
811
+ const team = mkTeam();
812
+ expect(team.mesh).toBeDefined();
813
+ expect(team.signals).toBeDefined();
814
+ expect(team.gossip).toBeDefined();
815
+ });
816
+
817
+ it('peers() returns empty initially', () => {
818
+ const team = mkTeam();
819
+ expect(team.peers()).toEqual([]);
820
+ });
821
+
822
+ it('registerPeer() + peers() returns registered peer', () => {
823
+ const team = mkTeam();
824
+ team.registerPeer({
825
+ id: 'peer-1', hostname: 'localhost', port: 3000,
826
+ version: '1.0.0', agentCount: 2, capabilities: ['code'],
827
+ lastSeen: Date.now(),
828
+ });
829
+ expect(team.peers()).toHaveLength(1);
830
+ expect(team.peers()[0].id).toBe('peer-1');
831
+ });
832
+
833
+ it('broadcastCapabilities() creates an agent-host signal', () => {
834
+ const team = mkTeam();
835
+ team.broadcastCapabilities('https://my-team.local');
836
+ const signals = team.signals.discoverSignals('agent-host');
837
+ expect(signals).toHaveLength(1);
838
+ expect(signals[0].url).toBe('https://my-team.local');
839
+ expect(signals[0].capabilities).toContain('code-gen');
840
+ expect(signals[0].capabilities).toContain('testing');
841
+ });
842
+
843
+ it('shareKnowledge() + syncFromPeer() transfers gossip', () => {
844
+ const teamA = mkTeam();
845
+ const teamB = defineTeam({ name: 'team-b', agents: [agent1] });
846
+
847
+ teamA.shareKnowledge({ insight: 'tests should run fast' });
848
+ teamA.shareKnowledge({ insight: 'avoid any type' });
849
+
850
+ const absorbed = teamB.syncFromPeer(teamA.gossip.getPool());
851
+ expect(absorbed).toBe(2);
852
+ expect(teamB.gossip.getPoolSize()).toBe(2);
853
+ });
854
+
855
+ it('syncFromPeer() deduplicates existing packets', () => {
856
+ const teamA = mkTeam();
857
+ const teamB = defineTeam({ name: 'team-b', agents: [agent1] });
858
+
859
+ teamA.shareKnowledge({ insight: 'deduplicate me' });
860
+
861
+ teamB.syncFromPeer(teamA.gossip.getPool());
862
+ const absorbed2 = teamB.syncFromPeer(teamA.gossip.getPool());
863
+ expect(absorbed2).toBe(0);
864
+ });
865
+ });