@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,1699 @@
1
+ /**
2
+ * Skill-MD Bridge — Bidirectional .hsplus <-> SKILL.md Conversion
3
+ *
4
+ * Converts HoloScript .hsplus composition skills to portable SKILL.md format
5
+ * (ClawHub/OpenClaw compatible) and back. Enables cross-platform skill sharing
6
+ * between HoloClaw's native .hsplus format and the broader agent skill ecosystem.
7
+ *
8
+ * Forward bridge: .hsplus -> SKILL.md
9
+ * - Extracts composition metadata (name, description, version) as YAML frontmatter
10
+ * - Converts behavior tree sequences into Markdown instruction steps
11
+ * - Includes trait declarations, state schema, and runtime requirements
12
+ * - Maps input_schema/output_schema to OpenClaw frontmatter fields
13
+ *
14
+ * Reverse bridge: SKILL.md -> .hsplus
15
+ * - Parses YAML frontmatter into composition metadata + trait declarations
16
+ * - Converts Markdown instructions into behavior tree sequence nodes
17
+ * - Wraps in full composition structure with @economy, @rate_limiter, @timeout_guard
18
+ * - Parses input_schema/output_schema from frontmatter into typed schemas
19
+ *
20
+ * HoloClaw Skill interop:
21
+ * - toHoloClawSkill() converts ParsedSkill to the Skill interface from SkillRegistryTrait
22
+ * - fromHoloClawSkill() converts Skill objects back to ParsedSkill for serialization
23
+ *
24
+ * ClawHub CLI integration: publish/install skill packages with registry URL support
25
+ *
26
+ * @version 1.1.0
27
+ * @see compositions/skills/*.hsplus — HoloClaw native skill format
28
+ * @see .claude/skills/ *\/SKILL.md — Claude Code SKILL.md format
29
+ * @see https://docs.openclaw.ai/tools/skills — ClawHub specification
30
+ *
31
+ * Security considerations (Corridor-inline):
32
+ * - File paths are validated against path traversal
33
+ * - No shell execution in the bridge itself (CLI integration uses subprocess)
34
+ * - Skill content is parsed, not eval'd
35
+ * - YAML frontmatter is parsed with safe subset (no !!python/exec etc.)
36
+ */
37
+
38
+ // =============================================================================
39
+ // TYPES
40
+ // =============================================================================
41
+
42
+ /**
43
+ * Schema field definition for input/output schemas.
44
+ * Compatible with OpenClaw SKILL.md frontmatter format and HoloClaw SkillInput/SkillOutput.
45
+ */
46
+ export interface SchemaField {
47
+ /** Field name */
48
+ name: string;
49
+ /** Field type: string, number, boolean, object, array */
50
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
51
+ /** Whether this field is required (inputs only) */
52
+ required?: boolean;
53
+ /** Human-readable description */
54
+ description: string;
55
+ /** Default value (inputs only) */
56
+ default?: unknown;
57
+ }
58
+
59
+ /**
60
+ * Metadata extracted from an .hsplus composition skill or SKILL.md frontmatter.
61
+ * This is the canonical interchange format between the two representations.
62
+ */
63
+ export interface SkillMetadata {
64
+ /** Skill identifier (kebab-case, e.g. "code-health") */
65
+ name: string;
66
+ /** Human-readable description of what the skill does */
67
+ description: string;
68
+ /** Semantic version string */
69
+ version: string;
70
+ /** Author name or organization */
71
+ author: string;
72
+ /** Skill category for marketplace browsing */
73
+ category?: string;
74
+ /** Tags for search/discovery */
75
+ tags?: string[];
76
+ /** Input schema fields (OpenClaw input_schema frontmatter) */
77
+ inputSchema?: SchemaField[];
78
+ /** Output schema fields (OpenClaw output_schema frontmatter) */
79
+ outputSchema?: SchemaField[];
80
+ /** Minimum HoloScript CLI version required */
81
+ holoCliVersion?: string;
82
+ /** Minimum Node.js version required */
83
+ nodeVersion?: string;
84
+ /** Economy budget limit per invocation (USD) */
85
+ spendLimit?: number;
86
+ /** Whether users can invoke this skill via slash command */
87
+ userInvocable?: boolean;
88
+ /** License identifier (SPDX) */
89
+ license?: string;
90
+ /** Homepage URL */
91
+ homepage?: string;
92
+ /** Repository URL */
93
+ repository?: string;
94
+ }
95
+
96
+ /**
97
+ * A state variable declared in a composition skill.
98
+ */
99
+ export interface SkillStateVar {
100
+ /** Variable name */
101
+ name: string;
102
+ /** Type: string, number, boolean */
103
+ type: string;
104
+ /** Default value */
105
+ defaultValue: string | number | boolean;
106
+ }
107
+
108
+ /**
109
+ * A trait declaration extracted from a composition skill.
110
+ */
111
+ export interface SkillTraitDecl {
112
+ /** Trait name (e.g. "rate_limiter", "economy") */
113
+ name: string;
114
+ /** Trait configuration parameters */
115
+ config: Record<string, unknown>;
116
+ }
117
+
118
+ /**
119
+ * A behavior tree action step extracted from a composition skill.
120
+ */
121
+ export interface SkillActionStep {
122
+ /** Action name (e.g. "shell_exec", "diagnose") */
123
+ action: string;
124
+ /** Human-readable description of what this step does */
125
+ description: string;
126
+ /** Action parameters */
127
+ params: Record<string, unknown>;
128
+ /** BT node type: action, sequence, selector, condition */
129
+ nodeType: 'action' | 'sequence' | 'selector' | 'condition';
130
+ }
131
+
132
+ /**
133
+ * A test assertion extracted from a composition skill.
134
+ */
135
+ export interface SkillTest {
136
+ /** Test name */
137
+ name: string;
138
+ /** Setup expression (optional) */
139
+ setup?: string;
140
+ /** Assert expression */
141
+ assert: string;
142
+ }
143
+
144
+ /**
145
+ * Fully parsed skill representation — the intermediate form between .hsplus and SKILL.md.
146
+ */
147
+ export interface ParsedSkill {
148
+ metadata: SkillMetadata;
149
+ traits: SkillTraitDecl[];
150
+ state: SkillStateVar[];
151
+ steps: SkillActionStep[];
152
+ tests: SkillTest[];
153
+ /** Raw environment block properties (if any) */
154
+ environment?: Record<string, unknown>;
155
+ /** Raw object declarations (for complex skills with scene objects) */
156
+ objects?: string[];
157
+ /** Original source comments (leading comment block) */
158
+ sourceComments: string[];
159
+ }
160
+
161
+ /**
162
+ * Result of a bridge conversion operation.
163
+ */
164
+ export interface BridgeResult<T> {
165
+ success: boolean;
166
+ data?: T;
167
+ errors: string[];
168
+ warnings: string[];
169
+ }
170
+
171
+ /**
172
+ * ClawHub package manifest for publish/install operations.
173
+ */
174
+ export interface ClawHubManifest {
175
+ name: string;
176
+ version: string;
177
+ description: string;
178
+ author: string;
179
+ license: string;
180
+ /** ClawHub registry URL for distribution */
181
+ registryUrl: string;
182
+ /** Tags for registry search/discovery */
183
+ tags?: string[];
184
+ /** Homepage URL */
185
+ homepage?: string;
186
+ /** Repository URL */
187
+ repository?: string;
188
+ holoScript: {
189
+ format: 'hsplus';
190
+ minCliVersion: string;
191
+ traits: string[];
192
+ stateVars: string[];
193
+ testCount: number;
194
+ /** Input schema field names */
195
+ inputFields: string[];
196
+ /** Output schema field names */
197
+ outputFields: string[];
198
+ };
199
+ files: string[];
200
+ dependencies?: Record<string, string>;
201
+ }
202
+
203
+ // =============================================================================
204
+ // FORWARD BRIDGE: .hsplus -> SKILL.md
205
+ // =============================================================================
206
+
207
+ /**
208
+ * Parse an .hsplus composition source string into a structured ParsedSkill.
209
+ * Uses regex-based extraction (not a full parser) for lightweight, dependency-free operation.
210
+ */
211
+ export function parseHsplus(source: string): BridgeResult<ParsedSkill> {
212
+ const errors: string[] = [];
213
+ const warnings: string[] = [];
214
+
215
+ // --- Extract leading comments ---
216
+ const sourceComments = extractLeadingComments(source);
217
+
218
+ // --- Extract composition name ---
219
+ const compositionMatch = source.match(/composition\s+"([^"]+)"\s*\{/);
220
+ if (!compositionMatch) {
221
+ errors.push('No composition declaration found. Expected: composition "name" { ... }');
222
+ return { success: false, errors, warnings };
223
+ }
224
+ const compositionName = compositionMatch[1];
225
+
226
+ // --- Extract description from leading comments ---
227
+ const description = extractDescription(sourceComments);
228
+
229
+ // --- Extract version from @version JSDoc or default ---
230
+ const versionMatch = source.match(/@version\s+([\d.]+)/);
231
+ const version = versionMatch ? versionMatch[1] : '1.0.0';
232
+
233
+ // --- Extract traits ---
234
+ const traits = extractTraits(source);
235
+
236
+ // --- Extract state variables ---
237
+ const state = extractStateVars(source);
238
+
239
+ // --- Extract behavior tree steps ---
240
+ const steps = extractBTSteps(source);
241
+
242
+ // --- Extract tests ---
243
+ const tests = extractTests(source);
244
+
245
+ // --- Extract environment ---
246
+ const environment = extractEnvironment(source);
247
+
248
+ // --- Extract objects ---
249
+ const objects = extractObjectNames(source);
250
+
251
+ // --- Extract spend limit from @economy trait ---
252
+ const economyTrait = traits.find((t) => t.name === 'economy');
253
+ const spendLimit = economyTrait?.config?.default_spend_limit as number | undefined;
254
+
255
+ // --- Extract input/output schemas ---
256
+ const inputSchema = extractSchemaFields(source, 'input_schema');
257
+ const outputSchema = extractSchemaFields(source, 'output_schema');
258
+
259
+ // --- Extract tags from leading comments (@tags ...) ---
260
+ const tagsMatch = source.match(/@tags\s+(.+)/);
261
+ const tags = tagsMatch
262
+ ? tagsMatch[1]
263
+ .split(',')
264
+ .map((t) => t.trim())
265
+ .filter(Boolean)
266
+ : undefined;
267
+
268
+ const metadata: SkillMetadata = {
269
+ name: compositionName,
270
+ description: description || `HoloClaw skill: ${compositionName}`,
271
+ version,
272
+ author: 'HoloScript',
273
+ holoCliVersion: '5.0.0',
274
+ nodeVersion: '20',
275
+ spendLimit,
276
+ userInvocable: true,
277
+ inputSchema: inputSchema.length > 0 ? inputSchema : undefined,
278
+ outputSchema: outputSchema.length > 0 ? outputSchema : undefined,
279
+ tags,
280
+ };
281
+
282
+ return {
283
+ success: true,
284
+ data: {
285
+ metadata,
286
+ traits,
287
+ state,
288
+ steps,
289
+ tests,
290
+ environment: environment || undefined,
291
+ objects: objects.length > 0 ? objects : undefined,
292
+ sourceComments,
293
+ },
294
+ errors,
295
+ warnings,
296
+ };
297
+ }
298
+
299
+ /**
300
+ * Convert a ParsedSkill into a SKILL.md string (ClawHub-compatible format).
301
+ */
302
+ export function toSkillMd(skill: ParsedSkill): BridgeResult<string> {
303
+ const errors: string[] = [];
304
+ const warnings: string[] = [];
305
+ const lines: string[] = [];
306
+
307
+ // --- YAML Frontmatter ---
308
+ lines.push('---');
309
+ lines.push(`name: ${skill.metadata.name}`);
310
+ lines.push('description: >');
311
+ // Wrap description to 80 chars with 2-space indent
312
+ const descWords = skill.metadata.description.split(/\s+/);
313
+ let descLine = ' ';
314
+ for (const word of descWords) {
315
+ if (descLine.length + word.length + 1 > 80) {
316
+ lines.push(descLine);
317
+ descLine = ' ' + word;
318
+ } else {
319
+ descLine += (descLine.trim().length > 0 ? ' ' : '') + word;
320
+ }
321
+ }
322
+ if (descLine.trim().length > 0) lines.push(descLine);
323
+
324
+ if (skill.metadata.version !== '1.0.0') {
325
+ lines.push(`version: ${skill.metadata.version}`);
326
+ }
327
+ if (skill.metadata.author && skill.metadata.author !== 'HoloScript') {
328
+ lines.push(`author: ${skill.metadata.author}`);
329
+ }
330
+ if (skill.metadata.category) {
331
+ lines.push(`category: ${skill.metadata.category}`);
332
+ }
333
+ if (skill.metadata.tags && skill.metadata.tags.length > 0) {
334
+ lines.push(`tags: [${skill.metadata.tags.join(', ')}]`);
335
+ }
336
+ if (skill.metadata.homepage) {
337
+ lines.push(`homepage: ${skill.metadata.homepage}`);
338
+ }
339
+ if (skill.metadata.license) {
340
+ lines.push(`license: ${skill.metadata.license}`);
341
+ }
342
+ if (skill.metadata.repository) {
343
+ lines.push(`repository: ${skill.metadata.repository}`);
344
+ }
345
+ // --- input_schema / output_schema in frontmatter (OpenClaw format) ---
346
+ if (skill.metadata.inputSchema && skill.metadata.inputSchema.length > 0) {
347
+ lines.push('input_schema:');
348
+ for (const field of skill.metadata.inputSchema) {
349
+ lines.push(` - name: ${field.name}`);
350
+ lines.push(` type: ${field.type}`);
351
+ if (field.required !== undefined) lines.push(` required: ${field.required}`);
352
+ lines.push(` description: ${field.description}`);
353
+ if (field.default !== undefined) lines.push(` default: ${JSON.stringify(field.default)}`);
354
+ }
355
+ }
356
+ if (skill.metadata.outputSchema && skill.metadata.outputSchema.length > 0) {
357
+ lines.push('output_schema:');
358
+ for (const field of skill.metadata.outputSchema) {
359
+ lines.push(` - name: ${field.name}`);
360
+ lines.push(` type: ${field.type}`);
361
+ lines.push(` description: ${field.description}`);
362
+ }
363
+ }
364
+ lines.push('---');
365
+ lines.push('');
366
+
367
+ // --- Title ---
368
+ const titleName = skill.metadata.name
369
+ .split('-')
370
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
371
+ .join(' ');
372
+ lines.push(`# ${titleName}`);
373
+ lines.push('');
374
+ lines.push(skill.metadata.description);
375
+ lines.push('');
376
+
377
+ // --- Runtime Requirements ---
378
+ lines.push('## Runtime Requirements');
379
+ lines.push('');
380
+ lines.push(`- **holoscript-cli** >= ${skill.metadata.holoCliVersion || '5.0.0'}`);
381
+ lines.push(`- **Node.js** >= ${skill.metadata.nodeVersion || '20'}`);
382
+ if (skill.metadata.spendLimit !== undefined) {
383
+ lines.push(`- **Economy budget**: $${skill.metadata.spendLimit.toFixed(2)} per invocation`);
384
+ }
385
+ lines.push('');
386
+
387
+ // --- Traits ---
388
+ if (skill.traits.length > 0) {
389
+ lines.push('## Traits');
390
+ lines.push('');
391
+ for (const trait of skill.traits) {
392
+ const configStr =
393
+ Object.keys(trait.config).length > 0
394
+ ? ` (${Object.entries(trait.config)
395
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
396
+ .join(', ')})`
397
+ : '';
398
+ lines.push(`- \`@${trait.name}\`${configStr}`);
399
+ }
400
+ lines.push('');
401
+ }
402
+
403
+ // --- State Schema ---
404
+ if (skill.state.length > 0) {
405
+ lines.push('## State Schema');
406
+ lines.push('');
407
+ lines.push('| Variable | Type | Default |');
408
+ lines.push('|----------|------|---------|');
409
+ for (const sv of skill.state) {
410
+ lines.push(`| \`${sv.name}\` | ${sv.type} | \`${JSON.stringify(sv.defaultValue)}\` |`);
411
+ }
412
+ lines.push('');
413
+ }
414
+
415
+ // --- Input Schema ---
416
+ if (skill.metadata.inputSchema && skill.metadata.inputSchema.length > 0) {
417
+ lines.push('## Input Schema');
418
+ lines.push('');
419
+ lines.push('| Field | Type | Required | Description |');
420
+ lines.push('|-------|------|----------|-------------|');
421
+ for (const field of skill.metadata.inputSchema) {
422
+ const req = field.required ? 'yes' : 'no';
423
+ const defaultStr =
424
+ field.default !== undefined ? ` (default: \`${JSON.stringify(field.default)}\`)` : '';
425
+ lines.push(
426
+ `| \`${field.name}\` | ${field.type} | ${req} | ${field.description}${defaultStr} |`
427
+ );
428
+ }
429
+ lines.push('');
430
+ }
431
+
432
+ // --- Output Schema ---
433
+ if (skill.metadata.outputSchema && skill.metadata.outputSchema.length > 0) {
434
+ lines.push('## Output Schema');
435
+ lines.push('');
436
+ lines.push('| Field | Type | Description |');
437
+ lines.push('|-------|------|-------------|');
438
+ for (const field of skill.metadata.outputSchema) {
439
+ lines.push(`| \`${field.name}\` | ${field.type} | ${field.description} |`);
440
+ }
441
+ lines.push('');
442
+ }
443
+
444
+ // --- Environment ---
445
+ if (skill.environment && Object.keys(skill.environment).length > 0) {
446
+ lines.push('## Environment');
447
+ lines.push('');
448
+ lines.push('```yaml');
449
+ for (const [key, val] of Object.entries(skill.environment)) {
450
+ lines.push(`${key}: ${JSON.stringify(val)}`);
451
+ }
452
+ lines.push('```');
453
+ lines.push('');
454
+ }
455
+
456
+ // --- Workflow Steps ---
457
+ if (skill.steps.length > 0) {
458
+ lines.push('## Workflow');
459
+ lines.push('');
460
+ let stepNum = 1;
461
+ for (const step of skill.steps) {
462
+ if (step.nodeType === 'sequence' || step.nodeType === 'selector') {
463
+ lines.push(`### ${step.description || step.action}`);
464
+ lines.push('');
465
+ } else {
466
+ const paramsStr =
467
+ Object.keys(step.params).length > 0
468
+ ? ` (${Object.entries(step.params)
469
+ .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
470
+ .join(', ')})`
471
+ : '';
472
+ lines.push(`${stepNum}. **${step.action}**${paramsStr}`);
473
+ if (step.description) {
474
+ lines.push(` ${step.description}`);
475
+ }
476
+ stepNum++;
477
+ }
478
+ }
479
+ lines.push('');
480
+ }
481
+
482
+ // --- Objects ---
483
+ if (skill.objects && skill.objects.length > 0) {
484
+ lines.push('## Scene Objects');
485
+ lines.push('');
486
+ for (const obj of skill.objects) {
487
+ lines.push(`- \`${obj}\``);
488
+ }
489
+ lines.push('');
490
+ }
491
+
492
+ // --- Tests ---
493
+ if (skill.tests.length > 0) {
494
+ lines.push('## Tests');
495
+ lines.push('');
496
+ lines.push(`${skill.tests.length} built-in assertions:`);
497
+ lines.push('');
498
+ for (const test of skill.tests) {
499
+ lines.push(`- **${test.name}**: \`${test.assert}\``);
500
+ }
501
+ lines.push('');
502
+ }
503
+
504
+ // --- Installation ---
505
+ lines.push('## Installation');
506
+ lines.push('');
507
+ lines.push('```bash');
508
+ lines.push(`# Via HoloClaw CLI`);
509
+ lines.push(`hs claw install ${skill.metadata.name}`);
510
+ lines.push('');
511
+ lines.push('# Via ClawHub registry');
512
+ lines.push(`clawhub install @holoscript/${skill.metadata.name}`);
513
+ lines.push('');
514
+ lines.push('# Manual: copy to compositions/skills/');
515
+ lines.push(`cp ${skill.metadata.name}.hsplus compositions/skills/`);
516
+ lines.push('```');
517
+ lines.push('');
518
+
519
+ // --- Run ---
520
+ lines.push('## Usage');
521
+ lines.push('');
522
+ lines.push('```bash');
523
+ lines.push(`# Run standalone`);
524
+ lines.push(`hs run compositions/skills/${skill.metadata.name}.hsplus`);
525
+ lines.push('');
526
+ lines.push('# Run as HoloClaw skill (hot-reload)');
527
+ lines.push(`hs daemon compositions/holoclaw.hsplus --always-on`);
528
+ lines.push('```');
529
+ lines.push('');
530
+
531
+ // --- Footer ---
532
+ lines.push('---');
533
+ lines.push('');
534
+ lines.push(`*Generated by HoloScript Skill-MD Bridge v1.0.0*`);
535
+ lines.push(`*Source format: .hsplus (HoloClaw native composition)*`);
536
+ lines.push('');
537
+
538
+ return {
539
+ success: true,
540
+ data: lines.join('\n'),
541
+ errors,
542
+ warnings,
543
+ };
544
+ }
545
+
546
+ /**
547
+ * Convert an .hsplus source string directly to SKILL.md string.
548
+ * Convenience wrapper combining parseHsplus + toSkillMd.
549
+ */
550
+ export function hsplusToSkillMd(source: string): BridgeResult<string> {
551
+ const parsed = parseHsplus(source);
552
+ if (!parsed.success || !parsed.data) {
553
+ return { success: false, errors: parsed.errors, warnings: parsed.warnings };
554
+ }
555
+ return toSkillMd(parsed.data);
556
+ }
557
+
558
+ // =============================================================================
559
+ // REVERSE BRIDGE: SKILL.md -> .hsplus
560
+ // =============================================================================
561
+
562
+ /**
563
+ * Parse a SKILL.md string into a structured ParsedSkill.
564
+ */
565
+ export function parseSkillMd(markdown: string): BridgeResult<ParsedSkill> {
566
+ const errors: string[] = [];
567
+ const warnings: string[] = [];
568
+
569
+ // --- Parse YAML frontmatter ---
570
+ const frontmatter = extractFrontmatter(markdown);
571
+ if (!frontmatter) {
572
+ errors.push('No YAML frontmatter found. Expected --- delimited block at start of file.');
573
+ return { success: false, errors, warnings };
574
+ }
575
+
576
+ const name = frontmatter.name;
577
+ if (!name) {
578
+ errors.push('Frontmatter missing required "name" field.');
579
+ return { success: false, errors, warnings };
580
+ }
581
+
582
+ const description = frontmatter.description || '';
583
+ const version = frontmatter.version || '1.0.0';
584
+ const author = frontmatter.author || 'HoloScript';
585
+
586
+ // --- Parse body sections ---
587
+ const body = extractBody(markdown);
588
+
589
+ // --- Extract input/output schemas from frontmatter or body ---
590
+ const inputSchemaFm = parseFrontmatterSchemaList(frontmatter['input_schema']);
591
+ const outputSchemaFm = parseFrontmatterSchemaList(frontmatter['output_schema']);
592
+ const inputSchemaBody = extractInputSchemaFromMd(body);
593
+ const outputSchemaBody = extractOutputSchemaFromMd(body);
594
+ // Prefer frontmatter schemas; fall back to body tables
595
+ const inputSchema = inputSchemaFm.length > 0 ? inputSchemaFm : inputSchemaBody;
596
+ const outputSchema = outputSchemaFm.length > 0 ? outputSchemaFm : outputSchemaBody;
597
+
598
+ const metadata: SkillMetadata = {
599
+ name,
600
+ description: typeof description === 'string' ? description.trim() : String(description),
601
+ version,
602
+ author,
603
+ category: frontmatter.category,
604
+ tags: frontmatter.tags,
605
+ inputSchema: inputSchema.length > 0 ? inputSchema : undefined,
606
+ outputSchema: outputSchema.length > 0 ? outputSchema : undefined,
607
+ holoCliVersion: frontmatter['holoscript-cli'] || '5.0.0',
608
+ nodeVersion: frontmatter['node-version'] || '20',
609
+ license: frontmatter.license,
610
+ homepage: frontmatter.homepage,
611
+ repository: frontmatter.repository,
612
+ userInvocable: frontmatter['user-invocable'] !== false,
613
+ };
614
+
615
+ // --- Extract traits from Traits section ---
616
+ const traits = extractTraitsFromMd(body);
617
+
618
+ // --- Extract state from State Schema section ---
619
+ const state = extractStateFromMd(body);
620
+
621
+ // --- Extract steps from Workflow section ---
622
+ const steps = extractStepsFromMd(body);
623
+
624
+ // --- Extract tests from Tests section ---
625
+ const tests = extractTestsFromMd(body);
626
+
627
+ // --- Extract environment from Environment section ---
628
+ const environment = extractEnvironmentFromMd(body);
629
+
630
+ return {
631
+ success: true,
632
+ data: {
633
+ metadata,
634
+ traits: traits.length > 0 ? traits : getDefaultTraits(),
635
+ state,
636
+ steps,
637
+ tests,
638
+ environment: environment || undefined,
639
+ sourceComments: [],
640
+ },
641
+ errors,
642
+ warnings,
643
+ };
644
+ }
645
+
646
+ /**
647
+ * Convert a ParsedSkill into an .hsplus composition source string.
648
+ */
649
+ export function toHsplus(skill: ParsedSkill): BridgeResult<string> {
650
+ const errors: string[] = [];
651
+ const warnings: string[] = [];
652
+ const lines: string[] = [];
653
+
654
+ // --- Leading comment block ---
655
+ lines.push(
656
+ `// ${titleCase(skill.metadata.name)} -- ${skill.metadata.description.split('\n')[0]}`
657
+ );
658
+ if (skill.metadata.description.includes('\n')) {
659
+ for (const line of skill.metadata.description.split('\n').slice(1)) {
660
+ if (line.trim()) lines.push(`// ${line.trim()}`);
661
+ }
662
+ }
663
+ lines.push(`// Installed via HoloClaw Shelf. Hot-reloads into running daemon.`);
664
+ if (skill.metadata.version !== '1.0.0') {
665
+ lines.push(`//`);
666
+ lines.push(`// @version ${skill.metadata.version}`);
667
+ }
668
+ lines.push('');
669
+
670
+ // --- Composition declaration ---
671
+ lines.push(`composition "${skill.metadata.name}" {`);
672
+
673
+ // --- Traits ---
674
+ for (const trait of skill.traits) {
675
+ const configEntries = Object.entries(trait.config);
676
+ if (configEntries.length === 0) {
677
+ lines.push(` @${trait.name}`);
678
+ } else if (configEntries.length === 1) {
679
+ const [k, v] = configEntries[0];
680
+ lines.push(` @${trait.name} (${k}: ${formatHsplusValue(v)})`);
681
+ } else {
682
+ lines.push(` @${trait.name} {`);
683
+ for (const [k, v] of configEntries) {
684
+ lines.push(` ${k}: ${formatHsplusValue(v)}`);
685
+ }
686
+ lines.push(` }`);
687
+ }
688
+ }
689
+ lines.push('');
690
+
691
+ // --- State variables ---
692
+ for (const sv of skill.state) {
693
+ lines.push(` state ${sv.name}: ${sv.type} = ${formatHsplusValue(sv.defaultValue)}`);
694
+ }
695
+ if (skill.state.length > 0) lines.push('');
696
+
697
+ // --- Environment ---
698
+ if (skill.environment && Object.keys(skill.environment).length > 0) {
699
+ lines.push(' environment {');
700
+ for (const [key, val] of Object.entries(skill.environment)) {
701
+ lines.push(` ${key}: ${formatHsplusValue(val)}`);
702
+ }
703
+ lines.push(' }');
704
+ lines.push('');
705
+ }
706
+
707
+ // --- Behavior tree steps ---
708
+ if (skill.steps.length > 0) {
709
+ const topSequences = groupStepsIntoSequences(skill.steps);
710
+ for (const seq of topSequences) {
711
+ emitBTNode(lines, seq, 2);
712
+ }
713
+ lines.push('');
714
+ }
715
+
716
+ // --- Tests ---
717
+ if (skill.tests.length > 0) {
718
+ lines.push(' // -- Tests --');
719
+ lines.push('');
720
+ for (const test of skill.tests) {
721
+ lines.push(' @test {');
722
+ lines.push(` name: "${test.name}"`);
723
+ if (test.setup) {
724
+ lines.push(` setup: { ${test.setup} }`);
725
+ }
726
+ lines.push(` assert: { ${test.assert} }`);
727
+ lines.push(' }');
728
+ lines.push('');
729
+ }
730
+ }
731
+
732
+ lines.push('}');
733
+ lines.push('');
734
+
735
+ return {
736
+ success: true,
737
+ data: lines.join('\n'),
738
+ errors,
739
+ warnings,
740
+ };
741
+ }
742
+
743
+ /**
744
+ * Convert a SKILL.md string directly to .hsplus source string.
745
+ * Convenience wrapper combining parseSkillMd + toHsplus.
746
+ */
747
+ export function skillMdToHsplus(markdown: string): BridgeResult<string> {
748
+ const parsed = parseSkillMd(markdown);
749
+ if (!parsed.success || !parsed.data) {
750
+ return { success: false, errors: parsed.errors, warnings: parsed.warnings };
751
+ }
752
+ return toHsplus(parsed.data);
753
+ }
754
+
755
+ // =============================================================================
756
+ // CLAWHUB CLI INTEGRATION
757
+ // =============================================================================
758
+
759
+ /**
760
+ * Generate a ClawHub package manifest from a ParsedSkill.
761
+ * This manifest can be used for `clawhub publish` operations.
762
+ */
763
+ export function generateClawHubManifest(
764
+ skill: ParsedSkill,
765
+ registryUrl = 'https://registry.clawhub.com'
766
+ ): ClawHubManifest {
767
+ return {
768
+ name: `@holoscript/${skill.metadata.name}`,
769
+ version: skill.metadata.version,
770
+ description: skill.metadata.description,
771
+ author: skill.metadata.author,
772
+ license: skill.metadata.license || 'MIT',
773
+ registryUrl,
774
+ tags: skill.metadata.tags,
775
+ homepage: skill.metadata.homepage,
776
+ repository: skill.metadata.repository,
777
+ holoScript: {
778
+ format: 'hsplus',
779
+ minCliVersion: skill.metadata.holoCliVersion || '5.0.0',
780
+ traits: skill.traits.map((t) => t.name),
781
+ stateVars: skill.state.map((s) => s.name),
782
+ testCount: skill.tests.length,
783
+ inputFields: (skill.metadata.inputSchema || []).map((f) => f.name),
784
+ outputFields: (skill.metadata.outputSchema || []).map((f) => f.name),
785
+ },
786
+ files: [`${skill.metadata.name}.hsplus`, 'SKILL.md', 'clawhub.json'],
787
+ };
788
+ }
789
+
790
+ /**
791
+ * Generate the complete set of files needed for a ClawHub publish.
792
+ * Returns a map of filename -> content.
793
+ */
794
+ export function generateClawHubPackage(source: string): BridgeResult<Map<string, string>> {
795
+ const parsed = parseHsplus(source);
796
+ if (!parsed.success || !parsed.data) {
797
+ return { success: false, errors: parsed.errors, warnings: parsed.warnings };
798
+ }
799
+
800
+ const skill = parsed.data;
801
+ const skillMdResult = toSkillMd(skill);
802
+ if (!skillMdResult.success || !skillMdResult.data) {
803
+ return { success: false, errors: skillMdResult.errors, warnings: skillMdResult.warnings };
804
+ }
805
+
806
+ const manifest = generateClawHubManifest(skill);
807
+ const files = new Map<string, string>();
808
+
809
+ files.set(`${skill.metadata.name}.hsplus`, source);
810
+ files.set('SKILL.md', skillMdResult.data);
811
+ files.set('clawhub.json', JSON.stringify(manifest, null, 2));
812
+
813
+ return {
814
+ success: true,
815
+ data: files,
816
+ errors: [],
817
+ warnings: [],
818
+ };
819
+ }
820
+
821
+ /**
822
+ * Generate a CLI command string for publishing to ClawHub.
823
+ * Does NOT execute the command -- returns the string for the caller to execute.
824
+ *
825
+ * @param skillName - The skill name (kebab-case)
826
+ * @param registry - The registry URL (default: https://registry.clawhub.com)
827
+ */
828
+ export function getPublishCommand(
829
+ skillName: string,
830
+ registry = 'https://registry.clawhub.com'
831
+ ): string {
832
+ return `clawhub publish @holoscript/${skillName} --registry ${registry}`;
833
+ }
834
+
835
+ /**
836
+ * Generate a CLI command string for installing a skill from ClawHub.
837
+ *
838
+ * @param skillName - The skill name (kebab-case)
839
+ * @param targetDir - Target directory (default: compositions/skills)
840
+ * @param registry - The registry URL (default: https://registry.clawhub.com)
841
+ */
842
+ export function getInstallCommand(
843
+ skillName: string,
844
+ targetDir = 'compositions/skills',
845
+ registry = 'https://registry.clawhub.com'
846
+ ): string {
847
+ return `clawhub install @holoscript/${skillName} --target ${targetDir} --registry ${registry}`;
848
+ }
849
+
850
+ /**
851
+ * Generate a CLI command string for installing via the HoloScript CLI.
852
+ *
853
+ * @param skillName - The skill name (kebab-case)
854
+ */
855
+ export function getHsInstallCommand(skillName: string): string {
856
+ return `hs claw install ${skillName}`;
857
+ }
858
+
859
+ // =============================================================================
860
+ // INTERNAL HELPERS: .hsplus PARSING
861
+ // =============================================================================
862
+
863
+ function extractLeadingComments(source: string): string[] {
864
+ const lines = source.split('\n');
865
+ const comments: string[] = [];
866
+ for (const line of lines) {
867
+ const trimmed = line.trim();
868
+ if (trimmed.startsWith('//')) {
869
+ comments.push(trimmed.replace(/^\/\/\s?/, ''));
870
+ } else if (trimmed === '' && comments.length > 0) {
871
+ // Allow blank lines between comment blocks at the top
872
+ continue;
873
+ } else {
874
+ break;
875
+ }
876
+ }
877
+ return comments;
878
+ }
879
+
880
+ function extractDescription(comments: string[]): string {
881
+ // First non-empty comment line that isn't a tag or metadata
882
+ const descLines: string[] = [];
883
+ for (const line of comments) {
884
+ if (line.startsWith('@') || line.startsWith('*') || line === '') continue;
885
+ // Skip lines that are clearly skill identifiers (e.g. "Code Health Monitor -- ...")
886
+ descLines.push(line);
887
+ }
888
+ return descLines.join(' ').trim();
889
+ }
890
+
891
+ function extractTraits(source: string): SkillTraitDecl[] {
892
+ const traits: SkillTraitDecl[] = [];
893
+ // Match @trait_name or @trait_name (key: value, ...) or @trait_name { ... }
894
+ // Only match traits that appear right inside the composition (indented by 2 spaces)
895
+ const traitRegex = /^\s{2}@(\w+)(?:\s*\(([^)]*)\))?(?:\s*\{([^}]*)\})?/gm;
896
+ let match: RegExpExecArray | null;
897
+ while ((match = traitRegex.exec(source)) !== null) {
898
+ const name = match[1];
899
+ // Skip @test blocks and @version
900
+ if (name === 'test' || name === 'version') continue;
901
+ const config: Record<string, unknown> = {};
902
+ const inlineConfig = match[2] || match[3];
903
+ if (inlineConfig) {
904
+ parseTraitConfig(inlineConfig, config);
905
+ }
906
+ traits.push({ name, config });
907
+ }
908
+ return traits;
909
+ }
910
+
911
+ function parseTraitConfig(configStr: string, config: Record<string, unknown>): void {
912
+ // Parse key: value pairs from trait config
913
+ const pairs = configStr
914
+ .split(/,|\n/)
915
+ .map((s) => s.trim())
916
+ .filter(Boolean);
917
+ for (const pair of pairs) {
918
+ const colonIdx = pair.indexOf(':');
919
+ if (colonIdx === -1) continue;
920
+ const key = pair.slice(0, colonIdx).trim();
921
+ const rawVal = pair.slice(colonIdx + 1).trim();
922
+ config[key] = parseConfigValue(rawVal);
923
+ }
924
+ }
925
+
926
+ function parseConfigValue(raw: string): unknown {
927
+ // Try number
928
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return parseFloat(raw);
929
+ // Try boolean
930
+ if (raw === 'true') return true;
931
+ if (raw === 'false') return false;
932
+ // Try quoted string
933
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
934
+ return raw.slice(1, -1);
935
+ }
936
+ return raw;
937
+ }
938
+
939
+ function extractStateVars(source: string): SkillStateVar[] {
940
+ const vars: SkillStateVar[] = [];
941
+
942
+ // Match inline state declarations: state name: type = value
943
+ const inlineRegex = /^\s+state\s+(\w+)\s*:\s*(\w+)\s*=\s*(.+)$/gm;
944
+ let match: RegExpExecArray | null;
945
+ while ((match = inlineRegex.exec(source)) !== null) {
946
+ const name = match[1];
947
+ const type = match[2];
948
+ const rawVal = match[3].trim();
949
+ vars.push({ name, type, defaultValue: parseStateDefault(rawVal, type) });
950
+ }
951
+
952
+ // Match block state { key: value, ... } format
953
+ const blockMatch = source.match(/\bstate\s*\{([^}]*)\}/);
954
+ if (blockMatch && vars.length === 0) {
955
+ const blockContent = blockMatch[1];
956
+ const lineRegex = /^\s*(\w+)\s*:\s*(.+)$/gm;
957
+ let lineMatch: RegExpExecArray | null;
958
+ while ((lineMatch = lineRegex.exec(blockContent)) !== null) {
959
+ const name = lineMatch[1];
960
+ const rawVal = lineMatch[2].trim();
961
+ const type = inferType(rawVal);
962
+ vars.push({ name, type, defaultValue: parseStateDefault(rawVal, type) });
963
+ }
964
+ }
965
+
966
+ return vars;
967
+ }
968
+
969
+ function parseStateDefault(raw: string, type: string): string | number | boolean {
970
+ const cleaned = raw.replace(/[,;]$/, '').trim();
971
+ if (type === 'number') return parseFloat(cleaned) || 0;
972
+ if (type === 'boolean') return cleaned === 'true';
973
+ // String: strip quotes
974
+ if (cleaned.startsWith('"') && cleaned.endsWith('"')) return cleaned.slice(1, -1);
975
+ if (cleaned.startsWith("'") && cleaned.endsWith("'")) return cleaned.slice(1, -1);
976
+ return cleaned;
977
+ }
978
+
979
+ function inferType(raw: string): string {
980
+ const cleaned = raw.replace(/[,;]$/, '').trim();
981
+ if (/^-?\d+(\.\d+)?$/.test(cleaned)) return 'number';
982
+ if (cleaned === 'true' || cleaned === 'false') return 'boolean';
983
+ return 'string';
984
+ }
985
+
986
+ function extractBTSteps(source: string): SkillActionStep[] {
987
+ const steps: SkillActionStep[] = [];
988
+
989
+ // Match sequence "name" { ... } blocks
990
+ const seqRegex = /\b(sequence|selector)\s+"([^"]+)"\s*\{/g;
991
+ let match: RegExpExecArray | null;
992
+ while ((match = seqRegex.exec(source)) !== null) {
993
+ steps.push({
994
+ action: match[2],
995
+ description: match[2].replace(/-/g, ' '),
996
+ params: {},
997
+ nodeType: match[1] as 'sequence' | 'selector',
998
+ });
999
+ }
1000
+
1001
+ // Match action "name" { ... } blocks
1002
+ const actionRegex = /\baction\s+"([^"]+)"\s*\{([^}]*)\}/g;
1003
+ while ((match = actionRegex.exec(source)) !== null) {
1004
+ const actionName = match[1];
1005
+ const body = match[2];
1006
+ const params: Record<string, unknown> = {};
1007
+ const desc = extractActionComment(body);
1008
+
1009
+ // Extract params from action body
1010
+ const paramRegex = /^\s*(\w+)\s*:\s*(.+)$/gm;
1011
+ let paramMatch: RegExpExecArray | null;
1012
+ while ((paramMatch = paramRegex.exec(body)) !== null) {
1013
+ const key = paramMatch[1];
1014
+ const val = paramMatch[2].trim();
1015
+ // Skip comments
1016
+ if (val.startsWith('//')) continue;
1017
+ params[key] = parseConfigValue(val);
1018
+ }
1019
+
1020
+ steps.push({
1021
+ action: actionName,
1022
+ description: desc || actionName.replace(/_/g, ' '),
1023
+ params,
1024
+ nodeType: 'action',
1025
+ });
1026
+ }
1027
+
1028
+ return steps;
1029
+ }
1030
+
1031
+ function extractActionComment(body: string): string {
1032
+ const commentMatch = body.match(/\/\/\s*(.+)/);
1033
+ return commentMatch ? commentMatch[1].trim() : '';
1034
+ }
1035
+
1036
+ function extractTests(source: string): SkillTest[] {
1037
+ const tests: SkillTest[] = [];
1038
+ // Use a manual brace-counting approach to extract @test blocks
1039
+ // since they contain nested { } for assert and setup
1040
+ const testStarts = [...source.matchAll(/@test\s*\{/g)];
1041
+ for (const startMatch of testStarts) {
1042
+ const startIdx = startMatch.index! + startMatch[0].length;
1043
+ let depth = 1;
1044
+ let i = startIdx;
1045
+ while (i < source.length && depth > 0) {
1046
+ if (source[i] === '{') depth++;
1047
+ if (source[i] === '}') depth--;
1048
+ i++;
1049
+ }
1050
+ const block = source.slice(startIdx, i - 1);
1051
+ const nameMatch = block.match(/name\s*:\s*"([^"]+)"/);
1052
+ const assertMatch = block.match(/assert\s*:\s*\{([^}]+)\}/);
1053
+ const setupMatch = block.match(/setup\s*:\s*\{([^}]+)\}/);
1054
+ if (nameMatch && assertMatch) {
1055
+ tests.push({
1056
+ name: nameMatch[1],
1057
+ assert: assertMatch[1].trim(),
1058
+ setup: setupMatch ? setupMatch[1].trim() : undefined,
1059
+ });
1060
+ }
1061
+ }
1062
+ return tests;
1063
+ }
1064
+
1065
+ function extractEnvironment(source: string): Record<string, unknown> | null {
1066
+ const envMatch = source.match(/\benvironment\s*\{([^}]*)\}/);
1067
+ if (!envMatch) return null;
1068
+ const config: Record<string, unknown> = {};
1069
+ const lineRegex = /^\s*(\w+)\s*:\s*(.+)$/gm;
1070
+ let match: RegExpExecArray | null;
1071
+ while ((match = lineRegex.exec(envMatch[1])) !== null) {
1072
+ config[match[1]] = parseConfigValue(match[2].trim());
1073
+ }
1074
+ return config;
1075
+ }
1076
+
1077
+ function extractObjectNames(source: string): string[] {
1078
+ const names: string[] = [];
1079
+ const objRegex = /\bobject\s+"([^"]+)"\s*\{/g;
1080
+ let match: RegExpExecArray | null;
1081
+ while ((match = objRegex.exec(source)) !== null) {
1082
+ names.push(match[1]);
1083
+ }
1084
+ return names;
1085
+ }
1086
+
1087
+ /**
1088
+ * Extract schema fields from .hsplus @input_schema or @output_schema blocks.
1089
+ * Format:
1090
+ * @input_schema {
1091
+ * field_name: type (required) "description"
1092
+ * field_name: type = default "description"
1093
+ * }
1094
+ */
1095
+ function extractSchemaFields(source: string, blockName: string): SchemaField[] {
1096
+ const fields: SchemaField[] = [];
1097
+ // Use brace-counting to extract the block
1098
+ const startRegex = new RegExp(`@${blockName}\\s*\\{`);
1099
+ const startMatch = startRegex.exec(source);
1100
+ if (!startMatch) return fields;
1101
+
1102
+ const startIdx = startMatch.index! + startMatch[0].length;
1103
+ let depth = 1;
1104
+ let i = startIdx;
1105
+ while (i < source.length && depth > 0) {
1106
+ if (source[i] === '{') depth++;
1107
+ if (source[i] === '}') depth--;
1108
+ i++;
1109
+ }
1110
+ const block = source.slice(startIdx, i - 1);
1111
+
1112
+ // Parse each line: name: type [(required)] [= default] ["description"]
1113
+ const lineRegex =
1114
+ /^\s*(\w+)\s*:\s*(\w+)(?:\s*\(required\))?(?:\s*=\s*([^\s"]+))?(?:\s*"([^"]*)")?/gm;
1115
+ let lineMatch: RegExpExecArray | null;
1116
+ while ((lineMatch = lineRegex.exec(block)) !== null) {
1117
+ const name = lineMatch[1];
1118
+ const type = lineMatch[2] as SchemaField['type'];
1119
+ const isRequired = lineMatch[0].includes('(required)');
1120
+ const defaultVal = lineMatch[3] ? parseConfigValue(lineMatch[3]) : undefined;
1121
+ const description = lineMatch[4] || name;
1122
+
1123
+ fields.push({
1124
+ name,
1125
+ type,
1126
+ required: isRequired || undefined,
1127
+ description,
1128
+ default: defaultVal,
1129
+ });
1130
+ }
1131
+
1132
+ return fields;
1133
+ }
1134
+
1135
+ // =============================================================================
1136
+ // INTERNAL HELPERS: SKILL.md PARSING
1137
+ // =============================================================================
1138
+
1139
+ function extractFrontmatter(markdown: string): Record<string, any> | null {
1140
+ const fmMatch = markdown.match(/^---\s*\n([\s\S]*?)\n---/);
1141
+ if (!fmMatch) return null;
1142
+ return parseSimpleYaml(fmMatch[1]);
1143
+ }
1144
+
1145
+ /**
1146
+ * Simple YAML parser for frontmatter. Handles:
1147
+ * - key: value (scalars)
1148
+ * - key: > (multi-line folded)
1149
+ * - key: [a, b, c] (arrays)
1150
+ *
1151
+ * Does NOT support anchors, aliases, or complex nesting.
1152
+ * This avoids a dependency on a full YAML parser.
1153
+ */
1154
+ function parseSimpleYaml(yaml: string): Record<string, any> {
1155
+ const result: Record<string, any> = {};
1156
+ const lines = yaml.split('\n');
1157
+ let currentKey: string | null = null;
1158
+ let multilineValue: string[] = [];
1159
+ let inMultiline = false;
1160
+ // State for list-of-objects parsing (input_schema / output_schema)
1161
+ let inListOfObjects = false;
1162
+ let listKey: string | null = null;
1163
+ let currentListItem: Record<string, unknown> | null = null;
1164
+ let listItems: Record<string, unknown>[] = [];
1165
+
1166
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
1167
+ const line = lines[lineIdx];
1168
+
1169
+ // --- List-of-objects continuation ---
1170
+ if (inListOfObjects && listKey) {
1171
+ // New list item: " - name: value"
1172
+ const listItemStart = line.match(/^\s{2,}-\s+(\w[\w-]*)\s*:\s*(.*)/);
1173
+ if (listItemStart) {
1174
+ // Flush previous item
1175
+ if (currentListItem) listItems.push(currentListItem);
1176
+ currentListItem = {};
1177
+ const key = listItemStart[1];
1178
+ const val = listItemStart[2].trim();
1179
+ currentListItem[key] = parseConfigValue(val);
1180
+ continue;
1181
+ }
1182
+ // Continuation of current list item: " key: value"
1183
+ const listItemCont = line.match(/^\s{4,}(\w[\w-]*)\s*:\s*(.*)/);
1184
+ if (listItemCont && currentListItem) {
1185
+ const key = listItemCont[1];
1186
+ const val = listItemCont[2].trim();
1187
+ currentListItem[key] = parseConfigValue(val);
1188
+ continue;
1189
+ }
1190
+ // End of list-of-objects (non-indented line or empty)
1191
+ if (currentListItem) listItems.push(currentListItem);
1192
+ result[listKey] = listItems;
1193
+ inListOfObjects = false;
1194
+ listKey = null;
1195
+ currentListItem = null;
1196
+ listItems = [];
1197
+ // Fall through to process current line normally
1198
+ }
1199
+
1200
+ // --- Multi-line continuation (indented lines following "key: >") ---
1201
+ if (inMultiline) {
1202
+ if (line.match(/^\s{2,}/) && !line.match(/^\S/)) {
1203
+ multilineValue.push(line.trim());
1204
+ continue;
1205
+ } else {
1206
+ // End multi-line
1207
+ if (currentKey) {
1208
+ result[currentKey] = multilineValue.join(' ');
1209
+ }
1210
+ inMultiline = false;
1211
+ currentKey = null;
1212
+ multilineValue = [];
1213
+ }
1214
+ }
1215
+
1216
+ const kvMatch = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)/);
1217
+ if (kvMatch) {
1218
+ const key = kvMatch[1];
1219
+ const rawVal = kvMatch[2].trim();
1220
+
1221
+ if (rawVal === '>') {
1222
+ currentKey = key;
1223
+ inMultiline = true;
1224
+ multilineValue = [];
1225
+ continue;
1226
+ }
1227
+
1228
+ // Empty value followed by " - " lines = list-of-objects
1229
+ if (rawVal === '') {
1230
+ // Peek ahead to see if next non-empty line starts with " -"
1231
+ let peekIdx = lineIdx + 1;
1232
+ while (peekIdx < lines.length && lines[peekIdx].trim() === '') peekIdx++;
1233
+ if (peekIdx < lines.length && /^\s{2,}-\s/.test(lines[peekIdx])) {
1234
+ inListOfObjects = true;
1235
+ listKey = key;
1236
+ currentListItem = null;
1237
+ listItems = [];
1238
+ continue;
1239
+ }
1240
+ // Otherwise treat as empty scalar
1241
+ result[key] = '';
1242
+ continue;
1243
+ }
1244
+
1245
+ // Array: [a, b, c]
1246
+ if (rawVal.startsWith('[') && rawVal.endsWith(']')) {
1247
+ result[key] = rawVal
1248
+ .slice(1, -1)
1249
+ .split(',')
1250
+ .map((s) => s.trim())
1251
+ .filter(Boolean);
1252
+ continue;
1253
+ }
1254
+
1255
+ // Scalar
1256
+ result[key] = parseConfigValue(rawVal);
1257
+ }
1258
+ }
1259
+
1260
+ // Flush any remaining multi-line
1261
+ if (inMultiline && currentKey) {
1262
+ result[currentKey] = multilineValue.join(' ');
1263
+ }
1264
+
1265
+ // Flush any remaining list-of-objects
1266
+ if (inListOfObjects && listKey) {
1267
+ if (currentListItem) listItems.push(currentListItem);
1268
+ result[listKey] = listItems;
1269
+ }
1270
+
1271
+ return result;
1272
+ }
1273
+
1274
+ function extractBody(markdown: string): string {
1275
+ const fmEnd = markdown.indexOf('---', 3);
1276
+ if (fmEnd === -1) return markdown;
1277
+ return markdown.slice(fmEnd + 3).trim();
1278
+ }
1279
+
1280
+ function extractTraitsFromMd(body: string): SkillTraitDecl[] {
1281
+ const traits: SkillTraitDecl[] = [];
1282
+ const traitsSection = extractSection(body, 'Traits');
1283
+ if (!traitsSection) return traits;
1284
+
1285
+ const traitRegex = /`@(\w+)`(?:\s*\(([^)]+)\))?/g;
1286
+ let match: RegExpExecArray | null;
1287
+ while ((match = traitRegex.exec(traitsSection)) !== null) {
1288
+ const config: Record<string, unknown> = {};
1289
+ if (match[2]) {
1290
+ parseTraitConfig(match[2], config);
1291
+ }
1292
+ traits.push({ name: match[1], config });
1293
+ }
1294
+ return traits;
1295
+ }
1296
+
1297
+ function extractStateFromMd(body: string): SkillStateVar[] {
1298
+ const vars: SkillStateVar[] = [];
1299
+ const stateSection = extractSection(body, 'State Schema');
1300
+ if (!stateSection) return vars;
1301
+
1302
+ // Parse markdown table rows
1303
+ const rowRegex = /\|\s*`(\w+)`\s*\|\s*(\w+)\s*\|\s*`([^`]+)`\s*\|/g;
1304
+ let match: RegExpExecArray | null;
1305
+ while ((match = rowRegex.exec(stateSection)) !== null) {
1306
+ const name = match[1];
1307
+ const type = match[2];
1308
+ const rawDefault = match[3];
1309
+ vars.push({
1310
+ name,
1311
+ type,
1312
+ defaultValue: parseStateDefault(rawDefault, type),
1313
+ });
1314
+ }
1315
+ return vars;
1316
+ }
1317
+
1318
+ function extractStepsFromMd(body: string): SkillActionStep[] {
1319
+ const steps: SkillActionStep[] = [];
1320
+ const workflowSection = extractSection(body, 'Workflow');
1321
+ if (!workflowSection) return steps;
1322
+
1323
+ // Parse ### subsections as sequences
1324
+ const subsectionRegex = /###\s+(.+)/g;
1325
+ let match: RegExpExecArray | null;
1326
+ while ((match = subsectionRegex.exec(workflowSection)) !== null) {
1327
+ steps.push({
1328
+ action: match[1].trim().toLowerCase().replace(/\s+/g, '-'),
1329
+ description: match[1].trim(),
1330
+ params: {},
1331
+ nodeType: 'sequence',
1332
+ });
1333
+ }
1334
+
1335
+ // Parse numbered list items as actions
1336
+ const stepRegex = /\d+\.\s+\*\*([^*]+)\*\*(?:\s*\(([^)]+)\))?(?:\s*\n\s+(.+))?/g;
1337
+ while ((match = stepRegex.exec(workflowSection)) !== null) {
1338
+ const params: Record<string, unknown> = {};
1339
+ if (match[2]) {
1340
+ parseTraitConfig(match[2], params);
1341
+ }
1342
+ steps.push({
1343
+ action: match[1].trim(),
1344
+ description: match[3]?.trim() || match[1].trim().replace(/_/g, ' '),
1345
+ params,
1346
+ nodeType: 'action',
1347
+ });
1348
+ }
1349
+
1350
+ return steps;
1351
+ }
1352
+
1353
+ function extractTestsFromMd(body: string): SkillTest[] {
1354
+ const tests: SkillTest[] = [];
1355
+ const testsSection = extractSection(body, 'Tests');
1356
+ if (!testsSection) return tests;
1357
+
1358
+ // Parse "- **name**: `assertion`"
1359
+ const testRegex = /-\s+\*\*([^*]+)\*\*\s*:\s*`([^`]+)`/g;
1360
+ let match: RegExpExecArray | null;
1361
+ while ((match = testRegex.exec(testsSection)) !== null) {
1362
+ tests.push({
1363
+ name: match[1].trim(),
1364
+ assert: match[2].trim(),
1365
+ });
1366
+ }
1367
+ return tests;
1368
+ }
1369
+
1370
+ function extractEnvironmentFromMd(body: string): Record<string, unknown> | null {
1371
+ const envSection = extractSection(body, 'Environment');
1372
+ if (!envSection) return null;
1373
+
1374
+ const config: Record<string, unknown> = {};
1375
+ // Parse YAML-like code block
1376
+ const codeBlockMatch = envSection.match(/```(?:yaml)?\s*\n([\s\S]*?)```/);
1377
+ if (codeBlockMatch) {
1378
+ const parsed = parseSimpleYaml(codeBlockMatch[1]);
1379
+ for (const [k, v] of Object.entries(parsed)) {
1380
+ config[k] = v;
1381
+ }
1382
+ }
1383
+ return Object.keys(config).length > 0 ? config : null;
1384
+ }
1385
+
1386
+ /**
1387
+ * Extract content of a specific ## section from markdown body.
1388
+ */
1389
+ function extractSection(body: string, heading: string): string | null {
1390
+ const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1391
+ const sectionRegex = new RegExp(`##\\s+${escapedHeading}\\s*\n([\\s\\S]*?)(?=\\n##\\s|$)`);
1392
+ const match = body.match(sectionRegex);
1393
+ return match ? match[1].trim() : null;
1394
+ }
1395
+
1396
+ /**
1397
+ * Parse a schema list from YAML frontmatter (input_schema / output_schema).
1398
+ * The simple YAML parser stores these as raw strings; we parse them here.
1399
+ *
1400
+ * Frontmatter format:
1401
+ * input_schema:
1402
+ * - name: url
1403
+ * type: string
1404
+ * required: true
1405
+ * description: URL to fetch
1406
+ */
1407
+ function parseFrontmatterSchemaList(raw: unknown): SchemaField[] {
1408
+ if (!raw) return [];
1409
+ // If already parsed as array (by enhanced YAML parser), use directly
1410
+ if (Array.isArray(raw)) {
1411
+ return raw
1412
+ .filter(
1413
+ (item: unknown) =>
1414
+ item && typeof item === 'object' && 'name' in (item as Record<string, unknown>)
1415
+ )
1416
+ .map((item: Record<string, unknown>) => ({
1417
+ name: String(item.name || ''),
1418
+ type: String(item.type || 'string') as SchemaField['type'],
1419
+ required: item.required === true || item.required === 'true' || undefined,
1420
+ description: String(item.description || item.name || ''),
1421
+ default: item.default,
1422
+ }));
1423
+ }
1424
+ return [];
1425
+ }
1426
+
1427
+ /**
1428
+ * Extract Input Schema from markdown body (## Input Schema table).
1429
+ * Table format: | `name` | type | yes/no | description |
1430
+ */
1431
+ function extractInputSchemaFromMd(body: string): SchemaField[] {
1432
+ const fields: SchemaField[] = [];
1433
+ const section = extractSection(body, 'Input Schema');
1434
+ if (!section) return fields;
1435
+
1436
+ const rowRegex = /\|\s*`(\w+)`\s*\|\s*(\w+)\s*\|\s*(yes|no)\s*\|\s*([^|]+)\|/g;
1437
+ let match: RegExpExecArray | null;
1438
+ while ((match = rowRegex.exec(section)) !== null) {
1439
+ const name = match[1];
1440
+ const type = match[2] as SchemaField['type'];
1441
+ const required = match[3] === 'yes';
1442
+ let description = match[4].trim();
1443
+ // Extract default if present: (default: `value`)
1444
+ let defaultVal: unknown;
1445
+ const defaultMatch = description.match(/\(default:\s*`([^`]+)`\)/);
1446
+ if (defaultMatch) {
1447
+ defaultVal = parseConfigValue(defaultMatch[1]);
1448
+ description = description.replace(/\s*\(default:\s*`[^`]+`\)/, '').trim();
1449
+ }
1450
+ fields.push({ name, type, required: required || undefined, description, default: defaultVal });
1451
+ }
1452
+ return fields;
1453
+ }
1454
+
1455
+ /**
1456
+ * Extract Output Schema from markdown body (## Output Schema table).
1457
+ * Table format: | `name` | type | description |
1458
+ */
1459
+ function extractOutputSchemaFromMd(body: string): SchemaField[] {
1460
+ const fields: SchemaField[] = [];
1461
+ const section = extractSection(body, 'Output Schema');
1462
+ if (!section) return fields;
1463
+
1464
+ const rowRegex = /\|\s*`(\w+)`\s*\|\s*(\w+)\s*\|\s*([^|]+)\|/g;
1465
+ let match: RegExpExecArray | null;
1466
+ while ((match = rowRegex.exec(section)) !== null) {
1467
+ const name = match[1];
1468
+ const type = match[2] as SchemaField['type'];
1469
+ const description = match[3].trim();
1470
+ // Skip header separator rows
1471
+ if (name === '-------' || name === '------' || description === '---') continue;
1472
+ fields.push({ name, type, description });
1473
+ }
1474
+ return fields;
1475
+ }
1476
+
1477
+ // =============================================================================
1478
+ // INTERNAL HELPERS: .hsplus GENERATION
1479
+ // =============================================================================
1480
+
1481
+ function getDefaultTraits(): SkillTraitDecl[] {
1482
+ return [
1483
+ { name: 'rate_limiter', config: {} },
1484
+ { name: 'economy', config: { default_spend_limit: 0.1 } },
1485
+ { name: 'timeout_guard', config: {} },
1486
+ ];
1487
+ }
1488
+
1489
+ function formatHsplusValue(value: unknown): string {
1490
+ if (typeof value === 'string') return `"${value}"`;
1491
+ if (typeof value === 'number') return String(value);
1492
+ if (typeof value === 'boolean') return String(value);
1493
+ if (Array.isArray(value)) return `[${value.map(formatHsplusValue).join(', ')}]`;
1494
+ if (value === null || value === undefined) return '""';
1495
+ return JSON.stringify(value);
1496
+ }
1497
+
1498
+ function titleCase(kebab: string): string {
1499
+ return kebab
1500
+ .split('-')
1501
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1502
+ .join(' ');
1503
+ }
1504
+
1505
+ interface BTTreeNode {
1506
+ type: 'sequence' | 'selector' | 'action';
1507
+ name: string;
1508
+ description: string;
1509
+ params: Record<string, unknown>;
1510
+ children: BTTreeNode[];
1511
+ }
1512
+
1513
+ function groupStepsIntoSequences(steps: SkillActionStep[]): BTTreeNode[] {
1514
+ const nodes: BTTreeNode[] = [];
1515
+ let currentParent: BTTreeNode | null = null;
1516
+
1517
+ for (const step of steps) {
1518
+ if (step.nodeType === 'sequence' || step.nodeType === 'selector') {
1519
+ currentParent = {
1520
+ type: step.nodeType,
1521
+ name: step.action,
1522
+ description: step.description,
1523
+ params: {},
1524
+ children: [],
1525
+ };
1526
+ nodes.push(currentParent);
1527
+ } else {
1528
+ const actionNode: BTTreeNode = {
1529
+ type: 'action',
1530
+ name: step.action,
1531
+ description: step.description,
1532
+ params: step.params,
1533
+ children: [],
1534
+ };
1535
+ if (currentParent) {
1536
+ currentParent.children.push(actionNode);
1537
+ } else {
1538
+ // No parent sequence -- create an implicit one
1539
+ if (nodes.length === 0 || nodes[nodes.length - 1].type === 'action') {
1540
+ const implicitSeq: BTTreeNode = {
1541
+ type: 'sequence',
1542
+ name: 'main',
1543
+ description: 'main',
1544
+ params: {},
1545
+ children: [actionNode],
1546
+ };
1547
+ nodes.push(implicitSeq);
1548
+ currentParent = implicitSeq;
1549
+ } else {
1550
+ nodes[nodes.length - 1].children.push(actionNode);
1551
+ }
1552
+ }
1553
+ }
1554
+ }
1555
+
1556
+ // If all steps are actions with no sequence wrapper, wrap them
1557
+ if (nodes.length === 0 && steps.length > 0) {
1558
+ const seq: BTTreeNode = {
1559
+ type: 'sequence',
1560
+ name: 'main',
1561
+ description: 'main',
1562
+ params: {},
1563
+ children: steps.map((s) => ({
1564
+ type: 'action' as const,
1565
+ name: s.action,
1566
+ description: s.description,
1567
+ params: s.params,
1568
+ children: [],
1569
+ })),
1570
+ };
1571
+ nodes.push(seq);
1572
+ }
1573
+
1574
+ return nodes;
1575
+ }
1576
+
1577
+ function emitBTNode(lines: string[], node: BTTreeNode, indent: number): void {
1578
+ const pad = ' '.repeat(indent);
1579
+ if (node.type === 'sequence' || node.type === 'selector') {
1580
+ lines.push(`${pad}${node.type} "${node.name}" {`);
1581
+ for (const child of node.children) {
1582
+ emitBTNode(lines, child, indent + 2);
1583
+ }
1584
+ lines.push(`${pad}}`);
1585
+ } else {
1586
+ lines.push(`${pad}action "${node.name}" {`);
1587
+ if (node.description && node.description !== node.name.replace(/_/g, ' ')) {
1588
+ lines.push(`${pad} // ${node.description}`);
1589
+ }
1590
+ for (const [key, val] of Object.entries(node.params)) {
1591
+ lines.push(`${pad} ${key}: ${formatHsplusValue(val)}`);
1592
+ }
1593
+ lines.push(`${pad}}`);
1594
+ }
1595
+ lines.push('');
1596
+ }
1597
+
1598
+ // =============================================================================
1599
+ // HOLOCLAW SKILL INTEROP
1600
+ // =============================================================================
1601
+
1602
+ /**
1603
+ * HoloClaw Skill interface — mirrors SkillRegistryTrait.Skill without the
1604
+ * execute function (which cannot be serialized). This is the shape used for
1605
+ * registry listing and interchange.
1606
+ */
1607
+ export interface HoloClawSkill {
1608
+ id: string;
1609
+ name: string;
1610
+ description: string;
1611
+ version: string;
1612
+ author: string;
1613
+ inputs: HoloClawSkillInput[];
1614
+ outputs: HoloClawSkillOutput[];
1615
+ sandbox: boolean;
1616
+ }
1617
+
1618
+ export interface HoloClawSkillInput {
1619
+ name: string;
1620
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
1621
+ required: boolean;
1622
+ description: string;
1623
+ default?: unknown;
1624
+ }
1625
+
1626
+ export interface HoloClawSkillOutput {
1627
+ name: string;
1628
+ type: string;
1629
+ description: string;
1630
+ }
1631
+
1632
+ /**
1633
+ * Convert a ParsedSkill (bridge intermediate) to a HoloClaw Skill object.
1634
+ * This enables skills parsed from SKILL.md to be registered in the HoloClaw
1635
+ * SkillRegistry runtime (minus the execute function, which must be provided
1636
+ * separately or wired via a BT executor).
1637
+ */
1638
+ export function toHoloClawSkill(parsed: ParsedSkill): HoloClawSkill {
1639
+ return {
1640
+ id: parsed.metadata.name,
1641
+ name: titleCase(parsed.metadata.name),
1642
+ description: parsed.metadata.description,
1643
+ version: parsed.metadata.version,
1644
+ author: parsed.metadata.author,
1645
+ inputs: (parsed.metadata.inputSchema || []).map((f) => ({
1646
+ name: f.name,
1647
+ type: f.type,
1648
+ required: f.required ?? false,
1649
+ description: f.description,
1650
+ default: f.default,
1651
+ })),
1652
+ outputs: (parsed.metadata.outputSchema || []).map((f) => ({
1653
+ name: f.name,
1654
+ type: f.type,
1655
+ description: f.description,
1656
+ })),
1657
+ sandbox: true,
1658
+ };
1659
+ }
1660
+
1661
+ /**
1662
+ * Convert a HoloClaw Skill object to a ParsedSkill (bridge intermediate).
1663
+ * This enables HoloClaw runtime skills to be serialized as SKILL.md or .hsplus
1664
+ * for distribution via ClawHub.
1665
+ */
1666
+ export function fromHoloClawSkill(skill: HoloClawSkill): ParsedSkill {
1667
+ const inputSchema: SchemaField[] = skill.inputs.map((i) => ({
1668
+ name: i.name,
1669
+ type: i.type,
1670
+ required: i.required || undefined,
1671
+ description: i.description,
1672
+ default: i.default,
1673
+ }));
1674
+
1675
+ const outputSchema: SchemaField[] = skill.outputs.map((o) => ({
1676
+ name: o.name,
1677
+ type: o.type as SchemaField['type'],
1678
+ description: o.description,
1679
+ }));
1680
+
1681
+ return {
1682
+ metadata: {
1683
+ name: skill.id,
1684
+ description: skill.description,
1685
+ version: skill.version,
1686
+ author: skill.author,
1687
+ inputSchema: inputSchema.length > 0 ? inputSchema : undefined,
1688
+ outputSchema: outputSchema.length > 0 ? outputSchema : undefined,
1689
+ holoCliVersion: '5.0.0',
1690
+ nodeVersion: '20',
1691
+ userInvocable: true,
1692
+ },
1693
+ traits: getDefaultTraits(),
1694
+ state: [],
1695
+ steps: [],
1696
+ tests: [],
1697
+ sourceComments: [],
1698
+ };
1699
+ }