Add capture mode: /capture + per-room capture threads → cross-project inbox

A capture-thread message (or /capture <text> in any room) logs to standards/
INBOX.md via capture-note.sh — deterministic, no claude call — and confirms
in-thread; /triage stays the gate into each repo (D13). Thread roots seeded by
seed-capture-threads.py.
This commit is contained in:
Keysat
2026-06-16 14:59:38 -05:00
parent 4204b82c6b
commit 2bae2f3571
5 changed files with 310 additions and 21 deletions
+45 -5
View File
@@ -23,8 +23,10 @@ Matrix message in a project room
```
Room determines the repo; the message text becomes the initial prompt — the v1 trigger surface.
*Variant:* a `?`-prefixed message instead runs `ask-claude.sh` (headless `claude -p`) and posts
the full answer back into the room (ask mode, D12).
*Variants:* a `?`-prefixed message instead runs `ask-claude.sh` (headless `claude -p`) and posts
the full answer back into the room (ask mode, D12). A message in a room's **capture thread** (or a
`/capture <text>` message in any room) is logged to the cross-project inbox instead of launching —
deterministic, no Claude call (capture mode, D13).
## Stack
@@ -61,6 +63,10 @@ the full answer back into the room (ask mode, D12).
- **Bot — venv (dev/fallback):** `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`,
then `.venv/bin/python src/bot.py` — uses modelo's host `~/.ssh/config` for the alias.
`MB_SSH_ALIAS` overrides the SSH target for testing.
- **Seed capture threads:** `python3 scripts/seed-capture-threads.py` (reads `.env` + `config.toml`,
needs only Python stdlib; run anywhere the homeserver is reachable). Posts each room's capture-thread
root and prints the `capture_thread` event IDs to paste into `config.toml`. Skips rooms already set;
pass labels or `--force` to reseed, `--dry-run` to preview.
- **Deploy:** the Spark's `~/matrix-bridge` is a Gitea clone tracking `master`, so deploy =
`git fetch origin && git reset --hard origin/master && docker compose up -d --build` (run as
`modelo` from `~/matrix-bridge`). You normally don't run this by hand — the **Update** button on
@@ -86,9 +92,17 @@ the full answer back into the room (ask mode, D12).
- `scripts/ask-claude.sh` — headless `?`-ask wrapper (`#!/bin/zsh -l`): runs `claude -p` in the repo
and prints the answer to stdout for the bot to capture and post back. Uses `CLAUDE_CODE_OAUTH_TOKEN`
(Mac-side `.env`) because a non-GUI SSH session can't reach the login Keychain (D12).
- `scripts/capture-note.sh` — capture wrapper (`#!/bin/zsh -l`): appends one `/capture`-format line
to `~/Projects/standards/INBOX.md`, commits, best-effort pushes, and echoes the line back.
Deterministic — no `claude`, no token, no frontier call (D13).
- `scripts/seed-capture-threads.py` — one-time (re-runnable) helper that posts each room's
capture-thread root message and prints the resulting `capture_thread` event IDs to paste into
`config.toml`. Skips rooms already configured; run after adding a project.
- `src/bot.py` — the matrix-nio bot (Phase 1): listens in mapped rooms; a plain message runs
`ssh mac-bridge gui-launch.sh` (interactive, to the phone), a `?`-prefixed message runs
`ask-claude.sh` (headless, answer posted back); fans out for all-projects; reports failures back.
`ask-claude.sh` (headless, answer posted back), and a `/capture`/capture-thread message runs
`capture-note.sh` (logs to the inbox, confirms in-thread); fans out for all-projects; reports
failures back.
- `requirements.txt` (matrix-nio) · `.env.example` (credential schema; real `.env` gitignored).
- `.claude/` — Claude wiring (dir only for now).
- `Dockerfile` · `docker-compose.yml` · `docker-entrypoint.sh` · `.dockerignore` — the Phase 1
@@ -148,6 +162,18 @@ Condensed from the scoping workshop. Each: the call, why, what it beat.
`claude setup-token` (`CLAUDE_CODE_OAUTH_TOKEN`) that D11 deferred — kept **Mac-side only** (in
`.env`; the Spark never runs claude). Interactive launches keep the token-free GUI-Terminal path.
*Sovereignty unchanged:* `claude -p` uses the subscription, no frontier API touches message payloads.
- **D13 — Capture mode → central inbox + `/triage` gate, via a deterministic script (2026-06-16).**
A message in a room's **capture thread** (detected by its `m.relates_to` thread root, configured
per room as `capture_thread`), or a `/capture <text>` message in any room, is logged to
`~/Projects/standards/INBOX.md` tagged for that room's project — then the existing `/triage`
lands it in the repo. *Beat (deliberately rejected):* writing straight into a repo's
`AGENTS.md`/`ROADMAP.md` unattended — keeps the human approval gate, and the Current-state-vs-
ROADMAP call, where they belong (and AGENTS.md is load-bearing — "propose, don't silently
rewrite"). *Beat:* `claude -p /capture` for the write — a one-line append needs no model, so
`capture-note.sh` does it deterministically: no token, nothing leaves the Mac but the git push,
and message text never reaches a frontier model (upholds the sovereignty constraint / D8). The
bot confirms in-thread with the exact inbox line; `/capture` is the zero-config path, the thread
just drops the prefix. Thread roots are minted by `seed-capture-threads.py`.
## Sovereignty constraint
@@ -210,12 +236,26 @@ once" is not done.
- **Phase 3 (Spark Control) shipped 2026-06-16 in v0.21.0:** status badge + Update / Restart /
Stop-Start / Logs tile; the Spark's dir is now a Gitea clone and deploy = the Update button.
Detail in ROADMAP + `docs/spark-control-integration.md`; no matrix-bridge code change.
- **No active build work.** Next moves are all optional / triggered:
- **Capture mode (D13) built 2026-06-16 — pending deploy, not yet N=3.** `/capture <text>` in any
room and per-room capture threads both log to `standards/INBOX.md` via `capture-note.sh`; bot
confirms in-thread. All 11 rooms + all-projects already have their `capture_thread` roots seeded
and their IDs are in the Mac's `config.toml`. **To go live:** (1) push code + `git`-deploy on the
Spark (Update button); (2) refresh the Spark's `config.toml` — it's gitignored, so the Update
button does *not* carry it: on the Spark run
`scp mac-bridge:/Users/macpro/Projects/matrix-bridge/config.toml ~/matrix-bridge/config.toml`
(or hand-add `capture_launcher` + the `capture_thread` lines), then Restart. Until the Spark has
both the new code *and* config, **don't reply in the capture threads** (the old bot would treat a
thread reply as a launch).
- **Optional / triggered next moves:**
- Badge reflects container liveness only, not Synapse connectivity — add a Docker `HEALTHCHECK`
(bot-side liveness signal → read `{{.State.Health.Status}}`) when "running but silent" bites.
- A `?`-ask in a repo `claude` has never opened may stall on the folder-trust gate — add a trust
flag to `ask-claude.sh` if/when hit, not preemptively.
- Capture defaults every line to `[idea][P2]`; add inline `[type]`/`[Pn]` parsing to
`capture-note.sh` if reclassifying at `/triage` gets tedious.
- Phase 4+ (intent-routing brain D8, thread continuity) — see ROADMAP; not scoped.
- **Watch:** the Update button depends on modelo's Gitea ssh-config pin (`IdentitiesOnly yes`, see
Infra facts) — flag it if that account is ever rebuilt.
- **Repo:** `master` == `phase-1`, clean, pushed to Gitea. No test suite (pre-existing).
- **Repo:** `master` == `phase-1`. Uncommitted: capture mode (`src/bot.py`, `scripts/capture-note.sh`,
`scripts/seed-capture-threads.py`, `config.example.toml`, `AGENTS.md`) + gitignored `config.toml`.
No test suite (pre-existing).