hive-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +235 -0
  3. package/assets/LICENSE +32 -0
  4. package/assets/THIRD_PARTY_NOTICES.md +72 -0
  5. package/assets/manifest.json +1290 -0
  6. package/assets/skills/authored/code-review/composable/BUNDLE.md +192 -0
  7. package/assets/skills/authored/code-review/composable/INDEX.md +13 -0
  8. package/assets/skills/authored/code-review/composable/VERSION +1 -0
  9. package/assets/skills/authored/code-review/composable/mini/01-review-method.md +34 -0
  10. package/assets/skills/authored/code-review/composable/mini/02-security-review.md +20 -0
  11. package/assets/skills/authored/code-review/composable/mini/03-correctness-bugs.md +19 -0
  12. package/assets/skills/authored/code-review/composable/mini/04-concurrency.md +16 -0
  13. package/assets/skills/authored/code-review/composable/mini/05-error-handling.md +12 -0
  14. package/assets/skills/authored/code-review/composable/mini/06-performance.md +11 -0
  15. package/assets/skills/authored/code-review/composable/mini/07-api-design.md +20 -0
  16. package/assets/skills/authored/code-review/composable/mini/08-missing-tests.md +25 -0
  17. package/assets/skills/authored/code-review/composable/mini/09-refactoring-and-communication.md +32 -0
  18. package/assets/skills/authored/data-analysis/composable/BUNDLE.md +186 -0
  19. package/assets/skills/authored/data-analysis/composable/INDEX.md +13 -0
  20. package/assets/skills/authored/data-analysis/composable/VERSION +1 -0
  21. package/assets/skills/authored/data-analysis/composable/mini/01-data-quality-profiling.md +26 -0
  22. package/assets/skills/authored/data-analysis/composable/mini/02-aggregation-pitfalls.md +21 -0
  23. package/assets/skills/authored/data-analysis/composable/mini/03-time-series.md +19 -0
  24. package/assets/skills/authored/data-analysis/composable/mini/04-segmentation.md +23 -0
  25. package/assets/skills/authored/data-analysis/composable/mini/05-discount-pricing.md +17 -0
  26. package/assets/skills/authored/data-analysis/composable/mini/06-insight-generation.md +25 -0
  27. package/assets/skills/authored/data-analysis/composable/mini/07-executive-writing.md +26 -0
  28. package/assets/skills/authored/data-analysis/composable/mini/08-recommendations.md +20 -0
  29. package/assets/skills/authored/data-analysis/composable/mini/09-reproducibility-assumptions.md +20 -0
  30. package/assets/skills/authored/financial-analysis/composable/BUNDLE.md +1293 -0
  31. package/assets/skills/authored/financial-analysis/composable/INDEX.md +17 -0
  32. package/assets/skills/authored/financial-analysis/composable/VERSION +1 -0
  33. package/assets/skills/authored/financial-analysis/composable/mini/00-core.md +69 -0
  34. package/assets/skills/authored/financial-analysis/composable/mini/01-profitability-efficiency-ratios.md +173 -0
  35. package/assets/skills/authored/financial-analysis/composable/mini/02-liquidity-leverage-ratios.md +120 -0
  36. package/assets/skills/authored/financial-analysis/composable/mini/03-valuation-ratios-interpretation.md +107 -0
  37. package/assets/skills/authored/financial-analysis/composable/mini/04-dcf-projection-wacc.md +122 -0
  38. package/assets/skills/authored/financial-analysis/composable/mini/05-dcf-terminal-value-sensitivity.md +75 -0
  39. package/assets/skills/authored/financial-analysis/composable/mini/06-comparables-precedents.md +78 -0
  40. package/assets/skills/authored/financial-analysis/composable/mini/07-budget-variance-analysis.md +47 -0
  41. package/assets/skills/authored/financial-analysis/composable/mini/08-driver-based-forecasting-scenarios.md +111 -0
  42. package/assets/skills/authored/financial-analysis/composable/mini/09-rolling-forecasts-accuracy.md +114 -0
  43. package/assets/skills/authored/financial-analysis/composable/mini/10-reporting-exec-summary.md +58 -0
  44. package/assets/skills/authored/financial-analysis/composable/mini/11-industry-adaptations.md +130 -0
  45. package/assets/skills/authored/financial-analysis/composable/mini/12-toolkit-scripts-io.md +102 -0
  46. package/assets/skills/authored/python-api/composable/BUNDLE.md +389 -0
  47. package/assets/skills/authored/python-api/composable/INDEX.md +13 -0
  48. package/assets/skills/authored/python-api/composable/VERSION +1 -0
  49. package/assets/skills/authored/python-api/composable/mini/01-project-structure.md +44 -0
  50. package/assets/skills/authored/python-api/composable/mini/02-routing-and-app.md +38 -0
  51. package/assets/skills/authored/python-api/composable/mini/03-pydantic-models.md +45 -0
  52. package/assets/skills/authored/python-api/composable/mini/04-validation-and-errors.md +59 -0
  53. package/assets/skills/authored/python-api/composable/mini/05-dependencies-and-auth.md +60 -0
  54. package/assets/skills/authored/python-api/composable/mini/06-pagination-filtering.md +44 -0
  55. package/assets/skills/authored/python-api/composable/mini/07-async-performance.md +30 -0
  56. package/assets/skills/authored/python-api/composable/mini/08-testing.md +45 -0
  57. package/assets/skills/authored/python-api/composable/mini/09-middleware-observability.md +29 -0
  58. package/assets/skills/authored/tech-writing/composable/BUNDLE.md +220 -0
  59. package/assets/skills/authored/tech-writing/composable/INDEX.md +12 -0
  60. package/assets/skills/authored/tech-writing/composable/VERSION +1 -0
  61. package/assets/skills/authored/tech-writing/composable/mini/01-audience-analysis.md +26 -0
  62. package/assets/skills/authored/tech-writing/composable/mini/02-document-types.md +27 -0
  63. package/assets/skills/authored/tech-writing/composable/mini/03-readme-quickstart-changelog.md +22 -0
  64. package/assets/skills/authored/tech-writing/composable/mini/04-information-architecture.md +21 -0
  65. package/assets/skills/authored/tech-writing/composable/mini/05-style-clarity.md +29 -0
  66. package/assets/skills/authored/tech-writing/composable/mini/06-breaking-changes-migrations.md +33 -0
  67. package/assets/skills/authored/tech-writing/composable/mini/07-troubleshooting-security.md +30 -0
  68. package/assets/skills/authored/tech-writing/composable/mini/08-faq-voice-tone.md +32 -0
  69. package/assets/skills/converted/claude-api/composable/BUNDLE.md +14996 -0
  70. package/assets/skills/converted/claude-api/composable/INDEX.md +98 -0
  71. package/assets/skills/converted/claude-api/composable/VERSION +1 -0
  72. package/assets/skills/converted/claude-api/composable/mini/00-core.md +235 -0
  73. package/assets/skills/converted/claude-api/composable/mini/01-model-api-quick-reference.md +258 -0
  74. package/assets/skills/converted/claude-api/composable/mini/02-common-pitfalls.md +42 -0
  75. package/assets/skills/converted/claude-api/composable/mini/10-models.md +135 -0
  76. package/assets/skills/converted/claude-api/composable/mini/11-platform-availability.md +96 -0
  77. package/assets/skills/converted/claude-api/composable/mini/12-claude-platform-on-aws.md +59 -0
  78. package/assets/skills/converted/claude-api/composable/mini/13-live-sources.md +143 -0
  79. package/assets/skills/converted/claude-api/composable/mini/14-error-codes.md +256 -0
  80. package/assets/skills/converted/claude-api/composable/mini/15-prompt-caching.md +223 -0
  81. package/assets/skills/converted/claude-api/composable/mini/16-token-counting.md +56 -0
  82. package/assets/skills/converted/claude-api/composable/mini/17-tool-use-concepts.md +444 -0
  83. package/assets/skills/converted/claude-api/composable/mini/18-agent-design.md +101 -0
  84. package/assets/skills/converted/claude-api/composable/mini/19-anthropic-cli.md +246 -0
  85. package/assets/skills/converted/claude-api/composable/mini/20-model-migration.md +1301 -0
  86. package/assets/skills/converted/claude-api/composable/mini/30-managed-agents-overview.md +71 -0
  87. package/assets/skills/converted/claude-api/composable/mini/31-managed-agents-core.md +278 -0
  88. package/assets/skills/converted/claude-api/composable/mini/32-managed-agents-environments.md +219 -0
  89. package/assets/skills/converted/claude-api/composable/mini/33-managed-agents-tools.md +366 -0
  90. package/assets/skills/converted/claude-api/composable/mini/34-managed-agents-events.md +252 -0
  91. package/assets/skills/converted/claude-api/composable/mini/35-managed-agents-outcomes.md +106 -0
  92. package/assets/skills/converted/claude-api/composable/mini/36-managed-agents-multiagent.md +101 -0
  93. package/assets/skills/converted/claude-api/composable/mini/37-managed-agents-webhooks.md +123 -0
  94. package/assets/skills/converted/claude-api/composable/mini/38-managed-agents-memory.md +197 -0
  95. package/assets/skills/converted/claude-api/composable/mini/39-managed-agents-scheduled-deployments.md +146 -0
  96. package/assets/skills/converted/claude-api/composable/mini/40-managed-agents-client-patterns.md +211 -0
  97. package/assets/skills/converted/claude-api/composable/mini/41-managed-agents-onboarding.md +82 -0
  98. package/assets/skills/converted/claude-api/composable/mini/42-managed-agents-api-reference.md +441 -0
  99. package/assets/skills/converted/claude-api/composable/mini/43-managed-agents-self-hosted-sandboxes.md +174 -0
  100. package/assets/skills/converted/claude-api/composable/mini/50-python-readme.md +564 -0
  101. package/assets/skills/converted/claude-api/composable/mini/51-python-tool-use.md +590 -0
  102. package/assets/skills/converted/claude-api/composable/mini/52-python-streaming-batches-files.md +549 -0
  103. package/assets/skills/converted/claude-api/composable/mini/53-python-managed-agents.md +335 -0
  104. package/assets/skills/converted/claude-api/composable/mini/55-typescript-readme.md +404 -0
  105. package/assets/skills/converted/claude-api/composable/mini/56-typescript-tool-use.md +548 -0
  106. package/assets/skills/converted/claude-api/composable/mini/57-typescript-streaming-batches-files.md +384 -0
  107. package/assets/skills/converted/claude-api/composable/mini/58-typescript-managed-agents.md +358 -0
  108. package/assets/skills/converted/claude-api/composable/mini/60-csharp-readme.md +361 -0
  109. package/assets/skills/converted/claude-api/composable/mini/61-csharp-tool-use.md +163 -0
  110. package/assets/skills/converted/claude-api/composable/mini/62-csharp-streaming-batches-files.md +64 -0
  111. package/assets/skills/converted/claude-api/composable/mini/65-go-readme.md +185 -0
  112. package/assets/skills/converted/claude-api/composable/mini/66-go-tool-use.md +219 -0
  113. package/assets/skills/converted/claude-api/composable/mini/67-go-streaming-files.md +63 -0
  114. package/assets/skills/converted/claude-api/composable/mini/68-go-managed-agents.md +564 -0
  115. package/assets/skills/converted/claude-api/composable/mini/70-java-readme.md +237 -0
  116. package/assets/skills/converted/claude-api/composable/mini/71-java-tool-use.md +227 -0
  117. package/assets/skills/converted/claude-api/composable/mini/72-java-streaming-files.md +49 -0
  118. package/assets/skills/converted/claude-api/composable/mini/73-java-managed-agents.md +443 -0
  119. package/assets/skills/converted/claude-api/composable/mini/75-php-readme.md +173 -0
  120. package/assets/skills/converted/claude-api/composable/mini/76-php-tool-use.md +252 -0
  121. package/assets/skills/converted/claude-api/composable/mini/77-php-streaming-batches-files.md +53 -0
  122. package/assets/skills/converted/claude-api/composable/mini/78-php-managed-agents.md +443 -0
  123. package/assets/skills/converted/claude-api/composable/mini/80-ruby-readme.md +133 -0
  124. package/assets/skills/converted/claude-api/composable/mini/81-ruby-tool-use-streaming.md +56 -0
  125. package/assets/skills/converted/claude-api/composable/mini/82-ruby-managed-agents.md +394 -0
  126. package/assets/skills/converted/claude-api/composable/mini/85-curl-examples.md +255 -0
  127. package/assets/skills/converted/claude-api/composable/mini/86-curl-managed-agents.md +340 -0
  128. package/assets/skills/converted/claude-api/composable/presets/csharp.md +1151 -0
  129. package/assets/skills/converted/claude-api/composable/presets/curl.md +1154 -0
  130. package/assets/skills/converted/claude-api/composable/presets/go.md +1598 -0
  131. package/assets/skills/converted/claude-api/composable/presets/java.md +1523 -0
  132. package/assets/skills/converted/claude-api/composable/presets/php.md +1488 -0
  133. package/assets/skills/converted/claude-api/composable/presets/python.md +2605 -0
  134. package/assets/skills/converted/claude-api/composable/presets/ruby.md +1146 -0
  135. package/assets/skills/converted/claude-api/composable/presets/shared.md +6478 -0
  136. package/assets/skills/converted/claude-api/composable/presets/typescript.md +2261 -0
  137. package/assets/skills/converted/docx/composable/BUNDLE.md +617 -0
  138. package/assets/skills/converted/docx/composable/INDEX.md +12 -0
  139. package/assets/skills/converted/docx/composable/VERSION +1 -0
  140. package/assets/skills/converted/docx/composable/mini/00-core.md +26 -0
  141. package/assets/skills/converted/docx/composable/mini/01-reading-converting.md +36 -0
  142. package/assets/skills/converted/docx/composable/mini/02-creating-setup-layout.md +85 -0
  143. package/assets/skills/converted/docx/composable/mini/03-creating-elements.md +257 -0
  144. package/assets/skills/converted/docx/composable/mini/04-editing-existing.md +57 -0
  145. package/assets/skills/converted/docx/composable/mini/05-xml-reference.md +128 -0
  146. package/assets/skills/converted/internal-comms/composable/BUNDLE.md +220 -0
  147. package/assets/skills/converted/internal-comms/composable/INDEX.md +11 -0
  148. package/assets/skills/converted/internal-comms/composable/VERSION +1 -0
  149. package/assets/skills/converted/internal-comms/composable/mini/00-core.md +30 -0
  150. package/assets/skills/converted/internal-comms/composable/mini/01-3p-updates.md +49 -0
  151. package/assets/skills/converted/internal-comms/composable/mini/02-company-newsletter.md +67 -0
  152. package/assets/skills/converted/internal-comms/composable/mini/03-faq-answers.md +32 -0
  153. package/assets/skills/converted/internal-comms/composable/mini/04-general-comms.md +18 -0
  154. package/assets/skills/converted/mcp-builder/composable/BUNDLE.md +2460 -0
  155. package/assets/skills/converted/mcp-builder/composable/INDEX.md +23 -0
  156. package/assets/skills/converted/mcp-builder/composable/VERSION +1 -0
  157. package/assets/skills/converted/mcp-builder/composable/mini/00-core.md +43 -0
  158. package/assets/skills/converted/mcp-builder/composable/mini/01-workflow-research-planning.md +54 -0
  159. package/assets/skills/converted/mcp-builder/composable/mini/02-implementation-process.md +78 -0
  160. package/assets/skills/converted/mcp-builder/composable/mini/03-naming-conventions.md +33 -0
  161. package/assets/skills/converted/mcp-builder/composable/mini/04-response-formats.md +21 -0
  162. package/assets/skills/converted/mcp-builder/composable/mini/05-pagination.md +24 -0
  163. package/assets/skills/converted/mcp-builder/composable/mini/06-transport.md +47 -0
  164. package/assets/skills/converted/mcp-builder/composable/mini/07-annotations.md +20 -0
  165. package/assets/skills/converted/mcp-builder/composable/mini/08-security.md +35 -0
  166. package/assets/skills/converted/mcp-builder/composable/mini/09-error-handling.md +32 -0
  167. package/assets/skills/converted/mcp-builder/composable/mini/10-testing-and-documentation.md +21 -0
  168. package/assets/skills/converted/mcp-builder/composable/mini/11-python-implementation.md +432 -0
  169. package/assets/skills/converted/mcp-builder/composable/mini/12-python-advanced-and-checklist.md +233 -0
  170. package/assets/skills/converted/mcp-builder/composable/mini/13-node-implementation.md +717 -0
  171. package/assets/skills/converted/mcp-builder/composable/mini/14-node-advanced-and-checklist.md +200 -0
  172. package/assets/skills/converted/mcp-builder/composable/mini/15-evaluation-design.md +200 -0
  173. package/assets/skills/converted/mcp-builder/composable/mini/16-evaluation-running.md +198 -0
  174. package/assets/skills/converted/mcp-builder/composable/presets/node-server.md +1381 -0
  175. package/assets/skills/converted/mcp-builder/composable/presets/python-server.md +1129 -0
  176. package/assets/skills/converted/pdf/composable/BUNDLE.md +1284 -0
  177. package/assets/skills/converted/pdf/composable/INDEX.md +14 -0
  178. package/assets/skills/converted/pdf/composable/VERSION +1 -0
  179. package/assets/skills/converted/pdf/composable/mini/00-core.md +46 -0
  180. package/assets/skills/converted/pdf/composable/mini/01-creation.md +176 -0
  181. package/assets/skills/converted/pdf/composable/mini/02-extraction.md +178 -0
  182. package/assets/skills/converted/pdf/composable/mini/03-merge-split-manipulate.md +240 -0
  183. package/assets/skills/converted/pdf/composable/mini/04-images-ocr.md +168 -0
  184. package/assets/skills/converted/pdf/composable/mini/05-encryption.md +51 -0
  185. package/assets/skills/converted/pdf/composable/mini/06-forms.md +300 -0
  186. package/assets/skills/converted/pdf/composable/mini/07-libraries-reference.md +89 -0
  187. package/assets/skills/converted/pptx/composable/BUNDLE.md +911 -0
  188. package/assets/skills/converted/pptx/composable/INDEX.md +12 -0
  189. package/assets/skills/converted/pptx/composable/VERSION +1 -0
  190. package/assets/skills/converted/pptx/composable/mini/00-core.md +37 -0
  191. package/assets/skills/converted/pptx/composable/mini/01-reading.md +37 -0
  192. package/assets/skills/converted/pptx/composable/mini/02-creating-from-scratch.md +422 -0
  193. package/assets/skills/converted/pptx/composable/mini/03-editing-templates.md +207 -0
  194. package/assets/skills/converted/pptx/composable/mini/04-design.md +89 -0
  195. package/assets/skills/converted/pptx/composable/mini/05-qa-and-rendering.md +91 -0
  196. package/assets/skills/converted/pptx/composable/presets/creating.md +659 -0
  197. package/assets/skills/converted/pptx/composable/presets/editing.md +485 -0
  198. package/assets/skills/converted/skill-creator/composable/BUNDLE.md +1681 -0
  199. package/assets/skills/converted/skill-creator/composable/INDEX.md +21 -0
  200. package/assets/skills/converted/skill-creator/composable/VERSION +1 -0
  201. package/assets/skills/converted/skill-creator/composable/mini/00-core.md +59 -0
  202. package/assets/skills/converted/skill-creator/composable/mini/01-creating-a-skill.md +27 -0
  203. package/assets/skills/converted/skill-creator/composable/mini/02-skill-writing-guide.md +71 -0
  204. package/assets/skills/converted/skill-creator/composable/mini/03-test-cases.md +21 -0
  205. package/assets/skills/converted/skill-creator/composable/mini/04-running-and-evaluating.md +128 -0
  206. package/assets/skills/converted/skill-creator/composable/mini/05-improving-the-skill.md +30 -0
  207. package/assets/skills/converted/skill-creator/composable/mini/06-blind-comparison.md +5 -0
  208. package/assets/skills/converted/skill-creator/composable/mini/07-description-optimization.md +74 -0
  209. package/assets/skills/converted/skill-creator/composable/mini/08-packaging.md +11 -0
  210. package/assets/skills/converted/skill-creator/composable/mini/09-environment-specific.md +38 -0
  211. package/assets/skills/converted/skill-creator/composable/mini/10-scripts-and-references.md +34 -0
  212. package/assets/skills/converted/skill-creator/composable/mini/11-json-schemas.md +414 -0
  213. package/assets/skills/converted/skill-creator/composable/mini/12-agent-grader.md +225 -0
  214. package/assets/skills/converted/skill-creator/composable/mini/13-agent-comparator.md +204 -0
  215. package/assets/skills/converted/skill-creator/composable/mini/14-agent-analyzer.md +276 -0
  216. package/assets/skills/converted/skill-creator/composable/presets/authoring.md +198 -0
  217. package/assets/skills/converted/skill-creator/composable/presets/evaluating.md +1334 -0
  218. package/assets/skills/converted/skill-creator/composable/presets/optimizing.md +160 -0
  219. package/assets/skills/converted/skill-creator/composable/presets.json +5 -0
  220. package/assets/skills/meta/ccs-skill-creator/composable/BUNDLE.md +465 -0
  221. package/assets/skills/meta/ccs-skill-creator/composable/INDEX.md +8 -0
  222. package/assets/skills/meta/ccs-skill-creator/composable/VERSION +1 -0
  223. package/assets/skills/meta/ccs-skill-creator/composable/mini/00-core.md +46 -0
  224. package/assets/skills/meta/ccs-skill-creator/composable/mini/01-create-new-skill.md +167 -0
  225. package/assets/skills/meta/ccs-skill-creator/composable/mini/02-convert-existing-skill.md +120 -0
  226. package/assets/skills/meta/ccs-skill-creator/composable/mini/03-review-and-maintain.md +112 -0
  227. package/assets/skills/sources/anthropic/PROVENANCE.md +3 -0
  228. package/assets/skills/sources/anthropic/claude-api/LICENSE.txt +202 -0
  229. package/assets/skills/sources/anthropic/docx/LICENSE.txt +30 -0
  230. package/assets/skills/sources/anthropic/internal-comms/LICENSE.txt +202 -0
  231. package/assets/skills/sources/anthropic/mcp-builder/LICENSE.txt +202 -0
  232. package/assets/skills/sources/anthropic/pdf/LICENSE.txt +30 -0
  233. package/assets/skills/sources/anthropic/pptx/LICENSE.txt +30 -0
  234. package/assets/skills/sources/anthropic/skill-creator/LICENSE.txt +202 -0
  235. package/assets/skills/sources/financial-analyst/PROVENANCE.md +1 -0
  236. package/assets/tools/hive.py +728 -0
  237. package/dist/cli.js +7494 -0
  238. package/package.json +40 -0
@@ -0,0 +1,728 @@
1
+ #!/usr/bin/env python3
2
+ """hive.py: CCS (Compiled Composable Skills) framework CLI for Hive.
3
+
4
+ Tooling for the composable skill layout used under any <skill-root>/composable/
5
+ directory. A skills root MAY organize skills into category subdirectories
6
+ (e.g. skills/authored/<name>/, skills/converted/<name>/) or lay them out flat
7
+ (skills/<name>/); discovery (`report`) recurses to find every composable/ dir
8
+ at any depth:
9
+
10
+ skills/<name>/composable/
11
+ INDEX.md # loading menu: which mini to load when
12
+ presets.json # optional: {"preset-name": ["01", "04", ...]}
13
+ VERSION # optional: single semver line, skill-level version
14
+ mini/
15
+ 00-core.md # optional, always-loaded foundation
16
+ 01-....md
17
+ ...
18
+ BUNDLE.md # compiled: all minis concatenated, generated
19
+ presets/<name>.md # compiled: a named subset, generated
20
+
21
+ Minis may start with a YAML-ish frontmatter block:
22
+
23
+ ---
24
+ pairs-with:
25
+ - 09-refactoring-and-communication.md
26
+ version: 1.0.0
27
+ ---
28
+
29
+ Known frontmatter keys: model-hint, effort-hint, requires, pairs-with, version.
30
+
31
+ Subcommands: compile, lint, parity, report, bump. See --help on each.
32
+
33
+ Python 3.11, stdlib only.
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import json
39
+ import re
40
+ import sys
41
+ from dataclasses import dataclass, field
42
+ from pathlib import Path
43
+
44
+ KNOWN_FRONTMATTER_KEYS = {"model-hint", "effort-hint", "requires", "pairs-with", "version"}
45
+ LIST_KEYS = {"requires", "pairs-with"}
46
+ PATH_LIKE_RE = re.compile(r"^[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)*\.md$")
47
+ H1_RE = re.compile(r"^#\s+\S")
48
+ H2_H3_RE = re.compile(r"^(#{2,3})\s+(.*)$")
49
+ SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$")
50
+ STOPWORDS = {
51
+ "the", "and", "for", "with", "from", "into", "your", "this", "that",
52
+ "when", "what", "how", "are", "was", "were", "have", "has", "not",
53
+ "over", "under", "about", "per", "via", "vs", "step", "steps",
54
+ }
55
+
56
+ GENERATED_MARKER = "<!-- GENERATED by tools/hive.py compile. Do not hand-edit. -->"
57
+
58
+
59
+ # --------------------------------------------------------------------------
60
+ # Core parsing helpers
61
+ # --------------------------------------------------------------------------
62
+
63
+ def toks(text: str) -> float:
64
+ """Token estimate: chars / 4."""
65
+ return len(text) / 4.0
66
+
67
+
68
+ def fmt_toks(n: float) -> str:
69
+ return str(round(n))
70
+
71
+
72
+ @dataclass
73
+ class FrontmatterResult:
74
+ data: dict | None # None => no frontmatter block present
75
+ body: str
76
+ errors: list[str] = field(default_factory=list)
77
+
78
+
79
+ def split_frontmatter(text: str) -> FrontmatterResult:
80
+ """Parse an optional leading YAML-ish frontmatter block.
81
+
82
+ Only supports the shapes actually used in this repo: top-level
83
+ ``key: value`` scalars and ``key:`` followed by `` - item`` list
84
+ entries (also tolerates an inline ``[a, b]`` list).
85
+ """
86
+ if not text.startswith("---"):
87
+ return FrontmatterResult(None, text, [])
88
+
89
+ lines = text.split("\n")
90
+ if lines[0].strip() != "---":
91
+ return FrontmatterResult(None, text, [])
92
+
93
+ end_idx = None
94
+ for i in range(1, len(lines)):
95
+ if lines[i].strip() == "---":
96
+ end_idx = i
97
+ break
98
+
99
+ if end_idx is None:
100
+ return FrontmatterResult(None, text, ["unterminated frontmatter block (no closing '---')"])
101
+
102
+ fm_lines = lines[1:end_idx]
103
+ body = "\n".join(lines[end_idx + 1:])
104
+
105
+ data: dict[str, object] = {}
106
+ errors: list[str] = []
107
+ i = 0
108
+ while i < len(fm_lines):
109
+ line = fm_lines[i]
110
+ if not line.strip():
111
+ i += 1
112
+ continue
113
+ if line[0].isspace():
114
+ errors.append(f"unexpected indented line outside a list: {line!r}")
115
+ i += 1
116
+ continue
117
+ m = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
118
+ if not m:
119
+ errors.append(f"unparsable frontmatter line: {line!r}")
120
+ i += 1
121
+ continue
122
+ key, rest = m.group(1), m.group(2).strip()
123
+ if rest:
124
+ if rest.startswith("[") and rest.endswith("]"):
125
+ items = [x.strip() for x in rest[1:-1].split(",") if x.strip()]
126
+ data[key] = items
127
+ else:
128
+ data[key] = rest
129
+ i += 1
130
+ else:
131
+ items = []
132
+ j = i + 1
133
+ while j < len(fm_lines) and re.match(r"^\s*-\s+\S", fm_lines[j]):
134
+ items.append(re.sub(r"^\s*-\s+", "", fm_lines[j]).strip())
135
+ j += 1
136
+ data[key] = items
137
+ i = j
138
+
139
+ return FrontmatterResult(data, body, errors)
140
+
141
+
142
+ def strip_frontmatter_body(text: str) -> str:
143
+ """Return the mini's body with any frontmatter block removed and
144
+ surrounding blank lines normalized to a single trailing newline."""
145
+ return split_frontmatter(text).body.strip("\n") + "\n"
146
+
147
+
148
+ # --------------------------------------------------------------------------
149
+ # Skill directory resolution
150
+ # --------------------------------------------------------------------------
151
+
152
+ def resolve_composable_dir(path: Path) -> Path:
153
+ """Accept either a skill root (containing composable/) or the
154
+ composable/ dir itself."""
155
+ if (path / "composable").is_dir():
156
+ return path / "composable"
157
+ return path
158
+
159
+
160
+ def skill_name_for(composable_dir: Path) -> str:
161
+ # composable_dir is .../<name>/composable
162
+ return composable_dir.parent.name
163
+
164
+
165
+ def mini_files(composable_dir: Path) -> list[Path]:
166
+ mdir = composable_dir / "mini"
167
+ if not mdir.is_dir():
168
+ return []
169
+ return sorted(mdir.glob("*.md"), key=lambda p: p.name)
170
+
171
+
172
+ # --------------------------------------------------------------------------
173
+ # Versioning
174
+ # --------------------------------------------------------------------------
175
+
176
+ def is_semver(value: str) -> bool:
177
+ return bool(SEMVER_RE.match(value.strip()))
178
+
179
+
180
+ def version_path(composable_dir: Path) -> Path:
181
+ return composable_dir / "VERSION"
182
+
183
+
184
+ def read_skill_version(composable_dir: Path) -> str | None:
185
+ """Return the raw (unvalidated) skill-level VERSION contents, stripped,
186
+ or None if the file doesn't exist."""
187
+ vpath = version_path(composable_dir)
188
+ if not vpath.is_file():
189
+ return None
190
+ text = vpath.read_text(encoding="utf-8").strip()
191
+ return text or None
192
+
193
+
194
+ def bump_semver(version: str, part: str) -> str:
195
+ major, minor, patch = (int(x) for x in version.split("."))
196
+ if part == "major":
197
+ major, minor, patch = major + 1, 0, 0
198
+ elif part == "minor":
199
+ minor, patch = minor + 1, 0
200
+ elif part == "patch":
201
+ patch += 1
202
+ else:
203
+ raise ValueError(f"unknown version part: {part!r}")
204
+ return f"{major}.{minor}.{patch}"
205
+
206
+
207
+ # --------------------------------------------------------------------------
208
+ # compile
209
+ # --------------------------------------------------------------------------
210
+
211
+ def build_bundle_text(
212
+ skill_name: str,
213
+ minis: list[Path],
214
+ title_suffix: str = "",
215
+ skill_version: str | None = None,
216
+ ) -> str:
217
+ version_suffix = f" (v{skill_version})" if skill_version else ""
218
+ header = (
219
+ f"{GENERATED_MARKER}\n"
220
+ f"# {skill_name}: Compiled Skill Bundle{title_suffix}{version_suffix}\n\n"
221
+ f"All composable mini-skills for `{skill_name}` concatenated in index "
222
+ f"order, frontmatter stripped. Regenerate with "
223
+ f"`tools/hive.py compile <skill-dir>`. Do not edit by hand.\n"
224
+ )
225
+ parts = [header]
226
+ for mini in minis:
227
+ body = strip_frontmatter_body(mini.read_text(encoding="utf-8"))
228
+ parts.append(f"\n---\n<!-- module: {mini.name} -->\n\n{body}")
229
+ full = "".join(parts)
230
+ return full.rstrip("\n") + "\n"
231
+
232
+
233
+ def resolve_preset_ids(minis: list[Path], ids: list[str]) -> tuple[list[Path], list[str]]:
234
+ """Map preset id tokens (e.g. '01', '01-review-method', '01-review-method.md')
235
+ to mini file paths, preserving the given order. Returns (paths, unmatched)."""
236
+ by_full = {p.name: p for p in minis}
237
+ by_stem = {p.stem: p for p in minis}
238
+ by_prefix: dict[str, Path] = {}
239
+ for p in minis:
240
+ prefix = p.stem.split("-", 1)[0]
241
+ by_prefix.setdefault(prefix, p)
242
+
243
+ resolved: list[Path] = []
244
+ unmatched: list[str] = []
245
+ for token in ids:
246
+ token = token.strip()
247
+ if not token:
248
+ continue
249
+ match = by_full.get(token) or by_stem.get(token) or by_prefix.get(token)
250
+ if match is None:
251
+ unmatched.append(token)
252
+ else:
253
+ resolved.append(match)
254
+ return resolved, unmatched
255
+
256
+
257
+ def load_presets_json(composable_dir: Path) -> dict[str, list[str]]:
258
+ pj = composable_dir / "presets.json"
259
+ if not pj.is_file():
260
+ return {}
261
+ try:
262
+ data = json.loads(pj.read_text(encoding="utf-8"))
263
+ except json.JSONDecodeError as e:
264
+ print(f"[WARN] presets.json is not valid JSON: {e}", file=sys.stderr)
265
+ return {}
266
+ if not isinstance(data, dict):
267
+ print("[WARN] presets.json must be a JSON object of name -> [ids]", file=sys.stderr)
268
+ return {}
269
+ return {str(k): [str(x) for x in v] for k, v in data.items()}
270
+
271
+
272
+ def cmd_compile(args: argparse.Namespace) -> int:
273
+ composable_dir = resolve_composable_dir(Path(args.skill_dir))
274
+ if not composable_dir.is_dir():
275
+ print(f"error: composable dir not found: {composable_dir}", file=sys.stderr)
276
+ return 1
277
+ skill_name = skill_name_for(composable_dir)
278
+ minis = mini_files(composable_dir)
279
+ if not minis:
280
+ print(f"error: no mini/*.md files found under {composable_dir}", file=sys.stderr)
281
+ return 1
282
+
283
+ skill_version = read_skill_version(composable_dir)
284
+
285
+ bundle_text = build_bundle_text(skill_name, minis, skill_version=skill_version)
286
+ bundle_path = composable_dir / "BUNDLE.md"
287
+ bundle_path.write_text(bundle_text, encoding="utf-8")
288
+ print(f"wrote {bundle_path} ({fmt_toks(toks(bundle_text))} tokens, {len(minis)} minis)")
289
+
290
+ # Presets: merge CLI-provided with presets.json (CLI wins on name clash).
291
+ preset_specs: dict[str, list[str]] = load_presets_json(composable_dir)
292
+ for spec in args.presets or []:
293
+ if "=" not in spec:
294
+ print(f"error: --presets entry must be name=ids, got {spec!r}", file=sys.stderr)
295
+ return 1
296
+ name, ids_csv = spec.split("=", 1)
297
+ preset_specs[name.strip()] = [x.strip() for x in ids_csv.split(",") if x.strip()]
298
+
299
+ if preset_specs:
300
+ presets_dir = composable_dir / "presets"
301
+ presets_dir.mkdir(exist_ok=True)
302
+ for name, ids in preset_specs.items():
303
+ resolved, unmatched = resolve_preset_ids(minis, ids)
304
+ if unmatched:
305
+ print(f"error: preset {name!r} references unknown minis: {unmatched}", file=sys.stderr)
306
+ return 1
307
+ preset_text = build_bundle_text(
308
+ skill_name, resolved, title_suffix=f" (preset: {name})", skill_version=skill_version,
309
+ )
310
+ preset_path = presets_dir / f"{name}.md"
311
+ preset_path.write_text(preset_text, encoding="utf-8")
312
+ print(f"wrote {preset_path} ({fmt_toks(toks(preset_text))} tokens, {len(resolved)} minis)")
313
+
314
+ return 0
315
+
316
+
317
+ # --------------------------------------------------------------------------
318
+ # lint
319
+ # --------------------------------------------------------------------------
320
+
321
+ class Reporter:
322
+ def __init__(self) -> None:
323
+ self.lines: list[str] = []
324
+ self.counts = {"PASS": 0, "WARN": 0, "FAIL": 0}
325
+
326
+ def line(self, status: str, msg: str) -> None:
327
+ self.counts[status] += 1
328
+ self.lines.append(f"[{status}] {msg}")
329
+
330
+ def has_fail(self) -> bool:
331
+ return self.counts["FAIL"] > 0
332
+
333
+ def summary(self) -> str:
334
+ return f"{self.counts['PASS']} pass, {self.counts['WARN']} warn, {self.counts['FAIL']} fail"
335
+
336
+
337
+ def lint_skill(composable_dir: Path, r: Reporter) -> None:
338
+ skill_name = skill_name_for(composable_dir)
339
+ minis = mini_files(composable_dir)
340
+ mini_names = [p.name for p in minis]
341
+
342
+ index_path = composable_dir / "INDEX.md"
343
+ index_text = index_path.read_text(encoding="utf-8") if index_path.is_file() else None
344
+
345
+ if index_text is None:
346
+ r.line("FAIL", f"{skill_name}: INDEX.md not found at {index_path}")
347
+ else:
348
+ r.line("PASS", f"{skill_name}: INDEX.md exists")
349
+
350
+ # Every mini mentioned exactly once; no mini unlisted.
351
+ for name in mini_names:
352
+ count = index_text.count(name)
353
+ if count == 0:
354
+ r.line("FAIL", f"{skill_name}: mini '{name}' is not referenced in INDEX.md")
355
+ elif count > 1:
356
+ r.line("FAIL", f"{skill_name}: mini '{name}' is referenced {count} times in INDEX.md (expected exactly once)")
357
+ else:
358
+ r.line("PASS", f"{skill_name}: mini '{name}' referenced exactly once in INDEX.md")
359
+
360
+ # Index word count budget.
361
+ word_count = len(index_text.split())
362
+ threshold = 300 if len(minis) > 12 else 200
363
+ if word_count >= threshold:
364
+ r.line("WARN", f"{skill_name}: INDEX.md is {word_count} words (budget < {threshold} for {len(minis)} minis)")
365
+ else:
366
+ r.line("PASS", f"{skill_name}: INDEX.md word count {word_count} (< {threshold})")
367
+
368
+ # Knowledge-free heuristic: no single line should read like real content.
369
+ # The SPEC-mandated loading-policy header (§3.6) is exempt: it is
370
+ # required verbatim and is loading metadata, not knowledge.
371
+ long_lines = [
372
+ ln.strip() for ln in index_text.splitlines()
373
+ if len(ln.split()) > 30 and not ln.strip().startswith("Loading policy:")
374
+ ]
375
+ if long_lines:
376
+ r.line("WARN", f"{skill_name}: INDEX.md has {len(long_lines)} line(s) over ~30 words (looks like knowledge, not a menu)")
377
+ else:
378
+ r.line("PASS", f"{skill_name}: INDEX.md lines all under ~30 words")
379
+
380
+ # 00-core always-load marking.
381
+ core_path = composable_dir / "mini" / "00-core.md"
382
+ if core_path.is_file():
383
+ core_lines = [ln for ln in index_text.splitlines() if "00-core.md" in ln]
384
+ marked = any(
385
+ "always load" in ln.lower() or "always-load" in ln.lower()
386
+ for ln in core_lines
387
+ )
388
+ if marked:
389
+ r.line("PASS", f"{skill_name}: 00-core.md is marked always-load in INDEX.md")
390
+ else:
391
+ r.line("FAIL", f"{skill_name}: 00-core.md exists but is not marked always-load in INDEX.md")
392
+ else:
393
+ r.line("PASS", f"{skill_name}: no 00-core.md present (not required)")
394
+
395
+ # No mini unlisted: covered above per-mini when INDEX.md exists. Also
396
+ # flag stray files in mini/ that aren't .md (informational only; skip).
397
+
398
+ if not minis:
399
+ r.line("WARN", f"{skill_name}: no mini/*.md files found")
400
+
401
+ for mini in minis:
402
+ raw = mini.read_text(encoding="utf-8")
403
+ fm = split_frontmatter(raw)
404
+
405
+ if not fm.body.strip():
406
+ r.line("FAIL", f"{skill_name}/{mini.name}: file is empty after frontmatter")
407
+ else:
408
+ r.line("PASS", f"{skill_name}/{mini.name}: nonempty")
409
+
410
+ if any(H1_RE.match(ln) for ln in fm.body.splitlines()):
411
+ r.line("PASS", f"{skill_name}/{mini.name}: has an H1")
412
+ else:
413
+ r.line("FAIL", f"{skill_name}/{mini.name}: missing an H1 (# Title)")
414
+
415
+ if raw.startswith("---"):
416
+ if fm.data is None or fm.errors:
417
+ for err in (fm.errors or ["frontmatter present but unparsable"]):
418
+ r.line("FAIL", f"{skill_name}/{mini.name}: invalid frontmatter: {err}")
419
+ else:
420
+ unknown = sorted(set(fm.data) - KNOWN_FRONTMATTER_KEYS)
421
+ if unknown:
422
+ r.line("FAIL", f"{skill_name}/{mini.name}: unknown frontmatter key(s): {unknown}")
423
+ else:
424
+ r.line("PASS", f"{skill_name}/{mini.name}: frontmatter keys OK ({sorted(fm.data)})")
425
+
426
+ for key in LIST_KEYS & set(fm.data):
427
+ values = fm.data[key]
428
+ if isinstance(values, str):
429
+ values = [values]
430
+ bad = [v for v in values if not PATH_LIKE_RE.match(v)]
431
+ if bad:
432
+ r.line("FAIL", f"{skill_name}/{mini.name}: {key} has non-path-like value(s): {bad}")
433
+ else:
434
+ r.line("PASS", f"{skill_name}/{mini.name}: {key} values look like paths")
435
+
436
+ # version: optional; silent when absent, FAIL on malformed semver.
437
+ if "version" in fm.data:
438
+ value = fm.data["version"]
439
+ if isinstance(value, str) and is_semver(value):
440
+ r.line("PASS", f"{skill_name}/{mini.name}: version {value!r} is valid semver")
441
+ else:
442
+ r.line("FAIL", f"{skill_name}/{mini.name}: version {value!r} is not valid semver (expected X.Y.Z)")
443
+ else:
444
+ r.line("PASS", f"{skill_name}/{mini.name}: no frontmatter (ok, optional)")
445
+
446
+ # Skill-level VERSION file: optional; silent when absent, FAIL on malformed semver.
447
+ skill_version = read_skill_version(composable_dir)
448
+ if skill_version is not None:
449
+ if is_semver(skill_version):
450
+ r.line("PASS", f"{skill_name}: VERSION is valid semver ({skill_version})")
451
+ else:
452
+ r.line("FAIL", f"{skill_name}: VERSION {skill_version!r} is not valid semver (expected X.Y.Z)")
453
+
454
+ # BUNDLE.md staleness.
455
+ bundle_path = composable_dir / "BUNDLE.md"
456
+ if not minis:
457
+ pass
458
+ elif not bundle_path.is_file():
459
+ r.line("WARN", f"{skill_name}: BUNDLE.md missing; run `compile`")
460
+ else:
461
+ expected = build_bundle_text(skill_name, minis, skill_version=skill_version)
462
+ actual = bundle_path.read_text(encoding="utf-8")
463
+ if actual != expected:
464
+ r.line("FAIL", f"{skill_name}: BUNDLE.md is stale (does not match mini/*.md); re-run `compile`")
465
+ else:
466
+ r.line("PASS", f"{skill_name}: BUNDLE.md is up to date")
467
+
468
+
469
+ def cmd_lint(args: argparse.Namespace) -> int:
470
+ composable_dir = resolve_composable_dir(Path(args.skill_dir))
471
+ if not composable_dir.is_dir():
472
+ print(f"error: composable dir not found: {composable_dir}", file=sys.stderr)
473
+ return 1
474
+ r = Reporter()
475
+ lint_skill(composable_dir, r)
476
+ print("\n".join(r.lines))
477
+ print(f"\n{r.summary()}")
478
+ return 1 if r.has_fail() else 0
479
+
480
+
481
+ # --------------------------------------------------------------------------
482
+ # parity
483
+ # --------------------------------------------------------------------------
484
+
485
+ def normalize_words(text: str) -> set[str]:
486
+ words = re.findall(r"[A-Za-z][A-Za-z']+", text.lower())
487
+ return {w for w in words if len(w) >= 4 and w not in STOPWORDS}
488
+
489
+
490
+ def heading_is_covered(heading: str, mini_text_lower: str, mini_words: set[str]) -> bool:
491
+ clean = re.sub(r"[^A-Za-z0-9 ]", " ", heading).strip().lower()
492
+ if clean and clean in mini_text_lower:
493
+ return True
494
+ sig = normalize_words(heading)
495
+ if not sig:
496
+ return False
497
+ overlap = sig & mini_words
498
+ return len(overlap) / len(sig) >= 0.6
499
+
500
+
501
+ def cmd_parity(args: argparse.Namespace) -> int:
502
+ composable_dir = resolve_composable_dir(Path(args.skill_dir))
503
+ source_dir = Path(args.source_dir)
504
+ if not composable_dir.is_dir():
505
+ print(f"error: composable dir not found: {composable_dir}", file=sys.stderr)
506
+ return 1
507
+ if not source_dir.is_dir():
508
+ print(f"error: source dir not found: {source_dir}", file=sys.stderr)
509
+ return 1
510
+
511
+ skill_name = skill_name_for(composable_dir)
512
+ minis = mini_files(composable_dir)
513
+ mini_bodies = [strip_frontmatter_body(p.read_text(encoding="utf-8")) for p in minis]
514
+ mini_union_text = "\n".join(mini_bodies)
515
+ mini_tokens = toks(mini_union_text)
516
+ mini_words = normalize_words(mini_union_text)
517
+ mini_text_lower = mini_union_text.lower()
518
+
519
+ source_files = sorted(source_dir.rglob("*.md"))
520
+ if not source_files:
521
+ print(f"error: no *.md files found under {source_dir}", file=sys.stderr)
522
+ return 1
523
+ source_texts = {p: p.read_text(encoding="utf-8") for p in source_files}
524
+ source_tokens = sum(toks(t) for t in source_texts.values())
525
+
526
+ ratio = (mini_tokens / source_tokens) if source_tokens else 0.0
527
+
528
+ print(f"Parity report: {skill_name} (source: {source_dir})")
529
+ print(f" source files : {len(source_files)}")
530
+ print(f" source tokens: {fmt_toks(source_tokens)}")
531
+ print(f" mini tokens : {fmt_toks(mini_tokens)} ({len(minis)} minis)")
532
+ print(f" ratio : {ratio:.1%}")
533
+
534
+ fail = False
535
+ if ratio < 0.85:
536
+ print(f"[FAIL] mini tokens are only {ratio:.1%} of source tokens (< 85%); summarization suspected")
537
+ fail = True
538
+ else:
539
+ print(f"[PASS] mini tokens are {ratio:.1%} of source tokens (>= 85%)")
540
+
541
+ dropped = []
542
+ for path, text in source_texts.items():
543
+ for line in text.splitlines():
544
+ m = H2_H3_RE.match(line.strip())
545
+ if not m:
546
+ continue
547
+ heading = m.group(2).strip()
548
+ if not heading:
549
+ continue
550
+ if not heading_is_covered(heading, mini_text_lower, mini_words):
551
+ dropped.append(f"{path.relative_to(source_dir)}: {heading}")
552
+
553
+ if dropped:
554
+ print(f"\n[WARN] {len(dropped)} source heading(s) with no fuzzy match in any mini (possibly dropped):")
555
+ for d in dropped:
556
+ print(f" - {d}")
557
+ else:
558
+ print("\n[PASS] every source ## / ### heading has a fuzzy match in the minis")
559
+
560
+ return 1 if fail else 0
561
+
562
+
563
+ # --------------------------------------------------------------------------
564
+ # report
565
+ # --------------------------------------------------------------------------
566
+
567
+ def find_skill_roots(root: Path) -> list[Path]:
568
+ """Recursively find every skill root under ``root`` (any directory at any
569
+ depth containing a ``composable/`` subdirectory) so a categorized layout
570
+ (e.g. ``skills/authored/code-review/``) and a flat one are both discovered."""
571
+ skill_roots = [p.parent for p in root.rglob("composable") if p.is_dir()]
572
+ return sorted(skill_roots, key=lambda p: p.relative_to(root).as_posix())
573
+
574
+
575
+ def cmd_report(args: argparse.Namespace) -> int:
576
+ root = Path(args.skills_root)
577
+ if not root.is_dir():
578
+ print(f"error: skills root not found: {root}", file=sys.stderr)
579
+ return 1
580
+
581
+ rows = []
582
+ any_fail = False
583
+ for child in find_skill_roots(root):
584
+ composable_dir = resolve_composable_dir(child)
585
+ minis = mini_files(composable_dir)
586
+ if not minis and not (composable_dir / "INDEX.md").is_file():
587
+ continue # not a CCS skill dir
588
+
589
+ name = child.relative_to(root).as_posix()
590
+ version = read_skill_version(composable_dir) or "-"
591
+ index_path = composable_dir / "INDEX.md"
592
+ index_tokens = toks(index_path.read_text(encoding="utf-8")) if index_path.is_file() else 0.0
593
+
594
+ core_path = composable_dir / "mini" / "00-core.md"
595
+ core_tokens = toks(strip_frontmatter_body(core_path.read_text(encoding="utf-8"))) if core_path.is_file() else None
596
+
597
+ bundle_path = composable_dir / "BUNDLE.md"
598
+ bundle_tokens = toks(bundle_path.read_text(encoding="utf-8")) if bundle_path.is_file() else None
599
+
600
+ mini_token_counts = [toks(strip_frontmatter_body(p.read_text(encoding="utf-8"))) for p in minis]
601
+
602
+ r = Reporter()
603
+ lint_skill(composable_dir, r)
604
+ if r.has_fail():
605
+ any_fail = True
606
+
607
+ rows.append({
608
+ "name": name,
609
+ "version": version,
610
+ "minis": len(minis),
611
+ "index_tokens": index_tokens,
612
+ "core_tokens": core_tokens,
613
+ "bundle_tokens": bundle_tokens,
614
+ "min_mini": min(mini_token_counts) if mini_token_counts else None,
615
+ "max_mini": max(mini_token_counts) if mini_token_counts else None,
616
+ "lint": f"{r.counts['PASS']}P/{r.counts['WARN']}W/{r.counts['FAIL']}F",
617
+ })
618
+
619
+ if not rows:
620
+ print(f"no CCS skill directories found under {root}")
621
+ return 0
622
+
623
+ headers = ["skill", "version", "minis", "index_tok", "core_tok", "bundle_tok", "min_mini_tok", "max_mini_tok", "lint (P/W/F)"]
624
+
625
+ def fmt(v):
626
+ if v is None:
627
+ return "-"
628
+ if isinstance(v, float):
629
+ return fmt_toks(v)
630
+ return str(v)
631
+
632
+ table = [headers]
633
+ for row in rows:
634
+ table.append([
635
+ row["name"], row["version"], str(row["minis"]), fmt(row["index_tokens"]), fmt(row["core_tokens"]),
636
+ fmt(row["bundle_tokens"]), fmt(row["min_mini"]), fmt(row["max_mini"]), row["lint"],
637
+ ])
638
+
639
+ widths = [max(len(r[i]) for r in table) for i in range(len(headers))]
640
+ for i, row in enumerate(table):
641
+ line = " ".join(cell.ljust(widths[j]) for j, cell in enumerate(row))
642
+ print(line)
643
+ if i == 0:
644
+ print(" ".join("-" * w for w in widths))
645
+
646
+ return 1 if any_fail else 0
647
+
648
+
649
+ # --------------------------------------------------------------------------
650
+ # bump
651
+ # --------------------------------------------------------------------------
652
+
653
+ def cmd_bump(args: argparse.Namespace) -> int:
654
+ composable_dir = resolve_composable_dir(Path(args.skill_dir))
655
+ if not composable_dir.is_dir():
656
+ print(f"error: composable dir not found: {composable_dir}", file=sys.stderr)
657
+ return 1
658
+
659
+ part = args.part
660
+ vpath = version_path(composable_dir)
661
+ old_raw = read_skill_version(composable_dir)
662
+ if old_raw is None:
663
+ # No VERSION file yet: initialize it rather than bumping from 0.0.0.
664
+ old_display = "(none)"
665
+ new = "0.1.0"
666
+ else:
667
+ if not is_semver(old_raw):
668
+ print(f"error: existing VERSION {old_raw!r} is not valid semver (expected X.Y.Z)", file=sys.stderr)
669
+ return 1
670
+ old_display = old_raw
671
+ new = bump_semver(old_raw, part)
672
+
673
+ vpath.write_text(f"{new}\n", encoding="utf-8")
674
+ print(f"{skill_name_for(composable_dir)}: {old_display} -> {new}")
675
+ return 0
676
+
677
+
678
+ # --------------------------------------------------------------------------
679
+ # CLI
680
+ # --------------------------------------------------------------------------
681
+
682
+ def build_parser() -> argparse.ArgumentParser:
683
+ parser = argparse.ArgumentParser(
684
+ prog="hive.py",
685
+ description="Hive skill framework CLI (CCS spec): compile, lint, parity, report, bump.",
686
+ )
687
+ sub = parser.add_subparsers(dest="command", required=True)
688
+
689
+ p_compile = sub.add_parser("compile", help="Regenerate BUNDLE.md (and optional presets) from mini/*.md")
690
+ p_compile.add_argument("skill_dir", help="Path to the skill root or its composable/ dir")
691
+ p_compile.add_argument(
692
+ "--presets", nargs="+", metavar="NAME=IDS",
693
+ help="e.g. --presets quick=01,04,05 deep=01,02,03,04,05,06",
694
+ )
695
+ p_compile.set_defaults(func=cmd_compile)
696
+
697
+ p_lint = sub.add_parser("lint", help="Check a skill's composable directory for structural issues")
698
+ p_lint.add_argument("skill_dir", help="Path to the skill root or its composable/ dir")
699
+ p_lint.set_defaults(func=cmd_lint)
700
+
701
+ p_parity = sub.add_parser("parity", help="Compare a compiled skill's minis against its source material")
702
+ p_parity.add_argument("skill_dir", help="Path to the skill root or its composable/ dir")
703
+ p_parity.add_argument("source_dir", help="Path to the original (pre-conversion) markdown source")
704
+ p_parity.set_defaults(func=cmd_parity)
705
+
706
+ p_report = sub.add_parser("report", help="Summarize all CCS skills under a skills root")
707
+ p_report.add_argument("skills_root", help="Path to a directory containing one subdir per skill")
708
+ p_report.set_defaults(func=cmd_report)
709
+
710
+ p_bump = sub.add_parser("bump", help="Bump (or initialize) a skill's composable/VERSION")
711
+ p_bump.add_argument("skill_dir", help="Path to the skill root or its composable/ dir")
712
+ p_bump.add_argument(
713
+ "part", nargs="?", choices=["major", "minor", "patch"], default="patch",
714
+ help="Version component to bump (default: patch)",
715
+ )
716
+ p_bump.set_defaults(func=cmd_bump)
717
+
718
+ return parser
719
+
720
+
721
+ def main(argv: list[str] | None = None) -> int:
722
+ parser = build_parser()
723
+ args = parser.parse_args(argv)
724
+ return args.func(args)
725
+
726
+
727
+ if __name__ == "__main__":
728
+ raise SystemExit(main())