Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<a href="https://github.com/scrimshawlife-ctrl/Hyperlex/actions/workflows/docs.yml"><img src="https://img.shields.io/github/actions/workflow/status/scrimshawlife-ctrl/Hyperlex/docs.yml?branch=main&label=docs&logo=github" alt="Docs CI"></a>
<a href="https://scrimshawlife-ctrl.github.io/Hyperlex/"><img src="https://img.shields.io/badge/docs-GitHub%20Pages-0d9488?logo=markdown" alt="Docs"></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
<a href="./VERSION"><img src="https://img.shields.io/badge/version-0.4.0-informational" alt="Version 0.4.0"></a>
<a href="./VERSION"><img src="https://img.shields.io/badge/version-0.4.1-informational" alt="Version 0.4.1"></a>
<img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-3776AB?logo=python&logoColor=white" alt="Python">
<img src="https://img.shields.io/badge/hermes-skill-7c3aed" alt="Hermes skill">
<img src="https://img.shields.io/badge/offline--first-mock%20route-22c55e" alt="Offline-first">
Expand Down Expand Up @@ -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 |
Expand Down
253 changes: 75 additions & 178 deletions SKILL.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion STATUS.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.0
0.4.1
4 changes: 4 additions & 0 deletions docs/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ hide:
# Hyperlex

<div class="hlx-status" markdown>
<span><span class="hlx-dot"></span><strong>v0.4.0</strong></span>
<span><span class="hlx-dot"></span><strong>v0.4.1</strong></span>
<span>Hermes skill · Python package</span>
<span>Settled Brier only</span>
<span>Local-first · offline mock ready</span>
Expand Down Expand Up @@ -126,5 +126,5 @@ Full glossary: [start/glossary.md](start/glossary.md)
<p class="hlx-splash-brand-foot">READ DEEPER. THINK WIDER.</p>

<p class="hlx-posture">
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
</p>
2 changes: 1 addition & 1 deletion docs/status.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Operator desk on Pages — **publish-safe**, not a live dashboard.
Real state lives in `~/.hyperlex/` (receipts, score log, vector DB).

<div class="hlx-status" markdown>
<span><span class="hlx-dot"></span><strong>v0.4.0</strong></span>
<span><span class="hlx-dot"></span><strong>v0.4.1</strong></span>
<span>Hermes skill · auto pipeline</span>
<span>Atomic multi-term · settled Brier only</span>
<span>Pages = static history</span>
Expand Down
2 changes: 1 addition & 1 deletion hyperlex.manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: hyperlex
version: 0.4.0
version: 0.4.1
display_name: Hyperlex
kind: hermes-openclaw-skill
description: >
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,4 @@ exclude_docs: |
README.md

extra:
version: 0.4.0
version: 0.4.1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
4 changes: 4 additions & 0 deletions references/hermes-runtime-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
193 changes: 193 additions & 0 deletions scripts/hyperlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import argparse
import hashlib
import json
import re
import sys
import traceback
from dataclasses import dataclass
Expand Down Expand Up @@ -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"))
Expand Down
13 changes: 13 additions & 0 deletions tests/test_hyperlex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading