Sibling to journal/, implements the Community Organizer spec (docs/community-organizer-spec.md) over maubot: !add <text> freeform inbox capture; rules classify !task <text> [#tag…] explicit task !sidequest [#tag…] optional / passion-project item !remind in <N>(s|m|h|d) <text> chat-side timed reminder !done <id> close task or sidequest !list [type] query open items !setup add/remove per-room shortcut config One @command.passive dispatcher routes universal verbs to handlers and unknown verbs through the per-room shortcut table. Avoids the multi-line @command.new footgun (per ~/dev/CLAUDE.md) and lets shortcuts coexist with universal verbs without decorator priority games. Rules classifier (classify.py) is intentionally conservative — only buckets on clear shapes (buy keywords, past-tense markers, todo intent, remind prefixes); ambiguous capture lands in `!list inbox`. LLM fallback is Phase 2b on a dedicated inference node. Reminders (scheduler.py) replay from DB on bot start so restarts don't lose pending timers; missed-while-down stay open for query. `nostr_event_id` column reserved for Phase 2a — Matrix-local only for now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
176 lines
6.9 KiB
Markdown
176 lines
6.9 KiB
Markdown
# tracker
|
|
|
|
Community-organizer bot for Matrix. Implements the
|
|
[Community Organizer spec](../docs/community-organizer-spec.md) over
|
|
maubot — capture/list/close tasks, sidequests, reminders, and freeform
|
|
inbox entries scoped per room.
|
|
|
|
Sibling to [`journal/`](../journal/) (which owns `!journal`); the two
|
|
work side-by-side on the same maubot instance if you want both.
|
|
|
|
## Quickstart
|
|
|
|
```
|
|
!add door handles from Laura # freeform, classifier sorts it
|
|
!task fix the south fence #urgent # explicit task
|
|
!sidequest #priority:3 build a chicken-tractor
|
|
!remind in 30m check the eggs # reminder; fires in chat at due time
|
|
!list # open items in this room
|
|
!list sidequest # only sidequests
|
|
!done 17 # close item 17
|
|
!setup # show room shortcuts
|
|
!setup add buy task #buy # define a !buy shortcut for this room
|
|
```
|
|
|
|
Multi-line input works either inline or after a newline:
|
|
|
|
```
|
|
!task
|
|
- fix the alpaca fence
|
|
- patch the duck pond liner
|
|
- replace the gate latch
|
|
```
|
|
|
|
## Commands
|
|
|
|
| Command | Purpose |
|
|
|---|---|
|
|
| `!add <text>` | Freeform capture. Rules classifier sorts into a kind (task/journal/remind/etc.) or leaves it in the inbox as `unclassified` for later sorting. |
|
|
| `!task <text> [#tag…]` | Direct task capture. Tags become NIP-52 `t` tags. |
|
|
| `!sidequest <text> [#tag…]` | Optional / passion-project item. Same lifecycle as a task but listed separately so the day-to-day list stays clean. |
|
|
| `!remind in <N>(s\|m\|h\|d) <text>` | Schedule a chat-side reminder. Fires by posting in the room at the due time. |
|
|
| `!done <id>` | Close a task or sidequest by id. |
|
|
| `!list [task\|sidequest\|remind\|inbox\|all]` | List open items in this room (last 20). Default is everything except journals. |
|
|
| `!setup` | Show current room shortcuts. |
|
|
| `!setup add <verb> <kind> [#tag…]` | Add a per-room shortcut (e.g. `!setup add buy task #buy`). |
|
|
| `!setup remove <verb>` | Drop a shortcut. |
|
|
|
|
Universal verbs are hardcoded; per-room shortcuts are stored in the
|
|
plugin DB and apply only to the room they're defined in. A bakery
|
|
co-op can define `!harvest` / `!market` in their rooms; Ariège can
|
|
define `!buy` / `!steward` / `!chores` in theirs. Same plugin code.
|
|
|
|
## Priority
|
|
|
|
Items can carry a priority tag (`#priority:1` through `#priority:5`)
|
|
per the spec's 5-level scale: urgent / crucial / important / future /
|
|
frequent-ongoing. The bot surfaces the label in `!list` output;
|
|
renderers (eink panel, etc.) decide how to use it for sorting and
|
|
visual emphasis.
|
|
|
|
The most ergonomic capture is via room shortcuts:
|
|
|
|
```
|
|
!setup add urgent task #priority:1
|
|
!setup add chores task #priority:5
|
|
```
|
|
|
|
Then `!urgent the alpaca fence is down` records a priority-1 task.
|
|
|
|
## Classification
|
|
|
|
`!add` runs through a tiny rules engine (`classify.py`):
|
|
|
|
| Pattern | Bucket |
|
|
|---|---|
|
|
| `remind me to…` / `don't forget…` / `remember to…` | `remind` (no auto-time; you'll need to re-capture with `!remind in <N>…` to set a fire-time) |
|
|
| `buy` / `purchase` / `order` / `pick up` / `grab` / `get more` | `task #buy` |
|
|
| past-tense verbs (`did`, `finished`, `cleaned`, `watered`, `fed`, …) | `journal` |
|
|
| `need to` / `should` / `have to` / `must` / `todo` | `task` |
|
|
| anything else | `unclassified` (sits in `!list inbox` until sorted) |
|
|
|
|
This is intentionally **conservative** — better to leave something
|
|
unclassified than to mis-bucket it. The Phase 2b plan is an LLM
|
|
fallback on a dedicated inference node that handles the long tail.
|
|
|
|
## Storage
|
|
|
|
One SQLite DB per maubot instance, schema managed by
|
|
`mautrix.util.async_db.UpgradeTable`. Two tables:
|
|
|
|
```sql
|
|
-- The items log
|
|
CREATE TABLE items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
kind TEXT NOT NULL, -- task | sidequest | journal | remind | unclassified
|
|
status TEXT NOT NULL, -- open | done | canceled
|
|
tags TEXT NOT NULL DEFAULT '', -- comma-separated; e.g. "buy,priority:2"
|
|
room TEXT NOT NULL,
|
|
user TEXT NOT NULL, -- originating MXID
|
|
ts BIGINT NOT NULL, -- captured at (ms since epoch)
|
|
due_at BIGINT, -- for reminders/timed tasks
|
|
body TEXT NOT NULL,
|
|
classification_source TEXT, -- explicit | shortcut | rules | llm | manual
|
|
nostr_event_id TEXT -- populated in Phase 2 (Nostr bridge)
|
|
);
|
|
|
|
-- Per-room shortcut config + (future) publish posture
|
|
CREATE TABLE community_config (
|
|
room TEXT PRIMARY KEY,
|
|
config_json TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Wipe via the maubot UI's per-instance **Database** tab:
|
|
|
|
```sql
|
|
DELETE FROM items;
|
|
DELETE FROM community_config;
|
|
DELETE FROM sqlite_sequence WHERE name = 'items';
|
|
```
|
|
|
|
## Reminders survive restarts
|
|
|
|
Pending reminders are reloaded from the DB when the bot starts —
|
|
nothing in flight when maubot restarts, but anything scheduled for
|
|
the future re-arms. Reminders whose due time elapsed while the bot
|
|
was down stay open (they aren't auto-fired late); query with
|
|
`!list remind` to find them.
|
|
|
|
## Known quirks
|
|
|
|
- **Edited messages don't re-trigger the bot.** Matrix sends edits
|
|
as a separate `m.replace` event that maubot doesn't pass to
|
|
handlers. Type a fresh message instead of editing.
|
|
- **Time parsing is minimal in v1.** Only `in <N>(s|m|h|d)` works.
|
|
No "tomorrow 9am", no calendar dates. Use Phase 2b's LLM layer or
|
|
upstream `dateparser` for richer parsing later.
|
|
- **`!done` accepts only the id.** Closing by partial text match
|
|
(e.g. `!done flypaper`) isn't implemented in v1.
|
|
- **Phase 1 is Matrix-local.** Nothing publishes to Nostr yet — the
|
|
`nostr_event_id` column exists but is always NULL until Phase 2a
|
|
lands the bridge.
|
|
- **Universal verbs win over shortcuts.** You can't define a shortcut
|
|
named `task`, `add`, etc. — those are protocol-level.
|
|
|
|
## Build + iterate
|
|
|
|
```sh
|
|
cd ~/dev/maubot-plugins/tracker
|
|
# bump version in maubot.yaml so the UI surfaces the new build
|
|
zip -j ../tracker.mbp maubot.yaml *.py
|
|
```
|
|
|
|
Upload via maubot UI → Plugins → click existing → upload new `.mbp`.
|
|
**Hit Save on the affected instance** after upload — toggling
|
|
Enabled alone doesn't persist (easy facepalm). Create a fresh bot
|
|
client (e.g. `@trackerbot:ariege.io`) or attach to an existing one;
|
|
invite to the rooms you want it active in.
|
|
|
|
See `~/dev/CLAUDE.md` "Maubot plugin development" for the multi-line
|
|
command footgun and other plugin-wide gotchas.
|
|
|
|
## Architecture notes
|
|
|
|
This plugin uses `@command.passive` with a single dispatch regex
|
|
(`^!(\w+)…`) rather than per-verb `@command.new` decorators. Two
|
|
reasons:
|
|
|
|
1. **Multi-line freeform input.** Per `~/dev/CLAUDE.md`, `@command.new`
|
|
silently drops `!task\n<body>` because maubot's parser only
|
|
treats space as the command/args delimiter. Passive regex with
|
|
`re.DOTALL` catches every form.
|
|
2. **Per-room shortcuts.** A single dispatch handler can route
|
|
universal verbs to fixed handlers AND check the room's shortcut
|
|
table for unknown verbs, all in one place. Cleaner than declaring
|
|
N decorators and N matchers.
|