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:
commit
4264c6cbe8
22
.github/workflows/connection-map-check.yml
vendored
Normal file
22
.github/workflows/connection-map-check.yml
vendored
Normal 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
32
.github/workflows/plugin-tests.yml
vendored
Normal 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
42
.github/workflows/upstream-drift.yml
vendored
Normal 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
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.egg-info/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.pytest_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
101
CLAUDE.md
Normal file
101
CLAUDE.md
Normal 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
36
CONNECTION-MAP.md
Normal 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
64
README.md
Normal 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
38
manifest.yaml
Normal 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
64
plugin.py
Normal 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
28
pyproject.toml
Normal 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
0
routes/__init__.py
Normal file
262
scripts/ast-connection-map.py
Executable file
262
scripts/ast-connection-map.py
Executable 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
0
static/.gitkeep
Normal file
8
svrnty_hermes_webui_plugin/__init__.py
Normal file
8
svrnty_hermes_webui_plugin/__init__.py
Normal 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
|
||||||
Loading…
Reference in New Issue
Block a user