diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f1646..e0397f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,13 @@ - CLI auto-loads `~/.hermes/.env` (and `~/.hyperlex/.env`); accepts official `CHROMA_API_KEY` / `CHROMA_TENANT` / `CHROMA_DATABASE` aliases - Cloud client only requires API key; tenant/database optional when Chroma can infer them +## 0.4.1 — Hermes Agent v0.21 skill index (2026-09-01) + +- `SKILL.md` matches the Hermes v0.21 (tag v2026.8.31) hardline: one-sentence + description ≤60 characters, human-first author, required frontmatter keys, + modern section order, CLI framed through the Hermes `terminal` tool +- CLI `check` asserts the skill-index description budget and required keys + ## 0.4.0 — Automatic backend pipeline (2026-08-05) - `run_pipeline` / CLI `pipeline`: ingest → analyze → receipt → forecasts → score log → Phase 5 risk diff --git a/README.md b/README.md index d3c79ac..b5b0ac8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Docs CI Docs License: MIT - Version 0.4.0 + Version 0.4.1 Python Hermes skill Offline-first @@ -120,7 +120,7 @@ Phases **0–4** are production skill surface. **5.0–5.3** adds research simul | | | |--|--| -| **Version** | **0.4.0** | +| **Version** | **0.4.1** | | **Posture** | Hermes skill · Python package (≥3.10) · MIT | | **Primary store** | `~/.hyperlex/` | | **Public PyPI** | Not planned | diff --git a/SKILL.md b/SKILL.md index 83746cd..e260163 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,19 +1,10 @@ --- name: hyperlex -description: > - Use when the user wants memetic emergence analysis, slang detection, - hyperstition / virality scoring, slang lineage matching, forecast extraction, - operator settlement, or Brier calibration on cultural signals. Triggers - include slang, memetics, hyperstition, virality, lineage, Brier, settle - forecasts, score-series, betting slang, crypto-degen slang, ai-native slang, - brainrot, and receipt-backed cultural signal scans. Not for general web - research (use agent-reach), product audits (neon-genie), or cinematic work - (kubrick). -version: 0.4.0 -author: Applied Alchemy Labs / Hermes +description: Catch slang while it is still becoming culture. +version: 0.4.1 +author: Daniel Meyer (scrimshawlife-ctrl), Hermes Agent license: MIT platforms: [linux, macos] -dependencies: [] metadata: hermes: tags: @@ -26,158 +17,96 @@ metadata: - Brier - Receipts - Forecasting - category: analysis related_skills: [] + category: analysis openclaw: requires: bins: [python3] os: [darwin, linux] emoji: "🌀" -triggers: - - hyperlex - - memetic - - memetics - - slang - - slang lineage - - hyperstition - - virality - - neologism - - betting slang - - sharp money - - brainrot - - brier - - settle forecast - - score series - - cultural signal - - memetic receipt - - hyperlex wizard - - get started with hyperlex - - hyperlex onboarding --- # Hyperlex -Standalone **Hermes skill** for memetic emergence analysis. - -Hermes loads this directory and uses `SKILL.md` as the behavior contract. -The runtime is the bundled Python package under `src/hyperlex/` plus the CLI -at `scripts/hyperlex.py`. No Abraxas import, no required network for baseline -(`mock`) mode. +Hyperlex is a Hermes skill for memetic emergence analysis: slang detection, lineage matching, virality and hyperstition scoring, integrity-hashed receipts, and Brier calibration after operator settlement. It does not invent numeric Brier on open analysis. Applied Alchemy Labs built the Python package under `src/hyperlex/`; the CLI is `scripts/hyperlex.py`. Baseline `mock` mode needs no network and no Abraxas import. -Resolve paths from the installed skill root. Set: - -```bash -export HERMES_SKILL_DIR="${HERMES_SKILL_DIR:-$HOME/.hermes/skills/hyperlex}" -``` +Hermes substitutes `${HERMES_SKILL_DIR}` with the installed skill directory. Call the bundled CLI through the Hermes `terminal` tool, not as a bare shell aside. ## When to Use -- Detect slang / neologisms and score virality, memetics, hyperstition -- Match slang into historical **lineage families** with transparent confidence -- **Backfill** YTD slang packs and **backpropagate** lineage onto historical receipts (non-mutating) -- **Phase 5 simulate** cultural transmission, multi-agent memetics, hyperstition risk, phylogeny scaffold -- **Risk-schedule** advisory LIVE_EMERGENCE_SCAN cadence from risk tiers (never auto-registers cron) -- Emit integrity-hashed **receipts** for auditable runs -- Extract **forecasts** from analysis (probabilities only — no fake Brier) -- **Settle** forecasts as an operator and recompute Brier series from the score log -- Scan betting-sharp, crypto-degen, ai-native, brainrot, kinship, political-status families - -## When Not to Use +- Detect slang or neologisms and score virality, memetics, or hyperstition +- Match slang into historical lineage families with a transparent confidence breakdown +- Backfill YTD slang packs and backpropagate lineage onto historical receipts (report only; non-mutating) +- Run Phase 5 cultural-transmission simulation (always SPECULATIVE; `brier: null`) +- Propose advisory `LIVE_EMERGENCE_SCAN` cadence with `risk-schedule` (never auto-register Hermes cron) +- Emit integrity-hashed receipts and extract forecast probabilities +- Settle forecasts as an operator and recompute Brier series from the score log +- Scan betting-sharp, crypto-degen, ai-native, brainrot, kinship, or political-status families -- General multi-platform web research → agent-reach -- Product / opportunity intelligence → neon-genie -- Cinematic continuity / storyboards → kubrick -- Symbolic code architecture mapping → orchestra +Don't use for general web research, product audits, cinematic continuity, or symbolic architecture mapping. ## Prerequisites -- Python 3.10+ -- `python3` on PATH -- Optional: `requests`, `jsonschema`, `crawl4ai` for richer ingest / validation +- Python 3.10+ with `python3` on PATH +- Optional: `requests`, `jsonschema`, `crawl4ai` for richer ingest and validation - Optional: network for non-`mock` sources +- Offline force: `HYPERLEX_OFFLINE=1` -## Install +## How to Run -```bash -bash install.sh --dry-run -bash install.sh -# installs to ~/.hermes/skills/hyperlex by default -python3 "$HOME/.hermes/skills/hyperlex/scripts/hyperlex.py" check -python3 "$HOME/.hermes/skills/hyperlex/scripts/hyperlex.py" smoke -``` +After `skill_view` loads this skill, `${HERMES_SKILL_DIR}` is already substituted. Call `terminal`: -## Commands (prefer simplified path) - -```bash -HLX="python3 $HERMES_SKILL_DIR/scripts/hyperlex.py" - -$HLX wizard --auto # week-one guided path (offline) -$HLX wizard # interactive (TTY) -$HLX commands # full simplified map (JSON) -$HLX sources # sources + routes -# AUTO backend — ingest → full results (receipt, forecasts, phase5 risk) -$HLX pipeline "rizz" --route offline -$HLX ingest "locked in" # same as pipeline (use --raw-only for signal only) -$HLX pipeline "sigma rizz locked in" # expands to atoms automatically -$HLX pending # open forecasts -$HLX settle --forecast-id --decision TRUE -$HLX score-series --mean-shift --verify-chain -$HLX scan --route offline --receipt --forecasts --append-log -$HLX risk-schedule --tier MODERATE --schedule-out /tmp/hlx-cron -$HLX doctor && $HLX smoke +``` +terminal(command="bash install.sh --dry-run", timeout=60) +terminal(command="bash install.sh", timeout=120) ``` -**Ingest routing:** prefer `--route offline|live|glossary|social` over raw `--source`. -Aliases: `real`→glossary, `x`→x_search, `firecrawl`→crawl4ai. Offline env: `HYPERLEX_OFFLINE=1`. - -Research / advanced (still available): `simulate`, `vector-*`, `archive-export`, -`lineage-backfill`, `lineage-backprop`, `relay`, `signal`, `diagram`, `ledger-*`. +Default install path: `~/.hermes/skills/hyperlex`. Then prove the install: -Docs: `docs/operator-loop.md`, `docs/commands.md`. +``` +terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py check", timeout=60) +terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py smoke", timeout=120) +``` -## Guided wizard +Reuse `HLX="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py"` in later `terminal` commands. -When the user is new to Hyperlex, asks to get started, or wants a guided -operator path: +## Quick Reference -1. Ensure skill install (`bash install.sh` if missing). -2. Run `$HLX wizard --auto` (or `$HLX wizard --auto --query ""`). -3. Summarize steps: env → doctor → demo → first pipeline → calibration coach. -4. **Never invent Brier.** Open analysis keeps `brier: null`. -5. Show open forecasts from the wizard output / `$HLX pending`. -6. **Settlement requires operator authority** — ask for TRUE|FALSE|VOID, then: - `$HLX settle --forecast-id --decision …` -7. `$HLX score-series --mean-shift --verify-chain` -8. Optional advisory only: `$HLX risk-schedule --tier MODERATE --schedule-out /tmp/hlx-cron` - (never auto-register Hermes cron). +| Action | `terminal` command | +|--------|-------------------| +| Guided week-one path | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py wizard --auto", timeout=180)` | +| Interactive wizard | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py wizard", timeout=300)` | +| Command map (JSON) | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py commands", timeout=30)` | +| Source and route catalog | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py sources", timeout=30)` | +| Offline pipeline (receipt + forecasts) | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py pipeline \"rizz\" --route offline", timeout=120)` | +| Signal only | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py ingest \"locked in\" --raw-only", timeout=120)` | +| Multi-term atoms | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py pipeline \"sigma rizz locked in\" --route offline", timeout=180)` | +| Open forecasts | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py pending", timeout=30)` | +| Operator settle | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py settle --forecast-id --decision TRUE", timeout=60)` | +| Brier series | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py score-series --mean-shift --verify-chain", timeout=60)` | +| Multi-query scan | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py scan --route offline --receipt --forecasts --append-log", timeout=300)` | +| Advisory schedule only | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py risk-schedule --tier MODERATE --schedule-out /tmp/hlx-cron", timeout=60)` | +| Health | `terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py doctor", timeout=120)` | -Step IDs (stable): `env_intro`, `doctor`, `demo`, `first_pipeline`, -`calibration_coach`, `score_series_hint`, `handoff`. +Ingest routing: prefer `--route offline|live|glossary|social` over raw `--source`. Aliases: `real`→glossary, `x`→x_search, `firecrawl`→crawl4ai. -### Operator calibration path +Research commands (still available): `simulate`, `vector-*`, `archive-export`, `lineage-backfill`, `lineage-backprop`, `relay`, `signal`, `diagram`, `ledger-*`. Docs: `docs/operator-loop.md`, `docs/commands.md`. -```text -run "" --route offline - → pending - → settle --forecast-id … --decision TRUE|FALSE - → score-series [--mean-shift] [--verify-chain] -``` +## Procedure -- Score log default: `~/.hyperlex/score_log.jsonl` -- Override: `HYPERLEX_SCORE_LOG`, `--log`, or `--repo-log` → `out/calibration/score_log.jsonl` -- **Never** emit numeric Brier without settlement. Empty series → `NOT_COMPUTABLE`. +1. **Onboard.** If the user is new to Hyperlex, call `terminal` with `wizard --auto` (or `wizard --auto --query ""`). Done when the JSON lists step IDs `env_intro`, `doctor`, `demo`, `first_pipeline`, `calibration_coach`, `score_series_hint`, and `handoff`. +2. **Analyze.** Call `terminal` with `pipeline "" --route offline` (or `run` / `ingest`; use `--raw-only` for signal only). Multi-term bags expand to lexicon atoms automatically. Done when a receipt exists and `provenance.brier` is `null`. +3. **Calibrate.** Call `pending`, ask the operator for `TRUE|FALSE|VOID`, then `settle --forecast-id --decision …`, then `score-series --mean-shift --verify-chain`. Never invent Brier. Done when the series status is `SCORED` or `NOT_COMPUTABLE`. +4. **Scan (optional).** Call `scan --route offline --receipt --forecasts --append-log`. For cadence, call `risk-schedule` and write the advisory job under `--schedule-out`. Done when receipts land and no Hermes cron job was registered. +5. **Live ingest.** Use `--route live` (or glossary/social) only when the user allows network. Done when ingest metadata records the intended source and any offline fallback. +6. **Label claims.** Mark findings `OBSERVED`, `INFERRED`, or `SPECULATIVE`. Fail closed on missing outcomes. Done when every numeric score has a matching authority class. +7. **Research.** `simulate`, archive, and vector commands stay optional and SPECULATIVE. Phase 5 packets keep `brier: null`. -## Preferred sequence +Score log default: `~/.hyperlex/score_log.jsonl`. Override with `HYPERLEX_SCORE_LOG`, `--log`, or `--repo-log` → `out/calibration/score_log.jsonl`. -1. **`wizard --auto`** then `commands` / `run` — week-one guided path, then map or one-shot. -2. **`run --route offline`** — one-shot analyze + receipt + forecasts + score log. -3. **`pending` → `settle` → `score-series`** — Brier only after operator settlement. -4. **Cron** — `risk-schedule` (advisory) + `scan --route offline` for multi-query. -5. **Live ingest** — only when network allowed: `--route live` (or glossary/social). -6. **Label claims** — `OBSERVED` / `INFERRED` / `SPECULATIVE`; fail closed on missing outcomes. -7. **Research** — `simulate` / archive / vector are optional and SPECULATIVE. +**Authority.** Hyperlex may ingest, match lineage, write receipts and score-log events, compute Brier only from settled pairs, and export Abraxas-compatible ledger shapes (no Abraxas import). Hyperlex may not invent numeric Brier on open analysis, auto-settle without an authority marker, promote speculative hyperstition stages as hard truth, rewrite historical receipt integrity during lineage backprop, invent Brier from Phase 5, or mutate other systems. -## Public API (package) +**Library path** (when `src/` is on `PYTHONPATH`; the CLI inserts `src/` automatically): ```python from hyperlex import ( @@ -189,72 +118,40 @@ from hyperlex import ( result = detect_memetic_patterns(query="rizz", ingest_source="mock") forecasts = extract_forecasts(result) -# later, after operator review: # settle_and_log(forecast, outcome_value=1.0, settlement_decision="TRUE") # recompute_series() ``` -Ensure `src/` is on `PYTHONPATH` when importing outside the CLI -(CLI inserts `src/` automatically). - -## Authority boundaries - -Hyperlex **may**: - -- ingest and analyze signals -- match lineages with transparent score breakdowns -- extract forecasts and write receipts / score-log events -- compute Brier only from settled pairs -- export Abraxas-compatible ledger shapes (no Abraxas import) - -Hyperlex **may not**: - -- invent numeric Brier on open analysis (`provenance.brier` stays `null`) -- auto-settle without authority marker -- promote speculative hyperstition stages as hard truth -- rewrite historical receipt integrity during lineage backprop (report only) -- invent Brier from Phase 5 simulation (always `brier: null`, SPECULATIVE) -- mutate Abraxas or other systems (export is optional and offline) - ## Pitfalls -- `scripts/hyperlex.py` must not shadow the package: always run via the skill path so `src/` is first on `sys.path`. -- Non-`mock` sources need network and may degrade gracefully — check ingest metadata. -- Lineage confidence is **INFERRED**; do not treat it as observed ground truth. -- Mean-shift from `score-series --mean-shift` is **advisory** for future forecasts only. -- Score log is append-only; series is recomputed from the log, not stored as sole truth. +- Always invoke `scripts/hyperlex.py` through `${HERMES_SKILL_DIR}` so `src/` is first on `sys.path` and the script cannot shadow the package. +- `platforms` is `[linux, macos]`. `install.sh` is bash; Windows is not proven. +- Non-`mock` sources need network and may degrade to mock. Read ingest metadata before treating the source as live. +- Lineage confidence is INFERRED. Do not treat it as observed ground truth. +- `score-series --mean-shift` is advisory for future forecasts only. +- The score log is append-only. Recompute the series from the log; do not treat a stored series as sole truth. +- `risk-schedule` is advisory. Do not add a `metadata.hermes.blueprint` block and do not auto-register cron. ## Verification -```bash -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" check -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" doctor -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" ledger-stats -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" signal --input -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" feedback --signal-key hyperstition.stage -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" diagram --from-golden --out-dir out/diagrams -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" scan --config "$HERMES_SKILL_DIR/examples/cron/scan-queries.json" --source mock --receipt --forecasts -python3 "$HERMES_SKILL_DIR/scripts/hyperlex.py" smoke +Call `terminal`: + +``` +terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py check", timeout=60) +terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py doctor", timeout=120) +terminal(command="python3 ${HERMES_SKILL_DIR}/scripts/hyperlex.py smoke", timeout=120) ``` +Optional deeper proof: `ledger-stats`, `signal --input `, `feedback --signal-key hyperstition.stage`, `diagram --from-golden --out-dir out/diagrams`, and `scan --config ${HERMES_SKILL_DIR}/examples/cron/scan-queries.json --source mock --receipt --forecasts`. + Successful packaging: - `~/.hermes/skills/hyperlex/SKILL.md` exists - `check` returns `"ok": true` +- Frontmatter `description` is ≤60 characters and ends with `.` - `smoke` writes a receipt under `out/smoke/` - Open analysis has `"brier": null` -## Design references - -- `DESIGN.md` — principles (incl. 11 lineage, 12 Brier requires settlement) -- `docs/brier-calibration.md` — forecast → settlement → score -- `docs/slang-lineages.md` — family methodology -- `docs/phase5.md` / `docs/modules/simulation.md` — Phase 5 research simulation -- `schemas/` — ingest, result, receipt, forecast, settlement, brier_series, lineage -- `examples/slang-families/` — Mermaid + HTML family diagrams -- `data/backfill/2026/` — YTD slang packs -- `references/hermes-runtime-contract.md` — path / authority policy - -## Security +Design references: `DESIGN.md`, `docs/brier-calibration.md`, `docs/slang-lineages.md`, `docs/phase5.md`, `docs/modules/simulation.md`, `schemas/`, `examples/slang-families/`, `data/backfill/2026/`, `references/hermes-runtime-contract.md`. Local stdlib-first CLI. Baseline (`mock`) needs no network. Real ingest may call public web APIs. Score log and receipts are local files under `~/.hyperlex/` or skill `out/`. diff --git a/STATUS.md b/STATUS.md index b77464f..f31b285 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,6 +1,6 @@ # Hyperlex Skill Status -**Version:** 0.4.0 +**Version:** 0.4.1 **Posture:** Hermes skill (Python package repo) **Install:** `bash install.sh` → `~/.hermes/skills/hyperlex` **Track:** Phases 0–4 complete · Phase 5.0–5.3 · Pages static run history · Hallmark desk UI diff --git a/VERSION b/VERSION index 1d0ba9e..267577d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index 7ed4297..bd4443c 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release notes +## 0.4.1 +- Hermes Agent v0.21 SKILL.md contract: ≤60-character description, required frontmatter, `terminal` framing. +- CLI `check` asserts the skill-index description budget. + ## 0.4.0 - Automatic backend pipeline: `pipeline` / `run` / `ingest` → full results packet. - Ingest defaults to full results (`--raw-only` for signal-only). diff --git a/docs/index.md b/docs/index.md index a76fa45..c7b043a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ hide: # Hyperlex
-v0.4.0 +v0.4.1 Hermes skill · Python package Settled Brier only Local-first · offline mock ready @@ -126,5 +126,5 @@ Full glossary: [start/glossary.md](start/glossary.md)

READ DEEPER. THINK WIDER.

-v0.4.0 · Hermes skill · settled Brier only · offline mock default for first success +v0.4.1 · Hermes skill · settled Brier only · offline mock default for first success

diff --git a/docs/status.md b/docs/status.md index b922352..5cc2072 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,6 +1,6 @@ # Hyperlex Skill Status -**Version:** 0.4.0 +**Version:** 0.4.1 **Posture:** Hermes skill (Python package repo) **Install:** `bash install.sh` → `~/.hermes/skills/hyperlex` **Track:** Phases 0–4 complete · Phase 5.0–5.3 · Pages static run history · Hallmark desk UI diff --git a/docs/telemetry.md b/docs/telemetry.md index b8f758f..0a9af7f 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -4,7 +4,7 @@ Operator desk on Pages — **publish-safe**, not a live dashboard. Real state lives in `~/.hyperlex/` (receipts, score log, vector DB).
-v0.4.0 +v0.4.1 Hermes skill · auto pipeline Atomic multi-term · settled Brier only Pages = static history diff --git a/hyperlex.manifest.yaml b/hyperlex.manifest.yaml index 450e2f6..1c7ee19 100644 --- a/hyperlex.manifest.yaml +++ b/hyperlex.manifest.yaml @@ -1,5 +1,5 @@ name: hyperlex -version: 0.4.0 +version: 0.4.1 display_name: Hyperlex kind: hermes-openclaw-skill description: > diff --git a/mkdocs.yml b/mkdocs.yml index c3dad2a..79e6c35 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -113,4 +113,4 @@ exclude_docs: | README.md extra: - version: 0.4.0 + version: 0.4.1 diff --git a/pyproject.toml b/pyproject.toml index 1f140ec..26dc5dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hyperlex" -version = "0.4.0" +version = "0.4.1" description = "Standalone memetic emergence engine for slang detection, hyperstition, virality, lineage, and settled Brier calibration." authors = [ {name = "Applied Alchemy Labs", email = "scrimshawlife@gmail.com"} diff --git a/references/hermes-runtime-contract.md b/references/hermes-runtime-contract.md index bffb78a..65294a7 100644 --- a/references/hermes-runtime-contract.md +++ b/references/hermes-runtime-contract.md @@ -13,6 +13,10 @@ Hermes discovers skills by scanning `$HERMES_HOME/skills/**/SKILL.md`. This skill is a **directory skill** (not a single markdown file). +Hermes Agent v0.21 indexes `description` in the system prompt at 57 characters +plus `...`. Keep the field to one sentence of ≤60 characters that ends with a +period. Use `${HERMES_SKILL_DIR}` in command examples; Hermes substitutes it. + ## Paths ```text diff --git a/scripts/hyperlex.py b/scripts/hyperlex.py index a20689c..991605b 100755 --- a/scripts/hyperlex.py +++ b/scripts/hyperlex.py @@ -11,6 +11,7 @@ import argparse import hashlib import json +import re import sys import traceback from dataclasses import dataclass @@ -148,10 +149,202 @@ def _load_manifest() -> Tuple[Dict[str, Any], bool, str]: return {}, False, f"manifest parse failed: {exc}" +_SKILL_DESC_LIMIT = 60 +_SKILL_REQUIRED_KEYS = ("name", "description", "version", "author", "license", "platforms") + + +def _coerce_yaml_scalar(raw: str) -> Any: + text = raw.strip() + if text in ("[]",): + return [] + if text.startswith("[") and text.endswith("]"): + inner = text[1:-1].strip() + if not inner: + return [] + return [_coerce_yaml_scalar(part) for part in inner.split(",")] + if (text.startswith('"') and text.endswith('"')) or (text.startswith("'") and text.endswith("'")): + return text[1:-1] + return text + + +def _parse_frontmatter_stdlib(block: str) -> Dict[str, Any]: + """Parse the SKILL.md frontmatter subset without PyYAML.""" + data: Dict[str, Any] = {} + hermes: Dict[str, Any] = {} + in_hermes = False + in_tags = False + tags: List[str] = [] + for raw_line in block.splitlines(): + if not raw_line.strip() or raw_line.strip().startswith("#"): + continue + stripped = raw_line.strip() + indent = len(raw_line) - len(raw_line.lstrip(" ")) + if indent == 0: + in_hermes = False + in_tags = False + if stripped.startswith("metadata:"): + data.setdefault("metadata", {}) + continue + if ":" in stripped: + key, _, val = stripped.partition(":") + data[key.strip()] = _coerce_yaml_scalar(val) + continue + if stripped == "hermes:": + in_hermes = True + in_tags = False + data.setdefault("metadata", {}) + data["metadata"]["hermes"] = hermes + continue + if in_hermes and stripped == "tags:": + in_tags = True + hermes["tags"] = tags + continue + if in_hermes and in_tags and stripped.startswith("- "): + tags.append(stripped[2:].strip().strip('"').strip("'")) + continue + if in_hermes and stripped.startswith("related_skills:"): + in_tags = False + hermes["related_skills"] = _coerce_yaml_scalar(stripped.split(":", 1)[1]) + continue + if in_hermes and ":" in stripped and not stripped.startswith("- "): + in_tags = False + key, _, val = stripped.partition(":") + if val.strip(): + hermes[key.strip()] = _coerce_yaml_scalar(val) + if tags and "tags" not in hermes: + hermes["tags"] = tags + if hermes: + data.setdefault("metadata", {}) + data["metadata"]["hermes"] = hermes + return data + + +def _load_skill_frontmatter() -> Tuple[Dict[str, Any], str]: + path = ROOT / "SKILL.md" + if not path.exists(): + return {}, "SKILL.md missing" + raw = path.read_bytes() + if not raw.startswith(b"---"): + return {}, "SKILL.md must start at byte 0 with ---" + text = raw.decode("utf-8") + match = re.search(r"\n---\n", text[3:]) + if match is None: + return {}, "SKILL.md frontmatter must close with \\n---\\n" + block = text[3 : 3 + match.start()] + try: + import yaml # type: ignore + + data = yaml.safe_load(block) + if not isinstance(data, dict): + return {}, "SKILL.md frontmatter is not a mapping" + return data, "ok" + except Exception: + data = _parse_frontmatter_stdlib(block) + if not data.get("name"): + return {}, "SKILL.md frontmatter parse failed" + return data, "ok (stdlib yaml fallback)" + + +def _hermes_meta(frontmatter: Dict[str, Any]) -> Dict[str, Any]: + metadata = frontmatter.get("metadata") + if not isinstance(metadata, dict): + return {} + hermes = metadata.get("hermes") + return hermes if isinstance(hermes, dict) else {} + + +def _skill_frontmatter_checks(frontmatter: Dict[str, Any], parse_msg: str) -> List[_Check]: + checks: List[_Check] = [] + parsed = bool(frontmatter) + checks.append(_check(parsed, "skill_frontmatter", parse_msg, parse_msg)) + if not parsed: + return checks + for key in _SKILL_REQUIRED_KEYS: + present = key in frontmatter and frontmatter.get(key) not in (None, "") + checks.append( + _check( + present, + f"skill_frontmatter.{key}", + f"frontmatter includes {key}", + f"frontmatter missing {key}", + ) + ) + description = frontmatter.get("description") + desc = description.strip() if isinstance(description, str) else "" + if desc in (">", "|"): + checks.append( + _check( + False, + "skill_description_len", + "", + "description is a folded YAML block; Hermes indexes ≤60 chars", + ) + ) + checks.append(_check(False, "skill_description_period", "", "folded description has no period")) + else: + checks.append( + _check( + 0 < len(desc) <= _SKILL_DESC_LIMIT, + "skill_description_len", + f"description {len(desc)} chars (≤{_SKILL_DESC_LIMIT})", + f"description {len(desc)} chars exceeds {_SKILL_DESC_LIMIT}", + ) + ) + checks.append( + _check( + desc.endswith("."), + "skill_description_period", + "description ends with a period", + "description must end with a period", + ) + ) + author = frontmatter.get("author") + author_text = author.strip() if isinstance(author, str) else "" + checks.append( + _check( + bool(author_text) and author_text != "Hermes Agent", + "skill_author_human", + "author credits the human first", + "author must credit the human first, then Hermes Agent", + ) + ) + hermes = _hermes_meta(frontmatter) + tags = hermes.get("tags") + checks.append( + _check( + isinstance(tags, list) and all(isinstance(t, str) and t.strip() for t in tags), + "skill_frontmatter.tags", + "metadata.hermes.tags present", + "metadata.hermes.tags missing", + ) + ) + checks.append( + _check( + "related_skills" in hermes and isinstance(hermes.get("related_skills"), list), + "skill_frontmatter.related_skills", + "metadata.hermes.related_skills present", + "metadata.hermes.related_skills missing", + ) + ) + expected = _read_version() + version = str(frontmatter.get("version") or "").strip() + checks.append( + _check( + version == expected, + "skill_frontmatter.version_match", + f"SKILL.md version {version} matches VERSION", + f"SKILL.md version {version!r} != VERSION {expected!r}", + ) + ) + return checks + + def cmd_check(_args: argparse.Namespace) -> int: checks: List[_Check] = [] checks.append(_check((ROOT / "VERSION").exists(), "version_file", "VERSION exists", "VERSION missing")) checks.append(_check((ROOT / "SKILL.md").exists(), "skill_contract", "SKILL.md exists", "SKILL.md missing")) + frontmatter, fm_msg = _load_skill_frontmatter() + checks.extend(_skill_frontmatter_checks(frontmatter, fm_msg)) checks.append(_check((ROOT / "schemas/ingest.v1.schema.json").exists(), "schema_ingest", "ingest schema present", "ingest schema missing")) checks.append(_check((ROOT / "schemas/result.v1.schema.json").exists(), "schema_result", "result schema present", "result schema missing")) checks.append(_check((ROOT / "schemas/receipt.v1.schema.json").exists(), "schema_receipt", "receipt schema present", "receipt schema missing")) diff --git a/tests/test_hyperlex.py b/tests/test_hyperlex.py index f6bc6fc..6326988 100644 --- a/tests/test_hyperlex.py +++ b/tests/test_hyperlex.py @@ -39,6 +39,19 @@ def test_cli_check_ok() -> None: checks = {entry["name"]: entry["ok"] for entry in body["checks"]} assert checks["version_file"] assert checks["schema_ingest"] + assert checks["skill_frontmatter"] + assert checks["skill_description_len"] + assert checks["skill_description_period"] + assert checks["skill_frontmatter.name"] + assert checks["skill_frontmatter.description"] + assert checks["skill_frontmatter.version"] + assert checks["skill_frontmatter.author"] + assert checks["skill_frontmatter.license"] + assert checks["skill_frontmatter.platforms"] + assert checks["skill_frontmatter.tags"] + assert checks["skill_frontmatter.related_skills"] + assert checks["skill_author_human"] + assert checks["skill_frontmatter.version_match"] def test_sources_command() -> None: