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).
This commit is contained in:
+27
@@ -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
|
||||||
@@ -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 <repo_dir> <prompt>` — 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.
|
||||||
@@ -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 <repo_dir> <message>
|
||||||
|
→ 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.
|
||||||
+47
@@ -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.
|
||||||
@@ -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://<your-synapse-host>" # existing StartOS Synapse, reached over WireGuard/LAN
|
||||||
|
user = "@matrix-bridge-bot:<your-domain>" # 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"
|
||||||
Executable
+24
@@ -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 <repo_dir> <prompt...>
|
||||||
|
# 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 <repo_dir> <prompt>"
|
||||||
|
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"
|
||||||
Reference in New Issue
Block a user