diff --git a/CLAUDE.md b/CLAUDE.md index 6f309ce..94f24c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,6 @@ -# CLAUDE.md — maubot-plugins +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What this repo is @@ -17,6 +19,14 @@ their source dir after `zip -j ../.mbp ...`. - **Per-plugin docs:** `/README.md` covers commands, schema, quirks, etc. - **Repo-wide build/upload flow:** root `README.md`. +- **Community-organizer protocol spec:** `docs/community-organizer-spec.md` + defines the vocabulary, event shapes (NIP-52 + NIP-72), lifecycle + states, and tag conventions shared across the `tracker` plugin, the + `inky-impression` renderer, and any future surface (webapp form, CLI, + etc.). **Read this before changing any verb behavior, tag shape, or + event structure** — it's the contract other implementations (and + other communities) build against. Don't redesign these in plugin code; + update the spec first. - **Maubot patterns and footguns:** `~/dev/CLAUDE.md` under "Maubot plugin development" — covers `database_type` semantics, `@command.new` vs `@command.passive`, multi-line caveats, etc. @@ -46,9 +56,27 @@ zip -j ../.mbp maubot.yaml *.py ``` For brand-new plugins, also create the bot's Matrix account first -(via Continuwuity registration token), add it as a Client in the -maubot UI, then create an Instance binding the plugin to that -client. +(via Continuwuity registration token from the admin room — `!admin +token issue --once`, then register through Element), add it as a +Client in the maubot UI, then create an Instance binding the plugin +to that client. Existing example: `@journalbot:ariege.io` for +`journal/`. + +### `maubot.yaml` conventions for new plugins + +- **`id`: use the `dev.aiolabs.` namespace.** Maubot keys plugins + by this string in its DB and on disk (`/var/lib/maubot/plugins/ + dev.aiolabs.-v.mbp`), so it must be globally unique across + every maubot ecosystem — reverse-DNS is the convention (cf. + `xyz.maubot.reminder`). Reserving `dev.aiolabs.*` for our plugins + keeps ids predictable and rename-safe. Changing the id later is a + fork, not a rename: every existing instance gets orphaned. +- **`database_type:` if you need storage → `asyncpg`** (or `sqlalchemy` + for legacy code). That field names the API style, NOT the storage + backend. `sqlite` or `postgres` there fails at instance start with + `RuntimeError: Unrecognized database type ...` — the storage backend + is chosen at the daemon level via `plugin_databases.{sqlite,postgres}` + in the maubot config and is independent of what the plugin declares. ## Commits diff --git a/README.md b/README.md index 5b7c973..bf869d1 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,17 @@ castle hosts; the actual plugin code lives here. |---|---| | [`journal/`](./journal/) | Farm-journal bot. `!journal ` records what you did, scoped per-user/room/timestamp. `!journal show [@user]` and `!journal today` query back. | +## Community Organizer protocol + +`docs/community-organizer-spec.md` defines the protocol the plugins in +this repo (and companion renderers like +[`inky-impression`](https://git.atitlan.io/aiolabs/inky-impression)) use +to coordinate community life — tasks, journals, reminders, shopping +lists — over Matrix capture + Nostr storage. Designed to be adopted by +other communities; reuses NIP-52 + NIP-72 instead of inventing new +event kinds. Read it before changing verb behavior or event shapes in +any plugin. + ## Building a plugin A `.mbp` is just a zip containing `maubot.yaml` + the plugin's Python diff --git a/docs/community-organizer-spec.md b/docs/community-organizer-spec.md new file mode 100644 index 0000000..4cc1fe3 --- /dev/null +++ b/docs/community-organizer-spec.md @@ -0,0 +1,642 @@ +# Community Organizer Spec + +**Status:** Draft 0.1 +**Authors:** aiolabs / Château du Faune (Ariège, France) +**License:** CC0 — adopt, fork, remix freely + +A protocol for coordinating the day-to-day operations of a physical +community — tasks, reminders, shopping lists, journals, infrastructure +backlog — over chat (capture) and an open event store (canonical +record), so the same data can drive multiple display surfaces (eink +panels in shared spaces, web dashboards, mobile, etc.). + +This document is the contract. Implementations are interchangeable: a +Matrix maubot plugin, a Discord bot, a CLI, a web form, an SMS gateway +— all can produce the same events and consume from the same store. + +--- + +## 0. Why a spec, not just an app + +Every community organizing itself eventually invents some flavor of +"the list of stuff we need to do, the log of what we did, the things +we need to buy". Tools rot, maintainers move on, the data gets stuck +in proprietary stores. Communities re-implement the same wheel. + +This spec puts the *data* on open protocols (Nostr, NIP-52, NIP-72) so +that: + +- Multiple input surfaces can write to the same record +- Multiple output surfaces (displays, dashboards, mobile) can consume + without coordination +- A community's organizational memory survives any single tool +- Other communities can adopt the same shape and benefit from + interoperable implementations +- Self-hosting is achievable end-to-end — no SaaS lock-in + +The spec deliberately reuses existing standards (RFC 5545 VTODO, +NIP-52, NIP-72, ActivityStreams vocabulary) rather than inventing new +ones. If we need something a NIP doesn't cover, we lean on tag +conventions before proposing a new kind. + +--- + +## 1. Prior art + +A community-organizer-via-chat-bot isn't new territory — there's a +lot to pull from rather than reinvent. Each item below names what +we're borrowing and why it maps to our context. + +1. **GTD (Getting Things Done, Allen, 2001)** — the + capture/clarify/organize/reflect/engage workflow. The observation + that *"clean the wound on the dog should be a task, not a journal + entry"* is literally the GTD "clarify" step. Capture is + frictionless and unbucketed; clarification happens on a cadence. + This is the conceptual backbone for an `!add`-style capture surface + that later sorts into typed buckets (rules → LLM → human override). + +2. **Bullet Journal (Carroll)** — daily log + signifiers (•, ○, ☐) + + "migration" (moving incomplete items to the next page). Maps + cleanly to a `!journal` daily-log + a weekly review that migrates + open items into tasks. The persistence-across-time framing ("new + person arriving can reference") is exactly Bullet Journal's + migration archive. + +3. **iCalendar VTODO (RFC 5545)** — battle-tested TODO data model: + `STATUS` (NEEDS-ACTION / IN-PROCESS / COMPLETED / CANCELLED), + `PRIORITY`, `DUE`, `COMPLETED`, `CATEGORIES`. NIP-52 is loosely + modeled on iCalendar; aligning our task shape to VTODO gives us + interop with the entire CalDAV ecosystem essentially for free. + +4. **ActivityStreams 2.0 / ActivityPub** — + Actor-Verb-Object-Context vocabulary. The semantic primitive is + "actor did verb on object in context", which is what every + `!command` is. Established vocab. + +5. **MagicMirror² + Home Assistant Lovelace** — proven *modular + display* pattern: data sources feed a render graph, plugins/cards + subscribe. The lesson for output surfaces: don't bake a layout, + expose a scene/card plugin API. + +6. **CalDAV/CardDAV** — the architectural lesson: *standardized data + on a shared store enables a plurality of input/output clients*. + The store and schema are the contract; clients are + interchangeable. + +7. **Linear's Project / Cycle / Issue triad + Loomio's + proposal/decision flow** — modern open-source patterns for triage + and decision-making at the community level. Linear's status + lifecycle (Triage → Backlog → Todo → In Progress → Done → + Canceled) is borrowable. + +8. **Logseq / Obsidian daily-notes** — capture appended to a daily + log, structured later via block refs/tags. Validates the "log + first, structure second" posture and shows how tags become the + organizational graph. + +9. **maubot/reminder, maubot/rss** — Matrix-side prior art for + command shape, recurring schedules, room-scoped subscriptions. + Worth reading before writing handlers. + +10. **NIP-52 + NIP-72 (Nostr)** — Nostr-native primitives for + calendar events and community scoping. Reusing these instead of + inventing new kinds is what makes the protocol portable to anyone + with a Nostr relay. + +The generalization insight: *the bot is one reference implementation +of a protocol-level contract*. The contract (event shapes + community +model + vocabulary) is what other groups adopt; their implementations +can be different bots, different UIs, different displays. + +--- + +## 2. Roles + +| Role | Description | +|---|---| +| **Member** | A human in the community. Identified by chat handle (e.g. MXID for Matrix). May or may not have their own Nostr identity. | +| **Bot** | An automated process holding one Nostr keypair, capturing input from a chat surface and writing to the relay. The bot acts as a *trusted publisher* on behalf of members in a single trust boundary (one chat room = one community). | +| **Community** | A trust boundary. Defined by a NIP-72 community definition event (kind 34550). Maps 1:1 to a chat room. Members of the room can record, edit, query within the community. | +| **Relay** | A Nostr relay (or set of relays) holding the canonical event log. May be public, community-private (auth-gated), or both. | +| **Renderer** | Any output surface that subscribes to the relay and displays events: eink panel, web dashboard, mobile push, voice assistant, etc. | + +--- + +## 3. Vocabulary + +### 3.1 Universal verbs + +Every conformant implementation MUST recognize these verbs. Verbs are +the *surface form* — the underlying event is the same regardless of +whether the user typed `!task #buy milk` or `!buy milk` (via a +configured shortcut) or `!add we need milk` (LLM-classified into +`task #buy`). + +| Verb | Purpose | Underlying kind | +|---|---|---| +| `!add ` | Frictionless inbox capture; classifier sorts later | 31922 with `["t", "unclassified"]` initially; tag mutated on classification | +| `!task [#tag…]` | Record an actionable task | 31922 (date-based) or 31923 (timed) with `["t", "task"]` + user tags | +| `!journal ` | Past-tense log entry; append-only; never "done" | 31922 with `["t", "journal"]` | +| `!remind ` | Time-bound prompt that fires a chat ping at `due_at` | 31923 with `["t", "remind"]` + `start` tag | +| `!done ` | Close a task | 31925 status event with `accepted` / equivalent | +| `!list [type] [#tag…]` | Query recent items in current community | read-only | +| `!setup` | Per-community configuration (verb shortcuts, publish posture, etc.) | writes to community config (see §6) | + +### 3.2 Lifecycle states + +Aligned with RFC 5545 VTODO `STATUS` values: + +| State | VTODO equivalent | Notes | +|---|---|---| +| `unclassified` | NEEDS-ACTION | Inbox; awaiting classification | +| `open` | NEEDS-ACTION | Classified, not started | +| `in_progress` | IN-PROCESS | Someone's actively working it | +| `done` | COMPLETED | Closed via `!done` | +| `canceled` | CANCELLED | Closed without completion | + +Journal entries do NOT participate in this lifecycle — they're +append-only. `!done` on a journal entry is undefined behavior; +implementations SHOULD reject it. + +### 3.3 Tag conventions + +NIP-52 events use `["t", ""]` tags for free-form categorization. +This spec uses: + +- A **kind tag** (exactly one): `task`, `journal`, `remind`, + `unclassified` — identifies the entry's role in the lifecycle. +- **Domain tags** (zero or more): `buy`, `steward`, `kitchen`, + `animals`, `harvest`, etc. — community-specific categories. +- A **priority tag** (zero or one): `priority:low`, `priority:high`. +- A **classification-source tag** (exactly one): `src:explicit`, + `src:shortcut`, `src:rules`, `src:llm`, `src:manual`. Records how + the entry was bucketed; useful for audit and for tuning the + classifier. + +Example complete tag set for a `!buy door handles` typed via the +`buy` shortcut configured for the room: + +``` +["t", "task"] +["t", "buy"] +["t", "src:shortcut"] +``` + +### 3.4 Per-community shortcuts + +Communities declare their own surface verbs that expand to a +universal verb + tags. Example (Ariège): + +```yaml +verbs: + buy: { kind: task, tags: [buy] } + steward: { kind: task, tags: [steward, priority:low] } + kitchen: { kind: task, tags: [kitchen] } +``` + +A bakery co-op's config might declare `harvest`, `market`, `brewery` +instead. **The spec mandates the universal verbs; per-community +shortcuts are configuration, not protocol.** This means a renderer +written for one community works for another without code changes — +it just filters on different tags. + +--- + +## 4. Event shapes + +All events follow NIP-52 (calendar events) and are scoped to a +community via NIP-72 `a`-tag. + +### 4.1 Task / journal / unclassified entry (kind 31922) + +Date-based event (no specific time). Used for items where the +capture moment matters more than a scheduled time. + +```json +{ + "kind": 31922, + "pubkey": "", + "created_at": 1716559200, + "tags": [ + ["d", ""], + ["title", ""], + ["t", "task"], + ["t", "buy"], + ["t", "src:shortcut"], + ["a", "34550::"], + ["client", "", ""], + ["author", ""] + ], + "content": "door handles from Laura", + "id": "", + "sig": "" +} +``` + +Required tags: +- `d` — replaceable-event identifier; unique within `(kind, pubkey)`. + Recommend `:` or a ULID. +- `title` — short summary for renderers; ≤80 chars. +- exactly one kind tag from §3.3 +- `a` — NIP-72 community reference (see §5). +- `author` — original chat handle for attribution. The bot signs as + itself, but human accountability needs to be visible. + +Optional tags: +- Domain tags, priority tags, classification-source tag (§3.3) +- `client` — bot identifier + version +- `e` — references to related events (e.g. a journal entry that + spawned a follow-up task) + +### 4.2 Timed entry (kind 31923) + +Used for `!remind` and any task with a specific time. Adds: + +```json +"tags": [ + ... + ["start", ""], + ["end", ""] // optional +] +``` + +If only `start` is present, the entry is a point-in-time prompt. + +### 4.3 Status / done (kind 31925) + +NIP-52 RSVP event. Used by `!done` to close a task: + +```json +{ + "kind": 31925, + "tags": [ + ["d", ""], + ["a", "31922::"], + ["status", "accepted"], + ["a", "34550::"] + ], + "content": "" +} +``` + +`status` values per NIP-52: `accepted` (we map this to `done`), +`tentative` (we map to `in_progress`), `declined` (we map to +`canceled`). + +### 4.4 Deletions (kind 5) + +Standard NIP-09 deletion event. Used to retract an entry recorded in +error. Renderers MUST honor deletions. + +--- + +## 5. Community model (NIP-72) + +Every community is identified by a **kind 34550 community definition +event** published by the community's founder (or the bot on their +behalf at `!setup` time). The community is referenced by an `a`-tag: + +``` +a = "34550::" +``` + +All entries scoped to a community MUST include this `a`-tag. +Renderers filter on it to surface the right community's data. + +### 5.1 Community definition event + +```json +{ + "kind": 34550, + "pubkey": "", + "tags": [ + ["d", ""], + ["name", "Château du Faune — Animals"], + ["description", "Daily chores and infra for alpacas, hens, ducks, LGDs"], + ["image", ""], + ["moderator", "", ""], + ["relay", "", "author"], + ["relay", "", "requests"] + ], + "content": "" +} +``` + +### 5.2 Chat room ↔ community mapping + +A conformant bot SHOULD maintain a 1:1 mapping from chat room → NIP-72 +community. The room IS the trust boundary — anyone in the room can +record/edit/close in the community. + +Lazy creation is permitted: the first `!setup` (or first capture +event) in a room MAY create the community definition. + +### 5.3 Publish posture + +Each community declares its publish posture (configurable via +`!setup`): + +| Posture | Behavior | +|---|---| +| `matrix-only` | Bot records to local cache; does NOT publish to any relay. Use for sensitive rooms. | +| `community-relay` | Bot publishes to the community's own relay (typically authenticated, member-only). | +| `public-relays` | Bot publishes to all configured public relays. Use for outward-facing rooms (events, marketplace). | + +The posture is a hint to the publisher — the spec does not mandate +relay authentication; that's the relay operator's choice. + +--- + +## 6. Inbox and classification + +The `!add` verb supports frictionless capture for low-literacy users +who shouldn't have to remember verb taxonomy. Entries land +immediately as `kind: unclassified` and progress through: + +``` +captured (unclassified) → rules classifier → [classified] OR [inbox] + ↓ + LLM classifier (optional) → [classified] + ↓ + manual review → [classified] +``` + +**Classification is never a gatekeeper.** The entry is always +recorded immediately. Classification mutates tags asynchronously and +is reversible by: +- a chat reaction on the bot's ack message (UI-defined) +- the `!sort` or `!reclassify` verb (implementation-defined) +- direct edit by a moderator + +The classification source MUST be recorded in the +`src:` tag. This preserves audit trail and +enables classifier tuning. + +### 6.1 Conformance levels for classifiers + +- **Level 0** — no classifier. Entries captured via `!add` stay + `unclassified` forever until human-sorted. Acceptable for small + communities. +- **Level 1** — rules-based. Keyword/regex matchers cover the + obvious cases (e.g. "buy/purchase/order" → `task #buy`; past-tense + verbs → `journal`). Transparent, deterministic, no external + dependency. +- **Level 2** — LLM fallback. Anything the rules don't catch is + classified by a small local model (or external API). Recommended: + local-first to preserve the self-hostable property. + +--- + +## 7. Permission model + +### 7.1 v1 (trust-the-bot) + +- The bot owns one Nostr keypair. +- All events for the community are signed by the bot. +- Human attribution is carried in the `author` tag (originating chat + handle). +- Trust derives from chat-room membership: if you're in the room, + you can record/edit/close in the community. +- Relay operators MAY auth-gate (require NIP-42) to prevent + community-scoped writes from non-members. Out of band coordination. + +### 7.2 Future (bunker / remote signers) + +Future spec versions will support per-user signing via NIP-46 +(remote signing) so that events are attributable to individual users' +Nostr identities, not the bot. The chat-room → community mapping +stays the same; only the signer changes. + +Implementations SHOULD design their signing layer with this +forward-compatibility in mind (i.e. don't hardcode "the bot is +always the signer" deep in the data model). + +--- + +## 8. Worked examples + +### 8.1 Capture: explicit verb + tag + +User types in Matrix room `#animals:matrix.example`: + +``` +!task #buy goat dewormer from the co-op +``` + +Bot publishes: + +```json +{ + "kind": 31922, + "pubkey": "", + "created_at": 1716559200, + "tags": [ + ["d", "cdf-animals:01HXXJ8K7VP"], + ["title", "goat dewormer from the co-op"], + ["t", "task"], + ["t", "buy"], + ["t", "src:explicit"], + ["a", "34550::cdf-animals"], + ["author", "@alice:matrix.example"] + ], + "content": "goat dewormer from the co-op" +} +``` + +Bot replies: `✅ Task recorded (#buy). React with ❌ to delete.` + +### 8.2 Capture: community shortcut + +Same room has `!buy` configured as a shortcut. User types: + +``` +!buy door handles from Laura +``` + +Bot expands to `!task #buy door handles from Laura` and publishes +the same shape as §8.1 but with `["t", "src:shortcut"]`. + +### 8.3 Capture: freeform with rules classification + +User types: + +``` +!add need to roll out flypaper for the stable +``` + +Bot writes immediately as `unclassified`, classifier sees "need to" +and "flypaper" (matches `buy` rule), updates tags within 100ms: + +```json +"tags": [ + ["t", "task"], + ["t", "buy"], + ["t", "src:rules"], + ... +] +``` + +Bot edits its ack: `📥 Logged as a #buy task. React with 🔄 to +re-classify.` + +### 8.4 Capture: freeform falling through to LLM (Level 2) + +User types: + +``` +!add the duck babies straw needs scraping clean tomorrow +``` + +Rules don't match cleanly (no buy keyword, future-tense not past). +Bot writes as `unclassified`; LLM classifier (running async on a +local Ollama model) returns `task #animals` within 2–5s. Bot updates +tags, edits ack with the result. + +### 8.5 Close + +User reacts to the bot's original ack message with ✅, OR types: + +``` +!done 17 +``` + +Bot publishes a kind 31925 event referencing the task's `a`-tag with +`status: accepted`. + +### 8.6 Journal entry (past-tense) + +User types: + +``` +!journal +- opened, watered, and fed the hens, alpacas, and chicks +- put insulators on the rebars so they're ready for the fence +- mentally plotted potential hidden-in-plain-sight stable locations +``` + +Bot publishes a single kind 31922 with `["t", "journal"]`, +`content` containing the full multi-line body verbatim. + +### 8.7 Reminder + +User types: + +``` +!remind tomorrow 9am check sapphi's wound +``` + +Bot publishes kind 31923 with `start = `, +`["t", "remind"]`. At fire time, the bot posts a chat message +pinging the originating user. + +### 8.8 Query + +User types: + +``` +!list task #buy +``` + +Bot returns the most recent N open tasks in the current community +tagged `buy`. Implementation-defined formatting. + +### 8.9 Inbound: external publisher + +A community member uses the webapp (or any other client) to create +a NIP-52 event with the right community `a`-tag. The bot, which +subscribes to events tagged with its communities, mirrors it into +its local cache. The next `!list` includes the externally-created +entry. The eink renderer (also subscribed) shows it. + +--- + +## 9. Conformance + +An implementation is **conformant** if: + +1. It produces events matching §4 for all universal verbs in §3.1. +2. It scopes events with NIP-72 `a`-tags per §5. +3. It honors NIP-09 deletions (§4.4). +4. It records classification-source tags (§3.3). +5. It supports at least classifier Level 0 (§6.1). +6. It documents its surface vocabulary (universal verbs + any + per-community shortcuts) in a form usable by community members. + +Bots SHOULD additionally: + +- Subscribe to events tagged with their communities (not just + publish) — bidirectional sync makes the spec actually portable. +- Carry the `author` tag for human attribution (§4.1). +- Maintain a local cache for read latency and offline tolerance. + +Renderers SHOULD: + +- Filter by `a`-tag, not by `pubkey` (so events from any bot OR any + external publisher targeting the community are surfaced). +- Apply NIP-09 deletions before display. +- Surface a stale-data indicator when relay is unreachable. + +--- + +## 10. Extension points + +This spec is intentionally minimal. Communities and implementations +can extend without breaking conformance via: + +- **New domain tags** — add whatever `t` tags make sense locally. + Renderers ignore tags they don't recognize. +- **New shortcut verbs** — add via per-community config; never + modify universal verbs. +- **New classification sources** — `src:` is open. +- **Richer content formats** — `content` is freeform text in v1; + Markdown or other formats are permitted (signal via a `format` + tag) but renderers fall back to plain text. + +If you find yourself wanting a new event kind or a new universal +verb, please open an issue against this spec — protocol-level +additions need broader review. + +--- + +## 11. Open questions + +Not yet decided in this draft: + +- **Recurring reminders.** RFC 5545 has `RRULE`; NIP-52 doesn't + formalize recurrence yet. Need a tag convention or wait for an + upstream NIP. +- **Cross-community references.** A task in `#kitchen` that depends + on a buy entry in `#purchasing`. Needs convention for `e`-tags + across community boundaries. +- **Assignment.** Today `author` records who captured; we don't + formalize "who's assigned". Could use `p`-tag with a role + qualifier. +- **Migration / weekly review.** Bullet-journal-style migration of + stale open items into a new period. Needs a verb (`!migrate`?) + and a state transition spec. +- **External signer story.** NIP-46 bunker integration concrete + shape. + +Contributions welcome on any of these. + +--- + +## 12. Reference implementation + +The aiolabs reference implementation lives at +`git.atitlan.io/aiolabs/maubot-plugins/` (the `tracker/` plugin) and +serves the Château du Faune community at Ariège, France. It targets +Matrix as the chat surface and uses NIP-52 + NIP-72 over a +self-hosted Nostr relay. + +A companion eink renderer at +`git.atitlan.io/aiolabs/inky-impression/` consumes the same events +to drive a 13.3" panel in the community's shared foyer. + +Both serve as worked examples of the spec; neither is mandatory. + +--- + +## Changelog + +- **0.1** (2026-05-24) — initial draft.