docs: add community-organizer protocol spec
Defines the vocabulary, NIP-52 event shapes, NIP-72 community model, and lifecycle for a chat-captured + Nostr-stored community organizer spanning the `tracker` maubot plugin (forthcoming) and renderers like inky-impression. Reuses existing standards (RFC 5545 VTODO, NIP-52, NIP-72, ActivityStreams vocab) instead of inventing new event kinds, so other communities can adopt the same shape and renderers interop across implementations. Spec lands before any plugin code so the contract isn't an after-the-fact derivation from the implementation. CLAUDE.md + README now point at the spec as the source of truth for verb/event/tag changes — future sessions update the spec first, not the plugin code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28d4d19eba
commit
1f195fb36d
3 changed files with 685 additions and 4 deletions
36
CLAUDE.md
36
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 ../<plugin>.mbp ...`.
|
|||
- **Per-plugin docs:** `<plugin>/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 ../<plugin>.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.<name>` namespace.** Maubot keys plugins
|
||||
by this string in its DB and on disk (`/var/lib/maubot/plugins/
|
||||
dev.aiolabs.<name>-v<ver>.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
|
||||
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -11,6 +11,17 @@ castle hosts; the actual plugin code lives here.
|
|||
|---|---|
|
||||
| [`journal/`](./journal/) | Farm-journal bot. `!journal <text>` 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
|
||||
|
|
|
|||
642
docs/community-organizer-spec.md
Normal file
642
docs/community-organizer-spec.md
Normal file
|
|
@ -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 <text>` | Frictionless inbox capture; classifier sorts later | 31922 with `["t", "unclassified"]` initially; tag mutated on classification |
|
||||
| `!task <text> [#tag…]` | Record an actionable task | 31922 (date-based) or 31923 (timed) with `["t", "task"]` + user tags |
|
||||
| `!journal <text>` | Past-tense log entry; append-only; never "done" | 31922 with `["t", "journal"]` |
|
||||
| `!remind <when> <text>` | Time-bound prompt that fires a chat ping at `due_at` | 31923 with `["t", "remind"]` + `start` tag |
|
||||
| `!done <id-or-recent>` | 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", "<tag>"]` 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": "<bot-pubkey>",
|
||||
"created_at": 1716559200,
|
||||
"tags": [
|
||||
["d", "<unique-identifier>"],
|
||||
["title", "<one-line summary, truncated from body>"],
|
||||
["t", "task"],
|
||||
["t", "buy"],
|
||||
["t", "src:shortcut"],
|
||||
["a", "34550:<community-pubkey>:<community-d-tag>"],
|
||||
["client", "<bot-name>", "<bot-version>"],
|
||||
["author", "<originating chat handle, e.g. @alice:matrix.example>"]
|
||||
],
|
||||
"content": "door handles from Laura",
|
||||
"id": "<computed>",
|
||||
"sig": "<computed>"
|
||||
}
|
||||
```
|
||||
|
||||
Required tags:
|
||||
- `d` — replaceable-event identifier; unique within `(kind, pubkey)`.
|
||||
Recommend `<community-d>:<random>` 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", "<unix-timestamp-seconds>"],
|
||||
["end", "<unix-timestamp-seconds>"] // 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", "<unique-identifier>"],
|
||||
["a", "31922:<task-pubkey>:<task-d-tag>"],
|
||||
["status", "accepted"],
|
||||
["a", "34550:<community-pubkey>:<community-d-tag>"]
|
||||
],
|
||||
"content": "<optional completion note>"
|
||||
}
|
||||
```
|
||||
|
||||
`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:<community-pubkey>:<community-d-tag>"
|
||||
```
|
||||
|
||||
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": "<community-owner-pubkey>",
|
||||
"tags": [
|
||||
["d", "<short-identifier, e.g. cdf-animals>"],
|
||||
["name", "Château du Faune — Animals"],
|
||||
["description", "Daily chores and infra for alpacas, hens, ducks, LGDs"],
|
||||
["image", "<optional URL>"],
|
||||
["moderator", "<pubkey>", "<relay-url>"],
|
||||
["relay", "<wss://...>", "author"],
|
||||
["relay", "<wss://...>", "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:<rules|llm|manual|...>` 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": "<bot-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:<owner-pk>: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 = <tomorrow 9am unix ts>`,
|
||||
`["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:<custom>` 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue