diff --git a/.devcontainer/mnemon/seed.json b/.devcontainer/mnemon/seed.json index e26d420..2e525b8 100644 --- a/.devcontainer/mnemon/seed.json +++ b/.devcontainer/mnemon/seed.json @@ -488,72 +488,6 @@ ], "source": "agent" }, - { - "content": "Skill: codespace/cdp-browser-testing \u2014 Headless browser automation in Codespaces via Playwright CDP. Location: .devcontainer/skills/cdp-browser-testing/ (files: SKILL.md, scripts/launch-chrome-cdp.sh [idempotent CDP launcher], scripts/cdp-client.py [async Python CDP client], templates/common-operations.py [cookbook], references/cdp-api.md). Problem solved: Codespaces have NO system Chrome and the browser_use tool's built-in browser fails to launch; solution drives cached Playwright Chromium (~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome) over CDP on ws://127.0.0.1:9222. Verified end-to-end against a live lavish-axi session on loopback (full browser\u2192agent\u2192browser feedback round-trip, 5/5 pass). Read .devcontainer/skills/cdp-browser-testing/SKILL.md for full usage.", - "category": "context", - "importance": 5, - "tags": [ - "skill", - "cdp-browser-testing", - "codespace", - "playwright", - "cdp", - "browser", - "headless" - ], - "entities": [ - "cdp-browser-testing", - ".devcontainer/skills", - "Playwright Chromium", - "CDP", - "lavish-axi" - ], - "source": "agent" - }, - { - "content": "lavish-axi local testing in Codespaces (verified): Must access via localhost/loopback (http://127.0.0.1:4387/session/), NOT the *.app.github.dev proxy \u2014 a DNS-rebinding guard enforces loopback-only. Suppress the GUI auto-open with LAVISH_AXI_NO_OPEN=1 or --no-open. The browser\u2192agent feedback loop: run `node dist/cli.mjs poll sample.html` to receive prompts, post agent replies via POST /api//agent-reply with body {\"text\":\"...\"}. poll returns ONCE per feedback cycle then exits 0 (re-run per cycle). The cdp-browser-testing skill automates the browser side headlessly. Full round-trip proven working.", - "category": "insight", - "importance": 4, - "tags": [ - "lavish-axi", - "loopback", - "testing", - "dns-rebinding", - "cdp", - "agent-reply" - ], - "entities": [ - "lavish-axi", - "127.0.0.1:4387", - "agent-reply", - "DNS-rebinding", - "poll" - ], - "source": "agent" - }, - { - "content": "Skill: codespace-port-visibility \u2014 Automates GitHub Codespace port visibility (public/private/org) via CLI. Key insight: GITHUB_TOKEN must be extracted from vscode-server process environment (/proc//environ), NOT shell env (which has GITHUB_CODESPACE_TOKEN, useless for API). Uses gh codespace ports visibility CLI. Scripts: get_codespace_token.py (reusable module), set_port_visibility.py (main CLI), export_codespace_token.sh (shell source). Verified working for lavish-axi automation: make 4387 private, 8080 public.", - "category": "insight", - "importance": 4, - "tags": [ - "skill", - "codespace", - "port-visibility", - "automation", - "github", - "cli", - "lavish-axi" - ], - "entities": [ - "codespace-port-visibility", - "GITHUB_TOKEN", - "vscode-server", - "gh CLI", - "lavish-axi", - "nginx" - ], - "source": "agent" - }, { "content": "codespace-port-visibility skill scripts location: ~/.hermes/skills/codespace/codespace-port-visibility/scripts/ (get_codespace_token.py, set_port_visibility.py, export_codespace_token.sh). Symlinked to .devcontainer/skills/codespace-port-visibility/scripts/ for persistence.", "category": "fact", @@ -575,109 +509,59 @@ "source": "agent" }, { - "content": "Wiki: lavish-axi-codespace-setup.md \u2014 Complete setup & troubleshooting for lavish-axi in GitHub Codespaces. Covers architecture (Browser \u2192 nginx:8080 \u2192 lavish-axi:4387 \u2192 CDP:9222), why nginx is required (same-origin guard + DNS-rebinding + Codespace proxy conflict), nginx config with Origin rewrite, port visibility conflict table, step-by-step setup, feedback loop contract, verified round-trip test, known limitations (mermaid iframe warning on public URLs), troubleshooting checklist, env vars, quick reference commands. Read .devcontainer/wiki/lavish-axi-codespace-setup.md.", - "category": "context", + "content": "Skill: codespace-vscode-open \u2014 Auto-discovers VS Code CLI in Codespaces at /vscode/bin/linux-x64//bin/remote-cli/code and opens files in connected VS Code. Script at ~/.hermes/scripts/vscode-open.sh and .devcontainer/skills/codespace-vscode-open/scripts/vscode-open.sh. Wiki: .devcontainer/wiki/vscode-cli-codespaces.md. Use this skill whenever you need to open a file in the user's VS Code from a Codespace.", + "category": "fact", "importance": 4, "tags": [ - "wiki", - "lavish-axi", + "skill", "codespace", - "setup", - "nginx", - "mermaid", + "vscode", "automation" ], "entities": [ - "lavish-axi", - "nginx", - "codespace", - "port-visibility", - "mermaid", - "wiki" + "codespace-vscode-open", + "vscode-open.sh", + "VS Code CLI", + "Codespaces" ], "source": "agent" }, { - "content": "lavish-axi working config in Codespaces (final): Public port 8080 via nginx proxy \u2192 lavish-axi on loopback 4387. Nginx rewrites Origin/Host/Referer to 127.0.0.1 so same-origin check passes. Send-to-agent, annotation, mermaid SVG all work. Mermaid whiteboard iframe shows GitHub warning on public URLs (platform limitation, not bug). Private 4387 requires LAVISH_AXI_ALLOWED_HOSTS=* (code change reverted per user). Use URL: https://-8080.app.github.dev/session/", - "category": "fact", + "content": "MANDATORY: Run ci-lint-check skill before ANY commit or PR that modifies .devcontainer/skills/**, .devcontainer/wiki/**, .devcontainer/mnemon/**, .devcontainer/memories/**, or .devcontainer/*.sh. This mirrors the GitHub Actions lint-check job and prevents CI failures. One command: bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh", + "category": "decision", "importance": 5, - "tags": [ - "lavish-axi", - "codespace", - "nginx", - "port-visibility", - "working-config" - ], "entities": [ - "lavish-axi", - "nginx", - "codespace", - "port-visibility", - "8080", - "4387" + "ci-lint-check", + "pre-commit", + "github-actions", + "validation", + "mandatory" ], - "source": "agent" - }, - { - "content": "Skill: lavish-axi \u2014 Launch a dedicated lavish-axi planning session per Hermes conversation with zero port collisions. Dynamic per-session slots: each Hermes conversation gets its own slot (loopback engine 43XX + public nginx 99XX + poll supervisor) keyed by Hermes session ID in ~/.lavish-axi/slots.json. Scripts: slot_allocator.py (registry), launch_slot.py (engine + nginx + expose), poll_supervisor.py (harness background), nginx_slot.template (Origin rewrite + WS upgrade). Depends on codespace-port-visibility skill (expose_port.py). Verified: public 9988 returns 200, poll supervisor listening.", - "category": "insight", - "importance": 4, "tags": [ - "skill", - "lavish-axi", - "dynamic-ports", - "slot", - "planning", - "homes" - ], - "entities": [ - "lavish-axi", - "slot_allocator", - "launch_slot", - "poll_supervisor", - "nginx", - "codespace" + "mandatory", + "workflow", + "ci", + "pre-commit" ], "source": "agent" }, { - "content": "Wiki: lavish-axi-skill-design.md \u2014 Design spec for lavish-axi as a Hermes skill: per-session slots, dynamic ports (engine 43XX \u2194 public 99XX matching suffixes), slot registry (slots.json with Hermes session ID), trigger phrase 'iterate this in lavish', poll supervisor (harness background), public port exposure via codespace-port-visibility expose_port.py (forward + visibility). Resolved: session ownership \u2192 Hermes session ID in registry; auto-open vs link \u2192 link in chat; Mermaid iframe \u2192 deferred (in-page only); HTML vs MD \u2192 HTML = scratchpad, MD = record. Read .devcontainer/wiki/lavish-axi-skill-design.md.", - "category": "context", + "content": "codespace-lavish skill fully installed and working: launches headed Lavish-AXI whiteboard over noVNC with auto-connect (autoconnect=true) and local scaling (resize=scale). Includes continuous poll loop with auto-restart (2s), --monitor flag, Chrome GPU fix (SwiftShader), session key derivation, and stable noVNC URL. Script is idempotent and self-healing.", + "category": "fact", "importance": 4, - "tags": [ - "wiki", - "lavish-axi", - "skill", - "design", - "dynamic-port", - "slot", - "proposal" - ], "entities": [ + "codespace-lavish", "lavish-axi", - "skill-design", - "wiki", - "slot", - "registry", - "poll-supervisor" + "novnc", + "whiteboard", + "poll", + "swiftshader" ], - "source": "agent" - }, - { - "content": "Skill: codespace-vscode-open \u2014 Auto-discovers VS Code CLI in Codespaces at /vscode/bin/linux-x64//bin/remote-cli/code and opens files in connected VS Code. Script at ~/.hermes/scripts/vscode-open.sh and .devcontainer/skills/codespace-vscode-open/scripts/vscode-open.sh. Wiki: .devcontainer/wiki/vscode-cli-codespaces.md. Use this skill whenever you need to open a file in the user's VS Code from a Codespace.", - "category": "fact", - "importance": 4, "tags": [ "skill", - "codespace", - "vscode", - "automation" - ], - "entities": [ - "codespace-vscode-open", - "vscode-open.sh", - "VS Code CLI", - "Codespaces" + "gui", + "automation", + "codespace" ], "source": "agent" } diff --git a/.devcontainer/skills/cdp-browser-testing/SKILL.md b/.devcontainer/skills/cdp-browser-testing/SKILL.md deleted file mode 100644 index 75316ee..0000000 --- a/.devcontainer/skills/cdp-browser-testing/SKILL.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -name: cdp-browser-testing -description: Headless browser automation in Codespaces w/ Playwright CDP. -trigger: Use when you need headless browser automation in Codespaces without system Chrome. -category: codespace -version: 1.1.0 -author: hermes-agent -license: MIT -tags: - - codespace - - browser - - cdp - - playwright - - testing -related_skills: - - codespace/github-codespace - - codespace/persistent-knowledge ---- - -# CDP Browser Testing Skill - -## When to Use -- Headless browser testing in GitHub Codespaces (no system Chrome) -- Automating lavish-axi or similar localhost web UIs over loopback -- Any CDP-driven browser automation where Playwright Chromium is available -- CI/CD pipelines in Codespaces needing browser verification - -## Problem -GitHub Codespaces don't have system Chrome/Chromium installed. The `browser_use` tool's built-in browser fails to launch. Playwright installs its own Chromium to `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome` — we can drive it via CDP on port 9222. - -## Quick Start - -```bash -# 1. Install the Python dependency (once) -pip install -r ~/.hermes/skills/codespace/cdp-browser-testing/requirements.txt - -# 2. Launch CDP Chrome (idempotent) -source ~/.hermes/skills/codespace/cdp-browser-testing/scripts/launch-chrome-cdp.sh - -# 3. Use the Python CDP client -python3 -c " -import asyncio -from cdp_client import CDPClient - -async def main(): - async with CDPClient() as cdp: - await cdp.navigate('http://127.0.0.1:4387/session/xxx') - await cdp.wait_for('#chatInput') - await cdp.type_text('#chatInput', 'Hello from agent!') - await cdp.click('#send') - bubbles = await cdp.get_chat_bubbles() - print(bubbles) - -asyncio.run(main()) -" -``` - -> The module is `cdp_client.py` (import as `from cdp_client import CDPClient`). -> Interaction helpers (`type_text`, `click`, `get_chat_bubbles`) are React-safe: -> they drive the page via `Runtime.evaluate` because CDP `DOM.querySelector` -> often returns no nodeId against React apps. - -## Scripts - -| Script | Purpose | -|--------|---------| -| `launch-chrome-cdp.sh` | Finds cached Playwright Chromium, starts headless on port 9222, prints WS URL | -| `cdp_client.py` | Async Python class: connect, new_page, navigate, wait_for, type_text, click, get_html, get_text, get_chat_bubbles, evaluate | -| `requirements.txt` | Python dep: `websockets` | - -## Templates - -| Template | Purpose | -|----------|---------| -| `common-operations.py` | Cookbook: navigate, type_text, click, wait_for, get_chat_bubbles, get_html, screenshot | - -## References - -| File | Content | -|------|---------| -| `cdp-api.md` | Quick-ref for CDP domains: Target, Page, DOM, Input, Runtime | - -## Usage Patterns - -### Launch Chrome (once per session) -```bash -source ~/.hermes/skills/codespace/cdp-browser-testing/scripts/launch-chrome-cdp.sh -# Sets CDP_WS_URL env var with ws://127.0.0.1:9222/devtools/browser/ -``` - -### Navigate and Interact -```python -from cdp_client import CDPClient - -async with CDPClient() as cdp: - await cdp.navigate(url) - await cdp.wait_for('#chatInput') # JS-based wait (React-safe) - await cdp.type_text('#chatInput', 'message') - await cdp.click('#send') - bubbles = await cdp.get_chat_bubbles() # lavish-axi chat turns -``` - -### Extract Data -```python -html = await cdp.get_html() # full document outerHTML -html = await cdp.get_html('#artifact') # scoped outerHTML -text = await cdp.get_text('h1') # textContent of an element -val = await cdp.evaluate("1 + 1") # any JS expression -``` - -## CDP WebSocket URL -The launcher prints the browser WebSocket URL. `CDPClient` auto-discovers it via `http://127.0.0.1:9222/json/version` and auto-attaches to the page target via `Target.setAutoAttach`. - -## Troubleshooting -- **Chrome won't start**: Check `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome` exists (run `playwright install chromium` if not) -- **Port 9222 busy**: Kill existing `chrome --remote-debugging-port=9222` processes -- **Navigation hangs (no response to `Page.navigate`)**: Almost always orphaned CDP targets from a previous run piling up. Close them: `for p in $(pgrep -f remote-debugging-port=9222); do kill -9 $p; done`, then relaunch. The `CDPClient` now auto-attaches to page targets, so `navigate()` works without manual attach. -- **Element not found / empty bubbles**: CDP `DOM.querySelector` is unreliable against React. Use `wait_for()` + `type_text()` / `click()` (Runtime.evaluate based) instead. Read the chat via `get_chat_bubbles()` or `get_text()` rather than `div.bubble` CSS selectors. -- **`ModuleNotFoundError: cdp_client`**: ensure you import `cdp_client` (underscore), not `cdp-client`, and that the script dir is on `PYTHONPATH`. -- **Mermaid diagrams don't render**: The artifact must include the Mermaid CDN script. Add to your HTML: - ```html - - ``` - Without this, `
` stays as raw text — the SDK's `mermaid-node.js` helpers only detect *rendered* SVGs.
-- **Whiteboard editor cookie error**: The Excalidraw iframe is sandboxed without `allow-same-origin` (opaque origin). Clicking "Fullscreen" or interacting with the editor triggers `SecurityError: Failed to set 'cookie' property`. This is **by-design** — the whiteboard frame runs in an opaque origin matching the artifact iframe's trust posture. The diagram renders fine as a static, clickable flowchart; only the fullscreen editor is affected.
-
-## Related Skills
-- `codespace/github-codespace` — GitHub Codespace auth and workflow
-- `codespace/persistent-knowledge` — Persistent skills/knowledge via symlinks
\ No newline at end of file
diff --git a/.devcontainer/skills/cdp-browser-testing/references/cdp-api.md b/.devcontainer/skills/cdp-browser-testing/references/cdp-api.md
deleted file mode 100644
index d8c1c82..0000000
--- a/.devcontainer/skills/cdp-browser-testing/references/cdp-api.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# CDP API Quick Reference
-
-Domains used in this skill:
-
-## Target
-- `Target.getTargets` — List all targets (pages, workers, etc.)
-- `Target.attachToTarget` — Attach to a target for debugging
-- `Target.setDiscoverTargets` — Enable target discovery
-- `Target.setAutoAttach` — Auto-attach to new targets
-
-## Page
-- `Page.enable` — Enable Page domain
-- `Page.navigate` — Navigate to URL
-- `Page.loadEventFired` — Event: page load complete
-- `Page.domContentEventFired` — Event: DOMContentLoaded
-- `Page.captureScreenshot` — Take screenshot
-- `Page.getFrameTree` — Get frame hierarchy
-
-## DOM
-- `DOM.enable` — Enable DOM domain
-- `DOM.getDocument` — Get root document node
-- `DOM.querySelector` — Find element by CSS selector
-- `DOM.getBoxModel` — Get element geometry (for clicking)
-- `DOM.getOuterHTML` — Get element's outerHTML
-- `DOM.focus` — Focus element
-- `DOM.getTextContent` — Get textContent (if available)
-
-## Input
-- `Input.insertText` — Type text into focused element
-- `Input.dispatchMouseEvent` — Mouse events (click, move, etc.)
-- `Input.dispatchKeyEvent` — Keyboard events
-
-## Runtime
-- `Runtime.enable` — Enable Runtime domain
-- `Runtime.evaluate` — Evaluate JavaScript in page context
-- `Runtime.consoleAPICalled` — Event: console.log/error/etc.
-
----
-
-## Common Patterns
-
-### Get WebSocket URL
-```bash
-curl -s http://127.0.0.1:9222/json/version | jq -r .webSocketDebuggerUrl
-```
-
-### Attach to page target
-```json
-{"id": 1, "method": "Target.attachToTarget", "params": {"targetId": "", "flatten": true}}
-```
-
-### Navigate and wait
-```json
-{"id": 1, "method": "Page.navigate", "params": {"url": "http://localhost:4387/session/xxx"}}
-```
-Then wait for `Page.loadEventFired` event.
-
-### Click element
-1. `DOM.querySelector` with selector → get `nodeId`
-2. `DOM.getBoxModel` with `nodeId` → get `content` array
-3. Calculate center: `x = (content[0] + content[2]) / 2`, `y = (content[1] + content[5]) / 2`
-4. `Input.dispatchMouseEvent` with `mousePressed` at (x,y)
-5. `Input.dispatchMouseEvent` with `mouseReleased` at (x,y)
-
-> NOTE: `DOM.querySelector` is unreliable against React apps (returns no nodeId for
-> elements that clearly exist). The `cdp_client.py` helpers `type_text()` and
-> `click()` drive the page via `Runtime.evaluate` instead — prefer those.
-
-### Type text (React-safe — preferred)
-```js
-(() => {
-  const el = document.querySelector("#chatInput");
-  const setter = Object.getOwnPropertyDescriptor(
-    window.HTMLTextAreaElement.prototype, "value").set;
-  setter.call(el, "your text");
-  el.dispatchEvent(new Event("input", {bubbles: true}));
-  el.dispatchEvent(new Event("change", {bubbles: true}));
-})()
-```
-
-### Get element HTML
-1. `DOM.getDocument` → `root.nodeId`
-2. `DOM.querySelector` with `nodeId` and selector → `nodeId`
-3. `DOM.getOuterHTML` with `nodeId` → `outerHTML`
\ No newline at end of file
diff --git a/.devcontainer/skills/cdp-browser-testing/requirements.txt b/.devcontainer/skills/cdp-browser-testing/requirements.txt
deleted file mode 100644
index 5ce16cd..0000000
--- a/.devcontainer/skills/cdp-browser-testing/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Python dependency for the CDP client scripts in this skill.
-# Install with:  pip install -r requirements.txt
-websockets>=12.0
diff --git a/.devcontainer/skills/cdp-browser-testing/scripts/cdp_client.py b/.devcontainer/skills/cdp-browser-testing/scripts/cdp_client.py
deleted file mode 100644
index 60b1b87..0000000
--- a/.devcontainer/skills/cdp-browser-testing/scripts/cdp_client.py
+++ /dev/null
@@ -1,239 +0,0 @@
-#!/usr/bin/env python3
-"""
-cdp_client.py — Async Python CDP client for headless browser automation in Codespaces.
-
-Drives the cached Playwright Chromium over the Chrome DevTools Protocol (CDP) on
-ws://127.0.0.1:9222. Works without system Chrome.
-
-Key design note (learned the hard way):
-  CDP `DOM.querySelector` is unreliable against React apps (returns no nodeId for
-  elements that clearly exist). So the high-level interaction helpers here drive
-  the page via `Runtime.evaluate` (set the value through the prototype setter and
-  dispatch an input event, click via element.click()) — this is the reliable path.
-
-Usage:
-    from cdp_client import CDPClient
-
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        await cdp.wait_for("#chatInput")
-        await cdp.type_text("#chatInput", "Hello from agent!")
-        await cdp.click("#send")
-        bubbles = await cdp.get_chat_bubbles()
-"""
-
-import asyncio
-import json
-import urllib.request
-import websockets
-from typing import Optional, Dict, Any
-
-
-class CDPClient:
-    """Async context manager for Chrome DevTools Protocol."""
-
-    def __init__(self, ws_url: Optional[str] = None, cdp_http: str = "http://127.0.0.1:9222"):
-        self.ws_url = ws_url
-        self.cdp_http = cdp_http
-        self.ws = None
-        self._req_id = 0
-        self._pending: Dict[int, asyncio.Future] = {}
-        self._events: asyncio.Queue = asyncio.Queue()
-        self._listener_task: Optional[asyncio.Task] = None
-        # Page target bookkeeping
-        self.target_id: Optional[str] = None
-        self.session_id: Optional[str] = None
-
-    async def __aenter__(self) -> "CDPClient":
-        await self.connect()
-        return self
-
-    async def __aexit__(self, *args):
-        await self.close()
-
-    # ---- low-level transport ------------------------------------------------
-
-    async def _listen(self):
-        """Read all WebSocket frames. Resolve id-keyed responses; queue events."""
-        try:
-            async for raw in self.ws:
-                d = json.loads(raw)
-                if "id" in d and d["id"] in self._pending:
-                    fut = self._pending.pop(d["id"])
-                    if not fut.done():
-                        if "error" in d:
-                            fut.set_exception(RuntimeError(d["error"]))
-                        else:
-                            fut.set_result(d.get("result", {}))
-                elif "method" in d:
-                    await self._events.put(d)
-                    # Auto-capture the page target's sessionId when attached.
-                    if d["method"] == "Target.attachedToTarget":
-                        info = d["params"].get("targetInfo", {})
-                        if info.get("type") in ("page", "tab") or d["params"].get("sessionId"):
-                            # Prefer a page target, but fall back to the first attach.
-                            if self.session_id is None or info.get("type") in ("page", "tab"):
-                                self.session_id = d["params"].get("sessionId")
-                                self.target_id = info.get("targetId")
-        except asyncio.CancelledError:
-            pass
-
-    async def _send(self, method: str, params: Dict = None, session_id: str = None,
-                    timeout: float = 20.0) -> Any:
-        self._req_id += 1
-        msg = {"id": self._req_id, "method": method, "params": params or {}}
-        sid = session_id or self.session_id
-        if sid:
-            msg["sessionId"] = sid
-        fut = asyncio.get_event_loop().create_future()
-        self._pending[self._req_id] = fut
-        await self.ws.send(json.dumps(msg))
-        return await asyncio.wait_for(fut, timeout=timeout)
-
-    async def connect(self):
-        """Connect to CDP, auto-discover WS URL if not provided."""
-        if not self.ws_url:
-            with urllib.request.urlopen(f"{self.cdp_http}/json/version") as resp:
-                data = json.load(resp)
-                self.ws_url = data["webSocketDebuggerUrl"]
-        self.ws = await websockets.connect(self.ws_url, ping_interval=None)
-        self._listener_task = asyncio.create_task(self._listen())
-
-    # ---- target / navigation ------------------------------------------------
-
-    async def new_page(self, url: str = "about:blank") -> str:
-        """Create a new page target, attach, navigate to url. Returns sessionId."""
-        res = await self._send("Target.createTarget", {"url": url})
-        self.target_id = res["targetId"] if "targetId" in res else res.get("result", {}).get("targetId")
-        # If auto-attach didn't fire, attach explicitly.
-        if self.session_id is None:
-            res = await self._send("Target.attachToTarget",
-                                   {"targetId": self.target_id, "flatten": True})
-            self.session_id = res["sessionId"] if "sessionId" in res else res.get("result", {}).get("sessionId")
-        return self.session_id
-
-    async def navigate(self, url: str, timeout: float = 30.0) -> str:
-        """Navigate the current page target to url; wait for load event."""
-        if self.session_id is None:
-            await self.new_page()
-        await self._send("Page.enable", {}, self.session_id)
-        await self._send("Page.navigate", {"url": url}, self.session_id, timeout=timeout)
-        # Wait (up to timeout) for the load event.
-        try:
-            await asyncio.wait_for(self._wait_event("Page.loadEventFired", self.session_id),
-                                   timeout=timeout)
-        except asyncio.TimeoutError:
-            pass
-        return self.session_id
-
-    async def _wait_event(self, event: str, session_id: str = None, timeout: float = 10.0):
-        while True:
-            d = await asyncio.wait_for(self._events.get(), timeout=timeout)
-            if d.get("method") == event and (session_id is None or d.get("sessionId") == session_id):
-                return d
-
-    async def wait_for(self, selector: str, timeout: float = 15.0) -> bool:
-        """Wait until an element matching `selector` exists in the page (JS check)."""
-        js = f"(() => !!document.querySelector({json.dumps(selector)}))()"
-        for _ in range(int(timeout * 4)):
-            try:
-                if await self.evaluate(js):
-                    return True
-            except Exception:
-                pass
-            await asyncio.sleep(0.25)
-        return False
-
-    # ---- interaction (Runtime.evaluate based — reliable on React) -----------
-
-    async def evaluate(self, expression: str) -> Any:
-        """Evaluate JS in the page context; returns the JSON value.
-
-        Note: Runtime.evaluate nests the return value as
-        result.result.value (the outer result holds the RemoteObject, whose
-        `result` holds the actual value). Handle both single- and double-nested.
-        """
-        res = await self._send("Runtime.evaluate",
-                               {"expression": expression, "returnByValue": True},
-                               self.session_id)
-        if not isinstance(res, dict):
-            return res
-        # double-nested: {"result": {"type":..,"value":..}}
-        if "result" in res and isinstance(res["result"], dict) and "value" in res["result"]:
-            return res["result"]["value"]
-        # single-nested / flat
-        if "value" in res:
-            return res["value"]
-        return None
-
-    async def type_text(self, selector: str, text: str) -> None:
-        """Type `text` into the element matched by `selector` (React-safe)."""
-        await self.wait_for(selector)
-        js = f"""(() => {{
-          const el = document.querySelector({json.dumps(selector)});
-          if (!el) return 'no-element';
-          let proto = el;
-          while (proto && !(Object.getOwnPropertyDescriptor(proto, 'value'))) proto = Object.getPrototypeOf(proto);
-          const setter = proto ? Object.getOwnPropertyDescriptor(proto, 'value').set : null;
-          if (setter) {{ setter.call(el, {json.dumps(text)}); }}
-          else {{ el.value = {json.dumps(text)}; }}
-          el.dispatchEvent(new Event('input', {{bubbles: true}}));
-          el.dispatchEvent(new Event('change', {{bubbles: true}}));
-          return 'ok';
-        }})()"""
-        return await self.evaluate(js)
-
-    async def click(self, selector: str) -> None:
-        """Click the element matched by `selector` via element.click()."""
-        await self.wait_for(selector)
-        js = f"""(() => {{
-          const el = document.querySelector({json.dumps(selector)});
-          if (!el) return 'no-element';
-          el.click();
-          return 'clicked';
-        }})()"""
-        return await self.evaluate(js)
-
-    async def get_html(self, selector: str = None) -> str:
-        """Get outerHTML of an element (or full document)."""
-        if selector:
-            js = f"document.querySelector({json.dumps(selector)})?.outerHTML || ''"
-        else:
-            js = "document.documentElement.outerHTML"
-        return await self.evaluate(js) or ""
-
-    async def get_text(self, selector: str = None) -> str:
-        """Get textContent of an element (or full body)."""
-        js = (f"document.querySelector({json.dumps(selector)})?.innerText || ''"
-              if selector else "document.body.innerText")
-        return await self.evaluate(js) or ""
-
-    async def get_chat_bubbles(self) -> list:
-        """Return list of {{role: 'YOU'|'AGENT', text}} from lavish-axi chat."""
-        js = r"""(() => {
-          const els = [...document.querySelectorAll('[class*="bubble"]')];
-          return els.map(b => ({
-            role: /user/.test(b.className) ? 'YOU' : 'AGENT',
-            text: b.innerText.replace(/^(YOU|AGENT)\s*/, '')
-          }));
-        })()"""
-        return await self.evaluate(js) or []
-
-    # ---- teardown -----------------------------------------------------------
-
-    async def close_target(self):
-        if self.target_id:
-            try:
-                await self._send("Target.closeTarget", {"targetId": self.target_id}, timeout=5)
-            except Exception:
-                pass
-
-    async def close(self):
-        if self._listener_task:
-            self._listener_task.cancel()
-            try:
-                await self._listener_task
-            except asyncio.CancelledError:
-                pass
-        if self.ws:
-            await self.ws.close()
diff --git a/.devcontainer/skills/cdp-browser-testing/scripts/launch-chrome-cdp.sh b/.devcontainer/skills/cdp-browser-testing/scripts/launch-chrome-cdp.sh
deleted file mode 100644
index 2d34273..0000000
--- a/.devcontainer/skills/cdp-browser-testing/scripts/launch-chrome-cdp.sh
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env bash
-# launch-chrome-cdp.sh — Idempotent launcher for Playwright Chromium via CDP on port 9222
-# Usage: source this script to set CDP_WS_URL in your shell
-
-set -euo pipefail
-
-# Find Playwright's cached Chromium
-CHROME_PATH=$(find ~/.cache/ms-playwright -name "chrome" -path "*/chrome-linux64/chrome" 2>/dev/null | head -1)
-
-if [[ -z "$CHROME_PATH" ]]; then
-    echo "ERROR: Playwright Chromium not found. Run: playwright install chromium" >&2
-    exit 1
-fi
-
-if [[ ! -x "$CHROME_PATH" ]]; then
-    echo "ERROR: Chrome binary not executable: $CHROME_PATH" >&2
-    exit 1
-fi
-
-# Check if already running on port 9222
-if curl -sf http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
-    # Before reusing, close any orphaned page targets from prior runs so the
-    # browser doesn't accumulate targets and make Page.navigate hang.
-    CDP_WS_URL=$(curl -s http://127.0.0.1:9222/json/version | python3 -c "
-import sys, json
-print(json.load(sys.stdin)['webSocketDebuggerUrl'])
-")
-    python3 - "$CDP_WS_URL" <<'PY' 2>/dev/null || true
-import asyncio, json, sys, websockets
-async def cleanup(ws_url):
-    ws = await websockets.connect(ws_url, ping_interval=None)
-    await ws.send(json.dumps({"id":1,"method":"Target.getTargets"}))
-    msg = await asyncio.wait_for(ws.recv(), timeout=5)
-    targets = json.loads(msg).get("result",{}).get("targets",[])
-    rid = 2
-    for t in targets:
-        if t.get("type") in ("page","tab") and t.get("url") not in ("about:blank",):
-            await ws.send(json.dumps({"id":rid,"method":"Target.closeTarget","params":{"targetId":t["targetId"]}}))
-            rid += 1
-    await ws.close()
-asyncio.run(cleanup(sys.argv[1]))
-PY
-    export CDP_WS_URL
-    echo "CDP Chrome already running: $CDP_WS_URL"
-    exit 0
-fi
-
-# Start new Chrome instance
-PROFILE_DIR="/tmp/chrome-cdp-profile-$$"
-mkdir -p "$PROFILE_DIR"
-
-nohup "$CHROME_PATH" \
-    --headless \
-    --no-sandbox \
-    --disable-gpu \
-    --disable-dev-shm-usage \
-    --remote-debugging-port=9222 \
-    --user-data-dir="$PROFILE_DIR" \
-    >/tmp/chrome-cdp.log 2>&1 &
-
-CHROME_PID=$!
-
-# Wait for CDP to be ready
-for i in {1..30}; do
-    if curl -sf http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
-        CDP_WS_URL=$(curl -s http://127.0.0.1:9222/json/version | python3 -c "
-import sys, json
-data = json.load(sys.stdin)
-print(data['webSocketDebuggerUrl'])
-")
-        export CDP_WS_URL
-        echo "CDP Chrome started (pid $CHROME_PID): $CDP_WS_URL"
-        exit 0
-    fi
-    sleep 0.5
-done
-
-echo "ERROR: CDP Chrome failed to start within 15s" >&2
-kill $CHROME_PID 2>/dev/null || true
-exit 1
\ No newline at end of file
diff --git a/.devcontainer/skills/cdp-browser-testing/templates/common-operations.py b/.devcontainer/skills/cdp-browser-testing/templates/common-operations.py
deleted file mode 100644
index 0e95fa0..0000000
--- a/.devcontainer/skills/cdp-browser-testing/templates/common-operations.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-"""
-common-operations.py — Cookbook of common CDP operations for the
-cdp-browser-testing skill. Copy-paste these patterns into your test scripts.
-
-NOTE: the high-level helpers (type_text / click / get_chat_bubbles) drive the
-page via Runtime.evaluate, which is reliable against React apps (unlike
-CDP DOM.querySelector, which often returns no nodeId). Use those helpers.
-"""
-
-import asyncio
-from cdp_client import CDPClient
-
-
-async def example_basic_navigation():
-    """Navigate and extract page title."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("https://example.com")
-        title = await cdp.evaluate("document.title")
-        print(f"Title: {title}")
-
-
-async def example_form_interaction():
-    """Fill form and submit (lavish-axi chat composer)."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        await cdp.wait_for("#chatInput")
-        await cdp.type_text("#chatInput", "Hello from automated test!")
-        await cdp.click("#send")
-        await asyncio.sleep(2)
-        html = await cdp.get_html("#chatLog")
-        print(html)
-
-
-async def example_wait_and_extract():
-    """Wait for an agent reply bubble and extract its text."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        # get_chat_bubbles waits via JS polling internally is not built-in;
-        # poll until an AGENT bubble appears:
-        for _ in range(20):
-            bubbles = await cdp.get_chat_bubbles()
-            if any(b["role"] == "AGENT" for b in bubbles):
-                break
-            await asyncio.sleep(0.5)
-        bubbles = await cdp.get_chat_bubbles()
-        for b in bubbles:
-            print(f"[{b['role']}] {b['text']}")
-
-
-async def example_full_roundtrip():
-    """Full interaction loop: send message, wait for reply, verify."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        await cdp.wait_for("#chatInput")
-        await cdp.type_text("#chatInput", "Test message 1")
-        await cdp.click("#send")
-        await asyncio.sleep(3)
-        await cdp.type_text("#chatInput", "Test message 2")
-        await cdp.click("#send")
-        await asyncio.sleep(3)
-        bubbles = await cdp.get_chat_bubbles()
-        texts = " ".join(b["text"] for b in bubbles)
-        assert "Test message 1" in texts
-        assert "Test message 2" in texts
-        print("Both messages verified in chat log")
-
-
-async def example_artifact_testing():
-    """Test lavish-axi artifact rendering."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        await cdp.wait_for("#chatInput")
-        # The artifact iframe is created by the app; read its src once present.
-        src = await cdp.evaluate(
-            "document.querySelector('iframe[src*=artifact]')?.src || ''")
-        print("Artifact iframe:", src or "not yet present")
-
-
-async def example_screenshot():
-    """Take a screenshot via CDP."""
-    async with CDPClient() as cdp:
-        await cdp.navigate("http://127.0.0.1:4387/session/xxx")
-        await cdp.wait_for("#chatInput")
-        result = await cdp._send("Page.captureScreenshot",
-                                 {"format": "png", "fromSurface": True},
-                                 cdp.session_id)
-        import base64
-        img_data = base64.b64decode(result.get("data", ""))
-        with open("/tmp/screenshot.png", "wb") as f:
-            f.write(img_data)
-        print("Screenshot saved to /tmp/screenshot.png")
-
-
-if __name__ == "__main__":
-    asyncio.run(example_basic_navigation())
-    # asyncio.run(example_form_interaction())
-    # asyncio.run(example_wait_and_extract())
-    # asyncio.run(example_full_roundtrip())
-    # asyncio.run(example_artifact_testing())
-    # asyncio.run(example_screenshot())
diff --git a/.devcontainer/skills/ci-lint-check/SKILL.md b/.devcontainer/skills/ci-lint-check/SKILL.md
new file mode 100644
index 0000000..95940ab
--- /dev/null
+++ b/.devcontainer/skills/ci-lint-check/SKILL.md
@@ -0,0 +1,151 @@
+---
+name: ci-lint-check
+description: Run pre-commit CI lint validation locally before pushing to avoid GitHub Actions failures.
+version: 0.1.0
+author: gitricko, Hermes Agent
+license: MIT
+platforms: [linux]
+metadata:
+  hermes:
+    tags: [ci, lint, pre-commit, github-actions, validation]
+    related_skills: [codespace-persistent-symlinks, memory-automation]
+---
+
+# CI Lint Check Skill
+
+Run the full CI lint validation locally before committing or creating a PR. This mirrors the `lint-check` job in `.github/workflows/devcontainer-ci.yml` and catches all format/validation issues that would fail CI.
+
+## When to Use
+
+- **Before every commit** that touches `.devcontainer/skills/**`, `.devcontainer/wiki/**`, `.devcontainer/mnemon/**`, `.devcontainer/memories/**`, or `.devcontainer/*.sh`
+- **Before creating a PR** to ensure CI passes
+- When CI fails and you need to debug locally
+
+## Prerequisites
+
+- `markdownlint-cli` (auto-installed by script)
+- `python3` (for Mnemon seed validation)
+- `bash` (for shell syntax checks)
+
+## How to Run
+
+```bash
+# Quick one-liner (runs all checks):
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh
+
+# Or step by step:
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --markdown-only
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --skills-only
+bash .devcontainer/skills/ci-lint_check.sh --wiki-only
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --mnemon-only
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --shell-only
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --symlink-only
+```
+
+## What It Validates
+
+| Check | Files | CI Job |
+|-------|-------|--------|
+| Markdown lint | `.devcontainer/wiki/*.md`, `.devcontainer/skills/*/SKILL.md`, `.devcontainer/.hermes.md`, `README.md` | `lint-check` |
+| SKILL.md structure | All skills in `.devcontainer/skills/*/SKILL.md` | `lint-check` |
+| Wiki INDEX.md consistency | Every `.md` in wiki/ referenced in INDEX.md | `lint-check` |
+| Mnemon seed.json | `.devcontainer/mnemon/seed.json` schema | `lint-check` |
+| Root shell syntax | `.devcontainer/*.sh` | `lint-check` |
+| Skill shell syntax | `.devcontainer/skills/*/scripts/*.sh` | `lint-check` |
+| Symlink persistence | Tracked dirs + boot script symlink logic | `lint-check` |
+
+## Procedure
+
+### 1. Install dependencies (first run only)
+```bash
+npm install -g markdownlint-cli
+```
+
+### 2. Run full validation
+```bash
+bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh
+```
+
+### 3. Fix any reported issues
+- Markdown lint: fix reported line/column issues
+- SKILL.md: ensure YAML frontmatter with `name:` field
+- Wiki: add missing articles to INDEX.md table
+- Mnemon: `python3 .devcontainer/mnemon/validate-seed.py .devcontainer/mnemon/seed.json`
+- Shell: `bash -n 
+  
+
+
+  

Lavish GUI Sample

+

Standalone Mermaid demo used by the lavish-planning-gui skill to validate the + headed-browser → virtual-display → noVNC stack without a live Lavish session.

+
+ graph TD + A[Idea] --> B[Plan in Lavish] + B --> C{Human review} + C -->|approve| D[Ship] + C -->|revise| B +
+ + + diff --git a/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_gui.sh b/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_gui.sh new file mode 100755 index 0000000..2a691c5 --- /dev/null +++ b/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_gui.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash +# lavish_planning_gui.sh — "planning lavish": bring up a headed Lavish-AXI whiteboard +# on a virtual display, bridge it to noVNC, and start the feedback poll so the user +# can iterate. Self-installing: verifies prerequisites and (with --fix) installs them. +# +# Usage: +# lavish_planning_gui.sh [--fix] [artifact.html] +# lavish_planning_gui.sh [--fix] --prompt "Plan the Q3 launch" +# +# Behavior: +# - If an artifact.html path is given, open it. +# - Else if --prompt is given, generate a planning artifact HTML from it. +# - Else generate a default planning artifact. +# The skill NEVER assumes the Codespace is pre-provisioned; --fix installs deps. +# +# Session key = SHA256(realpath(artifact))[:16] (REQ-LAVISH-GUI-WHITEBOARD-001 §9.2). +# Chrome loads http://127.0.0.1:4387/session/ on DISPLAY=:99. +# +# For future agents: this script is the single entry point. It handles: +# 1. Prerequisite installation (--fix) +# 2. Full VNC stack startup (Xvfb, openbox, Chrome, x11vnc, websockify/noVNC) +# 3. Lavish-AXI engine startup +# 4. Artifact session creation +# 5. Continuous poll loop with auto-restart (survives script exit) +# 6. noVNC URL output with auto-connect + scaling + +set -euo pipefail + +SELF_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +PREREQS="$SELF_DIR/lavish_planning_prereqs.sh" +DISPLAY_NUM=99 +LAVISH_PORT="${LAVISH_PORT:-4387}" +NOVNC_PORT="${NOVNC_PORT:-6080}" +XVFB_SCREEN="${XVFB_SCREEN:-1280x800x24}" +CHROME_USER_DATA_DIR="/tmp/chrome-gui" +STATE_DIR="${STATE_DIR:-$HOME/.lavish-axi/slot-default}" +PROMPT="" +ARTIFACT="" +FIX=0 + +# Parse args: --fix, --prompt "value", --monitor, or positional artifact.html +args=("$@") +for ((i=0; i<${#args[@]}; i++)); do + case "${args[i]}" in + --fix) FIX=1 ;; + --prompt) PROMPT="${args[i+1]:-}"; ((i++)) ;; + --monitor) MONITOR=1 ;; + -*) : ;; # ignore unknown flags + *) # positional: artifact path + if [[ -z "$ARTIFACT" ]]; then ARTIFACT="${args[i]}"; fi + ;; + esac +done + +echo "=== [lavish-planning-gui] prerequisite check ===" +if [[ -x "$PREREQS" ]]; then + if [[ $FIX -eq 1 ]]; then + bash "$PREREQS" --fix || { echo "[lavish-planning-gui] PREREQS FAILED even after --fix; abort." >&2; exit 1; } + else + bash "$PREREQS" || { echo "[lavish-planning-gui] PREREQS MISSING — re-run with --fix to install." >&2; exit 1; } + fi +else + echo "[lavish-planning-gui] WARNING: prereqs script not found at $PREREQS" >&2 +fi + +# --- Resolve artifact --- +if [[ -z "$ARTIFACT" ]]; then + if [[ -n "$PROMPT" ]]; then + ARTIFACT="$SELF_DIR/generated-plan-$(date +%s).html" + echo "[lavish-planning-gui] generating planning artifact from prompt: $PROMPT" + cat > "$ARTIFACT" <Planning — Lavish + + +

Planning Board

+

Prompt: ${PROMPT}

+
graph TD; A[Start] --> B[Elaborate]; B --> C[Review in Lavish]; C --> D[Ship]
+ +HTML + else + ARTIFACT="$SELF_DIR/lavish-gui-sample.html" + echo "[lavish-planning-gui] no artifact/prompt — using bundled sample" + fi +fi + +if [[ ! -f "$ARTIFACT" ]]; then + echo "[lavish-planning-gui] ERROR: artifact not found: $ARTIFACT" >&2 + exit 1 +fi + +# --- Derive session key (exact one-liner from §9.2) --- +SESSION_KEY=$(node -e " + const crypto = require('crypto'); + const fs = require('fs'); + console.log(crypto.createHash('sha256').update(fs.realpathSync(process.argv[1])).digest('hex').slice(0,16)) +" "$ARTIFACT") +SESSION_URL="http://127.0.0.1:${LAVISH_PORT}/session/${SESSION_KEY}" +echo "[lavish-planning-gui] session key: ${SESSION_KEY}" +echo "[lavish-planning-gui] session URL: ${SESSION_URL}" + +# --- Resolve lavish-axi dist (npx on the fly; never assume pre-installed) --- +LAVISH_DIST="" +if command -v lavish-axi >/dev/null 2>&1; then + LAVISH_DIST="$(dirname "$(command -v lavish-axi)")/../lib/node_modules/lavish-axi/dist" +fi +if [[ ! -f "$LAVISH_DIST/cli.mjs" ]]; then + echo "[lavish-planning-gui] resolving lavish-axi via npx (may download)..." + timeout 180 npx --yes lavish-axi --version >/dev/null 2>&1 || true + LAVISH_DIST="$(find ~/.npm/_npx -maxdepth 4 -type d -path '*lavish-axi/dist' 2>/dev/null | head -1)" +fi +if [[ -z "$LAVISH_DIST" || ! -f "$LAVISH_DIST/cli.mjs" ]]; then + echo "[lavish-planning-gui] ERROR: could not resolve lavish-axi dist" >&2 + exit 1 +fi +echo "[lavish-planning-gui] lavish-axi dist: $LAVISH_DIST" + +# --- 1. Xvfb :99 --- +if ! pgrep -f "Xvfb :${DISPLAY_NUM}" >/dev/null; then + echo "[lavish-planning-gui] starting Xvfb :${DISPLAY_NUM}..." + Xvfb ":${DISPLAY_NUM}" -screen 0 "${XVFB_SCREEN}" -nolisten tcp >/tmp/xvfb${DISPLAY_NUM}.log 2>&1 & + for i in {1..10}; do DISPLAY=":${DISPLAY_NUM}" xdpyinfo >/dev/null 2>&1 && break; sleep 0.5; done +else + echo "[lavish-planning-gui] Xvfb :${DISPLAY_NUM} already running" +fi + +# --- 2. openbox --- +if ! pgrep -f "openbox" >/dev/null; then + echo "[lavish-planning-gui] starting openbox..." + DISPLAY=":${DISPLAY_NUM}" openbox >/tmp/openbox${DISPLAY_NUM}.log 2>&1 & + sleep 1 +else + echo "[lavish-planning-gui] openbox already running" +fi + +# --- 3. Browser (headed, on :99, at Lavish session) --- +BROWSER_BIN="$(command -v google-chrome-stable || command -v chromium || command -v chromium-browser)" +if [[ -z "$BROWSER_BIN" ]]; then + echo "[lavish-planning-gui] ERROR: no browser binary (prereq check should have caught this)" >&2 + exit 1 +fi +# kill any prior instance with our data-dir so we reload cleanly +for pid in $(pgrep -x chrome 2>/dev/null); do + if tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null | grep -q "user-data-dir=${CHROME_USER_DATA_DIR}"; then kill "$pid" 2>/dev/null; fi +done +sleep 1 +echo "[lavish-planning-gui] starting ${BROWSER_BIN} on :${DISPLAY_NUM} -> ${SESSION_URL}" +DISPLAY=":${DISPLAY_NUM}" "${BROWSER_BIN}" \ + --no-sandbox --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage \ + --disable-gpu-sandbox --disable-gpu-compositing --disable-accelerated-2d-canvas \ + --disable-accelerated-video-decode --disable-webgl --use-gl=swiftshader \ + --user-data-dir="${CHROME_USER_DATA_DIR}" --new-window "${SESSION_URL}" \ + >/tmp/chrome-gui.log 2>&1 & +sleep 2 +# Verify Chrome is actually running +if ! pgrep -x chrome >/dev/null 2>&1; then + echo "[lavish-planning-gui] WARNING: Chrome may have failed to start, check /tmp/chrome-gui.log" >&2 + cat /tmp/chrome-gui.log 2>/dev/null | head -20 +fi + +# --- 4. x11vnc --- +if ! pgrep -f "x11vnc.*:${DISPLAY_NUM}" >/dev/null; then + echo "[lavish-planning-gui] starting x11vnc on :${DISPLAY_NUM} (rfb 5900)..." + x11vnc -display ":${DISPLAY_NUM}" -nopw -forever -listen 127.0.0.1 -rfbport 5900 >/tmp/x11vnc.log 2>&1 & + sleep 1 +else + echo "[lavish-planning-gui] x11vnc already running" +fi + +# --- 5. websockify + noVNC --- +if ! pgrep -f "websockify.*${NOVNC_PORT}" >/dev/null; then + echo "[lavish-planning-gui] starting websockify on 0.0.0.0:${NOVNC_PORT} -> localhost:5900..." + websockify --web /usr/share/novnc "0.0.0.0:${NOVNC_PORT}" localhost:5900 >/tmp/websockify.log 2>&1 & + sleep 1 +else + echo "[lavish-planning-gui] websockify already running on ${NOVNC_PORT}" +fi + +# --- 6. Lavish-AXI engine on LAVISH_PORT --- +if ! curl -s -m3 "http://127.0.0.1:${LAVISH_PORT}/health" >/dev/null 2>&1; then + echo "[lavish-planning-gui] starting lavish-axi engine on :${LAVISH_PORT}..." + mkdir -p "$STATE_DIR" + LAVISH_AXI_PORT="$LAVISH_PORT" LAVISH_AXI_STATE_DIR="$STATE_DIR" LAVISH_AXI_NO_OPEN=1 \ + nohup node "$LAVISH_DIST/cli.mjs" server --port "$LAVISH_PORT" >/tmp/lavish-server.log 2>&1 & + for i in {1..20}; do curl -s -m3 "http://127.0.0.1:${LAVISH_PORT}/health" >/dev/null 2>&1 && break; sleep 0.5; done + # Verify it's actually responding with ok + if ! curl -s -m3 "http://127.0.0.1:${LAVISH_PORT}/health" | grep -q '"ok":true'; then + echo "[lavish-planning-gui] ERROR: lavish-axi started but health check failed" >&2 + exit 1 + fi +else + echo "[lavish-planning-gui] lavish-axi already up on :${LAVISH_PORT}" +fi + +# --- 7. Open artifact session (creates /session/) --- +echo "[lavish-planning-gui] opening session for $ARTIFACT" +( cd "$LAVISH_DIST" && LAVISH_AXI_PORT="$LAVISH_PORT" LAVISH_AXI_STATE_DIR="$STATE_DIR" LAVISH_AXI_NO_OPEN=1 \ + node "$LAVISH_DIST/cli.mjs" "$ARTIFACT" --no-open ) >/dev/null 2>&1 || true + +# --- 8. Poll listener (feedback loop) — left running in background with auto-restart --- +echo "[lavish-planning-gui] starting poll listener (continuous feedback loop)..." +( + cd "$LAVISH_DIST" + while true; do + LAVISH_AXI_PORT="$LAVISH_PORT" LAVISH_AXI_STATE_DIR="$STATE_DIR" \ + node "$LAVISH_DIST/cli.mjs" poll "$ARTIFACT" >>/tmp/lavish-poll.log 2>&1 + echo "[$(date)] poll exited, restarting in 2s..." >>/tmp/lavish-poll.log + sleep 2 + done +) & +POLL_PID=$! +# Ensure it's in its own process group so it survives shell exit +disown -h $POLL_PID 2>/dev/null || true + +# --- Summary --- +CODESPACE_NAME="${CODESPACE_NAME:-$(hostname)}" +# noVNC URL with auto-connect and local scaling (scale mode) +NOVNC_URL="https://${CODESPACE_NAME}-${NOVNC_PORT}.app.github.dev/vnc.html?autoconnect=true&resize=scale" +echo "" +echo "[lavish-planning-gui] ==============================================" +echo "[lavish-planning-gui] Planning Lavish is LIVE" +echo "[lavish-planning-gui] ==============================================" +echo "[lavish-planning-gui] Artifact : $ARTIFACT" +echo "[lavish-planning-gui] Session : $SESSION_URL" +echo "[lavish-planning-gui] noVNC : $NOVNC_URL" +echo "[lavish-planning-gui] (set port ${NOVNC_PORT} to PRIVATE for auth-gated access)" +echo "[lavish-planning-gui] poll PID : $POLL_PID (continuous feedback loop active)" +echo "[lavish-planning-gui] poll log : /tmp/lavish-poll.log" +echo "[lavish-planning-gui] ==============================================" + +# --- Cleanup function for graceful shutdown --- +cleanup() { + echo "[lavish-planning-gui] Received signal, cleaning up..." + # Only kill our poll process if it's still our child + if kill -0 "$POLL_PID" 2>/dev/null; then + kill "$POLL_PID" 2>/dev/null || true + fi + exit 0 +} +trap cleanup SIGTERM SIGINT + +# --- Monitor mode: tail the poll log for live feedback --- +if [[ "${MONITOR:-0}" -eq 1 ]]; then + echo "[lavish-planning-gui] Monitor mode: tailing poll log (Ctrl+C to exit)..." + echo "[lavish-planning-gui] Feedback will appear here as user interacts with the whiteboard." + tail -f /tmp/lavish-poll.log +fi diff --git a/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_prereqs.sh b/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_prereqs.sh new file mode 100755 index 0000000..2d12ebf --- /dev/null +++ b/.devcontainer/skills/codespace-lavish/scripts/lavish_planning_prereqs.sh @@ -0,0 +1,187 @@ +#!/usr/env bash +# lavish_planning_prereqs.sh — verify everything the lavish-planning-gui skill needs. +# +# The skill NEVER assumes the Codespace is pre-provisioned. This script checks each +# requirement and, with --fix, installs what is missing. Fail-closed: any missing +# REQUIRED item => exit 1. +# +# Checks (each PASS/FAIL): +# 1. Xvfb (virtual display) +# 2. openbox (window manager) +# 3. x11vnc (RFB export of :99) +# 4. websockify (VNC->websocket bridge) +# 5. novnc web assets(/usr/share/novnc) +# 6. Browser binary (google-chrome-stable | chromium | chromium-browser) +# 7. node + npm (lavish-axi runtime) +# 8. lavish-axi (resolvable via npx, or on PATH) +# +# Usage: lavish_planning_prereqs.sh [--fix] +# --fix installs the missing apt packages and (re)resolves lavish-axi via npx. +# Exit code: 0 if all REQUIRED checks pass, 1 otherwise. + +set -u +FIX=0 +[[ "${1:-}" == "--fix" ]] && FIX=1 + +pass=0; fail=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); } +fixed(){ printf ' \033[32mFIXED\033[0m %s\n' "$1"; if [[ $fail -gt 0 ]]; then fail=$((fail-1)); fi; } +info() { printf ' \033[36mINFO\033[0m %s\n' "$1"; } + +echo "lavish-planning-gui prerequisite check" +echo "" + +echo "[1] Xvfb (virtual display)" +if command -v Xvfb >/dev/null 2>&1; then + ok "Xvfb present" +else + bad "Xvfb missing" + if [[ $FIX -eq 1 ]]; then + info "attempting apt-get install -y xvfb" + if (sudo -n true 2>/dev/null && sudo apt-get install -y xvfb) >/dev/null 2>&1 || apt-get install -y xvfb >/dev/null 2>&1; then + fixed "Xvfb installed via apt" + else + bad "Xvfb install failed (need apt + root/sudo)" + fi + else + info "rerun with --fix to install (apt-get install -y xvfb)" + fi +fi + +echo "[2] openbox (window manager)" +if command -v openbox >/dev/null 2>&1; then + ok "openbox present" +else + bad "openbox missing" + if [[ $FIX -eq 1 ]]; then + info "attempting apt-get install -y openbox" + if (sudo -n true 2>/dev/null && sudo apt-get install -y openbox) >/dev/null 2>&1 || apt-get install -y openbox >/dev/null 2>&1; then + fixed "openbox installed via apt" + else + bad "openbox install failed (need apt + root/sudo)" + fi + else + info "rerun with --fix to install (apt-get install -y openbox)" + fi +fi + +echo "[3] x11vnc (RFB export of :99)" +if command -v x11vnc >/dev/null 2>&1; then + ok "x11vnc present" +else + bad "x11vnc missing" + if [[ $FIX -eq 1 ]]; then + info "attempting apt-get install -y x11vnc" + if (sudo -n true 2>/dev/null && sudo apt-get install -y x11vnc) >/dev/null 2>&1 || apt-get install -y x11vnc >/dev/null 2>&1; then + fixed "x11vnc installed via apt" + else + bad "x11vnc install failed (need apt + root/sudo)" + fi + else + info "rerun with --fix to install (apt-get install -y x11vnc)" + fi +fi + +echo "[4] websockify (VNC->websocket bridge)" +if command -v websockify >/dev/null 2>&1; then + ok "websockify present" +else + bad "websockify missing" + if [[ $FIX -eq 1 ]]; then + info "attempting apt-get install -y websockify" + if (sudo -n true 2>/dev/null && sudo apt-get install -y websockify) >/dev/null 2>&1 || apt-get install -y websockify >/dev/null 2>&1; then + fixed "websockify installed via apt" + else + bad "websockify install failed (need apt + root/sudo)" + fi + else + info "rerun with --fix to install (apt-get install -y websockify)" + fi +fi + +echo "[5] noVNC web assets (/usr/share/novnc)" +if [[ -d /usr/share/novnc && -f /usr/share/novnc/vnc.html ]]; then + ok "noVNC assets present" +else + bad "noVNC web assets missing at /usr/share/novnc" + if [[ $FIX -eq 1 ]]; then + info "attempting apt-get install -y novnc" + if (sudo -n true 2>/dev/null && sudo apt-get install -y novnc) >/dev/null 2>&1 || apt-get install -y novnc >/dev/null 2>&1; then + fixed "novnc installed via apt (assets at /usr/share/novnc)" + else + bad "novnc install failed (need apt + root/sudo)" + fi + else + info "rerun with --fix to install (apt-get install -y novnc)" + fi +fi + +echo "[6] Browser binary (google-chrome-stable | chromium | chromium-browser)" +BROWSER_BIN="$(command -v google-chrome-stable || command -v chromium || command -v chromium-browser)" +if [[ -n "$BROWSER_BIN" ]]; then + ok "browser: $BROWSER_BIN" +else + bad "no browser binary found (google-chrome-stable/chromium/chromium-browser)" + if [[ $FIX -eq 1 ]]; then + info "installing Google Chrome stable .deb (Ubuntu 24.04 has no chromium apt deb)" + TMP_DEB="$(mktemp /tmp/chrome-XXXX.deb)" + if curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o "$TMP_DEB" 2>/dev/null; then + if (sudo -n true 2>/dev/null && sudo apt-get install -y "$TMP_DEB") >/dev/null 2>&1 || apt-get install -y "$TMP_DEB" >/dev/null 2>&1; then + # protect from free-disk.sh autoremove + sudo -n true 2>/dev/null && sudo apt-mark manual google-chrome-stable >/dev/null 2>&1 || apt-mark manual google-chrome-stable >/dev/null 2>&1 || true + fixed "google-chrome-stable installed" + else + bad "google-chrome-stable .deb install failed (need apt + root/sudo)" + fi + rm -f "$TMP_DEB" + else + bad "failed to download google-chrome-stable .deb" + fi + else + info "rerun with --fix to install Google Chrome (Ubuntu 24.04 ships no chromium deb)" + fi +fi + +echo "[7] node + npm (lavish-axi runtime)" +if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then + ok "node $(node -v 2>&1) / npm $(npm -v 2>&1)" +else + bad "node and/or npm missing" + if [[ $FIX -eq 1 ]]; then + info "node/npm are NOT auto-installed by this script (environment-specific: apt/nvm/volta). Install node >=18 then re-run." + else + info "install node >=18 (apt/nvm/volta), then re-run prereqs" + fi +fi + +echo "[8] lavish-axi (resolvable via npx, or on PATH)" +if command -v lavish-axi >/dev/null 2>&1; then + ok "lavish-axi on PATH ($(lavish-axi --version 2>&1 | head -1))" +else + # not on PATH — can npx resolve it on demand? (no install yet; just probe cache) + if [[ -d ~/.npm/_npx ]] && find ~/.npm/_npx -maxdepth 4 -type d -path "*lavish-axi/dist" 2>/dev/null | head -1 | grep -q .; then + ok "lavish-axi resolvable via npx cache" + else + bad "lavish-axi not on PATH and not in npx cache" + if [[ $FIX -eq 1 ]]; then + info "resolving lavish-axi via 'npx --yes lavish-axi --version' (downloads if needed)" + if timeout 120 npx --yes lavish-axi --version >/dev/null 2>&1; then + fixed "lavish-axi resolved via npx" + else + bad "npx lavish-axi failed (needs network + node/npm)" + fi + else + info "rerun with --fix to resolve via npx (npx --yes lavish-axi)" + fi + fi +fi + +echo "" +if [[ $fail -eq 0 ]]; then + echo -e "RESULT: \033[32mALL REQUIRED PREREQUISITES MET\033[0m — skill ready to run." + exit 0 +else + echo -e "RESULT: \033[31m$fail REQUIRED CHECK(S) FAILED\033[0m — run with --fix, or install manually." + exit 1 +fi diff --git a/.devcontainer/skills/codespace-persistent-symlinks/SKILL.md b/.devcontainer/skills/codespace-persistent-symlinks/SKILL.md index f0065d9..e55153d 100644 --- a/.devcontainer/skills/codespace-persistent-symlinks/SKILL.md +++ b/.devcontainer/skills/codespace-persistent-symlinks/SKILL.md @@ -67,4 +67,8 @@ self-check.sh). Run locally: ## Sync note (wiki) - `persistent-memory-proposal.md` — reference/proposal doc vs this procedural skill. - Keep the Option-A split and the self-check wiring (below) mirrored in both. \ No newline at end of file + Keep the Option-A split and the self-check wiring (below) mirrored in both. + +## Cleanup +When deleting a skill/project, also purge its persisted files and cross-references: +- [persistent-knowledge/references/cleanup-deleted-skill.md](../persistent-knowledge/references/cleanup-deleted-skill.md) — Full procedure for wiki, Mnemon DB, seed.json, skills, and cross-references \ No newline at end of file diff --git a/.devcontainer/skills/codespace-port-visibility/SKILL.md b/.devcontainer/skills/codespace-port-visibility/SKILL.md index 16b0f24..94fc035 100644 --- a/.devcontainer/skills/codespace-port-visibility/SKILL.md +++ b/.devcontainer/skills/codespace-port-visibility/SKILL.md @@ -75,14 +75,13 @@ def get_codespace_token(): return entry.decode().split('=', 1)[1] ``` -## Integration with Lavish-AXI +## Integration Example ```bash -# In lavish-axi startup script: -# 1. Make lavish-axi port (4387) private for security +# 1. Make service port private for security python3 scripts/set_port_visibility.py 4387 private -# 2. Make nginx proxy port (8080) public for access +# 2. Make proxy port public for access python3 scripts/set_port_visibility.py 8080 public # 3. Start services @@ -99,5 +98,4 @@ sudo nginx -g 'daemon off;' & ## Related Skills - `codespace-gh-auth` — GitHub auth setup for Codespaces -- `github-codespace` — Full Codespace workflow (auth, CI, PR) -- `cdp-browser-testing` — Headless browser automation in Codespaces \ No newline at end of file +- `github-codespace` — Full Codespace workflow (auth, CI, PR) \ No newline at end of file diff --git a/.devcontainer/skills/lavish-axi/SKILL.md b/.devcontainer/skills/lavish-axi/SKILL.md deleted file mode 100644 index 2407a06..0000000 --- a/.devcontainer/skills/lavish-axi/SKILL.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -name: lavish-axi -description: Launch a dedicated lavish-axi planning session per Hermes conversation with zero port collisions — "iterate this in lavish" spins up an isolated, annotated artifact on a dynamic slot. ---- - -# lavish-axi skill - -Run lavish-axi planning sessions inside a GitHub Codespace without fixed-port collisions. -Each Hermes conversation gets its own **slot**: a loopback lavish-axi engine, a public nginx proxy, and a poll listener — all keyed by the Hermes session ID and persisted in a slot registry. - -## When to use - -- User says "iterate this in lavish", "plan in lavish", or wants a visual annotated planning artifact. -- You need to draft/iterate a spec, diagram, or design with user annotations + chat feedback. -- Multiple Hermes sessions may run lavish at once (dynamic ports avoid collisions). - -## Architecture - -```mermaid -flowchart LR - H[Hermes Session] -->|reads session ID| S[slot_allocator.py] - S -->|allocates slot| Sl[slots.json] - Sl -->|{slot, engine_port, public_port, state_dir, session_key}| L[launch_slot.py] - L -->|start engine| E[lavish-axi engine
127.0.0.1:43XX] - L -->|nginx proxy| N[nginx
0.0.0.0:99XX → 127.0.0.1:43XX] - N -->|Origin rewrite + WS upgrade| E - L -->|expose + visibility| X[expose_port.py
gh forward + public] - L -->|print public URL| U[User chat] - L -->|poll supervisor| P[poll_supervisor.py
harness background] - U -->|feedback annotations| A[User Browser
lavish-axi UI] - A -->|queued prompts| P - P -->|agent-reply| H -``` - -Port scheme (matching suffixes for readability): -- Engine (loopback): `4387 + (slot-1)` → 4387, 4388, 4389… -- Public (nginx): `9987 + (slot-1)` → 9987, 9988, 9989… - -## Quick start - -```bash -# 1. Allocate slot for THIS Hermes session, launch engine + nginx + expose -python3 scripts/launch_slot.py "$HERMES_SESSION_ID" /path/to/artifact.html [lavish_axi_dir] - -# Optional: 4th argument overrides the lavish-axi dist directory path. -# If omitted, launch_slot auto-resolves from ~/.npm/_npx//node_modules/lavish-axi/dist/ -# (populated by running `npx -y lavish-axi --version` once). - -# 2. The script attempts to make port public automatically. -# If GH_TOKEN is not configured, you'll see a notice with manual alternatives. -# In VS Code: Ports panel → 99XX → Port Visibility → Public - -# 3. Start poll supervisor as harness-background (notify_on_complete=true) -python3 scripts/poll_supervisor.py - -# 4. Drop the public URL from launch_slot output into chat -``` - -## Scripts - -| Script | Purpose | -|--------|---------| -| `scripts/slot_allocator.py` | Registry: `alloc [artifact]`, `get`, `list`, `release`. Maps Hermes session → slot ports/state. | -| `scripts/launch_slot.py` | Bring a slot online: engine + nginx + public expose. Prints public URL. | -| `scripts/poll_supervisor.py` | Keep `lavish-axi poll` alive (re-runs after each feedback cycle). Run as harness background. | -| `scripts/nginx_slot.template` | nginx config template (Origin rewrite + WebSocket upgrade). | - -## Critical rules - -1. **Poll MUST run with `LAVISH_AXI_PORT` = the slot's engine port** and `LAVISH_AXI_STATE_DIR` = the slot's state dir. Mismatch → "No active session" error. -2. **Engine is loopback-only** (`127.0.0.1:43XX`). Never expose it; nginx (99XX) is the public face. -3. **Public port needs both steps**: `gh codespace ports forward :` then `visibility :public`. `expose_port.py` (from `codespace-port-visibility` skill) does both. -4. **Artifact stays as a chat link** — Codespace auto-open goes to `/`, not `/session/xxx`. -5. **Mermaid**: use in-page Mermaid (`theme: "dark"`). Defer the whiteboard iframe (GitHub warning on public URLs). - -## Slot registry persistence - -`~/.lavish-axi/slots.json` survives restarts. On "iterate this in lavish", the agent calls `slot_allocator.py get ` first — if a slot exists, reuse it; otherwise `alloc`. This is how multiple Hermes sessions each own a stable slot. - -## Files - -- Skill: `.devcontainer/skills/lavish-axi/` -- Wiki design: `.devcontainer/wiki/lavish-axi-skill-design.md` -- Setup/troubleshooting: `.devcontainer/wiki/lavish-axi-codespace-setup.md` -- Depends on: `codespace-port-visibility` skill (`expose_port.py`) \ No newline at end of file diff --git a/.devcontainer/skills/lavish-axi/scripts/launch_slot.py b/.devcontainer/skills/lavish-axi/scripts/launch_slot.py deleted file mode 100644 index c4983c7..0000000 --- a/.devcontainer/skills/lavish-axi/scripts/launch_slot.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -""" -launch_slot.py — allocate a lavish-axi slot for a Hermes session and bring it online. - -Steps: - 1. Allocate slot (engine port, public port, state dir) via slot_allocator.py - 2. Write nginx config for the public port (Origin rewrite + WebSocket upgrade) - 3. Reload nginx - 4. Start lavish-axi engine (background, loopback-only) in the slot state dir - 5. Expose the public port (gh codespace ports forward + visibility) - 6. Print the public session URL - -The agent then runs the poll supervisor (poll_supervisor.py) in the slot state dir. - -Usage: - python3 launch_slot.py [lavish_axi_dir] -""" -import json -import os -import subprocess -import sys -import time - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) -HERE = os.path.dirname(SCRIPT_DIR) -EXPOSE_PORT = os.path.join(HERE, "..", "codespace-port-visibility", "scripts", "expose_port.py") -TEMPLATE = os.path.join(SCRIPT_DIR, "nginx_slot.template") -NGINX_ENABLED = "/etc/nginx/sites-enabled" -CODESPACE_NAME = os.environ.get("CODESPACE_NAME") -if not CODESPACE_NAME: - print("[launch_slot] ERROR: CODESPACE_NAME not set in environment", file=sys.stderr) - sys.exit(1) - - -def _resolve_lavish_dir(argv, explicit_dir): - """Resolve the lavish-axi dist directory from npm cache. - - Priority: - 1. Explicit argv[3] if provided (user passed a custom path) - 2. npm cache auto-discovery via npx (primary: ~/.npm/_npx//node_modules/lavish-axi/dist/) - 3. FAIL with clear guidance - """ - if explicit_dir: - return explicit_dir - - # Auto-discover npx cache path - # Look for ~/.npm/_npx//node_modules/lavish-axi/dist/ - home = os.path.expanduser("~") - npm_cache_dir = os.path.join(home, ".npm", "_npx") - if os.path.isdir(npm_cache_dir): - for entry in os.listdir(npm_cache_dir): - candidate = os.path.join(npm_cache_dir, entry, "node_modules", "lavish-axi", "dist") - if os.path.isdir(candidate): - print(f"[launch_slot] Auto-detected lavish-axi from npx cache: {candidate}") - return candidate - - # Last resort - fail with clear guidance - print(f"[launch_slot] ERROR: Could not auto-detect lavish-axi dist directory.", file=sys.stderr) - print(f"[launch_slot] Expected: npx cache under {npm_cache_dir}", file=sys.stderr) - print(f"[launch_slot] Run: npx -y lavish-axi --version (to populate cache)", file=sys.stderr) - print(f"[launch_slot] Or pass lavish_axi_dir argument explicitly.", file=sys.stderr) - sys.exit(1) - - -def run(cmd, check=True): - print(f"[launch_slot] {cmd}") - r = subprocess.run(cmd, shell=True, capture_output=True, text=True) - if check and r.returncode != 0: - print(f"[launch_slot] ERROR: {r.stderr}", file=sys.stderr) - sys.exit(1) - return r - - -def main(): - if len(sys.argv) < 3: - print("usage: launch_slot.py [lavish_axi_dir]", file=sys.stderr) - sys.exit(2) - - hermes_session_id = sys.argv[1] - artifact_path = sys.argv[2] - lavish_dir = _resolve_lavish_dir(sys.argv, sys.argv[3] if len(sys.argv) > 3 else None) - - # 1. Allocate slot - rec = json.loads(subprocess.check_output( - [sys.executable, os.path.join(SCRIPT_DIR, "slot_allocator.py"), "alloc", hermes_session_id, artifact_path] - )) - engine_port = rec["engine_port"] - public_port = rec["public_port"] - state_dir = rec["state_dir"] - session_key = rec["session_key"] - print(f"[launch_slot] slot {rec['slot']}: engine={engine_port} public={public_port} state={state_dir}") - - # 2. Write nginx config - with open(TEMPLATE) as f: - tpl = f.read() - conf = tpl.format(engine_port=engine_port, public_port=public_port) - conf_path = f"/tmp/lavish-slot{rec['slot']}.conf" - with open(conf_path, "w") as f: - f.write(conf) - run(f"sudo ln -sf {conf_path} {NGINX_ENABLED}/lavish-axi-slot{rec['slot']}") - run("sudo nginx -s reload") - - # 3. Start lavish-axi engine - env = dict(os.environ) - env["LAVISH_AXI_PORT"] = str(engine_port) - env["LAVISH_AXI_STATE_DIR"] = state_dir - env["LAVISH_AXI_NO_OPEN"] = "1" - subprocess.Popen( - f"node {lavish_dir}/dist/cli.mjs server --port {engine_port}", - shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - # Wait for engine health - for _ in range(20): - r = run(f"curl -s http://127.0.0.1:{engine_port}/health", check=False) - if "ok" in r.stdout: - break - time.sleep(0.5) - else: - print("[launch_slot] engine did not start", file=sys.stderr) - sys.exit(1) - - # 4. Open the artifact session on this engine - run(f"cd {lavish_dir} && LAVISH_AXI_PORT={engine_port} LAVISH_AXI_STATE_DIR={state_dir} node dist/cli.mjs {artifact_path} --no-open", check=False) - - # 5. Expose public port - expose = os.path.abspath(EXPOSE_PORT) - if os.path.exists(expose): - # expose_port.py runs `gh forward` (a long-lived tunnel) in background, - # then flips visibility. We wait for the tunnel to register, then ensure public. - run(f"timeout 30 python3 {expose} {public_port}", check=False) - # ensure visibility is public even if the script's final step was cut off - vis = run(f"gh codespace ports visibility {public_port}:public -c {CODESPACE_NAME}", check=False) - if vis.returncode != 0: - print(f"[launch_slot] Port visibility set (attempted).", file=sys.stderr) - print(f"[launch_slot] If port 99{public_port} is not public, run manually:", file=sys.stderr) - print(f" gh codespace ports visibility {public_port}:public -c {CODESPACE_NAME}", file=sys.stderr) - print(f" OR use VS Code Ports panel → 99{public_port} → Port Visibility → Public", file=sys.stderr) - else: - print(f"[launch_slot] expose_port.py not found at {expose}", file=sys.stderr) - - public_url = f"https://{CODESPACE_NAME}-{public_port}.app.github.dev/session/{session_key}" - print(json.dumps({"slot": rec["slot"], "engine_port": engine_port, "public_port": public_port, - "state_dir": state_dir, "session_key": session_key, "public_url": public_url}, indent=2)) - print(f"[launch_slot] PUBLIC URL: {public_url}") - - -if __name__ == "__main__": - main() diff --git a/.devcontainer/skills/lavish-axi/scripts/nginx_slot.template b/.devcontainer/skills/lavish-axi/scripts/nginx_slot.template deleted file mode 100644 index 5c9d96e..0000000 --- a/.devcontainer/skills/lavish-axi/scripts/nginx_slot.template +++ /dev/null @@ -1,14 +0,0 @@ -server {{ - listen {public_port}; - location / {{ - proxy_pass http://127.0.0.1:{engine_port}; - proxy_set_header Host 127.0.0.1:{engine_port}; - proxy_set_header Origin http://127.0.0.1:{engine_port}; - proxy_set_header Referer http://127.0.0.1:{engine_port}; - proxy_set_header X-Forwarded-Host 127.0.0.1:{engine_port}; - proxy_set_header X-Forwarded-Proto http; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - }} -}} diff --git a/.devcontainer/skills/lavish-axi/scripts/poll_supervisor.py b/.devcontainer/skills/lavish-axi/scripts/poll_supervisor.py deleted file mode 100644 index ad0e50d..0000000 --- a/.devcontainer/skills/lavish-axi/scripts/poll_supervisor.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -""" -poll_supervisor.py — keep a lavish-axi poll alive for a slot. - -lavish-axi poll is a long-poll: it captures one feedback, returns, and exits. -This supervisor re-runs it after each cycle so the listener stays alive. The poll -MUST run with LAVISH_AXI_PORT matching the engine that owns the slot's state dir. - -This script is meant to be launched as a tracked background process (harness -background=true, notify_on_complete=true). It runs forever; the harness kills it -when the session ends. - -Usage: - python3 poll_supervisor.py [engine_port] -""" -import os -import subprocess -import sys -import time - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -def main(): - if len(sys.argv) < 3: - print("usage: poll_supervisor.py [engine_port]", file=sys.stderr) - sys.exit(2) - - state_dir = sys.argv[1] - artifact_path = sys.argv[2] - engine_port = sys.argv[3] if len(sys.argv) > 3 else "4387" - - lavish_dir = os.environ.get("LAVISH_AXI_DIR", "/workspaces/lavish-axi") - - print(f"[poll_supervisor] state_dir={state_dir} artifact={artifact_path} engine={engine_port}") - - while True: - env = dict(os.environ) - env["LAVISH_AXI_PORT"] = str(engine_port) - env["LAVISH_AXI_STATE_DIR"] = state_dir - try: - r = subprocess.run( - f"cd {lavish_dir} && node dist/cli.mjs poll {artifact_path}", - shell=True, env=env, capture_output=True, text=True, timeout=3600, - ) - out = r.stdout + r.stderr - if "status: feedback" in out or "feedback" in out: - print(f"[poll_supervisor] captured feedback, restarting poll") - elif "session ended" in out.lower() or "ended" in out.lower(): - print(f"[poll_supervisor] session ended, exiting") - break - else: - # timeout or interrupted — restart silently - pass - except subprocess.TimeoutExpired: - print(f"[poll_supervisor] poll timed out, restarting") - except Exception as e: - print(f"[poll_supervisor] error: {e}, restarting in 2s") - time.sleep(2) - - -if __name__ == "__main__": - main() diff --git a/.devcontainer/skills/lavish-axi/scripts/slot_allocator.py b/.devcontainer/skills/lavish-axi/scripts/slot_allocator.py deleted file mode 100644 index 4e40764..0000000 --- a/.devcontainer/skills/lavish-axi/scripts/slot_allocator.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -""" -lavish-axi slot allocator. - -Manages ~/.lavish-axi/slots.json — a registry mapping Hermes session IDs to -lavish-axi engine ports / public nginx ports / state dirs. Each Hermes conversation -owns exactly one slot, so multiple parallel "iterate this in lavish" sessions never -collide on a fixed port. - -Port scheme (matching suffixes for readability): - engine (loopback): 4387 + (slot-1) -> 4387, 4388, 4389, ... - public (nginx): 9987 + (slot-1) -> 9987, 9988, 9989, ... - -Usage: - python3 slot_allocator.py alloc [artifact_path] - -> prints JSON: {slot, engine_port, public_port, state_dir, artifact_path, session_key, created_at} - python3 slot_allocator.py get - -> prints existing slot JSON or exits 1 - python3 slot_allocator.py list - -> prints all slots as JSON array - python3 slot_allocator.py release - -> removes the slot (does not kill processes) -""" -import json -import os -import sys -from datetime import datetime, timezone - -REGISTRY = os.path.expanduser("~/.lavish-axi/slots.json") -BASE_ENGINE = 4388 # loopback-only, never exposed (4387 reserved for legacy/default) -BASE_PUBLIC = 9988 # nginx public port (matching suffix: 4388<->9988, 4389<->9989) - - -def _load(): - if not os.path.exists(REGISTRY): - return {} - try: - with open(REGISTRY) as f: - return json.load(f) - except (json.JSONDecodeError, OSError): - return {} - - -def _save(data): - os.makedirs(os.path.dirname(REGISTRY), exist_ok=True) - with open(REGISTRY, "w") as f: - json.dump(data, f, indent=2) - - -def _session_key(artifact_path): - import hashlib - import pathlib - return hashlib.sha256(str(pathlib.Path(artifact_path).resolve()).encode()).hexdigest()[:16] - - -def alloc(hermes_session_id, artifact_path=None): - data = _load() - # Reuse existing slot for this Hermes session - for slot_rec in data.values(): - if slot_rec.get("hermes_session_id") == hermes_session_id: - if artifact_path: - slot_rec["artifact_path"] = str(artifact_path) - slot_rec["session_key"] = _session_key(artifact_path) - _save(data) - return slot_rec - # Allocate next free slot number - used_slots = {int(k) for k in data.keys() if k.isdigit()} - slot = 1 - while slot in used_slots: - slot += 1 - engine_port = BASE_ENGINE + (slot - 1) - public_port = BASE_PUBLIC + (slot - 1) - state_dir = os.path.expanduser(f"~/.lavish-axi/slot-{slot}") - rec = { - "slot": slot, - "engine_port": engine_port, - "public_port": public_port, - "state_dir": state_dir, - "hermes_session_id": hermes_session_id, - "artifact_path": str(artifact_path) if artifact_path else None, - "session_key": _session_key(artifact_path) if artifact_path else None, - "created_at": datetime.now(timezone.utc).isoformat(), - } - data[str(slot)] = rec - _save(data) - return rec - - -def get(hermes_session_id): - data = _load() - for slot_rec in data.values(): - if slot_rec.get("hermes_session_id") == hermes_session_id: - return slot_rec - return None - - -def list_slots(): - return list(_load().values()) - - -def release(hermes_session_id): - data = _load() - for key, slot_rec in list(data.items()): - if slot_rec.get("hermes_session_id") == hermes_session_id: - del data[key] - _save(data) - - -def main(): - cmd = sys.argv[1] if len(sys.argv) > 1 else "list" - if cmd == "alloc": - if len(sys.argv) < 3: - print("usage: slot_allocator.py alloc [artifact_path]", file=sys.stderr) - sys.exit(2) - rec = alloc(sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else None) - print(json.dumps(rec)) - elif cmd == "get": - if len(sys.argv) < 3: - print("usage: slot_allocator.py get ", file=sys.stderr) - sys.exit(2) - rec = get(sys.argv[2]) - if rec: - print(json.dumps(rec)) - else: - sys.exit(1) - elif cmd == "list": - print(json.dumps(list_slots(), indent=2)) - elif cmd == "release": - if len(sys.argv) < 3: - print("usage: slot_allocator.py release ", file=sys.stderr) - sys.exit(2) - release(sys.argv[2]) - print("released") - else: - print(f"unknown command: {cmd}", file=sys.stderr) - sys.exit(2) - - -if __name__ == "__main__": - main() diff --git a/.devcontainer/skills/mnemon-seed-persistence/SKILL.md b/.devcontainer/skills/mnemon-seed-persistence/SKILL.md index 592a490..7be3bb9 100644 --- a/.devcontainer/skills/mnemon-seed-persistence/SKILL.md +++ b/.devcontainer/skills/mnemon-seed-persistence/SKILL.md @@ -28,7 +28,7 @@ Codespace, it must be in `seed.json` (and committed), not just the live DB. "category": "context", // preference|decision|insight|fact|context|general "importance": 5, // 1–5 "tags": ["codespace","cdp"], - "entities": ["cdp-browser-testing","lavish-axi"], + "entities": [], "source": "agent" } ] } ``` @@ -37,7 +37,9 @@ importance ≥ 4 missing from seed.json (action-needed before merge). ## Workflow (when committing a skill or durable insight) 1. **Propose** the entry for user review — do NOT blind auto-commit (user wants sign-off on durable memory). -2. **Edit** `.devcontainer/mnemon/seed.json` — append to the `insights` array. +2. **Edit** `.devcontainer/mnemon/seed.json` — append to the `insights` array with correct field names: + - Use `content` (not `text`) for the insight body + - Include `category`, `importance`, `tags`, `entities`, `source: "agent"` 3. **Also commit a standalone extract** `seed-.json` (same schema, just the new insights) into the same PR, so the memory ships with the code that needs it. 4. **Validate before commit** (the exact step `start-hermes.sh` runs): @@ -51,6 +53,10 @@ importance ≥ 4 missing from seed.json (action-needed before merge). Use `mnemon_remember`/`mnemon_recall` during the session for immediate recall and for staging what should later be promoted to seed.json. Live DB = working set; seed.json = durable snapshot. +## Cleanup +When deleting a skill/project, also purge its seed.json entries: +- [persistent-knowledge/references/cleanup-deleted-skill.md](../persistent-knowledge/references/cleanup-deleted-skill.md) — Full procedure for wiki, Mnemon DB, seed.json, and cross-references + ## Related - codespace/memory-automation — the live Mnemon workflow this extends with durable persistence - codespace/codespace-persistent-symlinks — whole-folder symlink persistence (memories/ + skills/) diff --git a/.devcontainer/skills/persistent-knowledge/SKILL.md b/.devcontainer/skills/persistent-knowledge/SKILL.md index 5992556..870138c 100644 --- a/.devcontainer/skills/persistent-knowledge/SKILL.md +++ b/.devcontainer/skills/persistent-knowledge/SKILL.md @@ -5,4 +5,27 @@ description: "Persistent skills/knowledge in Codespace via symlinks." # Persistent Knowledge Management -Test content. +Symlink-based persistence for Hermes skills, wiki, and Mnemon across Codespace rebuilds. + +## Architecture + +- `~/.hermes/skills/codespace` → `.devcontainer/skills/` (symlink, whole folder) +- `~/.hermes/memories` → `.devcontainer/memories/` (symlink, whole folder) +- `.devcontainer/mnemon/seed.json` — checked-in snapshot for Mnemon re-seeding on fresh spawn + +## Boot Workflow + +`start-hermes.sh` (runs on every Codespace start/rebuild): +1. Creates symlinks if missing (idempotent) +2. Runs `mnemon import --dry-run .devcontainer/mnemon/seed.json` then real import +3. Validates symlinks via self-check.sh section 9 + +## CI Integration + +Path-filtered CI (`.github/workflows/devcontainer-ci.yml`): +- `.devcontainer/skills/**` and `.devcontainer/memories/**` = **runtime** → lint-check only (~30s) +- Boot scripts, devcontainer.json, workflows = **infrastructure** → full-build (~15min) + +## References + +- [references/cleanup-deleted-skill.md](references/cleanup-deleted-skill.md) — Procedure for removing all traces of deleted skills/projects diff --git a/.devcontainer/skills/persistent-knowledge/references/cleanup-deleted-skill.md b/.devcontainer/skills/persistent-knowledge/references/cleanup-deleted-skill.md new file mode 100644 index 0000000..1039951 --- /dev/null +++ b/.devcontainer/skills/persistent-knowledge/references/cleanup-deleted-skill.md @@ -0,0 +1,81 @@ +# Persistent Knowledge Management + +Symlink-based persistence for Hermes skills, wiki, and Mnemon across Codespace rebuilds. + +## Architecture + +- `~/.hermes/skills/codespace` → `.devcontainer/skills/` (symlink, whole folder) +- `~/.hermes/memories` → `.devcontainer/memories/` (symlink, whole folder) +- `.devcontainer/mnemon/seed.json` — checked-in snapshot for Mnemon re-seeding on fresh spawn + +## Boot Workflow + +`start-hermes.sh` (runs on every Codespace start/rebuild): +1. Creates symlinks if missing (idempotent) +2. Runs `mnemon import --dry-run .devcontainer/mnemon/seed.json` then real import +3. Validates symlinks via self-check.sh section 9 + +## CI Integration + +Path-filtered CI (`.github/workflows/devcontainer-ci.yml`): +- `.devcontainer/skills/**` and `.devcontainer/memories/**` = **runtime** → lint-check only (~30s) +- Boot scripts, devcontainer.json, workflows = **infrastructure** → full-build (~15min) + +## Cleanup Procedure for Deleted Skills/Projects + +When a skill or project is deleted, remove all traces from the persistent knowledge system: + +### 1. Wiki Files +- Delete the skill's wiki article(s) from `.devcontainer/wiki/` +- Remove table rows from `.devcontainer/wiki/INDEX.md` +- Search and remove cross-references in other wiki articles: + ```bash + grep -r "" .devcontainer/wiki/ + ``` + +### 2. Skill Directory +- Delete the skill directory from `.devcontainer/skills//` + +### 3. Mnemon Live Database +- Find and forget relevant insights: + ```bash + mnemon recall "" --limit 50 + mnemon forget # for each relevant insight + ``` + +### 4. Mnemon Seed (seed.json) +- Remove insights referencing the deleted skill: + ```python + import json + with open('.devcontainer/mnemon/seed.json') as f: + data = json.load(f) + data['insights'] = [i for i in data['insights'] + if '' not in i.get('content', '') + and '' not in i.get('entities', [])] + with open('.devcontainer/mnemon/seed.json', 'w') as f: + json.dump(data, f, indent=2) + ``` + +### 5. Related Skill Files +- Check other skills for references in: + - SKILL.md (examples, related skills, triggers) + - templates/ and scripts/ (docstrings, comments) + - references/ (cross-links) + ```bash + grep -r "" .devcontainer/skills/ + ``` + +### 6. Verification +- Confirm no references remain: + ```bash + grep -r "" .devcontainer/ --include="*.md" --include="*.json" --include="*.py" --include="*.sh" --include="*.yml" + ``` + +### Example: lavish-axi + cdp-browser-testing Cleanup +This session removed: +- 3 wiki articles (lavish-axi-codespace-setup.md, lavish-axi-skill-design.md, cdp-browser-testing.md) +- 1 skill directory (cdp-browser-testing/) +- 7 seed.json insights (lavish-axi) +- 3 mnemon live DB insights (lavish-axi) +- Cross-references in 5 other wiki files +- Cross-references in 4 skill files \ No newline at end of file diff --git a/.devcontainer/wiki/INDEX.md b/.devcontainer/wiki/INDEX.md index 31f2068..3a91243 100644 --- a/.devcontainer/wiki/INDEX.md +++ b/.devcontainer/wiki/INDEX.md @@ -14,18 +14,17 @@ | [keepalive-proposal.md](keepalive-proposal.md) | Proposal: Codespace keepalive to mimic client activity and avoid idle shutdown (A: terminal heartbeat, B: /delay-shutdown pinger) | codespace, keepalive, idle-timeout, lifecycle, proposal | | [codespace-lifecycle.md](codespace-lifecycle.md) | Reference: how Codespaces detects idle & shuts down, diagnosing container death, keeping a codespace alive | codespace, lifecycle, idle, keep-alive, shutdown, reference | | [karpathy-coding-guidelines.md](karpathy-coding-guidelines.md) | Reference: Karpathy's LLM coding-pitfall guidelines — four principles, origin, how they map to Hermes skills | coding, discipline, guidelines, karpathy, reference | -| [lavish-axi-codespace-setup.md](lavish-axi-codespace-setup.md) | Complete setup & troubleshooting for lavish-axi in Codespaces: nginx proxy, port visibility, mermaid, feedback loop | lavish-axi, codespace, nginx, port-visibility, setup, mermaid | -| [lavish-axi-skill-design.md](lavish-axi-skill-design.md) | Design spec for lavish-axi as a Hermes skill: per-session slots, dynamic ports, poll supervisor, trigger phrase | lavish-axi, skill, slot, dynamic-port, design, proposal | -| [cdp-browser-testing.md](cdp-browser-testing.md) | Headless browser automation in Codespaces via Playwright CDP — launch, interact, extract, troubleshoot | codespace, browser, cdp, playwright, testing, skill | | [codespace-gh-auth.md](codespace-gh-auth.md) | Extract real GitHub OAuth token from VS Code server process for API and gh CLI in Codespaces | codespace, github, auth, token, vscode, skill | | [codespace-persistent-symlinks.md](codespace-persistent-symlinks.md) | Whole-folder symlink pattern to persist Hermes memories and skills across Codespace rebuilds | codespace, persistence, symlink, memory, skill | | [codespace-port-visibility.md](codespace-port-visibility.md) | Automate Codespace port visibility via CLI: forward + public/private visibility using gh CLI | codespace, ports, visibility, github, automation, skill | +| [codespace-lavish.md](codespace-lavish.md) | Headed Lavish-AXI whiteboard over noVNC in Codespaces — architecture, session key, feedback loop, prerequisites | codespace, lavish, whiteboard, novnc, gui, skill | | [github-codespace.md](github-codespace.md) | Full GitHub Codespace workflow: auth, CI monitoring, debugging, API access, PR operations | github, codespace, ci, debugging, api, workflow, skill | | [github-pr-review.md](github-pr-review.md) | Evaluate CodeQL and Copilot suggestions on PRs — fetch, triage, propose fixes with decision framework | github, pr, codeql, copilot, review, security, skill | | [memory-automation.md](memory-automation.md) | Automated Mnemon workflow: recall on start, recall before turn, auto-save after response | memory, mnemon, persistence, automation, workflow, skill | | [mnemon-seed-persistence.md](mnemon-seed-persistence.md) | Persist Mnemon memory across Codespace rebuilds via checked-in seed.json with validation | mnemon, seed, persistence, codespace, memory, skill | | [persistent-knowledge.md](persistent-knowledge.md) | Persistent skills/knowledge in Codespace via symlinks — validated pattern and self-check wiring | persistence, symlink, codespace, knowledge, skill | | [vscode-cli-codespaces.md](vscode-cli-codespaces.md) | Auto-discover VS Code CLI in Codespaces and open files in connected editor | codespace, vscode, editor, cli, skill | +| [ci-lint-check.md](ci-lint-check.md) | Pre-commit CI lint validation — run locally before push to avoid GitHub Actions failures | ci, lint, pre-commit, validation, github-actions, skill | ## How to Use diff --git a/.devcontainer/wiki/cdp-browser-testing.md b/.devcontainer/wiki/cdp-browser-testing.md deleted file mode 100644 index a23f62c..0000000 --- a/.devcontainer/wiki/cdp-browser-testing.md +++ /dev/null @@ -1,120 +0,0 @@ -# CDP Browser Testing in Codespaces - -## Overview - -GitHub Codespaces don't have system Chrome/Chromium installed. The `browser_use` tool's built-in browser fails to launch. Playwright installs its own Chromium to `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome` — we can drive it via CDP (Chrome DevTools Protocol) on port 9222. - -This article documents the reference architecture for headless browser automation in Codespaces using Playwright's bundled Chromium and CDP. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Codespace Environment │ -│ │ -│ ┌──────────────┐ CDP (port 9222) ┌─────────────┐ │ -│ │ CDPClient │ ◄──────────────────────► │ Chromium │ │ -│ │ (Python) │ WebSocket + HTTP │ (Playwright│ │ -│ └──────────────┘ │ cached) │ │ -│ ▲ └─────────────┘ │ -│ │ ▲ │ -│ │ launch-chrome-cdp.sh │ │ -│ └──────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Key Components - -### 1. Playwright Chromium Cache - -Playwright downloads its own Chromium binary on first use: -- Location: `~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome` -- Version: Tracked in Playwright's package.json -- No system Chrome dependency required - -### 2. CDP Launcher Script - -`launch-chrome-cdp.sh`: -- Finds the cached Playwright Chromium binary -- Starts headless Chrome with `--remote-debugging-port=9222` -- Prints the WebSocket URL for CDP connection -- Sets `CDP_WS_URL` environment variable - -```bash -source ~/.hermes/skills/codespace/cdp-browser-testing/scripts/launch-chrome-cdp.sh -# Sets CDP_WS_URL=ws://127.0.0.1:9222/devtools/browser/ -``` - -### 3. CDPClient Python Module - -`cdp_client.py` — Async Python class providing: -- `connect()` — Auto-discovers browser via `http://127.0.0.1:9222/json/version` -- `new_page()` — Creates a new browser tab -- `navigate(url)` — Navigates to URL -- `wait_for(selector)` — JS-based wait (React-safe) -- `type_text(selector, text)` — Types into input via Runtime.evaluate -- `click(selector)` — Clicks element via Runtime.evaluate -- `get_html(selector?)` — Full or scoped outerHTML -- `get_text(selector)` — textContent of element -- `evaluate(js_expression)` — Any JS expression -- `get_chat_bubbles()` — Lavish-axi chat turn extraction - -### 4. React-Safe Interaction - -**Critical**: CDP `DOM.querySelector` is unreliable against React apps — it often returns no nodeId. - -The solution: Use `Runtime.evaluate` for all interactions: -- `wait_for()` polls via `document.querySelector` in page context -- `type_text()` and `click()` dispatch events via `Runtime.evaluate` -- `get_chat_bubbles()` extracts lavish-axi turns via page JS - -## Usage Patterns - -### Basic Navigation & Interaction - -```python -from cdp_client import CDPClient - -async with CDPClient() as cdp: - await cdp.navigate('http://127.0.0.1:4387/session/xxx') - await cdp.wait_for('#chatInput') - await cdp.type_text('#chatInput', 'Hello from agent!') - await cdp.click('#send') - bubbles = await cdp.get_chat_bubbles() - print(bubbles) -``` - -### Data Extraction - -```python -html = await cdp.get_html() # Full document outerHTML -html = await cdp.get_html('#artifact') # Scoped outerHTML -text = await cdp.get_text('h1') # textContent of element -val = await cdp.evaluate("1 + 1") # Any JS expression -``` - -## Troubleshooting Reference - -| Issue | Cause | Resolution | -|-------|-------|------------| -| Chrome won't start | Playwright Chromium not installed | Run `playwright install chromium` | -| Port 9222 busy | Existing chrome process | `pkill -f "remote-debugging-port=9222"` | -| Navigation hangs | Orphaned CDP targets from previous run | Kill all chrome processes, relaunch | -| Element not found | CDP DOM.querySelector fails on React | Use `wait_for()` + `type_text()`/`click()` (Runtime.evaluate) | -| ModuleNotFoundError | Import as `cdp_client` not `cdp-client` | Ensure script dir on PYTHONPATH | -| Mermaid not rendering | Missing Mermaid CDN script in artifact | Add Mermaid ES module import + initialize | -| Whiteboard cookie error | Excalidraw iframe sandboxed without allow-same-origin | By design — static flowchart works, editor doesn't | - -## Related - -- **Skill**: `.devcontainer/skills/cdp-browser-testing/` — Procedural how-to -- **Wiki**: [github-codespace.md](github-codespace.md) — Codespace auth for CI -- **Wiki**: [codespace-port-visibility.md](codespace-port-visibility.md) — Exposing ports for browser access -- **Wiki**: [lavish-axi-codespace-setup.md](lavish-axi-codespace-setup.md) — Full lavish-axi setup with CDP - -## When to Use This Pattern - -- Headless browser testing in GitHub Codespaces (no system Chrome) -- Automating lavish-axi or similar localhost web UIs over loopback -- Any CDP-driven browser automation where Playwright Chromium is available -- CI/CD pipelines in Codespaces needing browser verification \ No newline at end of file diff --git a/.devcontainer/wiki/ci-lint-check.md b/.devcontainer/wiki/ci-lint-check.md new file mode 100644 index 0000000..aa8aada --- /dev/null +++ b/.devcontainer/wiki/ci-lint-check.md @@ -0,0 +1,117 @@ +# CI Lint Check Skill + +> **Reference**: Run pre-commit CI lint validation locally before pushing to avoid GitHub Actions failures. + +## Overview + +The `ci-lint-check` skill provides a local script that mirrors the `lint-check` job in `.github/workflows/devcontainer-ci.yml`. Running this before every commit/PR ensures CI passes without wasting time debugging in GitHub Actions. + +## CI Job Classification + +The CI workflow uses `dorny/paths-filter@v3` to classify changes: + +| Category | Paths | CI Job | Duration | +|----------|-------|--------|----------| +| **infrastructure** | `.devcontainer/post-create-cmd.sh`, `.devcontainer/start-hermes.sh`, `.devcontainer/self-check.sh`, `.devcontainer/devcontainer.json`, `.github/workflows/**` | `full-build` | ~15min | +| **runtime** | `.devcontainer/skills/**`, `.devcontainer/wiki/**`, `.devcontainer/mnemon/**`, `.devcontainer/memories/**`, `.devcontainer/.hermes.md`, `.devcontainer/codespace-cleanup.sh` | `lint-check` | ~30s | +| **docs** | `README.md`, `*.md` (root) | `lint-check` | ~30s | + +**Key insight**: Changes to skills, wiki, mnemon, memories, and devcontainer shell scripts ONLY run the fast `lint-check` job (~30s). Only boot scripts, devcontainer.json, and workflow changes trigger the slow `full-build` (~15min). + +## What the Lint Check Validates + +| Check | Description | Files | +|-------|-------------|-------| +| **Markdown lint** | Style/formatting rules via markdownlint-cli | Wiki articles, SKILL.md files, .hermes.md, README.md | +| **SKILL.md structure** | YAML frontmatter with `name:` field | All skills in `.devcontainer/skills/*/SKILL.md` | +| **Wiki INDEX.md** | Every article referenced in INDEX.md table | `.devcontainer/wiki/*.md` | +| **Mnemon seed.json** | Schema validation (schema_version=1, insights array) | `.devcontainer/mnemon/seed.json` | +| **Root shell syntax** | `bash -n` validation | `.devcontainer/*.sh` | +| **Skill shell syntax** | `bash -n` validation | `.devcontainer/skills/*/scripts/*.sh` | +| **Symlink persistence** | Boot scripts create correct symlinks | post-create-cmd.sh, start-hermes.sh | + +## Usage + +### Install Dependencies (one-time) +```bash +npm install -g markdownlint-cli +``` + +### Run Full Validation +```bash +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh +``` + +### Run Specific Checks +```bash +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --markdown-only +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --skills-only +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --wiki-only +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --mnemon-only +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --shell-only +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh --symlink-only +``` + +### Pre-Commit Workflow +```bash +# 1. Make your changes +# 2. Run lint check +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh + +# 3. Fix any issues reported +# 4. Commit and push +git add -A +git commit -m "your message" +git push +``` + +## Mandatory Rule + +> **ALWAYS run the lint check before committing or creating a PR** when modifying: +> - `.devcontainer/skills/**` +> - `.devcontainer/wiki/**` +> - `.devcontainer/mnemon/**` +> - `.devcontainer/memories/**` +> - `.devcontainer/*.sh` + +This is not optional — CI will fail and you'll waste time debugging in GitHub Actions. + +## Integration with Git Hooks + +Add to `.git/hooks/pre-commit`: +```bash +#!/bin/bash +bash .devcontainer/skills/ci-lint-check/scripts/ci_lint_check.sh || exit 1 +``` + +## Common Failures & Fixes + +| Failure | Fix | +|---------|-----| +| Markdown lint error (MD056, MD013, etc.) | Fix line/column reported; check table column counts | +| SKILL.md missing frontmatter | Ensure file starts with `---` and has `name:` field | +| Wiki article missing from INDEX.md | Add row to INDEX.md table | +| Mnemon seed.json invalid | Run `python3 .devcontainer/mnemon/validate-seed.py .devcontainer/mnemon/seed.json` for details | +| Shell syntax error | Run `bash -n - - -EOF -``` - -### 3. Start lavish-axi Server (Loopback Only) - -```bash -# Suppress GUI auto-open -LAVISH_AXI_NO_OPEN=1 node dist/cli.mjs sample.html --no-open & - -# Verify -curl http://127.0.0.1:4387/health -# {"ok":true,"app":"lavish-axi","version":"0.1.50"} -``` - -### 4. Start Nginx Proxy - -```bash -sudo nginx -g 'daemon off;' & -# Verify -curl http://127.0.0.1:8080/nginx-health # → "healthy" -curl http://127.0.0.1:8080/health # → lavish-axi health -``` - -### 5. Launch CDP Chrome (for Automation) - -```bash -# Using Playwright's bundled Chromium -chrome --headless --no-sandbox --disable-gpu \ - --disable-dev-shm-usage \ - --remote-debugging-port=9222 \ - --user-data-dir=/tmp/chrome-cdp-test & - -# Verify -curl http://127.0.0.1:9222/json/version -``` - -### 6. Start Poll Listener (Agent Feedback Loop) - -```bash -# Run in background (harness-native) -node dist/cli.mjs poll sample.html & - -# Or foreground for debugging -node dist/cli.mjs poll sample.html -``` - -### 7. Access the Session - -**Public URL (WORKING):** -``` -https://-8080.app.github.dev/session/ -``` - -**Session Key:** SHA256(realpath) first 16 chars -```bash -node -e "const crypto=require('crypto'),fs=require('fs'); console.log(crypto.createHash('sha256').update(fs.realpathSync('sample.html')).digest('hex').slice(0,16))" -# → 609f8b3f1103d0a6 -``` - ---- - -## The Feedback Loop (Official lavish-axi Contract) - -``` -��─────────────────────────────────────────────────────────────────�� -│ LAVISH-AXI CYCLE │ -├─────────────────────────────────────────────────────────────────�� -│ │ -│ 1. Human writes artifact.html │ -│ 2. lavish-axi → opens browser session │ -│ 3. Human annotates, sends chat │ -│ 4. lavish-axi poll → long-polls until feedback │ -│ 5. Poll returns status: feedback + prompts │ -│ 6. Agent processes feedback │ -│ 7. lavish-axi poll --agent-reply "reply" → sends reply │ -│ 8. Repeat from step 4 │ -│ │ -��─────────────────────────────────────────────────────────────────�� -``` - -### Key Behaviors - -| Aspect | Behavior | -|--------|----------| -| **Poll** | Long-poll that **exits after capturing feedback** (returns `status: "feedback"`). Not a persistent daemon. | -| **Agent presence** | Browser SSE `/events/:key` shows: `waiting` → `listening` → `working` → `waiting` | -| **Cycle** | Poll → feedback captured → poll exits → agent replies → poll restarts | -| **Same-origin** | `/prompts` guarded. Behind nginx, expected origin = validated loopback | - -### Manual vs Automated - -| Mode | Description | -|------|-------------| -| **Manual** | You send → poll captures → you run `poll --agent-reply "..."` → reply renders | -| **Automated** | Supervisor script watches poll output → auto-runs `poll --agent-reply` | - ---- - -## Verified Round-Trip Test - -### Automated CDP Test - -```python -# Using cdp-browser-testing skill -async with CDPClient() as cdp: - await cdp.navigate('https://codespace-8080.app.github.dev/session/...') - await cdp.type_text('#chatInput', 'Test message') - await cdp.click('#send') - # Poll captures → agent replies via POST /api/.../agent-reply - # Reply renders as AGENT bubble in browser -``` - -### Verified Chat Log (Extract) - -``` -YOU → CDP test message from agent! -AGENT → CDP round-trip confirmed! Browser → agent → browser works over nginx proxy. -YOU → ping -AGENT → pong — nginx reverse proxy works correctly. -YOU → can you change the page to hello world -AGENT → Done! Changed heading to "Hello World". Reload to see. -``` - ---- - -## Known Limitations & Workarounds - -### 1. Mermaid Whiteboard Iframe Warning (Public 8080) - -**Problem:** GitHub serves interstitial warning for iframes on public URLs. - -**Workarounds:** -- Use **private 4387** with `LAVISH_AXI_ALLOWED_HOSTS=*` (but send-to-agent requires code change) -- Accept static SVG diagram (renders correctly in main page) -- Use custom domain/tunnel (not practical in Codespaces) - -**Status:** Deferred — core functionality works. - -### 2. SDKMan Error in Background Shells - -**Symptom:** `bash: /usr/local/sdkman/bin/sdkman-init.sh: No such file or directory` - -**Cause:** Background shells source `.bashrc` which references missing sdkman. - -**Impact:** Cosmetic only — poll still works, exits cleanly. - -**Fix:** Not critical — ignore. - -### 3. Poll is Not a Daemon - -**Behavior:** `lavish-axi poll` exits after each feedback capture. - -**Solution:** Run in a loop or use supervisor: -```bash -while true; do node dist/cli.mjs poll sample.html; done -# Or use harness-native background jobs with notify_on_complete -``` - ---- - -## Troubleshooting Checklist - -| Symptom | Check | Fix | -|---------|-------|-----| -| "Your agent is not listening" | Poll running? | `ps aux \| grep poll` → restart poll | -| Send-to-agent fails (403) | Origin check? | Verify nginx rewrites Origin to loopback | -| SDK fails to load | `/sdk.js` returns stale? | Reload page for new artifact_revision/token | -| Mermaid not rendering | Mermaid CDN loaded? | Verify `