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.
- package/LICENSE +28 -0
- package/README.md +235 -0
- package/assets/LICENSE +32 -0
- package/assets/THIRD_PARTY_NOTICES.md +72 -0
- package/assets/manifest.json +1290 -0
- package/assets/skills/authored/code-review/composable/BUNDLE.md +192 -0
- package/assets/skills/authored/code-review/composable/INDEX.md +13 -0
- package/assets/skills/authored/code-review/composable/VERSION +1 -0
- package/assets/skills/authored/code-review/composable/mini/01-review-method.md +34 -0
- package/assets/skills/authored/code-review/composable/mini/02-security-review.md +20 -0
- package/assets/skills/authored/code-review/composable/mini/03-correctness-bugs.md +19 -0
- package/assets/skills/authored/code-review/composable/mini/04-concurrency.md +16 -0
- package/assets/skills/authored/code-review/composable/mini/05-error-handling.md +12 -0
- package/assets/skills/authored/code-review/composable/mini/06-performance.md +11 -0
- package/assets/skills/authored/code-review/composable/mini/07-api-design.md +20 -0
- package/assets/skills/authored/code-review/composable/mini/08-missing-tests.md +25 -0
- package/assets/skills/authored/code-review/composable/mini/09-refactoring-and-communication.md +32 -0
- package/assets/skills/authored/data-analysis/composable/BUNDLE.md +186 -0
- package/assets/skills/authored/data-analysis/composable/INDEX.md +13 -0
- package/assets/skills/authored/data-analysis/composable/VERSION +1 -0
- package/assets/skills/authored/data-analysis/composable/mini/01-data-quality-profiling.md +26 -0
- package/assets/skills/authored/data-analysis/composable/mini/02-aggregation-pitfalls.md +21 -0
- package/assets/skills/authored/data-analysis/composable/mini/03-time-series.md +19 -0
- package/assets/skills/authored/data-analysis/composable/mini/04-segmentation.md +23 -0
- package/assets/skills/authored/data-analysis/composable/mini/05-discount-pricing.md +17 -0
- package/assets/skills/authored/data-analysis/composable/mini/06-insight-generation.md +25 -0
- package/assets/skills/authored/data-analysis/composable/mini/07-executive-writing.md +26 -0
- package/assets/skills/authored/data-analysis/composable/mini/08-recommendations.md +20 -0
- package/assets/skills/authored/data-analysis/composable/mini/09-reproducibility-assumptions.md +20 -0
- package/assets/skills/authored/financial-analysis/composable/BUNDLE.md +1293 -0
- package/assets/skills/authored/financial-analysis/composable/INDEX.md +17 -0
- package/assets/skills/authored/financial-analysis/composable/VERSION +1 -0
- package/assets/skills/authored/financial-analysis/composable/mini/00-core.md +69 -0
- package/assets/skills/authored/financial-analysis/composable/mini/01-profitability-efficiency-ratios.md +173 -0
- package/assets/skills/authored/financial-analysis/composable/mini/02-liquidity-leverage-ratios.md +120 -0
- package/assets/skills/authored/financial-analysis/composable/mini/03-valuation-ratios-interpretation.md +107 -0
- package/assets/skills/authored/financial-analysis/composable/mini/04-dcf-projection-wacc.md +122 -0
- package/assets/skills/authored/financial-analysis/composable/mini/05-dcf-terminal-value-sensitivity.md +75 -0
- package/assets/skills/authored/financial-analysis/composable/mini/06-comparables-precedents.md +78 -0
- package/assets/skills/authored/financial-analysis/composable/mini/07-budget-variance-analysis.md +47 -0
- package/assets/skills/authored/financial-analysis/composable/mini/08-driver-based-forecasting-scenarios.md +111 -0
- package/assets/skills/authored/financial-analysis/composable/mini/09-rolling-forecasts-accuracy.md +114 -0
- package/assets/skills/authored/financial-analysis/composable/mini/10-reporting-exec-summary.md +58 -0
- package/assets/skills/authored/financial-analysis/composable/mini/11-industry-adaptations.md +130 -0
- package/assets/skills/authored/financial-analysis/composable/mini/12-toolkit-scripts-io.md +102 -0
- package/assets/skills/authored/python-api/composable/BUNDLE.md +389 -0
- package/assets/skills/authored/python-api/composable/INDEX.md +13 -0
- package/assets/skills/authored/python-api/composable/VERSION +1 -0
- package/assets/skills/authored/python-api/composable/mini/01-project-structure.md +44 -0
- package/assets/skills/authored/python-api/composable/mini/02-routing-and-app.md +38 -0
- package/assets/skills/authored/python-api/composable/mini/03-pydantic-models.md +45 -0
- package/assets/skills/authored/python-api/composable/mini/04-validation-and-errors.md +59 -0
- package/assets/skills/authored/python-api/composable/mini/05-dependencies-and-auth.md +60 -0
- package/assets/skills/authored/python-api/composable/mini/06-pagination-filtering.md +44 -0
- package/assets/skills/authored/python-api/composable/mini/07-async-performance.md +30 -0
- package/assets/skills/authored/python-api/composable/mini/08-testing.md +45 -0
- package/assets/skills/authored/python-api/composable/mini/09-middleware-observability.md +29 -0
- package/assets/skills/authored/tech-writing/composable/BUNDLE.md +220 -0
- package/assets/skills/authored/tech-writing/composable/INDEX.md +12 -0
- package/assets/skills/authored/tech-writing/composable/VERSION +1 -0
- package/assets/skills/authored/tech-writing/composable/mini/01-audience-analysis.md +26 -0
- package/assets/skills/authored/tech-writing/composable/mini/02-document-types.md +27 -0
- package/assets/skills/authored/tech-writing/composable/mini/03-readme-quickstart-changelog.md +22 -0
- package/assets/skills/authored/tech-writing/composable/mini/04-information-architecture.md +21 -0
- package/assets/skills/authored/tech-writing/composable/mini/05-style-clarity.md +29 -0
- package/assets/skills/authored/tech-writing/composable/mini/06-breaking-changes-migrations.md +33 -0
- package/assets/skills/authored/tech-writing/composable/mini/07-troubleshooting-security.md +30 -0
- package/assets/skills/authored/tech-writing/composable/mini/08-faq-voice-tone.md +32 -0
- package/assets/skills/converted/claude-api/composable/BUNDLE.md +14996 -0
- package/assets/skills/converted/claude-api/composable/INDEX.md +98 -0
- package/assets/skills/converted/claude-api/composable/VERSION +1 -0
- package/assets/skills/converted/claude-api/composable/mini/00-core.md +235 -0
- package/assets/skills/converted/claude-api/composable/mini/01-model-api-quick-reference.md +258 -0
- package/assets/skills/converted/claude-api/composable/mini/02-common-pitfalls.md +42 -0
- package/assets/skills/converted/claude-api/composable/mini/10-models.md +135 -0
- package/assets/skills/converted/claude-api/composable/mini/11-platform-availability.md +96 -0
- package/assets/skills/converted/claude-api/composable/mini/12-claude-platform-on-aws.md +59 -0
- package/assets/skills/converted/claude-api/composable/mini/13-live-sources.md +143 -0
- package/assets/skills/converted/claude-api/composable/mini/14-error-codes.md +256 -0
- package/assets/skills/converted/claude-api/composable/mini/15-prompt-caching.md +223 -0
- package/assets/skills/converted/claude-api/composable/mini/16-token-counting.md +56 -0
- package/assets/skills/converted/claude-api/composable/mini/17-tool-use-concepts.md +444 -0
- package/assets/skills/converted/claude-api/composable/mini/18-agent-design.md +101 -0
- package/assets/skills/converted/claude-api/composable/mini/19-anthropic-cli.md +246 -0
- package/assets/skills/converted/claude-api/composable/mini/20-model-migration.md +1301 -0
- package/assets/skills/converted/claude-api/composable/mini/30-managed-agents-overview.md +71 -0
- package/assets/skills/converted/claude-api/composable/mini/31-managed-agents-core.md +278 -0
- package/assets/skills/converted/claude-api/composable/mini/32-managed-agents-environments.md +219 -0
- package/assets/skills/converted/claude-api/composable/mini/33-managed-agents-tools.md +366 -0
- package/assets/skills/converted/claude-api/composable/mini/34-managed-agents-events.md +252 -0
- package/assets/skills/converted/claude-api/composable/mini/35-managed-agents-outcomes.md +106 -0
- package/assets/skills/converted/claude-api/composable/mini/36-managed-agents-multiagent.md +101 -0
- package/assets/skills/converted/claude-api/composable/mini/37-managed-agents-webhooks.md +123 -0
- package/assets/skills/converted/claude-api/composable/mini/38-managed-agents-memory.md +197 -0
- package/assets/skills/converted/claude-api/composable/mini/39-managed-agents-scheduled-deployments.md +146 -0
- package/assets/skills/converted/claude-api/composable/mini/40-managed-agents-client-patterns.md +211 -0
- package/assets/skills/converted/claude-api/composable/mini/41-managed-agents-onboarding.md +82 -0
- package/assets/skills/converted/claude-api/composable/mini/42-managed-agents-api-reference.md +441 -0
- package/assets/skills/converted/claude-api/composable/mini/43-managed-agents-self-hosted-sandboxes.md +174 -0
- package/assets/skills/converted/claude-api/composable/mini/50-python-readme.md +564 -0
- package/assets/skills/converted/claude-api/composable/mini/51-python-tool-use.md +590 -0
- package/assets/skills/converted/claude-api/composable/mini/52-python-streaming-batches-files.md +549 -0
- package/assets/skills/converted/claude-api/composable/mini/53-python-managed-agents.md +335 -0
- package/assets/skills/converted/claude-api/composable/mini/55-typescript-readme.md +404 -0
- package/assets/skills/converted/claude-api/composable/mini/56-typescript-tool-use.md +548 -0
- package/assets/skills/converted/claude-api/composable/mini/57-typescript-streaming-batches-files.md +384 -0
- package/assets/skills/converted/claude-api/composable/mini/58-typescript-managed-agents.md +358 -0
- package/assets/skills/converted/claude-api/composable/mini/60-csharp-readme.md +361 -0
- package/assets/skills/converted/claude-api/composable/mini/61-csharp-tool-use.md +163 -0
- package/assets/skills/converted/claude-api/composable/mini/62-csharp-streaming-batches-files.md +64 -0
- package/assets/skills/converted/claude-api/composable/mini/65-go-readme.md +185 -0
- package/assets/skills/converted/claude-api/composable/mini/66-go-tool-use.md +219 -0
- package/assets/skills/converted/claude-api/composable/mini/67-go-streaming-files.md +63 -0
- package/assets/skills/converted/claude-api/composable/mini/68-go-managed-agents.md +564 -0
- package/assets/skills/converted/claude-api/composable/mini/70-java-readme.md +237 -0
- package/assets/skills/converted/claude-api/composable/mini/71-java-tool-use.md +227 -0
- package/assets/skills/converted/claude-api/composable/mini/72-java-streaming-files.md +49 -0
- package/assets/skills/converted/claude-api/composable/mini/73-java-managed-agents.md +443 -0
- package/assets/skills/converted/claude-api/composable/mini/75-php-readme.md +173 -0
- package/assets/skills/converted/claude-api/composable/mini/76-php-tool-use.md +252 -0
- package/assets/skills/converted/claude-api/composable/mini/77-php-streaming-batches-files.md +53 -0
- package/assets/skills/converted/claude-api/composable/mini/78-php-managed-agents.md +443 -0
- package/assets/skills/converted/claude-api/composable/mini/80-ruby-readme.md +133 -0
- package/assets/skills/converted/claude-api/composable/mini/81-ruby-tool-use-streaming.md +56 -0
- package/assets/skills/converted/claude-api/composable/mini/82-ruby-managed-agents.md +394 -0
- package/assets/skills/converted/claude-api/composable/mini/85-curl-examples.md +255 -0
- package/assets/skills/converted/claude-api/composable/mini/86-curl-managed-agents.md +340 -0
- package/assets/skills/converted/claude-api/composable/presets/csharp.md +1151 -0
- package/assets/skills/converted/claude-api/composable/presets/curl.md +1154 -0
- package/assets/skills/converted/claude-api/composable/presets/go.md +1598 -0
- package/assets/skills/converted/claude-api/composable/presets/java.md +1523 -0
- package/assets/skills/converted/claude-api/composable/presets/php.md +1488 -0
- package/assets/skills/converted/claude-api/composable/presets/python.md +2605 -0
- package/assets/skills/converted/claude-api/composable/presets/ruby.md +1146 -0
- package/assets/skills/converted/claude-api/composable/presets/shared.md +6478 -0
- package/assets/skills/converted/claude-api/composable/presets/typescript.md +2261 -0
- package/assets/skills/converted/docx/composable/BUNDLE.md +617 -0
- package/assets/skills/converted/docx/composable/INDEX.md +12 -0
- package/assets/skills/converted/docx/composable/VERSION +1 -0
- package/assets/skills/converted/docx/composable/mini/00-core.md +26 -0
- package/assets/skills/converted/docx/composable/mini/01-reading-converting.md +36 -0
- package/assets/skills/converted/docx/composable/mini/02-creating-setup-layout.md +85 -0
- package/assets/skills/converted/docx/composable/mini/03-creating-elements.md +257 -0
- package/assets/skills/converted/docx/composable/mini/04-editing-existing.md +57 -0
- package/assets/skills/converted/docx/composable/mini/05-xml-reference.md +128 -0
- package/assets/skills/converted/internal-comms/composable/BUNDLE.md +220 -0
- package/assets/skills/converted/internal-comms/composable/INDEX.md +11 -0
- package/assets/skills/converted/internal-comms/composable/VERSION +1 -0
- package/assets/skills/converted/internal-comms/composable/mini/00-core.md +30 -0
- package/assets/skills/converted/internal-comms/composable/mini/01-3p-updates.md +49 -0
- package/assets/skills/converted/internal-comms/composable/mini/02-company-newsletter.md +67 -0
- package/assets/skills/converted/internal-comms/composable/mini/03-faq-answers.md +32 -0
- package/assets/skills/converted/internal-comms/composable/mini/04-general-comms.md +18 -0
- package/assets/skills/converted/mcp-builder/composable/BUNDLE.md +2460 -0
- package/assets/skills/converted/mcp-builder/composable/INDEX.md +23 -0
- package/assets/skills/converted/mcp-builder/composable/VERSION +1 -0
- package/assets/skills/converted/mcp-builder/composable/mini/00-core.md +43 -0
- package/assets/skills/converted/mcp-builder/composable/mini/01-workflow-research-planning.md +54 -0
- package/assets/skills/converted/mcp-builder/composable/mini/02-implementation-process.md +78 -0
- package/assets/skills/converted/mcp-builder/composable/mini/03-naming-conventions.md +33 -0
- package/assets/skills/converted/mcp-builder/composable/mini/04-response-formats.md +21 -0
- package/assets/skills/converted/mcp-builder/composable/mini/05-pagination.md +24 -0
- package/assets/skills/converted/mcp-builder/composable/mini/06-transport.md +47 -0
- package/assets/skills/converted/mcp-builder/composable/mini/07-annotations.md +20 -0
- package/assets/skills/converted/mcp-builder/composable/mini/08-security.md +35 -0
- package/assets/skills/converted/mcp-builder/composable/mini/09-error-handling.md +32 -0
- package/assets/skills/converted/mcp-builder/composable/mini/10-testing-and-documentation.md +21 -0
- package/assets/skills/converted/mcp-builder/composable/mini/11-python-implementation.md +432 -0
- package/assets/skills/converted/mcp-builder/composable/mini/12-python-advanced-and-checklist.md +233 -0
- package/assets/skills/converted/mcp-builder/composable/mini/13-node-implementation.md +717 -0
- package/assets/skills/converted/mcp-builder/composable/mini/14-node-advanced-and-checklist.md +200 -0
- package/assets/skills/converted/mcp-builder/composable/mini/15-evaluation-design.md +200 -0
- package/assets/skills/converted/mcp-builder/composable/mini/16-evaluation-running.md +198 -0
- package/assets/skills/converted/mcp-builder/composable/presets/node-server.md +1381 -0
- package/assets/skills/converted/mcp-builder/composable/presets/python-server.md +1129 -0
- package/assets/skills/converted/pdf/composable/BUNDLE.md +1284 -0
- package/assets/skills/converted/pdf/composable/INDEX.md +14 -0
- package/assets/skills/converted/pdf/composable/VERSION +1 -0
- package/assets/skills/converted/pdf/composable/mini/00-core.md +46 -0
- package/assets/skills/converted/pdf/composable/mini/01-creation.md +176 -0
- package/assets/skills/converted/pdf/composable/mini/02-extraction.md +178 -0
- package/assets/skills/converted/pdf/composable/mini/03-merge-split-manipulate.md +240 -0
- package/assets/skills/converted/pdf/composable/mini/04-images-ocr.md +168 -0
- package/assets/skills/converted/pdf/composable/mini/05-encryption.md +51 -0
- package/assets/skills/converted/pdf/composable/mini/06-forms.md +300 -0
- package/assets/skills/converted/pdf/composable/mini/07-libraries-reference.md +89 -0
- package/assets/skills/converted/pptx/composable/BUNDLE.md +911 -0
- package/assets/skills/converted/pptx/composable/INDEX.md +12 -0
- package/assets/skills/converted/pptx/composable/VERSION +1 -0
- package/assets/skills/converted/pptx/composable/mini/00-core.md +37 -0
- package/assets/skills/converted/pptx/composable/mini/01-reading.md +37 -0
- package/assets/skills/converted/pptx/composable/mini/02-creating-from-scratch.md +422 -0
- package/assets/skills/converted/pptx/composable/mini/03-editing-templates.md +207 -0
- package/assets/skills/converted/pptx/composable/mini/04-design.md +89 -0
- package/assets/skills/converted/pptx/composable/mini/05-qa-and-rendering.md +91 -0
- package/assets/skills/converted/pptx/composable/presets/creating.md +659 -0
- package/assets/skills/converted/pptx/composable/presets/editing.md +485 -0
- package/assets/skills/converted/skill-creator/composable/BUNDLE.md +1681 -0
- package/assets/skills/converted/skill-creator/composable/INDEX.md +21 -0
- package/assets/skills/converted/skill-creator/composable/VERSION +1 -0
- package/assets/skills/converted/skill-creator/composable/mini/00-core.md +59 -0
- package/assets/skills/converted/skill-creator/composable/mini/01-creating-a-skill.md +27 -0
- package/assets/skills/converted/skill-creator/composable/mini/02-skill-writing-guide.md +71 -0
- package/assets/skills/converted/skill-creator/composable/mini/03-test-cases.md +21 -0
- package/assets/skills/converted/skill-creator/composable/mini/04-running-and-evaluating.md +128 -0
- package/assets/skills/converted/skill-creator/composable/mini/05-improving-the-skill.md +30 -0
- package/assets/skills/converted/skill-creator/composable/mini/06-blind-comparison.md +5 -0
- package/assets/skills/converted/skill-creator/composable/mini/07-description-optimization.md +74 -0
- package/assets/skills/converted/skill-creator/composable/mini/08-packaging.md +11 -0
- package/assets/skills/converted/skill-creator/composable/mini/09-environment-specific.md +38 -0
- package/assets/skills/converted/skill-creator/composable/mini/10-scripts-and-references.md +34 -0
- package/assets/skills/converted/skill-creator/composable/mini/11-json-schemas.md +414 -0
- package/assets/skills/converted/skill-creator/composable/mini/12-agent-grader.md +225 -0
- package/assets/skills/converted/skill-creator/composable/mini/13-agent-comparator.md +204 -0
- package/assets/skills/converted/skill-creator/composable/mini/14-agent-analyzer.md +276 -0
- package/assets/skills/converted/skill-creator/composable/presets/authoring.md +198 -0
- package/assets/skills/converted/skill-creator/composable/presets/evaluating.md +1334 -0
- package/assets/skills/converted/skill-creator/composable/presets/optimizing.md +160 -0
- package/assets/skills/converted/skill-creator/composable/presets.json +5 -0
- package/assets/skills/meta/ccs-skill-creator/composable/BUNDLE.md +465 -0
- package/assets/skills/meta/ccs-skill-creator/composable/INDEX.md +8 -0
- package/assets/skills/meta/ccs-skill-creator/composable/VERSION +1 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/00-core.md +46 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/01-create-new-skill.md +167 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/02-convert-existing-skill.md +120 -0
- package/assets/skills/meta/ccs-skill-creator/composable/mini/03-review-and-maintain.md +112 -0
- package/assets/skills/sources/anthropic/PROVENANCE.md +3 -0
- package/assets/skills/sources/anthropic/claude-api/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/docx/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/internal-comms/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/mcp-builder/LICENSE.txt +202 -0
- package/assets/skills/sources/anthropic/pdf/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/pptx/LICENSE.txt +30 -0
- package/assets/skills/sources/anthropic/skill-creator/LICENSE.txt +202 -0
- package/assets/skills/sources/financial-analyst/PROVENANCE.md +1 -0
- package/assets/tools/hive.py +728 -0
- package/dist/cli.js +7494 -0
- 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())
|