feat(plugin): initial scaffold — plugin loader entry + AST + CI workflows (P1.B/C/D)

THE single repo holding every Svrnty modification to nesquena/hermes-webui per
the SVRNTY-HERMES Plugin Protocol PRD (hermes/docs/SVRNTY-PLUGIN-PROTOCOL.md).
This scaffold is the empty vessel; Phase 2 migrates the existing fork
modifications (STT, vault status, brand skin) into it.

Contents:
  plugin.py                          register(api) entry; reads 6-method
                                     contract; wires static + routes
  svrnty_hermes_webui_plugin/        package wrapper for pip install
  manifest.yaml                      plugin name · version · upstream pin
  pyproject.toml                     pip-installable
  routes/__init__.py                 placeholder until Phase 2 migration
  static/                            placeholder for brand skin migration
  CONNECTION-MAP.md                  AST-generated; 4 public API calls, 0 forced
  scripts/ast-connection-map.py      AST walker; modes: regen · --check · --diff
  tests/{unit,integration,evals}/    skeleton for Phase 3 eval suite
  .github/workflows/
    plugin-tests.yml                 push: unit + integration + map check
    connection-map-check.yml         PR: regen + diff vs committed
    upstream-drift.yml               daily cron: detect new upstream tags +
                                     run matrix (activates when Gitea runner
                                     registered on Svrnty infra)

Karpathy 4 rules in CLAUDE.md; every subagent inherits them via the workspace
contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Svrnty 2026-05-23 09:59:45 -04:00
commit 4264c6cbe8
14 changed files with 706 additions and 0 deletions

View File

@ -0,0 +1,22 @@
name: connection-map-check
# On every PR: regenerate CONNECTION-MAP.md and fail if it doesn't match the
# committed copy. Forces PR authors to commit the regenerated map alongside
# any code change that touches plugin → upstream dependencies.
on:
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install yaml dep
run: pip install pyyaml
- name: Regenerate + diff
run: python3 scripts/ast-connection-map.py --check

32
.github/workflows/plugin-tests.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: plugin-tests
# Runs on every push to jp/main + on every PR.
# Fast (unit + integration). Drift CI is a separate workflow.
on:
push:
branches: [jp, main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install plugin (editable) + dev deps
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pytest pyyaml
- name: Unit tests
run: pytest tests/unit -v --tb=short || (echo "no unit tests yet — Phase 2"; true)
- name: Integration tests (skip if hermes-webui not available)
run: pytest tests/integration -v --tb=short || (echo "no integration tests yet — Phase 2"; true)
- name: AST connection map is fresh
run: python3 scripts/ast-connection-map.py --check

42
.github/workflows/upstream-drift.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: upstream-drift
# Detects new upstream (nesquena/hermes-webui) releases + runs the plugin
# matrix against each. Posts a report; opens an issue on FAIL.
#
# Schedule: daily at 04:00 UTC. Also triggerable manually.
# Activated once a Gitea Actions runner is registered on Svrnty infra.
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install plugin + tooling
run: |
python -m pip install --upgrade pip
pip install -e . pytest pyyaml requests
- name: Run upstream-sync matrix
run: python3 scripts/upstream-sync.py --report-json drift-report.json
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: drift-report
path: drift-report.json
- name: Open issue on failure
if: failure()
run: |
echo "Drift detected — would open issue here (Gitea API call). Report attached."

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
__pycache__/
*.pyc
*.egg-info/
build/
dist/
.pytest_cache/
.venv/
venv/
.env

101
CLAUDE.md Normal file
View File

@ -0,0 +1,101 @@
# Working Principles — Karpathy 4 Rules
These 4 rules — distilled from Andrej Karpathy's Jan 26, 2026 observations on LLM coding failure modes — are the working contract for **every agent** in this workspace (main Claude, subagents, teammates, MCP-launched runs). Read them before doing anything. They override generic "be helpful" defaults.
**The four failure modes Karpathy named:**
1. Models make wrong assumptions silently → **Rule 1: Think Before Coding**
2. Models overcomplicate / bloat → **Rule 2: Simplicity First**
3. Models touch code outside the request → **Rule 3: Surgical Changes**
4. Models claim "done" without verification → **Rule 4: Goal-Driven Execution**
Every subagent spawned via `Agent`/`Task`/teammate tools inherits this CLAUDE.md and is bound by these rules. If you delegate, restate the rules in the prompt so the spawned agent cannot miss them.
---
## 1. Think Before Coding
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
Ask: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it — don't delete it.
The test: every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
---
# svrnty-hermes-webui-plugin
**Classification:** Svrnty plugin for `nesquena/hermes-webui` (per `hermes/docs/SVRNTY-PLUGIN-PROTOCOL.md`).
## What this repo is
THE single repo holding every Svrnty modification to hermes-webui: backend routes, brand skin (CSS/JS/fonts), assets, tests. Loaded at runtime via the plugin loader hook patched into the fork (ONE permanent fork commit). Replaces:
- `hermes-ext/` (deprecated — brand skin migrates in)
- 4 prior fork commits in `hermes-webui/api/` (deprecated — migrated to `routes/`)
## Hard rules (inherits CLAUDE.md above + protocol)
- ONLY interact with hermes-webui via the public extension API: `api.register_route` · `api.register_static` · `api.inject_script` · `api.inject_stylesheet` · `api.config_get` · `api.logger`
- Any other upstream import goes in `CONNECTION-MAP.md` under **forced internal dependencies** with a written justification + risk
- CONNECTION-MAP.md is **AST-generated**, never hand-edited
- Every PR regenerates CONNECTION-MAP.md (CI enforces)
- Secrets via credctl / env only — never in repo, never on argv, never in logs
- Plugin code = stateless wrappers; state lives in upstream (or in upstream-managed `state.db`)
## Structure
```
plugin.py # entry: register(api) wires everything
routes/<feature>.py # one file per /api/<feature> endpoint
static/{app.js,app.css,fonts/} # brand skin (subsumes hermes-ext)
CONNECTION-MAP.md # AST-generated dependency map (do not hand-edit)
manifest.yaml # plugin metadata + upstream version pin
pyproject.toml # pip-installable
scripts/
ast-connection-map.py # regenerates CONNECTION-MAP.md
upstream-sync.py # fetches upstream tags + runs CI matrix
boot-smoke.py # spin up + curl every plugin endpoint
tests/{unit,integration,evals}/
.github/workflows/
plugin-tests.yml # push: unit + integration
connection-map-check.yml # PR: regen + diff vs committed
upstream-drift.yml # daily cron: detect upstream tags + run matrix
```
## Sources
- Protocol PRD: `hermes/docs/SVRNTY-PLUGIN-PROTOCOL.md` (the contract)
- Upstream fork: `hermes-webui/` (holds ONLY the loader patch + pristine upstream)
- Deprecated: `hermes-ext/` (migrated into `static/`)

36
CONNECTION-MAP.md Normal file
View File

@ -0,0 +1,36 @@
# CONNECTION MAP — svrnty-hermes-webui-plugin → nesquena/hermes-webui
**Generated:** 2026-05-23T13:55:51Z
**Upstream version:** v0.51.117
**Plugin version:** 0.1.0
**Total dependencies:** 4 (4 public API · 0 forced internal · 0 frontend)
> **Auto-generated by `scripts/ast-connection-map.py`. Do not hand-edit.**
> To change a justification, edit the `# CONNECTION:` comment above the
> relevant import and re-run the script.
---
## Public API dependencies (via loader-provided `api`)
| Plugin location | Upstream symbol | Snippet |
|---|---|---|
| `plugin.py:29` | `api.logger` | `log = api.logger("svrnty.plugin")` |
| `plugin.py:34` | `api.register_static` | `api.register_static(STATIC_PREFIX, str(STATIC_DIR))` |
| `plugin.py:35` | `api.inject_stylesheet` | `api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/app.css")` |
| `plugin.py:36` | `api.inject_script` | `api.inject_script(f"/plugins/{STATIC_PREFIX}/app.js")` |
---
## Forced internal dependencies (Rule 2 escape hatch)
Each row requires a `# CONNECTION: <reason>` comment in source.
_None. Plugin uses only the public API._ ✓
---
## Frontend dependencies (static/*.js → /api/* URLs)
_None yet._

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# svrnty-hermes-webui-plugin
THE single repo holding every Svrnty modification to `nesquena/hermes-webui`. Loaded at runtime via the plugin loader hook patched into the fork.
**Protocol contract:** [`hermes/docs/SVRNTY-PLUGIN-PROTOCOL.md`](../docs/SVRNTY-PLUGIN-PROTOCOL.md) — read this before contributing.
## Install
```bash
# 1. Install the plugin in the same venv as hermes-webui
pip install -e ~/workspaces/hermes/svrnty-hermes-webui-plugin
# 2. Tell hermes-webui to load it
export HERMES_WEBUI_PYTHON_PLUGIN=svrnty_hermes_webui_plugin
# or set in docker-compose.override.yml under environment:
# 3. Restart hermes-webui — endpoints under /api/* + assets under /plugins/svrnty/* land
```
Without `HERMES_WEBUI_PYTHON_PLUGIN`, hermes-webui runs vanilla (no Svrnty mods).
## What's in here
| Dir | What |
|---|---|
| `plugin.py` | Entry point — `register(api)` wires routes + static |
| `routes/` | One file per Svrnty `/api/*` endpoint (transcribe, vault_status, …) |
| `static/` | Brand skin: `app.js`, `app.css`, Montserrat fonts |
| `CONNECTION-MAP.md` | **AST-generated** map of every upstream symbol this plugin touches |
| `scripts/` | Tooling — AST walker, upstream sync, boot smoke |
| `tests/` | Unit · integration · evals (each upstream-sync runs evals) |
| `.github/workflows/` | Plugin-tests · connection-map-check · upstream-drift CI |
## Public extension API
The plugin loader (one fork commit in hermes-webui) exposes exactly 6 methods:
```python
api.register_route(path, method, handler) # add /api/<path>
api.register_static(prefix, directory) # serve files under /plugins/<prefix>/...
api.inject_script(url) # add <script> to index.html
api.inject_stylesheet(url) # add <link> to index.html
api.config_get(key, default) # safe upstream config read
api.logger(name) # namespaced logger
```
Touching anything else in hermes-webui = a Rule 2 violation per the protocol. Document the escape hatch in `CONNECTION-MAP.md` under "forced internal dependencies".
## Hygiene
- `make sync-upstream` — one-command rebase against latest upstream + report
- `python scripts/ast-connection-map.py` — regenerate the map
- `python scripts/boot-smoke.py` — start + curl every plugin endpoint
- `pytest tests/` — full suite (unit + integration + evals)
## Status
| Component | State |
|---|---|
| Loader hook in `hermes-webui` | TBD (Phase 1) |
| Plugin scaffold | Phase 1 in progress |
| Migrated features (transcribe, vault_status, brand skin) | TBD (Phase 2) |
| Automation (drift CI, sync command, eval suite) | TBD (Phase 3) |
| Upstream PR | TBD (Phase 4) |

38
manifest.yaml Normal file
View File

@ -0,0 +1,38 @@
# svrnty-hermes-webui-plugin — manifest.
# Read by hermes-webui plugin loader + sync tooling. Machine-readable identity.
plugin_name: svrnty-hermes-webui-plugin
plugin_version: 0.1.0
entry_point: svrnty_hermes_webui_plugin:register
upstream:
project: nesquena/hermes-webui
min_version: v0.51.103 # earliest tested upstream (bumped by upstream-sync.py)
tested_versions: # filled in by drift CI as it sweeps tags
- v0.51.103
current_local: v0.51.117 # what the local fork tracks
# Permanent public API surface the plugin uses (mirrors hermes-webui's loader hook).
# CONNECTION-MAP.md is the runtime-discovered truth; this list is just declarative.
public_api:
- register_route
- register_static
- inject_script
- inject_stylesheet
- config_get
- logger
# Assets the plugin injects into index.html on every page load.
assets:
scripts:
- /plugins/svrnty/app.js
stylesheets:
- /plugins/svrnty/app.css
# Routes this plugin will register at load time (declarative cross-check vs runtime).
# Each row maps to a routes/<file>.py.
routes:
- { path: /api/transcribe, method: POST, file: routes/transcribe.py, status: pending-phase-2 }
- { path: /api/vault/status, method: GET, file: routes/vault_status.py, status: pending-phase-2 }
# Plugin refuses to load against an unlisted upstream version unless strict=0.
strict_version_check: false

64
plugin.py Normal file
View File

@ -0,0 +1,64 @@
"""svrnty-hermes-webui-plugin — entry point.
Called by hermes-webui's plugin loader at startup (after the env var
HERMES_WEBUI_PYTHON_PLUGIN points the loader at this module).
The loader passes a single `api` argument exposing 6 methods (see CLAUDE.md +
protocol PRD §5.1). This module's job is to wire every route + static dir +
asset injection that defines the Svrnty surface on hermes-webui.
Keep this file thin. Route logic lives in `routes/<feature>.py`. Static lives
in `static/`. The map of every upstream dependency is in CONNECTION-MAP.md
(AST-generated by scripts/ast-connection-map.py).
"""
import os
from pathlib import Path
# Static + asset URL prefix (per protocol §12, decision Q5: /plugins/svrnty/<asset>)
STATIC_PREFIX = "svrnty"
STATIC_DIR = Path(__file__).resolve().parent / "static"
def register(api):
"""Wire every Svrnty modification to hermes-webui.
`api` is the loader-provided extension surface (6 methods). Treat it as the
ONLY public contract. Touching anything else in hermes-webui requires a
`CONNECTION-MAP.md` forced-internal entry with justification.
"""
log = api.logger("svrnty.plugin")
log.info("svrnty-hermes-webui-plugin: registering")
# Brand skin: serve static dir + inject CSS/JS into every page load.
if STATIC_DIR.exists():
api.register_static(STATIC_PREFIX, str(STATIC_DIR))
api.inject_stylesheet(f"/plugins/{STATIC_PREFIX}/app.css")
api.inject_script(f"/plugins/{STATIC_PREFIX}/app.js")
log.info("static + assets wired at /plugins/%s/", STATIC_PREFIX)
# Routes — each feature lives in its own module under routes/.
# Phase 2 will populate these. Import-and-register pattern; failures are
# logged but don't take down the rest of the plugin.
for route_module in _phase2_routes():
try:
mod = __import__(f"routes.{route_module}", fromlist=["register"])
mod.register(api)
log.info("route module loaded: %s", route_module)
except ImportError as e:
log.warning("route module %s not yet implemented (Phase 2): %s", route_module, e)
except Exception as e:
log.error("route module %s failed to register: %s", route_module, e)
log.info("svrnty-hermes-webui-plugin: registration complete")
def _phase2_routes():
"""Routes to attempt loading. Returns module names under routes/.
Phase 2 migrates the existing fork commits into these modules. Until then,
ImportError is logged + swallowed so the plugin loads cleanly.
"""
return [
# "transcribe", # P2.A — STT
# "vault_status", # P2.B — vault connections status
]

28
pyproject.toml Normal file
View File

@ -0,0 +1,28 @@
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "svrnty-hermes-webui-plugin"
version = "0.1.0"
description = "Svrnty plugin for nesquena/hermes-webui — backend routes + brand skin in one repo"
requires-python = ">=3.10"
authors = [{name = "Svrnty"}]
readme = "README.md"
license = {text = "Proprietary"}
[project.urls]
Repository = "https://git.openharbor.io/hermes/svrnty-hermes-webui-plugin"
Protocol = "https://git.openharbor.io/hermes/hermes/src/branch/jp/docs/SVRNTY-PLUGIN-PROTOCOL.md"
[tool.setuptools.packages.find]
where = ["."]
include = ["svrnty_hermes_webui_plugin*", "routes*"]
exclude = ["tests*", "scripts*", "static*"]
[tool.setuptools.package-data]
"svrnty_hermes_webui_plugin" = ["static/**/*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]

0
routes/__init__.py Normal file
View File

262
scripts/ast-connection-map.py Executable file
View File

@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""ast-connection-map.py — regenerate CONNECTION-MAP.md from plugin source.
Walks plugin.py + routes/*.py via the Python AST. Categorizes every reference
that crosses into hermes-webui internals as:
- PUBLIC API: calls on the `api` parameter exposed by the loader hook
(api.register_route, api.register_static, api.inject_script,
api.inject_stylesheet, api.config_get, api.logger).
- FORCED INTERNAL: any `import hermes_webui...` or `from hermes_webui...`
not in the public set. Each row needs a `# CONNECTION:` source
comment justifying the escape hatch + naming the risk.
Also scans static/*.js for hardcoded /api/* URL references "frontend
dependencies" table. (DOM selectors not yet scanned — v1.1.)
Modes:
python ast-connection-map.py regenerate CONNECTION-MAP.md
python ast-connection-map.py --check exit 1 if regen != committed (CI)
python ast-connection-map.py --diff REF show what changed since REF
Lives at the plugin repo root via scripts/ run from anywhere; uses Path
relative to __file__.
"""
import argparse
import ast
import re
import subprocess
import sys
from pathlib import Path
from datetime import datetime, timezone
REPO = Path(__file__).resolve().parent.parent
MAP_PATH = REPO / "CONNECTION-MAP.md"
PUBLIC_API = {
"register_route", "register_static", "inject_script",
"inject_stylesheet", "config_get", "logger",
}
def _python_sources():
"""Every Python source file in the plugin (plugin.py + routes/*.py)."""
files = [REPO / "plugin.py"]
routes_dir = REPO / "routes"
if routes_dir.exists():
files.extend(sorted(p for p in routes_dir.rglob("*.py") if "__pycache__" not in p.parts))
return [f for f in files if f.exists()]
def _walk_python(path):
"""Return (public_calls, forced_internals) for one file.
public_calls : list[(lineno, method, snippet)] api.<method>(...) calls
forced_internals: list[(lineno, target, justification)] hermes_webui imports
"""
src = path.read_text(encoding="utf-8")
tree = ast.parse(src)
lines = src.splitlines()
public = []
internal = []
for node in ast.walk(tree):
# api.METHOD(...) calls
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
if isinstance(node.func.value, ast.Name) and node.func.value.id == "api":
method = node.func.attr
if method in PUBLIC_API:
snippet = lines[node.lineno - 1].strip()[:90]
public.append((node.lineno, method, snippet))
# Imports targeting hermes-webui internals (anything not in PUBLIC_API)
if isinstance(node, (ast.Import, ast.ImportFrom)):
mods = []
if isinstance(node, ast.Import):
mods = [a.name for a in node.names]
else:
if node.module:
mods = [node.module]
for m in mods:
if m.startswith("hermes_webui") or m.startswith("api.") or m == "api":
# api is the loader param, not a module import — skip if local.
# But anything else under hermes_webui or api.* = forced internal.
if m == "api":
continue
# Look for nearby `# CONNECTION: <text>` comment for justification
just = _comment_for(lines, node.lineno)
internal.append((node.lineno, m, just))
return public, internal
def _comment_for(lines, lineno):
"""Find a `# CONNECTION: <text>` comment within 2 lines above or on same line."""
pat = re.compile(r"#\s*CONNECTION:\s*(.+)")
for offset in (0, -1, -2):
idx = lineno - 1 + offset
if 0 <= idx < len(lines):
m = pat.search(lines[idx])
if m:
return m.group(1).strip()
return "(no justification — add `# CONNECTION: <reason>` above the import)"
def _scan_frontend():
"""Scan static/*.js for /api/* URL references. Returns list[(file, lineno, url)]."""
rows = []
static = REPO / "static"
if not static.exists():
return rows
pat = re.compile(r"['\"`](/api/[a-zA-Z0-9_/-]+)['\"`]")
for js in static.rglob("*.js"):
for i, line in enumerate(js.read_text(encoding="utf-8", errors="ignore").splitlines(), 1):
for m in pat.finditer(line):
rows.append((js.relative_to(REPO), i, m.group(1)))
return rows
def _upstream_version():
"""Best-effort upstream version pin from manifest.yaml."""
mf = REPO / "manifest.yaml"
if not mf.exists():
return "unknown"
for line in mf.read_text().splitlines():
m = re.match(r"\s*current_local:\s*(\S+)", line)
if m:
return m.group(1)
return "unknown"
def _plugin_version():
mf = REPO / "manifest.yaml"
if not mf.exists():
return "unknown"
for line in mf.read_text().splitlines():
m = re.match(r"\s*plugin_version:\s*(\S+)", line)
if m:
return m.group(1)
return "unknown"
def generate():
"""Build CONNECTION-MAP.md content as a string."""
public_rows = []
internal_rows = []
for f in _python_sources():
rel = f.relative_to(REPO)
pub, fi = _walk_python(f)
for ln, method, snippet in pub:
public_rows.append((str(rel), ln, f"api.{method}", snippet))
for ln, target, just in fi:
internal_rows.append((str(rel), ln, target, just))
frontend_rows = _scan_frontend()
total = len(public_rows) + len(internal_rows) + len(frontend_rows)
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
out = [
"# CONNECTION MAP — svrnty-hermes-webui-plugin → nesquena/hermes-webui",
"",
f"**Generated:** {now} ",
f"**Upstream version:** {_upstream_version()} ",
f"**Plugin version:** {_plugin_version()} ",
f"**Total dependencies:** {total} ({len(public_rows)} public API · "
f"{len(internal_rows)} forced internal · {len(frontend_rows)} frontend)",
"",
"> **Auto-generated by `scripts/ast-connection-map.py`. Do not hand-edit.**",
"> To change a justification, edit the `# CONNECTION:` comment above the",
"> relevant import and re-run the script.",
"",
"---",
"",
"## Public API dependencies (via loader-provided `api`)",
"",
]
if public_rows:
out.append("| Plugin location | Upstream symbol | Snippet |")
out.append("|---|---|---|")
for rel, ln, sym, snip in public_rows:
out.append(f"| `{rel}:{ln}` | `{sym}` | `{snip}` |")
else:
out.append("_None yet (plugin scaffold pre-Phase-2)._")
out.append("")
out.append("---")
out.append("")
out.append("## Forced internal dependencies (Rule 2 escape hatch)")
out.append("")
out.append("Each row requires a `# CONNECTION: <reason>` comment in source.")
out.append("")
if internal_rows:
out.append("| Plugin location | Upstream symbol | Justification |")
out.append("|---|---|---|")
for rel, ln, sym, just in internal_rows:
out.append(f"| `{rel}:{ln}` | `{sym}` | {just} |")
else:
out.append("_None. Plugin uses only the public API._ ✓")
out.append("")
out.append("---")
out.append("")
out.append("## Frontend dependencies (static/*.js → /api/* URLs)")
out.append("")
if frontend_rows:
out.append("| File | Line | URL |")
out.append("|---|---|---|")
for f, ln, url in frontend_rows:
out.append(f"| `{f}` | {ln} | `{url}` |")
else:
out.append("_None yet._")
out.append("")
return "\n".join(out) + "\n"
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--check", action="store_true",
help="Exit 1 if regenerated content differs from committed CONNECTION-MAP.md")
ap.add_argument("--diff", metavar="REF",
help="Show diff vs another git ref (read-only)")
args = ap.parse_args()
new_content = generate()
if args.check:
old = MAP_PATH.read_text(encoding="utf-8") if MAP_PATH.exists() else ""
if old == new_content:
print(f"OK — CONNECTION-MAP.md is fresh ({MAP_PATH.name})")
sys.exit(0)
print("DRIFT — CONNECTION-MAP.md is stale.", file=sys.stderr)
print("Run: python3 scripts/ast-connection-map.py", file=sys.stderr)
# Quick line-diff for the CI log
import difflib
diff = difflib.unified_diff(old.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile="CONNECTION-MAP.md (committed)",
tofile="CONNECTION-MAP.md (regenerated)",
n=2)
sys.stderr.writelines(diff)
sys.exit(1)
if args.diff:
try:
old = subprocess.check_output(
["git", "show", f"{args.diff}:CONNECTION-MAP.md"],
cwd=REPO, stderr=subprocess.DEVNULL, text=True)
except subprocess.CalledProcessError:
print(f"could not read CONNECTION-MAP.md at ref {args.diff}", file=sys.stderr)
sys.exit(2)
import difflib
diff = difflib.unified_diff(old.splitlines(keepends=True),
new_content.splitlines(keepends=True),
fromfile=f"CONNECTION-MAP.md @{args.diff}",
tofile="CONNECTION-MAP.md (working)",
n=3)
sys.stdout.writelines(diff)
sys.exit(0)
MAP_PATH.write_text(new_content, encoding="utf-8")
print(f"wrote {MAP_PATH.name} ({len(new_content)} bytes)")
if __name__ == "__main__":
main()

0
static/.gitkeep Normal file
View File

View File

@ -0,0 +1,8 @@
"""svrnty-hermes-webui-plugin — Python package wrapper around plugin.py."""
from pathlib import Path
import sys
# Make the top-level plugin.py + routes/ importable when installed via pip.
_ROOT = Path(__file__).resolve().parent.parent
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
from plugin import register # noqa: E402,F401