chore: gitignore hardening, VERSION, CI workflow, tests, docs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jon
2026-07-11 07:25:15 -05:00
parent 3d1168ff8c
commit 838860fdf0
6 changed files with 225 additions and 34 deletions
+43
View File
@@ -0,0 +1,43 @@
---
name: Ponyman
description: "Minimalist coding agent with a caveman-speaking toggle. Use for pragmatic bug fixes, small implementations, reviews, and cleanup where the shortest correct solution matters. Say 'caveman mode' for compressed speech or 'normal mode' for standard speech."
tools: [read, search, edit, execute, todo]
user-invocable: true
---
You are Ponytail Caveman, a pragmatic senior coding agent.
Your engineering rule is ponytail minimalism: understand the real control path, reuse existing code, prefer the standard library and native platform features, and make the smallest correct change. Fix root causes. Do not add speculative abstractions, dependencies, boilerplate, or unrelated refactors. Never simplify away security, validation, error handling, accessibility, or tests needed to protect changed behavior. After this statement, the rest of the readme will be in caveman talk to provide a reference for how it should sound.
Caveman talk dumb. Grunt words. "Me", "you", "big", "broke", "good". Short. Sound like cave person poke rock with stick. BUT point always land — reader still know what happen and what do next. Dumb sound, smart meaning. Keep code, file name, command, error word exact — no dumb those.
## How Me Talk
- No word say: me talk normal. Clear.
- You say `caveman mode`, `talk caveman`, or `/caveman`: me go dumb caveman. Still say enough, point land.
- You say `normal mode`, `talk normally`, or `/caveman normal`: me talk normal again.
- Me keep same talk till you change it.
- Talk change word only. Me brain and safe stay smart.
## Me Do Work Like This
1. Find thing. File, symbol, broke part, command, or test.
2. Look small part near. Make one guess me can prove wrong. Pick one cheap check.
3. Fix right code path. Smallest patch. No more.
4. Run small check. Now, not later.
5. Add or fix test for tricky part. Security, save-data, parse, error path — these most.
6. Run big check when change touch many module.
7. Other dirty change — no touch. Never reset, revert, commit, or make branch unless you ask.
## Me Pick Tool
- Read and look before me edit.
- Use pattern and command repo already got.
- Use real parser/API for structured data.
- Use `apply_patch` for hand edit.
- Like focused test, lint, typecheck, or build more than diff-only check.
- Comment rare, only useful. No talk what code already say.
## Me Say Back
Normal mode: say what change, what me check, what risk left. Few short line.
Caveman mode: dumb short grunt, point still land. Like this:
`Me fix big bug. Add test. pytest: 8 pass. One warning still there — old deprecation, no scare.`
Look-over work: bad thing first, worst on top, with file link and how me fix. No bad thing → say so, name test gap or risk left.
Regular → Executable
+17 -21
View File
@@ -1,30 +1,26 @@
# Large machine-specific / regenerable trees (never commit)
# Promethean/ = 16G ROCm venv (recreate from requirements*.txt)
# ollama/ = 7.4G bundled Ollama binary; models/ = 4.9G Ollama blobs
Promethean/ Promethean/
ollama/ ollama/
models/
interface/web/node_modules/ interface/web/node_modules/
# Runtime state & user data
runtime/
data/uploads/
data/exports/
*.db
*.db-wal
*.db-shm
# Build / cache artifacts
interface/web/dist/ interface/web/dist/
runtime/
__pycache__/ __pycache__/
*.pyc *.pyc
*.pyo *.pyo
synapse/memory/memory.db
# Editor / DE / local cruft synapse/memory/memory.db-wal
synapse/memory/memory.db-shm
*.db-wal
*.db-shm
.DS_Store
.directory .directory
*.bak
*.bak-*
*.tar.gz
# Local-only Claude config (machine-specific) # Ollama model blobs (~15G, regenerable via `ollama pull`)
.claude/settings.local.json models/
# runtime user data
data/uploads/
data/exports/
# local editor / assistant tooling
.vscode/
.claude/
Regular → Executable
+55 -12
View File
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## What is NexusOS ## What is NexusOS
NexusOS is a local AI assistant platform. It runs a Python/FastAPI backend (Synapse) that interfaces with a locally bundled Ollama instance, a dedicated memory microservice, and a React/Vite frontend. Everything runs on `localhost` with no external AI API calls — the LLM is served via Ollama. NexusOS is a local AI assistant platform. It runs a Python/FastAPI backend (Synapse) that interfaces with a locally bundled Ollama instance, a dedicated memory microservice, and a React/Vite frontend. All AI inference runs through Ollama on `localhost`; no external AI provider is configured or called.
## Running the Project ## Running the Project
@@ -29,12 +29,24 @@ uvicorn synapse.memory.service:app --host 0.0.0.0 --port 8001 --reload
cd interface/web && npm run dev cd interface/web && npm run dev
``` ```
**Management CLI** (start/stop individual services with PID tracking): **Management CLI** (`ncp`) — start/stop services with PID tracking, plus terminal
access to the same features as the web UI (all via the REST API on `:8000`):
```bash ```bash
./management/nexus-cli.sh start # starts backend + frontend ./management/nexus-cli.sh start # starts backend + frontend
./management/nexus-cli.sh stop ./management/nexus-cli.sh stop
./management/nexus-cli.sh start_backend / start_frontend ./management/nexus-cli.sh start --backend|-b / --frontend|-f / --memory|-m
# Feature commands (dispatch to management/nexus_api.py — httpx, no TUI):
ncp chat "<message>" # stream a reply (POST /chat/stream)
ncp memory list|add <text>|rm <id>
ncp playbook list|show <id> # first playbook (*) is the active system prompt
ncp history [query] # recent conversations
``` ```
The old curses TUIs (`nexus-chat.py`, `nexus-playbook.py`) were removed in favor of
these API-backed subcommands. The CLI covers chat, memory, playbooks, and history;
the web UI and control panel expose the remaining management features.
`management/controlpanel.py` (tkinter GUI, wired into the XFCE panel via
`bin/panel/nexus-popup.py`) stays.
**Frontend lint:** **Frontend lint:**
```bash ```bash
@@ -49,15 +61,17 @@ cd interface/web && npm run build
## Architecture ## Architecture
### Python venv ### Python venv
All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. The AMD ROCm PyTorch stack is installed here (`requirements.txt`). All Python code runs inside `Promethean/` (a local venv). Always activate it before running backend commands: `source Promethean/bin/activate`. Dependencies are layered: `requirements-base.txt` holds the GPU-agnostic core, and a thin overlay pins the right PyTorch build for the target — `requirements-amd.txt` (ROCm), `requirements-nvidia.txt` (CUDA, generated by `bin/gen-nvidia-reqs.py`), or `requirements-wsl.txt` (CPU-only). `bin/install.sh` selects NVIDIA, AMD, or CPU/WSL requirements from the host.
### Synapse Backend (`synapse/`) ### Synapse Backend (`synapse/`)
FastAPI app at `synapse/main.py`. Key responsibilities: FastAPI app at `synapse/main.py`. Key responsibilities:
- `/chat` and `/chat/stream` — chat with Ollama; streaming uses SSE. After each exchange the stream endpoint calls the Memory Service to auto-extract persistent facts. - `/chat/stream` — chat with Ollama; streaming uses SSE (the only chat endpoint — the non-stream `/chat` was removed). After each exchange the stream endpoint calls the Memory Service to auto-extract persistent facts.
- `/playbooks` — CRUD for playbooks stored in SQLite via `synapse/playbooks/store.py`. - `/playbooks` — CRUD for playbooks stored as YAML files in `data/playbooks/` via `synapse/playbooks/store.py`.
- `/memory` — CRUD for persistent facts (proxies the same SQLite store as the memory service). - `/memory` — CRUD for persistent facts (proxies the same SQLite store as the memory service).
- `/models` — lists, pulls, and deletes Ollama models by proxying `ollama/bin/ollama`'s HTTP API. - `/models` — lists, pulls, and deletes Ollama models by proxying Ollama's HTTP API.
- `/conversations` — persists and retrieves full chat history from SQLite. - `/settings` and `/ollama` — persist runtime settings and control Ollama lifecycle.
- `/conversations` — persists, retrieves, edits, deletes, and exports full chat history from SQLite.
- `/icons` — lists local application icons and applies NexusOS branding.
**System prompt assembly** (in `main.py` `chat_stream_endpoint`): the final system prompt is built by layering the active playbook instructions → reference playbook context → persistent memory facts → relevant past conversation snippets retrieved by `store.search_conversations`. **System prompt assembly** (in `main.py` `chat_stream_endpoint`): the final system prompt is built by layering the active playbook instructions → reference playbook context → persistent memory facts → relevant past conversation snippets retrieved by `store.search_conversations`.
@@ -65,7 +79,7 @@ FastAPI app at `synapse/main.py`. Key responsibilities:
A separate FastAPI app on port 8001. `service.py` exposes `/memories/extract` which calls `extractor.py` — an Ollama prompt that decides whether to persist a new fact from a conversation exchange. The main Synapse backend calls this asynchronously after each streaming response. Both services share the same SQLite database (`synapse/memory/memory.db`). A separate FastAPI app on port 8001. `service.py` exposes `/memories/extract` which calls `extractor.py` — an Ollama prompt that decides whether to persist a new fact from a conversation exchange. The main Synapse backend calls this asynchronously after each streaming response. Both services share the same SQLite database (`synapse/memory/memory.db`).
### Playbook System (`synapse/playbooks/` + `synapse/playbook_manager.py`) ### Playbook System (`synapse/playbooks/` + `synapse/playbook_manager.py`)
Playbooks are ordered records (title, goal, instructions, tags) stored in SQLite. The **first** playbook by order is the active system prompt; all subsequent playbooks are injected as reference context. `PlaybookManager` is the thin class the backend uses to retrieve and render them. Playbooks are ordered records (title, goal, instructions, tags), each persisted as a `{id}.yaml` file in `data/playbooks/` by `PlaybookFileStore` (the dir is `PLAYBOOK_DIR` in `nexus_config.py`). The **first** playbook by order is the active system prompt; all subsequent playbooks are injected as reference context. `PlaybookManager` is the thin class the backend uses to retrieve them and assemble the system prompt.
### Ollama (`ollama/bin/ollama`) ### Ollama (`ollama/bin/ollama`)
A bundled Ollama binary lives at `ollama/bin/ollama`. `OllamaManager` in `synapse/ollama_manager.py` manages its lifecycle (start/stop/health-check) and selects the best available model. GPU detection uses Vulkan (`vulkaninfo`) to prefer discrete AMD/NVIDIA GPUs. The Ollama HTTP API is at `http://127.0.0.1:11434` (overridable via `OLLAMA_HOST` env var). A bundled Ollama binary lives at `ollama/bin/ollama`. `OllamaManager` in `synapse/ollama_manager.py` manages its lifecycle (start/stop/health-check) and selects the best available model. GPU detection uses Vulkan (`vulkaninfo`) to prefer discrete AMD/NVIDIA GPUs. The Ollama HTTP API is at `http://127.0.0.1:11434` (overridable via `OLLAMA_HOST` env var).
@@ -74,10 +88,10 @@ A bundled Ollama binary lives at `ollama/bin/ollama`. `OllamaManager` in `synaps
React 19 + Vite. No routing library — `App.jsx` manages page state in a single `currentPage` useState. All API calls hit `http://localhost:8000` (configured in `src/config.js`). Pages: Chatbot, Playbook editor, Conversation History, Models, Memory, Settings. React 19 + Vite. No routing library — `App.jsx` manages page state in a single `currentPage` useState. All API calls hit `http://localhost:8000` (configured in `src/config.js`). Pages: Chatbot, Playbook editor, Conversation History, Models, Memory, Settings.
### Persistent Storage ### Persistent Storage
All data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory facts, conversations, messages, playbooks, app settings. `synapse/memory/store.py` (`PersistentMemoryStore`) owns the schema and all queries. `nexus_config.py` defines all paths; it also ensures all required directories exist on import. Most data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory facts, conversations, messages, app settings. `synapse/memory/store.py` (`PersistentMemoryStore`) owns the schema and all queries. Playbooks are the exception — they live as YAML files in `data/playbooks/` (see Playbook System). `nexus_config.py` defines all paths; it also ensures all required directories exist on import.
### Logs & Runtime State ### Logs & Runtime State
- `runtime/backend.log`, `runtime/frontend.log` — service stdout - `runtime/backend.log`, `runtime/frontend.log`, `runtime/memory.log` — service stdout
- `runtime/logs/ollama.log`, `runtime/logs/chat.log` - `runtime/logs/ollama.log`, `runtime/logs/chat.log`
- `runtime/pids/backend.pid`, `runtime/pids/frontend.pid` — used by the management CLI - `runtime/pids/backend.pid`, `runtime/pids/frontend.pid` — used by the management CLI
@@ -88,5 +102,34 @@ All data lands in `synapse/memory/memory.db` (SQLite, WAL mode). Tables: memory
| Ollama host | `OLLAMA_HOST` env var (default `http://127.0.0.1:11434`) | | Ollama host | `OLLAMA_HOST` env var (default `http://127.0.0.1:11434`) |
| All filesystem paths | `synapse/nexus_config.py` `Settings` class | | All filesystem paths | `synapse/nexus_config.py` `Settings` class |
| Frontend API base URL | `interface/web/src/config.js` | | Frontend API base URL | `interface/web/src/config.js` |
| Python dependencies (AMD) | `requirements.txt` / `amd_requirements.txt` |
| Python dependencies (base) | `requirements-base.txt` | | Python dependencies (base) | `requirements-base.txt` |
| Python dependencies (AMD/ROCm) | `requirements-amd.txt` |
| Python dependencies (NVIDIA/CUDA) | `requirements-nvidia.txt` (generated by `bin/gen-nvidia-reqs.py`) |
| Python dependencies (WSL/CPU) | `requirements-wsl.txt` |
## Cross-Machine Notes
A shared scratch log so the two Claude Code instances (this NexusOS box and the WSL box) can hand off context. **Protocol for both instances:** only append under *your own* machine's heading — never edit the other's — and prefix every entry with an ISO date, e.g. `- [2026-06-11] …`. Owning-your-own-section keeps the two sides conflict-free, so the file can be merged by plain concatenation whenever it syncs. **Handoff ritual:** when you start a session after a sync, scan the *other* machine's section for items addressed to you (e.g. a bold `**WSL box:**` / `**NexusOS box:**` callout), act on them, and append the outcome — done or still blocked — under your *own* heading with the date.
> This file does **not** auto-sync between machines; it has to be carried across (see handoff below). The split-by-machine layout is what makes that merge painless.
### NexusOS box — native Linux · T2 Mac / AMD · XFCE
- [2026-06-11] Started this shared-notes log.
- [2026-06-12] Canonical backup folder is now `router:/tmp/mnt/Wingdrive2/nexus-core` (resolved the old nexus-core/nexus-backup cross-wiring). `bin/backup.sh` now targets `nexus-core` (was `nexus-backup`) so backup and restore use the same folder.
- [2026-06-12] Added `-c | --claude` check mode to `bin/backup.sh` + `bin/restore.sh`: a `--checksum` content-level dry-run that changes nothing and skips the rebuild — use it to decide whether a real backup/restore is actually needed.
- [2026-06-12] Added `--no-owner --no-group` to the real backup/restore rsyncs so they stop re-syncing group metadata on every file against the busybox router (quieter, faster).
- [2026-06-12] Fixed `management/nexus-cli.sh` `ensure_router_reachable` (~line 412): the `nmcli connection up wgs_client` WireGuard fallback dies under WSL (no NetworkManager → "permission denied"). It's now conditional on nmcli + the wgs_client connection existing. **WSL box:** you're a desktop with a direct LAN link to the server, so just make `ssh router` work (Host `router` → 192.168.50.1:9001, user jon, key `id_iWings`, in `~/.ssh/config` — which is NOT synced, set it up there) — then `ncp restore` passes the first check and never hits the VPN fallback.
- [2026-07-10] Over-engineering purge. Removed dead backend endpoints (`POST /chat`, `/memory/{id}/move[_to]`, `/playbook/run`, `/search`) + their orphaned store methods, and the sync chat wrappers. Deleted the two curses TUIs (`nexus-chat.py`, `nexus-playbook.py`, ~1280 lines) and replaced them with API-backed `ncp` subcommands (`chat`/`memory`/`playbook`/`history``management/nexus_api.py`). `controlpanel.py` kept (panel GUI). `/conversations/export` kept (ShareGPT export for future fine-tuning).
- [2026-07-10] Nuked the ML + scientific/GUI stack from THIS venv only: torch/transformers/accelerate/bitsandbytes/etc + numpy/scipy/pandas/matplotlib/PySide6 + 53 orphans total. **Promethean 16G → 108M.** Requirements files are UNCHANGED — `pip install -r requirements-amd.txt` restores the declared ML stack. Caveat: some installed extras (peft, datasets, mergekit, gguf, hf-xet) were NOT in any requirements file; if the desktop ML work needs them, add them to requirements first. Rollback freeze snapshot was taken pre-purge. **WSL box:** your venv is separate and untouched; run the same `pip uninstall` sweep if you want the space back (nothing in `synapse/` imports any of it — Ollama does inference over HTTP).
### WSL box — Windows 11 · WSL2 Ubuntu · DESKTOP-WINGX
- [2026-06-11] `install-windows.ps1` had a PowerShell syntax error at line 73 (`Unexpected token '}'`); fixed brace mismatch in the script.
- [2026-06-11] `$USER` env var unset error at line 103 — PowerShell doesn't inherit Linux env vars; fixed by reading user via `wsl -- bash -c "echo $USER"` with proper escaping.
- [2026-06-11] Promethean venv was not landing inside `nexus-core/` in WSL; root cause was the bootstrap path resolving to a Windows path instead of `\\wsl.localhost\Ubuntu\home\jon\nexus-core`. Fixed by rewriting `bin/wsl-bootstrap.sh` to resolve paths from within WSL.
- [2026-06-11] Added GPU detection script to auto-select `requirements-amd.txt` vs `requirements-nvidia.txt`.
- [2026-06-11] Added `ncp` as a `~/.bashrc` alias and wired a "Promethean Terminal" entry into the Windows right-click context menu.
- [2026-06-11] Python dependency install was failing; traced to pip resolving inside the wrong environment before venv was fully activated.
- [2026-06-11] Added `sudo apt install snapd` + `sudo snap install ollama` to `wsl-bootstrap.sh`; systemd must be enabled in `/etc/wsl.conf` for snap to work — added that check to bootstrap.
- [2026-06-11] `wsl-bootstrap.sh` requires `sudo`; wired `sudo ~/nexus-core/bin/wsl-bootstrap.sh` invocation into `install-windows.ps1`.
- [2026-06-11] Nuked WSL and re-provisioned a fresh Ubuntu instance to validate the full install path. WSL provisioned successfully (user `jon` created) but bootstrap did not fire — timing/invocation issue in the PS script.
- [2026-06-11] Session ended with `ncp restore` returning permission denied on `nexus-cli.sh:412` and WireGuard (`wgs_client`) unable to reach the router. Unresolved at session close.
+1
View File
@@ -0,0 +1 @@
1.0.0
+13 -1
View File
@@ -47,5 +47,17 @@ FRONTEND_PID=$(_start "NEXUS FRONTEND SERVICE" "$FRONTEND_LOG" "$FRONTEND_DIR" \
sleep 5 sleep 5
_check "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG" _check "NEXUS FRONTEND SERVICE" "$FRONTEND_PID" "$FRONTEND_LOG"
trap "kill $MEMORY_PID $BACKEND_PID $FRONTEND_PID 2>/dev/null; echo ''; echo 'NEXUS STOPPED'; exit" SIGINT SIGTERM _shutdown() {
echo ''
echo 'Stopping NexusOS...'
# First TERM the processes we directly spawned.
kill "$MEMORY_PID" "$BACKEND_PID" "$FRONTEND_PID" 2>/dev/null
# uvicorn --reload leaves a worker child holding the port and `npm run dev`
# reparents vite, so the direct kills above orphan survivors. Delegate to the
# management CLI's port-based teardown, which reliably clears all three ports.
"$PROJECT_ROOT/management/nexus-cli.sh" stop >/dev/null 2>&1
echo 'NEXUS STOPPED'
exit
}
trap _shutdown SIGINT SIGTERM SIGHUP
wait wait
+96
View File
@@ -0,0 +1,96 @@
"""Smoke test for the Synapse backend's core wiring.
Run from nexus-core/ with the Promethean venv active: pytest -q
Deliberately tiny (ponytail): it guards core app wiring and local Ollama
behavior without needing a running Ollama service or network. Not a full suite.
"""
from fastapi.testclient import TestClient
import pytest
from synapse.main import app
from synapse.memory.store import MemoryItem, PersistentMemoryStore
from synapse.ollama_manager import OllamaManager
from synapse import ollama_manager
from synapse.icons.compositor import _is_allowed_path
def test_app_builds_and_root_responds():
# No `with` → startup event (which boots Ollama) does not run; `ollama`
# stays the module-level None and root() handles it. Pure liveness check.
resp = TestClient(app).get("/")
assert resp.status_code == 200
assert resp.json()["status"] == "online"
def test_keep_alive_pins_the_model():
# Default keeps the model resident so chats skip cold reloads...
assert PersistentMemoryStore._SETTINGS_DEFAULTS["keep_alive"] == "30m"
mgr = OllamaManager()
assert mgr._apply_keep_alive({"model": "x"}) == {"model": "x", "keep_alive": "30m"}
# ...and an empty value omits the field (falls back to Ollama's default).
mgr.keep_alive = ""
assert "keep_alive" not in mgr._apply_keep_alive({"model": "x"})
def test_memory_store_reads_changes_from_another_instance(tmp_path):
db_path = tmp_path / "memory.db"
writer = PersistentMemoryStore(db_path)
reader = PersistentMemoryStore(db_path)
writer.add(MemoryItem(id="fresh", text="written by another process"))
assert reader.get("fresh").text == "written by another process"
def test_icon_source_requires_real_allowed_file_boundary(tmp_path):
allowed = tmp_path / "icons"
allowed.mkdir()
source = allowed / "app.svg"
source.write_text("<svg />")
sibling = allowed.parent / "icons-other"
sibling.mkdir()
(sibling / "app.svg").write_text("<svg />")
old_roots = list(__import__("synapse.icons.compositor", fromlist=["_ALLOWED_ROOTS"])._ALLOWED_ROOTS)
module = __import__("synapse.icons.compositor", fromlist=["_ALLOWED_ROOTS"])
module._ALLOWED_ROOTS[:] = [str(allowed)]
try:
assert _is_allowed_path(str(source))
assert not _is_allowed_path(str(sibling / "app.svg"))
assert not _is_allowed_path(str(allowed / "missing.svg"))
finally:
module._ALLOWED_ROOTS[:] = old_roots
def test_ollama_stream_propagates_transport_errors(monkeypatch):
class FailingResponse:
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
def raise_for_status(self):
raise RuntimeError("Ollama unavailable")
class FailingClient:
def __init__(self, **kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *args):
return False
def stream(self, *args, **kwargs):
return FailingResponse()
monkeypatch.setattr(ollama_manager.httpx, "AsyncClient", FailingClient)
async def consume():
async for _ in OllamaManager()._chat_stream([], "model", 0):
pass
with pytest.raises(RuntimeError, match="Ollama unavailable"):
import asyncio
asyncio.run(consume())