From 78e0de2e5226e6e4e62e19ec631c2cb01611e500 Mon Sep 17 00:00:00 2001 From: Keysat Date: Sun, 14 Jun 2026 20:20:17 -0500 Subject: [PATCH] Scaffold matrix-bridge (standards-compliant; pre-Phase 0) Single-user Matrix -> Claude Code bridge bot, scaffolded from a prior scoping package (SPEC/DECISIONS/CLAUDE/KICKOFF) folded into the current new-project scheme: - AGENTS.md (canonical) with core flow, stack, placement table, condensed D1-D10 decisions, sovereignty constraint, and Phase 0 as the first milestone - CLAUDE.md -> AGENTS.md relative symlink; ROADMAP.md (Phases 1-4+, falsifiable exits) - scripts/launch-claude.sh first-draft Mac wrapper (D4); config.example.toml - canonical deny-by-default .gitignore + Python ignores No bot code yet, by design: Phase 0 is manual-chain validation (N=3). --- .gitignore | 27 ++++++++ AGENTS.md | 136 +++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + README.md | 31 +++++++++ ROADMAP.md | 47 ++++++++++++++ config.example.toml | 25 +++++++ scripts/launch-claude.sh | 24 +++++++ 7 files changed, 291 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 config.example.toml create mode 100755 scripts/launch-claude.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..000b69e --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Secrets & local env +.env +.env.* +!.env.example + +# Local config — real room IDs / repo paths / credentials. Commit only config.example.toml. +config.toml + +# Claude Code — deny by default, allow-list shared wiring. +# .claude/ also accumulates worktrees, editor configs, and OS cruft; commit +# only the shared parts so new local scratch (or a stray secret) stays out. +.claude/* +!.claude/rules/ +!.claude/agents/ +!.claude/commands/ +!.claude/skills/ +!.claude/settings.json + +# Python +__pycache__/ +*.py[cod] +.venv/ +venv/ +*.egg-info/ + +# OS cruft +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..136fb96 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,136 @@ +# matrix-bridge — AGENTS.md + +A single-user Matrix bot that turns a message in a project room into a live Claude Code +session in that project's repo on the Mac — surfaced to the phone via Claude Code Remote +Control. It makes the *trigger* portable: from anywhere on the WireGuard network, a Matrix +message starts a session on the Mac in the correct repo, and Remote Control pushes it to the +phone to drive interactively. Single user, private home network, no multi-user/product scope. + +> **Inbox check:** At session start, if `~/Projects/standards/INBOX.md` exists, scan it for +> items tagged `(matrix-bridge)` and surface them before proposing next steps; triage with +> `/triage`. + +## Core flow (v1) + +``` +Matrix message in a project room + → bot (matrix-nio, on the DGX Spark) receives it + → looks up which repo that room maps to (explicit config — no classification) + → SSHes to the Mac and runs scripts/launch-claude.sh with (repo_dir, message_text) + → wrapper cd's into the repo and launches `claude` with the message as the prompt + → Claude Code Remote Control (auto-enabled) pushes a notification to the phone + → tap in and drive the session from the Claude app +``` + +Room determines the repo; the message text becomes the initial prompt. That is the entire +v1 decision surface. + +## Stack + +- **Bot:** Python, **matrix-nio** (from the nio-template scaffold), single Docker container. +- **Runs on:** a DGX Spark (always-on Linux, Docker). *Not* Start9, *not* the Mac. +- **Mac seam:** `scripts/launch-claude.sh`, a zsh login-shell wrapper that owns all + environment setup and launches `claude`. +- **Config:** a readable room→repo mapping file (TOML) — adding a project is a config edit. +- **State:** none beyond config in v1; SQLite or flat files only if a later phase needs them. + +## Placement + +| Question | Decision | Rationale | +|---|---|---| +| Sensitivity / sovereignty | Local-only when an LLM is ever involved | v1 makes no LLM call; future intent-parsing must run on a local model via Spark Control — message content may reference investor/portfolio context. Never wire a frontier API to message payloads. | +| Runtime shape | Long-running service (always-listening bot) | Must be up unattended to catch messages. | +| Host | DGX Spark, Docker container | Always-on Linux with Docker; co-located with Qwen3 for future local intent-parsing; reaches both Synapse (network) and the Mac (SSH). | +| s9pk vs container | Plain container | Not on Start9 at all — StartOS only runs s9pk packages; don't pay packaging cost, don't touch Synapse. | +| Model routing | None in v1; future Qwen3 via Spark Control | Keeps the sovereignty boundary; deterministic core first. | +| Data layer | Config file (TOML) | v1 needs no datastore. | +| Interface | Matrix (Element) + phone via Remote Control | "Reachable from phone" already satisfied by WireGuard + Remote Control. | +| Repo home | Local + Gitea backup | `ssh://git@immense-voyage.local:59916/grant/matrix-bridge.git`. | + +## Commands + +- `scripts/launch-claude.sh ` — the Mac wrapper (Phase 0 deliverable; + validate by hand before any bot code). +- _TODO (Phase 1+):_ bot build/run (`docker build` / `docker compose up` on the Spark) once + `src/` exists. + +## Layout + +- `AGENTS.md` — this file (canonical; `CLAUDE.md` is a relative symlink to it). +- `ROADMAP.md` — Phases 1–4+ with falsifiable exits, plus deferred/future directions. +- `README.md` — human-facing intro. +- `scripts/launch-claude.sh` — the Mac-side launch wrapper (the only seam that knows the + Mac's environment). +- `config.example.toml` — room→repo mapping template; the real `config.toml` is gitignored. +- `.claude/` — Claude wiring (dir only for now). +- _Future:_ `src/` (the matrix-nio bot), `Dockerfile`, dependency manifest — Phase 1. + +## Decisions (already made — don't relitigate without new information) + +Condensed from the scoping workshop. Each: the call, why, what it beat. + +- **D1 — matrix-nio, not Maubot.** Full control for one custom bot with real SSH-orchestration + logic; keeps Spark Control as the single dashboard. *Beat:* Maubot (competing web UI, + management layer we don't need), SimpleMatrixBotLib. +- **D2 — Bot runs on the Spark, not Start9 or the Mac.** Always-on Linux + Docker, co-located + with Qwen3, reaches Synapse + the Mac. *Beat:* Start9 (no s9pk), Mac (not always-on; it's the + execution target, not the orchestrator). +- **D3 — Synapse stays untouched.** Treat the existing StartOS Synapse as a fixed external + homeserver; the bot logs in as an ordinary Matrix user over WireGuard/LAN. +- **D4 — The Mac wrapper is the environment seam.** A `#!/bin/zsh -l` wrapper owns + PATH/credentials/`cd`/`exec claude`; the bot stays dumb and only invokes it over SSH. + *Beat:* inlining `source ~/.zprofile && …` from the bot (brittle); relying on the default + non-interactive SSH shell (the core failure mode — minimal shell loads neither `.zprofile` + nor `.zshrc`). +- **D5 — Remote Control is the phone-control layer.** Native, E2EE, already auto-enabled; + execution stays on the Mac. The bot only needs to *start* the session. *Note:* outside server + mode, one remote session per Claude Code instance. +- **D6 — Room = repo; routing is deterministic in v1.** No classification, no LLM, no path + branching. *Beat (for v1):* LLM intent parsing → deferred to D8. +- **D7 — No Nextcloud / CalDAV in v1.** Not the pain point; the interesting future (routing + Claude/bot *outputs* into Nextcloud) is real but unscoped. +- **D8 — Intent parsing deferred, but as a "routing brain."** When added (Phase 4+): a smart + dispatcher that, knowing all repos/contexts, decides which repo applies and what context to + inject — not a task-vs-session classifier. MUST run on a local model via Spark Control. + *Revisit when:* the deterministic core (Phases 1–2) is proven. +- **D9 — E2EE deferred (documented tradeoff).** Single-user bot over WireGuard on a private + LAN; transport is already private and matrix-nio E2EE adds libolm overhead. *Revisit when:* + the bot ever handles sensitive content over untrusted transport. +- **D10 — Spark Control manages the bot (Phase 3).** Status on the dashboard + one-click + update/restart, the same SSH-behind-buttons pattern Spark Control uses for the Sparks today. + +## Sovereignty constraint + +v1 sends nothing to external services except what is deliberately typed into the Claude Code +session itself. The bot's own logic is fully local. **When intent parsing is added later it +MUST run on a local model via Spark Control — never a frontier API** — because it reads +message content that may reference investor/LP/portfolio context. Never wire an external API +call that carries message payloads. + +## Implementation guardrails (from the workshop) + +- **Quoting through SSH is the known footgun.** Message text crosses two shells (the Spark's, + then the Mac's). Use `shlex.quote` (or equivalent) when building the remote command — never + naive string-concatenate user text into the SSH command. +- **Fail loud on a bad directory.** If a room maps to a missing dir, the wrapper exits + non-zero (`cd "$1" || exit 1`) and the bot reports the failure back into the room — never + launches Claude in the wrong place. +- **Config over code** for the room→repo mapping. + +## Definition of done per phase + +Substance threshold **N = 3** real uses, defined per phase in `ROADMAP.md`. "Done" means +falsifiable, scaled substance (it worked 3 real times), never a checkbox. A phase that "works +once" is not done. + +## Current state + +- **Scaffolded 2026-06-15** from a prior scoping package (SPEC/DECISIONS/CLAUDE/KICKOFF), + folded into this AGENTS.md (decisions + placement), `ROADMAP.md` (phases), and the wrapper + + config skeleton. No bot code yet — by design. +- **Next: Phase 0 (manual chain validation, N=3)** — Matrix onboarding (Element, Space, first + room, a bot user), write + locally test `scripts/launch-claude.sh`, passwordless SSH from the + Spark to the Mac, prove the full chain (message → SSH → wrapper → Claude session → phone + notification I can drive) by hand at least 3 times, and record the first room→repo mapping. + Bot code starts only after Phase 0 is proven. The original KICKOFF prompt is the step-by-step + for Phase 0. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e19da5 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# matrix-bridge + +A single-user Matrix → Claude Code bridge. Send a message in a project's Matrix room and a +Claude Code session launches on the Mac in that project's repo, then surfaces to your phone +via Claude Code Remote Control. The point: make the *trigger* for a coding session portable +without moving execution off the Mac. + +Runs as a small **matrix-nio** bot in a Docker container on a DGX Spark; a zsh wrapper on the +Mac (`scripts/launch-claude.sh`) is the only piece that knows the Mac's environment. Routing +is deterministic in v1 — the room you message in decides the repo (an explicit config map). + +> Status: **scaffolded, pre–Phase 0.** No bot code yet. See `AGENTS.md` → `## Current state` +> for the active milestone and `ROADMAP.md` for the phase plan. + +## How it works (v1) + +``` +Matrix message in a project room + → bot on the Spark maps room → repo + → SSHes to the Mac, runs scripts/launch-claude.sh + → wrapper cd's into the repo and launches `claude` + → Remote Control notifies the phone; you drive the session there +``` + +## Setup + +_TODO — filled in as Phase 0 is proven:_ Matrix onboarding (Element + the existing Synapse +homeserver, a bot user), the Mac wrapper, passwordless SSH from the Spark to the Mac, and the +first room→repo mapping. Copy `config.example.toml` to `config.toml` (gitignored) and fill in +real room IDs and repo paths. The original scoping docs (SPEC / DECISIONS / KICKOFF) hold the +full background. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..00e70a7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,47 @@ +# ROADMAP — matrix-bridge + +Phased build plan. Near-term status lives in `AGENTS.md` → `## Current state`; this file is +the longer arc. Substance threshold is **N = 3** real uses per phase — exits are falsifiable +(it worked 3 real times), never checkboxes. + +Phase 0 (the current first milestone) lives in `AGENTS.md` `## Current state`; it writes no +bot code — foundation + proving the manual chain by hand. The phases below are what comes +after it. + +--- + +## Phase 1 — Single-room bot + +- matrix-nio bot in a container on the Spark, logged in as a bot Matrix user. +- One hardcoded room → one repo. Any message in it spawns a session via the Mac wrapper. +- **Exit (falsifiable):** 3 consecutive real messages each correctly launch a drivable + session on the phone. + +## Phase 2 — Multi-room routing + +- Room → repo mapping table; the bot routes by `room_id` (config over code). +- **Exit (falsifiable):** 3 real uses across ≥2 rooms, correct repo every time, zero + wrong-directory launches. + +## Phase 3 — Spark Control integration + +- Bot container status surfaced on the Spark Control dashboard. +- One-click update (pull + restart) wired the same way Spark Control drives the Sparks today + (SSH/commands behind a button). +- **Exit (falsifiable):** bot status is visible and the bot can be updated/restarted from the + panel. + +## Phase 4+ — Future direction (documented, not yet scoped to build) + +- **Intent-routing brain (D8).** Qwen3 via Spark Control as a smart dispatcher: given + knowledge of all repos/contexts, parse a freeform message and decide *which* repo/context + applies and *what* context to inject — not a task-vs-session classifier. MUST run on a local + model. Depends on the deterministic core (Phases 1–2) working first; the architecture must + not foreclose it. +- **Thread-based session continuity.** A Matrix thread = a distinct session/sub-context within + a repo. The first natural extension after multi-room routing. +- **Nextcloud / CalDAV output integration.** Routing Claude/bot *outputs* into Nextcloud + (Matrix ↔ Claude ↔ Nextcloud). Real interest, unscoped — not until Nextcloud Tasks/CalDAV + is actually in use. +- **E2EE (D9).** Add matrix-nio end-to-end encryption (libolm) if the bot ever handles + sensitive content over untrusted transport. Low priority while everything is WireGuard-local. diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..904a6cd --- /dev/null +++ b/config.example.toml @@ -0,0 +1,25 @@ +# matrix-bridge — room → repo mapping (EXAMPLE) +# +# Copy to config.toml (gitignored) and fill in real values during Phase 0. +# The room you message in determines which repo the Claude Code session launches in — +# routing is deterministic in v1 (decision D6). Adding a project is a config edit, not code. + +[homeserver] +url = "https://" # existing StartOS Synapse, reached over WireGuard/LAN +user = "@matrix-bridge-bot:" # a dedicated bot Matrix account (not your own user) +# Credentials (access token or password) come from the environment or a gitignored secret — +# never commit them. The bot reads the homeserver URL + bot creds at startup. + +# One [[room]] block per project. +# room_id — the internal Matrix room ID (starts with '!'), NOT the human alias (#name:domain) +# repo_dir — an absolute path on the Mac (note: ~/Projects uses a capital P) +# label — human-readable name, for logs and error messages +[[room]] +room_id = "!exampleRoomId:your-domain" +repo_dir = "/Users/macpro/Projects/recap" +label = "recap" + +[[room]] +room_id = "!anotherRoomId:your-domain" +repo_dir = "/Users/macpro/Projects/spark-control" +label = "spark-control" diff --git a/scripts/launch-claude.sh b/scripts/launch-claude.sh new file mode 100755 index 0000000..42bdb73 --- /dev/null +++ b/scripts/launch-claude.sh @@ -0,0 +1,24 @@ +#!/bin/zsh -l +# launch-claude.sh — the Mac-side environment seam for matrix-bridge (decision D4). +# +# Invoked over SSH by the bot: launch-claude.sh +# Runs as a LOGIN shell (-l) on purpose: a non-interactive SSH shell otherwise gets a +# minimal env that loads neither ~/.zprofile nor ~/.zshrc, so PATH/credentials are missing +# and `claude` isn't found. Keep ALL Mac-environment knowledge here, not in the bot. +# +# FIRST DRAFT — validate by hand in Phase 0 (see AGENTS.md "Current state") before any bot +# code calls it. Watch for the keychain/credential caveat on the very first remote launch. + +repo_dir="$1" +shift +prompt="$*" + +if [[ -z "$repo_dir" || -z "$prompt" ]]; then + print -u2 "usage: launch-claude.sh " + exit 2 +fi + +# Fail loud on a bad directory — never launch Claude in the wrong place. +cd "$repo_dir" || { print -u2 "launch-claude: no such repo dir: $repo_dir"; exit 1; } + +exec claude "$prompt"