diff --git a/docs/2026-06-02-chateau-pilot-review.md b/docs/2026-06-02-chateau-pilot-review.md deleted file mode 100644 index 670e2a0..0000000 --- a/docs/2026-06-02-chateau-pilot-review.md +++ /dev/null @@ -1,1161 +0,0 @@ ---- -title: "Château du Faune — Tracker / Journal pilot review" -subtitle: "Findings from the first week of !journal, !goals, !tasks usage across four Signal rooms" -date: 2026-06-02 -author: aiolabs / Château du Faune ---- - -# Executive summary - -For the past two weeks, members of the Château du Faune have been -"going through the motions" of using `!journal`, `!tasks`, `!goals`, -`!reminder`, `!buy`, `!sidequest`, and related commands across four -Signal rooms — Operations, Animals, Gardens & Food, Events, and the -general castle room. Nothing was hooked up yet: the chats are mirrors -of what the maubot stack on the new Matrix homeserver will eventually -capture. The point was to *generate signal* — to find out which -verbs people actually reach for, how they phrase entries, where the -syntax gets in the way, and which features the protocol spec is -quietly missing. - -This document reads that signal. It is written for both the chateau -members (who will see their words quoted back and choices made about -how the bot will treat their phrasing) and for the -[aiolabs/maubot-plugins][repo] developers (who will turn the -recommendations into code and spec changes). The -[community-organizer spec][spec] is the contract; the -[`tracker/`][tracker] and [`journal/`][journal] plugins are the -reference implementation; the data we collected over the past two -weeks is the field test. - -**Top-line findings:** - -1. **The capture habit is sticking.** The bot doesn't exist yet, and - people are still using the syntax — sometimes correcting each - other on it. The most surprising data point is that Coco told - Helena *"`!journal` is the required format"* unprompted. Social - enforcement of a syntax we haven't actually shipped is the best - possible adoption signal. - -2. **The vocabulary the community reaches for is broader than the - spec.** The current spec lists `!task`, `!sidequest`, `!journal`, - `!remind`, `!add`, `!done`, `!list`, `!setup`. The chats show - people typing `!buy`, `!need`, `!goals`, `!stewards`, `!tasks`, - `!task` (singular), `!reminders` (plural), `!idea`, `!plans`, - `!priority` (inline), `!side quests`, and a few more. Some of - these are per-room shortcuts the spec already accommodates; - others want a small protocol-level addition. - -3. **The 5-level priority scheme came directly from this pilot.** - Coco proposed "1 urgent / 2 crucial / 3 important / 4 future / 5 - frequent/ongoing" in the operations chat on May 24. It is now - §3.3.1 of the spec, exactly as proposed. This loop — observe → - spec → implement → re-observe — is the one we want to keep - running. - -4. **The bot will need to be more grammar-tolerant than the spec - currently implies.** Multiple verbs per message, edited messages - that add content, `!`-words inside bullet points used as - priority markers, pinning as a "this is the current list" - signal, no-prefix journals — these are all behaviors the bot - doesn't catch today but should. - -5. **The community is already designing the bot's voice.** Alfred - got named on May 24 ("we trusted him with resource management, - safety, tool use, etc."). Photos got captioned, journals got - pinned, Cocopis explained what `!tags` are *to other members*. - The chateau is treating the bot as a collaborator before it - exists. We should lean into that — bot personality is now a - design surface. - -The rest of this report: - -- **§1** What we observed, by room and by pattern -- **§2** What the existing spec + plugins handle well -- **§3** Spec changes proposed -- **§4** Plugin changes proposed -- **§5** New ideas the data surfaced -- **§6** How this fits the larger omnixy / aiolabs ecosystem -- **§7** A phased plan -- **§8** Open questions for the chateau - -[repo]: https://git.atitlan.io/aiolabs/maubot-plugins -[spec]: ../docs/community-organizer-spec.md -[tracker]: ../tracker/ -[journal]: ../journal/ - ---- - -# 1 — What we observed - -## 1.1 Verbs in actual use - -A frequency-sorted list of every `!verb` we saw across the four rooms -(case-folded), grouped by what the spec or current shortcut config -covers: - -| Verb seen | Already in spec | Status | -|---|---|---| -| `!journal` | Yes (universal) | Heavy use across all four rooms | -| `!reminder` / `!reminders` | Spec verb is `!remind` | Mismatch — see §3.1 | -| `!tasks` / `!task` | Spec verb is `!task` | `!tasks` plural also appears | -| `!goals` | No | New — see §3.2 | -| `!stewards` | No (per-room shortcut candidate) | Used as a list/backlog, not a one-off task | -| `!buy` | Per-room shortcut, fits cleanly | Should ship pre-configured for cdf rooms | -| `!need` | No | Distinct from `!buy` in usage — see §3.2 | -| `!sidequest` / `!side quests` | Yes | Used; "side quests" with space appeared once | -| `!idea` / `!plans` | No | Speculative / design-phase items | -| `!priority` (inline) | No (priority is a tag) | Used as a bullet-level marker — see §1.4 | -| `!move` | Mentioned by Ulo as a future verb | Not in spec | - -And a usage example per verb so the design instinct is grounded in -actual phrasing rather than imagined phrasing: - -**`!journal` — past-tense daily log, multi-line is the dominant -form:** - -> `!journal` -> `- opened/closed, watered and fed hens, babies & pacas` -> `- put insulators on the rebars so they're ready to go for fence` -> `- mentally plotted potential hidden-in-plain-sight stable locations. I want opinions next` -> `- should scrape clean duck babes straw tmrw` -> `- should clean and dry very well sapphis wound again` - -Note the mix of past-tense ("opened/closed") and forward-looking -("should scrape clean tmrw") bullets in the same `!journal`. People -do not bucket their day cleanly. §3.4 addresses this. - -**`!goals` — a *list of current intentions*, often nested with -sub-categories:** - -> `!goals & notes next week for garden/kitchen crew` -> -> `GARDEN` -> `- documentation & journaling process` -> `- prepping & starting the Lady Godiva pumpkin patch.` -> `…` -> -> `KITCHEN` -> `- forage, harvest & food processing resource improvements (ex. cherries into vinegars…)` -> `- filling the shop up (& soon looking into certifying our kitchen/lab)` -> `…` - -`!goals` lists evolve over days — Coco's gardens `!goals` on May 27 -and June 2 are *the same list*, refined. This is the strongest -argument we found for **replaceable-event semantics** (§3.3). - -**`!reminder` — a future-pointed list, often with embedded -@-mentions and `!priority` markers inside bullets:** - -> `!goals this week/soon` -> `- attempt to secure alpaca deposit & arrival of the boys` -> `- !priority confirm if teeth & nails will also be done by the shearer 'sometime in july'` -> `- !priority diagnosis of sapphis skin condition be made by a vet in person.` -> `!reminder following shearing, immediately start sapphis skin infection protocol` -> `- fencing add in doors & handles. Start soon on setting up 2nd stable paddock` - -Look at that message carefully: it is *one* `!goals` entry, with -`!priority` and `!reminder` appearing **inside bullets as inline -markers**, not as new commands. This is the single most important -parser decision we have to make — see §4.1. - -**`!buy` — shopping list, sometimes added-to over days via edit:** - -> `!buy` -> `- roll out flypaper for stable(s)` -> `- door handles for electric (or from laura)` -> `- la fourche order, sesame & produce will be thursday, 28th` -> `- buy more pans & x2 induction adapters` - -**`!need` — semantically distinct from `!buy`. Means "stock is -low" rather than "purchase this":** - -> `!need puppy & cat food, hay & straw soon` - -Coco used both in the same week. The distinction matters because -`!need` items become `!buy` items only after someone agrees to act -on them, where `!buy` is already a decision. - -## 1.2 Cadence patterns — the recurring chore problem - -The operations chat opened with a chores list dominated by -*recurring* items with fuzzy intervals: - -> `pacas:` -> `- every ~3 days scoop manure` -> `- every ~3 days fluff bedding` -> `- every 2 wks to 1 month - change out whole bedding` -> `- daily fresh water` -> `- daily light green/yellow hay` -> `- training - max 1x/day - approach with treats in a bucket…` -> -> `hens & ducks:` -> `- am let out, pm close in` -> `- every ~3 days fresh water & food` -> `- every 1 week change out straw` -> `- spend time with the puppies every day` - -Coco flagged the problem herself: *"weird thing is tasks that are -every few days or variable timeframes?"* - -The spec partially anticipates this with `priority:5` ("frequent / -ongoing"), but `priority:5` is a marker, not a cadence. "Every 3 -days" and "daily" are different cadences and the bot should know. -RFC 5545 `RRULE` is the canonical answer; NIP-52 doesn't yet -codify recurrence (§11 of the spec lists this as open). A -lightweight stopgap is proposed in §3.5. - -## 1.3 Multi-verb messages - -Helena posted in the Gardens & Food room: - -> `!journal` -> `• bigger Team meet and on board of Marta what we did the last weeks…` -> `• weeding` -> `• getting fresh alpaca manure and distributing` -> `• collecting seeds for next year planting` -> -> `!task` -> `- research thing for rain tank` -> `- update online garden map` -> `- check on sprouting pumpkin seeds and plant when ready` - -This is one Matrix message but **two distinct entries** — a -`!journal` and a `!task` list. The current tracker dispatch regex -matches the first verb at start-of-message and stops. The plugin -will silently drop Helena's `!task` block. - -This is a real bug, not a theoretical one — Helena was the first -person who naturally typed in this shape, and the bot would have -lost the task list. §4.2 fixes it. - -Skye independently did the same thing on the same day: - -> `!goals !tasks` -> `- [ ] organise conversation about image ai making with pat and coco` -> `- [ ] Look into creation of the funding via our socials…` -> `- [ ] For flyers/posters the !goals is to make one for pats new events app for ariege…` - -`!goals !tasks` on one line is yet another shape: two verbs sharing -one body. Likely "this entry belongs to both lists" — but the spec -needs to say so explicitly. - -## 1.4 Inline `!priority` and `!reminder` markers - -Returning to Coco's May 27 `!goals`: - -> `- !priority confirm if teeth & nails will also be done by the shearer 'sometime in july'` -> `- !priority diagnosis of sapphis skin condition be made by a vet in person.` -> `!reminder following shearing, immediately start sapphis skin infection protocol` - -`!priority` and `!reminder` are appearing **inside body content** as -bullet-level annotations, not as commands. A naïve parser that -matches `!\w+` anywhere in a message would interpret these as new -commands and produce four entries from one message. - -The fix is the rule the journal plugin already enforces: a `!verb` -counts as a verb **only** if it is at the very start of the message -(or, with §4.2 in place, at the very start of a top-level block). -Everything else is body content with inline tags. - -But "inline tags" is itself a feature worth keeping: a bullet -starting with `!priority` is a natural way to mark *that bullet* -as urgent within a larger list. The bot can: - -1. Treat the inline `!priority` as a *tag on the bullet body* - (preserves the user's intent). -2. Leave the parent entry's priority alone (the user didn't priority - the *list*; they priority'd one item in it). - -A renderer can then surface that bullet as priority-1 within the -parent entry. This is a richer model than the spec currently -sketches, but it matches what users are actually doing. §3.3 -proposes the wire format. - -## 1.5 Pinning as state - -Across all four rooms, the dominant `!goals` / `!tasks` / `!stewards` -messages are **pinned to the room**. When the list evolves, the new -version is pinned and the old one is unpinned. Matrix has native -support for this (`m.room.pinned_events`). - -We propose treating "the latest pinned message matching `!goals` -from a given room" as the **current state of that group's working -list**. This gives every room a queryable "what's on our plate -right now" view without requiring users to remember an item id. - -§3.3 + §4.5 develop this. - -## 1.6 Implicit subjects — animals, people, places - -The animals room is full of entries like: - -> `- thinking of long-term solutions for pups barking and neighbors.` -> `- 45 min in the alpaca paddock with both pups, no electricity.` -> `- carefully choose feeding station(s) so they are isolated and don't compete with any other animals` -> -> *(and elsewhere)* -> -> `lets clean sapphis wound again soon?` -> `also just realized the shearer will come in july` - -The community has named entities — *Sapphi*, *Loki*, *Leia*, -*Tango*, *Morrigan*, *the alpacas*, *the pups*, *the hens*, *the -south paddock*, *the orchard*, *the coop* — that recur across many -entries. A useful query is "show me everything mentioning Sapphi -this month" or "what tasks involve the south paddock?". - -NIP-52 already supports `["p", ""]` for *people*. Animals -and places need a convention — see §3.6. - -## 1.7 Inline @-mentions = informal assignment - -> `- location & plans for stable, cont. @coco` -> `- closing the main property gate? guardian dogs sign @coco` -> `- pruning & landscaping @Thomasss**]` -> `- following a budget per person for food orders, picking up produce @Skye 🌟` - -People mention each other inline to mean "this bullet is yours". The -bot can extract these into structured `["p", …]` tags and a -renderer can surface a per-person "your bullets" view. §4.6. - -## 1.8 Cross-group routing ambiguity - -Skye said in the Events room: - -> *"organise a mini meeting to talk about ai image making etc with -> pat/coco …. I'm not sure which chat to put these social things in -> hehe"* - -Some items belong to multiple groups. The current 1-room ↔ -1-community mapping makes this expensive — you'd have to type the -same entry twice. Coco's answer (*"i think events would be the -fitting place"*) is the human workaround; the bot should help. -§5.2 proposes optional multi-room fan-out. - -## 1.9 No-prefix journal-shaped content - -Lots of messages look like journals without using any command: - -> `today plan for me:` -> `- working in other groups on animals and operations` - -> `Today's plan for me:` -> `- making bulk buckwheat zucchini burgers some for today and rest in freezer` - -> `journal` -> `started but did not complete: bathroom clean, mapping stables, fencing the internal 150m paddock` - -The chateau is explicitly standardizing on `!journal` (Coco -corrected Helena on this in real time). So we **should not** make -the bot fuzzy-match — they want explicit syntax. But the data -suggests an opt-in *"capture every non-command message in this -specific room"* mode could be valuable for very-low-friction rooms -(e.g. a private journal room per member). Default: off. §5.1. - -## 1.10 Capitalization, punctuation, language - -The same intent appears as `!journal`, `Journal:`, `journal`, `!journal:`, -`Today plan for me`, `today plan:`, sometimes mixed with French. - -The bot's regex needs to be: -- case-insensitive on the verb (`!Journal` == `!journal`) -- tolerant of a trailing colon (`!journal:` is fine) -- bilingual on body content (already true — body is opaque text) - -The current `_CMD_RE = re.compile(r"^!(\w+)(?:[ \t\r\n]+(.*))?$", re.DOTALL)` -in `tracker.py:52` *does* match `!Journal` because `\w` is -case-insensitive in unicode mode, and the dispatch already calls -`.lower()`. But it does **not** match `!journal:` because the colon -follows the verb with no whitespace. One-line fix in §4.3. - -## 1.11 Edits matter - -Two distinct edit patterns appeared: - -**A. Edit-to-append.** Coco edited a `!journal` post-hoc and -commented: *"just added that i also fed puppies 2x today"*. The -intent is "this is the same entry, with more content". - -**B. Edit-to-correct.** Coco scheduled a vet appointment for "June -5 @ 14h", then edited the same entry to "May 30 @ 15h" with the -note *"updated to May 30 @ 15h because they need the vaccination -within the month"*. - -The tracker's current known-quirks note (`tracker/README.md:133`) -says: - -> *"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."* - -This is solvable — `m.replace` events *are* visible to maubot; the -handler just needs to be wired. §4.4. - ---- - -# 2 — What the existing spec and plugins handle well - -Before proposing changes, let's mark what's already right. The pilot -*validates* a lot of the design. - -## 2.1 The split between universal verbs and per-room shortcuts - -The spec says: every implementation must recognize the universal -verbs (`!task`, `!journal`, etc.); per-room shortcuts (`!buy`, -`!harvest`, …) expand to them. The data agrees: every room developed -its own dialect (animals uses `!journal` heavily, operations uses -`!buy` and `!stewards`, events uses `!goals`). A single plugin -serves all of them. - -We just need to **pre-configure** the cdf rooms with sensible -shortcuts so members don't have to `!setup add buy task #buy` on day -one. §4.7. - -## 2.2 The passive regex / multi-line story - -The journal plugin's switch to `@command.passive` with a regex was -right. Every multi-line entry we observed (and many one-line ones) -would have been silently dropped under `@command.new`. The CLAUDE.md -footgun is real and the plugin avoided it. - -## 2.3 The 5-level priority scheme - -`priority:1..5` with the labels urgent / crucial / important / future -/ frequent-ongoing came out of the pilot. The renderer plan is to -surface labels rather than numbers; the wire tag stays `priority:N` -for sort stability. The data didn't push back on this — Coco's -proposal landed exactly as written. - -## 2.4 Sidequests as a separate kind - -`!sidequest` (with a `🎯` emoji ack) gives a distinct surface for -"would be cool to do" items. Coco's *"`!side quests` lists which are -different from the rest of the tasks"* is exactly the use case. The -spec already excludes sidequests from default `!list` output, which -is the right default. - -## 2.5 The Matrix-only Phase 1 posture - -The tracker is `matrix-only` for v1 — no Nostr publishing yet. This -turns out to be the right call: the four-week dataset shows the -community is still finding its vocabulary. Publishing to a public -relay before the vocabulary stabilizes would make schema migrations -visible to anyone subscribed. Stay matrix-only until the verb set is -stable, then bridge. - -## 2.6 The journal plugin's append-only stance - -The spec's "`!done` on a journal entry is undefined behavior; -implementations SHOULD reject it" matches the data: nobody tried to -close a journal. They edit, they re-journal, they pin — they don't -close. Good call. - ---- - -# 3 — Spec changes proposed - -These are protocol-level additions to `docs/community-organizer-spec.md`. -Each is small. None breaks existing conformance — they extend, they -don't refactor. - -## 3.1 Plural aliases for universal verbs - -`!reminders` and `!tasks` should be accepted as equivalents of -`!remind` and `!task`. `!sidequests` likewise. This is a one-line -parser change; it costs nothing and removes a footgun (users will -naturally pluralize when a list is multi-item). - -> *Spec change:* in §3.1, add a row: -> *"Implementations MUST accept the bare plural form of each verb -> (`!tasks`, `!sidequests`, `!reminders`) as equivalent."* - -## 3.2 New universal verbs: `!goals` and `!need` - -Both verbs got real, repeated, distinct-from-existing usage. Both -deserve protocol-level status, not per-room shortcut status, because: - -- They had identical meaning across all four rooms. A bakery co-op - reading this spec should be able to use them without re-configuring. -- The shortcut mechanism is for *vocabulary the community invented* - (e.g. `!harvest`, `!brewery`). `!goals` and `!need` are not - vocabulary inventions — they're a category the spec missed. - -| New verb | Purpose | Underlying kind | -|---|---|---| -| `!goals ` | A list of current intentions for the author/group/period. Replaceable per `(author, room, period?)`. | 31922 with `["t", "goals"]` | -| `!need ` | A supply that is running low. Distinct from `!buy` (which is "I will purchase this"). Default lifecycle: open → `!buy` (converts to task) or `!done`. | 31922 with `["t", "need"]` | - -`!goals` deserves §3.3 (replaceable semantics) since the goals lists -visibly evolved over days in the pilot. - -## 3.3 Replaceable-event semantics for `!goals` (and stewards lists) - -NIP-52 kind 31922 is a *parameterized replaceable event* keyed by -`d`-tag. Today the tracker assigns a unique `d` per entry. For -`!goals` (and the per-room `!stewards` list pattern), we want the -opposite: the latest `!goals` from a given `(author, room)` pair -should *replace* the previous one. - -> *Spec change:* in §3.3, add: -> *"Implementations MAY designate certain verbs as replaceable per -> `(author, room, period?)`. When a verb is replaceable, the bot -> reuses the same `d`-tag for successive captures, so the relay -> retains only the latest version. Recommended replaceable verbs: -> `goals`, `stewards`."* - -The `period` qualifier lets `!goals this week` and `!goals next week` -coexist. Suggested encoding: a `period:` tag (e.g. `period:2026-w22`). -For v1 we keep it simple — no period qualifier, latest wins. - -The pinning UX hooks straight in: when a `!goals` message is the -latest replaceable entry from its author, the bot can ask Matrix to -pin it (and unpin the previous one). This makes the room's pinned -message a live mirror of the relay state. - -## 3.4 Forward + retrospective in a single `!journal` - -The data shows people blend "did X" and "will do Y tomorrow" in the -same `!journal`. The spec currently says journals are past-tense and -append-only. Rather than fight the user, we accept the blend. - -> *Spec change:* in §3.1, clarify: -> *"`!journal` is append-only; implementations MUST NOT close -> journals. Bodies MAY contain forward-looking statements; the -> classifier MAY surface these as candidate tasks at review time but -> MUST NOT auto-create derived task entries."* - -The "candidate task" idea is a v2 nicety: a weekly digest that pulls -"should…" bullets out of journals and offers them as `!task` -candidates. Not in v1. - -## 3.5 Recurrence tags (lightweight, pre-RRULE) - -For the chore-list problem (`every ~3 days scoop manure`, `daily -fresh water`), we propose a `frequency:` tag namespace that lives -inside the existing `["t", …]` mechanism. Renderers parse it. - -Suggested values: - -| Tag | Meaning | -|---|---| -| `frequency:daily` | Once per day | -| `frequency:weekly` | Once per week | -| `frequency:3d` | Every 3 days | -| `frequency:2w` | Every 2 weeks | -| `frequency:as-needed` | No fixed cadence; on demand | - -This is a stopgap. When NIP-52 or a sibling NIP defines proper -recurrence, we migrate. For now, the eink renderer can group -`frequency:daily` items into a "today" panel and `frequency:weekly` -into a "this week" panel without parsing RRULE. - -> *Spec change:* in §3.3, add: -> *"`frequency:` is reserved for recurrence hints. Recommended -> values: `daily`, `weekly`, `Nd` (every N days), `Nw` (every N weeks), -> `monthly`, `as-needed`. Implementations MAY define additional -> values."* - -## 3.6 Subject tags — animals and places, not just people - -NIP-52 has `["p", ]` for people but no convention for -non-human subjects. We propose a namespaced `t`-tag: - -| Tag | Meaning | -|---|---| -| `subject:animal/sapphi` | Animal named Sapphi | -| `subject:animal/loki` | Animal named Loki | -| `subject:place/south-paddock` | A named place on the property | -| `subject:object/electric-fence` | A named asset/object | - -A community can define its own subject vocabulary via `!setup`: - -``` -!setup subjects animal/sapphi animal/loki animal/leia -!setup subjects place/south-paddock place/orchard -``` - -When typing, members can reference subjects with a `@`-prefix as -shorthand: - -``` -!journal cleaned @sapphi's wound, fed @hens at noon -``` - -The bot expands `@sapphi` to `subject:animal/sapphi` tags and -preserves the original text in the body. Renderers can then offer -"recent entries about Sapphi" without the bot needing to do NLP. - -For *people* the existing `["p", ""]` already works; -inline `@person:name` (or just `@`-mentions resolved through the -Matrix room's member list) feeds straight into that. - -## 3.7 Inline `!verb` markers within a body are tags, not commands - -This is half a spec change, half a clarification: - -> *Spec change:* in §3.3, add: -> *"A `!verb` occurring at the start of a message (or, with multi-verb -> support, at the start of a top-level block) is a command. A `!verb` -> appearing inside body content is body content and MAY be extracted -> by the bot as an `inline:` tag on the parent entry. This -> preserves user intent without spawning ghost entries."* - -So Coco's `- !priority diagnosis of sapphis skin condition…` bullet -gets recorded as part of the parent `!goals` entry, *and* the parent -gets an `inline:priority` tag. A renderer can surface that bullet -with priority styling. - -## 3.8 Multi-line entries with sub-headers - -A small content-format convention so renderers can do something -sensible with Coco's `GARDEN` / `KITCHEN` sub-headers: - -> *Spec change:* in §10, add: -> *"Bodies MAY use Markdown-style headers (`#`, `##`) as in-body -> section markers. Renderers MAY surface them as visual groupings. -> The body remains a single content blob; sub-headers do not split -> the entry."* - ---- - -# 4 — Plugin changes proposed - -These are concrete edits to `tracker/`, `journal/`, and a small -amount of `wiki/` if we want cross-plugin discovery. Each maps to a -specific file path so the work is sized. - -## 4.1 Parse inline `!verb` markers as tags, not commands - -`tracker/tracker.py:184` — the `dispatch` handler matches a verb at -start-of-message. Today, anything else is unmatched and the message -is dropped (correct). We extend: after dispatching the top-level -verb, scan the body for inline `!verb` occurrences and add them as -`inline:` tags on the recorded entry. Implementation is a -single pass over the body before recording. - -This makes Coco's `- !priority …` bullets actually do something -useful at the entry level without inventing ghost entries. - -## 4.2 Split multi-verb messages - -`tracker/tracker.py:52` — the `_CMD_RE` regex anchors at -start-of-string and captures everything to end-of-string. We change -to a *two-pass* parse: - -1. Split the message at every line that starts with `!verb` (and - only such lines — bullets like `- !priority …` are NOT split - points). -2. Each split block becomes its own captured entry. - -Helena's `!journal … !task …` message becomes two entries. Skye's -`!goals !tasks` on a single line stays as one entry but gets *both* -tags applied. This requires a second regex for the multi-verb-one-line -case. - -The order matters: split first on newline-prefixed `!verb`s, then on -the single-line multi-verb case. - -## 4.3 Tolerate trailing colon and case on verb - -`tracker/tracker.py:52` — change the regex to allow an optional -trailing colon and explicit case-insensitive flag: - -```python -_CMD_RE = re.compile(r"^!(\w+):?(?:[ \t\r\n]+(.*))?$", re.DOTALL | re.IGNORECASE) -``` - -(The `.lower()` already happens in `dispatch`; the regex flag is for -unicode `\w` consistency. Trailing colon is the visible change.) - -Apply the same change to `journal/journal.py:34` for `_JOURNAL_RE`. - -## 4.4 Handle Matrix edits - -`tracker/tracker.py` — register a handler for `m.room.message` events -with `rel_type == "m.replace"`. The handler: - -1. Looks up the original event id in the items table (we need to - start recording it — schema change below). -2. If the original is a captured entry, treat the edit as a body - update on that entry. Bump a `last_edited_ts`. -3. Reply with a small ack ("✏️ Updated entry `42`"). - -Schema change: add `mx_event_id TEXT` column to `items`, indexed. -This is a real change to `upgrade_table` — add as `upgrade_v2`. - -`tracker/README.md:133` — strike the "Edited messages don't -re-trigger the bot" caveat. - -This is the single change that unlocks the most observed user -behavior. People *do* edit, and treating edits as no-ops loses -substantial information. - -## 4.5 Detect pin events; mark replaceable verbs as pinned - -`tracker/tracker.py` — subscribe to `m.room.pinned_events`. When a -captured entry's source Matrix message becomes pinned, mark the -entry as `pinned=true`. When a `!goals` entry is recorded, the bot -*itself* asks Matrix to pin the message (and unpin the previous -`!goals` from the same author). - -This makes the pinned-message UX a transparent mirror of the relay -state, without users needing to manually pin/unpin. - -(This requires the bot to have pin-event permission in the room. The -admin can grant it via Element. Document in the README.) - -## 4.6 Extract inline @-mentions as `p`-tags - -`tracker/tracker.py` — alongside the existing `_TAG_RE` extraction -for `#tag`, add an `_MENTION_RE` extraction for `@user`. Resolve -against the Matrix room's member list (best-effort; if it doesn't -resolve, store the literal string). - -The recorded entry gets `["p", ""]` tags for each resolved -mention, *and* keeps the `@user` substring in the body for human -readability. A renderer can then offer a "your bullets" view per -person. - -## 4.7 Ship sensible default shortcuts for the cdf rooms - -`tracker/tracker.py` — add a one-time bootstrap that, on first -`!setup` (or first capture) in a room whose name matches a known -chateau room, pre-populates shortcuts. Suggested defaults: - -| Room name | Shortcuts | -|---|---| -| Animals | `!buy task #buy`, `!chores task #priority:5`, `!vet task #vet`, `!need need` | -| Gardens | `!buy task #buy`, `!harvest task #harvest`, `!plant task #plant`, `!need need` | -| Operations | `!buy task #buy`, `!steward task #steward`, `!repair task #repair`, `!need need` | -| Events | `!host task #host`, `!ticket task #ticket` | - -These are starting points, not a final list. Communities edit them -via `!setup` once they're in. - -A safer way to ship this is a `default_shortcuts.yaml` config the -plugin reads, so changes don't need a code redeploy. - -## 4.8 New universal verbs implementation - -`tracker/tracker.py:56` — add to `UNIVERSAL_VERBS`: - -```python -UNIVERSAL_VERBS = { - "add", "task", "tasks", "sidequest", "sidequests", - "remind", "reminders", "done", "list", "setup", - "goals", "need", "needs", -} -``` - -(Plural aliases per §3.1.) - -Add `_handle_goals` and `_handle_need` handlers. `!goals` uses -replaceable-event semantics (§3.3) — for the matrix-local v1, this -means *upserting* on `(user, room, kind='goals')` rather than -inserting. One row per author per room. - -`!need` records a `kind='need'` item that stays open until -either `!done` (we got some) or another verb explicitly converts it -to a `!buy` task. - -## 4.9 Frequency tag parsing in `!list` - -`tracker/tracker.py:302` — when listing items, group by -`frequency:` tag so the chore-list renders as: - -``` -**Daily (3):** -- `5` fresh water for alpacas -- `7` fresh water for ducks -- `12` open & close hens - -**Every 3 days (2):** -- `19` scoop alpaca manure -- `23` fluff alpaca bedding - -**Weekly (1):** -- `4` change out hen straw -``` - -This is rendering, not protocol — the spec just stores tags. - -## 4.10 Replace the `tracker/README.md` quirk note - -After §4.4 lands, strike the edit caveat. Add a paragraph on the -new pin behavior, the new universal verbs, and the multi-verb -splitting. - ---- - -# 5 — New ideas the pilot surfaced - -## 5.1 Opt-in "private journal room" mode - -For a per-user *private* journal room (one bot account, one human -member), the prefix friction adds nothing — every message is -journal content by definition. We can add a per-room flag -`auto_journal: true` (default `false`) that records every plain -message as a `!journal` entry. This is *opt-in per room* and the -bot acks loudly the first time it activates so nobody is surprised -by it. - -For shared rooms — which is everywhere in the pilot — the default -stays explicit-prefix-only, matching Coco's "`!journal` is the -required format" enforcement. - -## 5.2 Multi-room fan-out - -Skye's "I'm not sure which chat to put these social things in" -suggests an entry can sometimes belong to multiple rooms. We -propose a `+#room` syntax: - -``` -!task +#events +#general organize image-AI brainstorm with pat and coco -``` - -This records the entry in *both* communities. The same entry has -two `a`-tags (one per NIP-72 community). Renderers in either room -see it. `!done` closes it in both. - -Implementation: parse `+#` tokens out of the body, resolve -to room ids the bot is also a member of, and insert one row per -target room with a shared `link_id`. v2 work, not v1. - -## 5.3 Photo / link / media in entries - -Helena's "checked feathered babies" entry was actually a photo + -caption. Thomas shares YouTube links. Coco shared a PDF on toxic -plants. The body field today is text; the bot can capture media -attachments by storing their `mxc://` URI in a `media` JSON column. -A renderer can fetch and display. - -Especially valuable for the eink panel — a photo of the daily chores -list, a graph of fed/watered checkmarks, the duck-pool-water-runoff -diagram. - -## 5.4 Calendar feed for timed entries - -Pat already saw the vet appointment on the castle calendar — *"nice -i already see it on the castle calendar"*. This integration should -be documented: where it lives, what feeds it. If it's manual, it -should be automated; if it's automated, it should be tested for -all `kind 31923` timed entries. - -The natural shape: an iCalendar feed at -`https://docs.ariege.io/calendar.ics` produced from the bot's -timed-entry table. Castle members subscribe in their calendar app -of choice. - -## 5.5 Daily / weekly digests - -The pilot showed the value of *summary*. Coco pins a current -`!goals`. Ulo posts meeting summaries. Helena writes "homework" -recaps. The bot can produce these on a cron: - -- **Morning digest** (per room): "Yesterday's journal entries (3), - Open tasks this week (12), Goals pinned (1)." -- **Weekly digest** (Sunday evening): "This week's completed tasks - (28), Open carry-over (15), Journal highlights (a sampling)." - -This is a content surface, not protocol. The data is already there. -Implementation = a cron handler + a markdown template + posting to -the room. Maubot has cron support via `asyncio` + `croniter`. - -## 5.6 Bot personality — Alfred - -The community named the bot Alfred (the butler). Replies should -match: a slightly formal, helpful, occasionally dry tone. - -Concretely: -- Current acks: `📥 Logged for @alice.` -- Alfred-flavored: `Very good, @alice. Logged.` - -A `personality` config field with `default | alfred | minimal` -lets other adopting communities pick. Keep `default` deadpan and -neutral. - -This is low-priority but it costs nothing and the data shows the -community will engage with the bot more if it feels like a -collaborator. "What should we name our assistant?" — *"Alfred would -be my go to"* — *"doiittt"* is a community taking ownership of the -tool before it exists. - -## 5.7 LLM fallback inside the chateau network - -The spec's classifier conformance level 2 is LLM fallback. The -chateau has an existing Ollama setup or can run one on the same -host as maubot. The fallback fires only when: - -- The user typed `!add` (explicit ambiguity), -- The rules classifier returned `unclassified`, -- The LLM is reachable. - -If any condition fails, the entry stays in the inbox until -human-sorted (level 0 fallback). The architecture stays -self-hostable. - -Smallest reasonable model for this is in the `llama3:1b` / -`phi3:mini` size range — runs comfortably on the maubot host. - -## 5.8 Wiki / docs lookup inside the tracker context - -The wiki plugin already exists. Inside a tracker reply, if an entry -mentions e.g. *"recycling"*, the bot could surface a `[?]` hint: -"Related: `docs.ariege.io/spaces/waste-recycling`". One-line link, -ignorable. - -This makes the chateau's docs site organically discoverable from -chat without anyone needing to remember URLs. Implementation: -inside the tracker's record path, fire a quick keyword query at the -wiki plugin's index and append top-1 result if confidence is high. - ---- - -# 6 — How this fits the larger omnixy / aiolabs ecosystem - -The Château pilot is one community using one piece of the stack. The -broader picture: - -## 6.1 The protocol survives the implementation - -The community-organizer spec is CC0 and references only open -standards (RFC 5545 VTODO, NIP-52, NIP-72, ActivityStreams). If the -maubot plugin disappears tomorrow, the events on the relay are still -readable by any NIP-52 client. The chateau's organizational memory -is not coupled to maubot. That's the design property we're -protecting. - -## 6.2 Nostr bridge (Phase 2) - -Today the tracker is matrix-only. When the vocabulary stabilizes -(probably 2-4 more weeks of pilot), Phase 2 bridges to Nostr: - -- Bot writes captured entries to a community Nostr relay. -- The relay can be private (auth-gated) or public per the - `publish` posture config. -- Renderers (the eink panel, a future webapp dashboard) subscribe - to the relay and surface the data. - -This is the unlock for the *inky-impression* panel in the foyer — -the data shape it consumes is exactly the events the bot will be -producing. - -## 6.3 Identity — the signer abstraction - -Today everything is signed by the bot. The spec's §7.2 / §12 -develop the per-user-signing endgame: a sidecar -`nsecbunkerd` on the same host, the bot holding only scoped NIP-46 -connection tokens. - -At chateau scale (10-20 members), this is overkill for v1. We run -v1 (trust the bot, `author` tag carries the human MXID) for as long -as the community is small and physical-trust-bounded. We migrate to -per-user signing when: - -- More than one community is using the same bot account, *or* -- An external party (visitor, woofer, neighbor) needs to write - entries without being a fully-trusted member, *or* -- LNbits #18 (the bunker integration) ships and we want a single - identity story across the stack. - -The signer abstraction is *already* in the spec, so the migration -is design-for-it-now, do-it-later. - -## 6.4 LNbits / Lightning - -Two Lightning hooks the pilot suggests: - -**Marketplace / payable buy items.** A `!buy` item could optionally -carry a price and produce a Lightning invoice via LNbits when -clicked through. "Roll-out flypaper — €12 — pay" with the invoice -generated against the chateau's funding wallet. Useful for outside -contributors who want to chip in a specific item. - -**Visa funding stream.** Coco's alpaca funding stream (€23k over -time) is exactly the kind of recurring-pledge use case LNbits has. -This is a partnership channel between the tracker and the LNbits -extension at the marketplace level, not v1. - -## 6.5 Events app - -Coco's events-app side-project is in the pilot data: -*"developing the app & biz plan for tech company of an -ariege-wide calendar of events app"*. The tracker's NIP-52 31923 -events are the right shape to feed that calendar. The webapp -consumes NIP-52 already (per the bunker spec discussion). One data -shape, two surfaces: the bot records, the events app displays. - -## 6.6 Continuwuity homeserver - -The Matrix homeserver running this is Continuwuity, deployed via -`deploy/server-deploy/modules/services/matrix.nix`. The maubot -daemon registers via the admin-room token flow (see -`~/dev/CLAUDE.md` Matrix-homeserver section). All bot accounts in -this report (`@journalbot`, future `@trackerbot`, `@wikibot`, etc.) -are regular Matrix users on that homeserver. - -The bot-as-user pattern means a chateau member can DM the bot -privately (e.g. for personal `!journal` entries the public room -shouldn't see). The current journal plugin already supports this — -just invite the bot to a 1-1 room. - -## 6.7 Wiki cross-cutting - -The wiki plugin (`!ask`, `!doc`) reads `docs.ariege.io`. The -chateau's documentation should grow alongside the tracker — when a -new community-life protocol gets agreed on at a meeting (e.g. -"who feeds the cat"), it should land in `docs.ariege.io/spaces/...` -and be `!ask`-able. The pilot showed several decisions that would -benefit from durable documentation: the chickens' opening time, the -puppy training schedule, the vet's number. - -This argues for a small `tracker → wiki` integration: when a -`!journal` entry contains a *decision* (heuristically: contains -"we decided" / "rule" / "from now on"), the bot pings the room with -"This looks like a durable decision — would you like to file it -as a docs page?". One-click → wiki page draft. - ---- - -# 7 — Phased plan - -A 5-phase rollout that keeps each step small, deployable, and -reversible. - -## Phase 0 — Verb normalization and edit handling (next 1-2 weeks) - -Small parser / dispatch changes. No schema migrations beyond -`mx_event_id`. - -- §4.1 inline `!verb` markers as tags -- §4.2 multi-verb-per-message splitter -- §4.3 trailing-colon + case -- §4.4 edit handling (schema v2: add `mx_event_id`) -- §4.8 new universal verbs (`!goals`, `!need`, plural aliases) -- §4.10 README updates - -After Phase 0, the bot won't drop any of the entries we observed. - -## Phase 1 — Replaceable goals and pinning (2-4 weeks) - -- §3.3 replaceable-event semantics in spec -- §4.5 pin/unpin behavior on `!goals` recording -- §4.7 default shortcuts for cdf rooms -- §4.9 `!list` grouping by frequency - -After Phase 1, `!goals` becomes a living list per author and the -chore-recurrence model is at least visible to renderers. - -## Phase 2 — Subjects, mentions, recurrence (4-6 weeks) - -- §3.5 `frequency:` tag in spec -- §3.6 subject tags (animals, places) -- §4.6 inline `@`-mention extraction -- §3.7, §3.8 grammar clarifications in spec -- §5.5 daily/weekly digests - -After Phase 2, the eink panel can render meaningful views without -needing protocol changes. - -## Phase 3 — Nostr bridge (6-10 weeks) - -- Wire the tracker's local SQLite to a Nostr publisher. -- Subscribe to events tagged with the bot's communities for - bidirectional sync. -- Eink panel goes live against the relay. - -## Phase 4 — Per-user signing and identity (when LNbits #18 ships) - -- Operator-IdP + sidecar bunker per spec §12. -- Chat-handle ↔ pubkey binding via LNbits. -- Bot acquires scoped NIP-46 tokens per member. -- `BotSigner` becomes the fallback only. - ---- - -# 8 — Open questions for the Château - -Things the team should weigh in on before we lock the next phase: - -1. **Verb names.** Is `!goals` the right word, or `!intentions` / - `!focus` / `!agenda`? Same for `!need` vs `!stock` vs `!low`. The - bot will accept what the community uses — but the spec name will - stick. - -2. **Default shortcuts per room.** §4.7 lists candidates. Anything - missing? Anything wrong? Specifically: does the Animals room - want `!vet` and `!med` as separate kinds, or one `!health`? - -3. **Subject vocabulary.** Which animals, places, and assets are - worth naming as subjects? Suggested first pass: **animals** — - Sapphi, Loki, Leia, Tango, Morrigan, plus group names - (alpacas, hens, ducks, pups, hatchlings). **Places** — orchard, - south-paddock, coop, garage, kitchen, lab, dependence. **Assets** — - tractor, electric-fence, van, irrigation. - -4. **Calendar integration.** Today *something* turns dated journal - entries into castle-calendar items. Who owns that pipeline? - Should the bot own it? Should we publish an `.ics` feed off the - maubot host? - -5. **Photo / media capture.** Does it matter that journal entries - capture the photos attached to them, or is text enough for v1? - (Materially affects storage and the eink panel.) - -6. **The "Alfred" persona.** Yes / no / opt-in per room? - -7. **The private-journal room mode.** Would members value a 1-1 DM - with the bot where every message is auto-journaled, or does the - explicit `!journal` everywhere feel cleaner? - -8. **LLM fallback.** Comfortable running a local model on the same - host that classifies `!add` entries? Or stay rules-only? - -9. **Cross-room fan-out.** Do members value `+#room1 +#room2` - syntax, or is "I'll just repost in the other room" fine? - ---- - -# Appendix A — Glossary - -- **Verb** — A `!`-prefixed word at the start of a message that the - bot recognizes as a command (e.g. `!journal`, `!task`). -- **Tag** — A `#`-prefixed word inside a message that gets attached - to the entry as metadata (e.g. `#buy`, `#priority:1`). -- **Subject** — A `@`-prefixed word that names an entity (animal, - place, asset) the entry is about. -- **Shortcut** — A per-room alias that expands to a universal verb - + tags (e.g. `!buy` expands to `!task #buy`). -- **Replaceable event** — A NIP-52 event keyed by a `d`-tag, where - newer events overwrite older ones with the same key. Used for - living lists like `!goals`. -- **Inbox** — Where `!add` entries land when the classifier can't - determine a kind. Cleared via `!sort` or `!reclassify`. -- **Bot** — The maubot plugin instance attached to a Matrix client - account (e.g. `@journalbot:ariege.io`). -- **Renderer** — Any output surface (eink panel, webapp dashboard, - mobile notification) that consumes captured events. - ---- - -# Appendix B — File map - -Concrete files this report's proposals would change: - -| File | Change | -|---|---| -| `docs/community-organizer-spec.md` | §3.1 plural aliases, §3.2 new verbs, §3.3 replaceable, §3.5 frequency, §3.6 subjects, §3.7 inline markers, §3.8 sub-headers | -| `tracker/tracker.py:52` | Regex updates (case-insensitive, trailing colon) | -| `tracker/tracker.py:56` | `UNIVERSAL_VERBS` additions | -| `tracker/tracker.py:184` | Dispatch — multi-verb + inline tag extraction | -| `tracker/tracker.py` | New `_handle_goals`, `_handle_need`, `_handle_edit` | -| `tracker/tracker.py` (upgrade table) | `upgrade_v2` — `mx_event_id`, `link_id`, `pinned` columns | -| `tracker/classify.py` | Optional — LLM fallback wiring | -| `tracker/README.md:133` | Strike the edit caveat, document new behaviors | -| `journal/journal.py:34` | Regex update consistent with tracker | -| `wiki/wiki.py` | Optional `!ask`-from-tracker integration | - ---- - -*End of report.* diff --git a/docs/2026-06-02-chateau-pilot-review.pdf b/docs/2026-06-02-chateau-pilot-review.pdf deleted file mode 100644 index b974d22..0000000 Binary files a/docs/2026-06-02-chateau-pilot-review.pdf and /dev/null differ diff --git a/docs/adr-0001-alfred-vault-trial.md b/docs/adr-0001-alfred-vault-trial.md deleted file mode 100644 index e73b814..0000000 --- a/docs/adr-0001-alfred-vault-trial.md +++ /dev/null @@ -1,75 +0,0 @@ -# ADR 0001 — Alfred: agent-runtime trial writing to a Markdown vault - -**Status:** trial, 2026-09-20 -**Owner:** padreug -**Code:** `~/Work/tries/2026-09-20-alfred-vm/` (bohm), not yet in any aiolabs repo - -## Context - -The community-organizer spec (this repo, `docs/community-organizer-spec.md`) -defines capture as NIP-52 events scoped by NIP-72 communities, produced by -the `tracker` maubot plugin. Phase 1 of `tracker` shipped rules-only; the -LLM tier (§6.1 level 2), Nostr publishing (§4) and per-user signing (§7.2) -never landed. The 2026-06-02 pilot review recorded that the community had -already named the bot "Alfred" and asked for digests, an LLM fallback and -grammar tolerance. - -On 2026-09-19 a separate "2nd brain" landed for the operator: plain-Markdown -zk vaults, one Forgejo repo per vault, `brain todos` scanning `- [ ]` lines -in `journal/` and `projects/`, `brain sync` for git. The chateau vault -(`padreug/brain-chateaudufaune`) is one of them. - -The May 2026 planning session had suggested an OpenClaw/ZeroClaw-style -agent "running on its own machine" as an alternate runtime (spec §12). - -## Decision - -Run a **trial** of that alternate runtime, sized to one community and one -vault: - -- **Runtime:** ZeroClaw (0.8.3 from nixpkgs, rebuilt with `channel-matrix`; - upstream NixOS module) in a QEMU VM on bohm. Explicitly **not** on cfaun. -- **Store:** the chateau vault — Markdown + git, `main`, same repo the - operator's laptop syncs. Alfred commits and pushes every write with a - repo-scoped write deploy key; force-push is blocked on Forgejo. -- **Inference:** the optimus box (`http://192.168.0.33:8080/v1`, llama-swap: - GLM-4.7-Flash for chat, GPT-OSS-120B for the nightly digest). -- **Behaviour:** mention-gated replies in one Matrix room; a matrix-nio - sidecar logs the whole room per day; a 22:00 Europe/Paris cron job - digests the log into `journal/YYYY-MM-DD.md` (`## Chat digest (Alfred)`) - and proposes tasks; "what needs doing" runs a deterministic port of - `brain todos` so answers match the laptop. -- **Bounds:** `workspace_only`, shell allowlist `git` + `vault-todos`, no - web/delegation tools, autonomy `full` (switch to `supervised` if it - misbehaves). Prompt injection from the room is bounded to that repo; - git history is the undo. - -## Consequences - -- **Not spec-conformant.** Alfred emits no §4 events and knows nothing of - §5 communities or §7 signing. Nothing downstream (eink renderer, relays, - third-party Nostr clients) sees its output. It shares §3.1 vocabulary - (task / journal / done / list) and the §6 rule that capture never blocks - on classification (unsure → `inbox/`). -- **Two writers, one repo.** Laptop and bot both `pull --rebase` before - push and stage explicit paths only; the bot never touches `hubs/`, - `notes/`, `README.md`, `.zk/`. Conflicts abort and ask a human. -- **Local-model quality is the unknown.** Multi-step tool loops (edit - file + git) on GLM-Flash are unproven; the deterministic todos path is - immune. -- **Dependency churn.** ZeroClaw moves fast; config keys were read from the - v0.8.3 source. The upstream v0.8.5 flake did not evaluate (Cargo hash - mismatch), hence the nixpkgs rebuild. - -## Revisit when - -- The trial holds up for a few weeks → promote to cfaun as a - `services.zeroclaw` instance in `server-deploy` (sops for env + deploy - key, `networking.hosts` for optimus) and decide whether `tracker` is - retired or bridged (a small publisher turning vault commits into §4 - events would restore conformance). -- The agent is unreliable → fall back to a deterministic maubot plugin - (`dev.aiolabs.alfred`): Python vault writers, `brain todos` port, - in-process git under an asyncio lock, model used only to parse JSON. - Sketched in the 2026-09-20 planning session; not built. -- Either way, update spec §12 "Alternate storage (trial)" and this ADR. diff --git a/docs/community-organizer-spec.md b/docs/community-organizer-spec.md index 675e007..cd40f8b 100644 --- a/docs/community-organizer-spec.md +++ b/docs/community-organizer-spec.md @@ -855,16 +855,6 @@ scoping in §5. A ZeroClaw-based implementation would carry the `["client", "maubot-tracker", "..."]`; renderers ignore the difference since they filter by community `a`-tag. -### Alternate storage (trial) - -Since 2026-09-20 a ZeroClaw-based "Alfred" trial targets a plain-Markdown -zk vault synced over git (`padreug/brain-chateaudufaune`) as its store -instead of the §4 events. It is **not** spec-conformant — no NIP-52 -events, no §5 community scoping, no §7 signing — but keeps the §3.1 -vocabulary and the §6 rule that capture never blocks on classification -(unsure items go to `inbox/`). Rationale, bounds and revisit criteria in -[`adr-0001-alfred-vault-trial.md`](adr-0001-alfred-vault-trial.md). - ### Reference identity provider — operator-IdP pattern The aiolabs reference implementation runs the **operator-IdP-with- @@ -958,9 +948,6 @@ the bot signing as itself and human attribution carried in the ## Changelog -- **0.3** (2026-09-20) — §12 gains "Alternate storage (trial)": the - Alfred / ZeroClaw trial writes a Markdown git vault, not §4 events; - see ADR 0001. - **0.2** (2026-06-28) — reflect shipped state of the reference IdP + sidecar bunker. `aiolabs/lnbits#9` and `#18` are no longer in-flight; `aiolabs/nsecbunkerd` (deploys from `dev`) is live with diff --git a/journal/README.md b/journal/README.md index ae41088..e7b84de 100644 --- a/journal/README.md +++ b/journal/README.md @@ -40,12 +40,10 @@ CREATE TABLE entries ( user TEXT NOT NULL, -- @sender:domain room TEXT NOT NULL, -- !roomid:domain ts BIGINT NOT NULL, -- ms since epoch (from evt.timestamp) - text TEXT NOT NULL, -- raw entry body - event_id TEXT -- source event, so edits update in place (v2) + text TEXT NOT NULL -- raw entry body ); CREATE INDEX entries_user_ts ON entries (user, ts DESC); CREATE INDEX entries_ts ON entries (ts DESC); -CREATE INDEX entries_event_id ON entries (event_id); ``` Wipe data via the maubot UI's per-instance **Database** tab: @@ -60,32 +58,11 @@ rather keep IDs monotonic across resets.) ## Known quirks -- **Editing a `!journal` message updates its entry** (since v0.3.0). - Fix a typo, add a line, and the stored entry changes in place — the - bot confirms with a 📝 reaction on your message rather than posting - a reply. The original send time is kept, since `ts` records when the - work was logged, not when the wording was corrected. - - This works because entries are keyed on the source event id, added in - the v2 migration. Entries recorded before v0.3.0 have `event_id IS - NULL` and so can't be matched — editing one of those is a no-op. - - Worth knowing if you write other maubot plugins: mautrix swaps an - edit's content for `m.new_content` before the handler sees it - (`mautrix/types/event/message.py:393-396`), stripping the `* ` - fallback prefix, so an edit is **indistinguishable from a new - command**. Neither `@command.passive` nor `@command.new` filters - them. A plugin that doesn't call `evt.content.get_edit()` will - silently record a duplicate on every edit — which is exactly what - this plugin did before v0.3.0. - -- **Reactions never trigger the bot.** `@command.passive` registers on - `EventType.ROOM_MESSAGE` and filters `msgtype` to `(m.text,)`, so an - `m.reaction` fails both checks. If a "Logged for" appears right after - someone reacts, the trigger was an edit landing at the same moment — - a reply to an edit event renders as "This event could not be - displayed" in Element, which makes it look unrelated to any message. - +- **Edited messages don't re-trigger the bot.** Matrix sends edits as + a separate `m.replace` event that bots don't react to. If you typed + `!journal` then edited the message to add content, the bot saw only + the empty `!journal` and won't record. Send a fresh message instead + of editing. - **`!journal show ` runs the show query with that text as the user filter.** If it doesn't match any MXID, you get "No entries." Use a fully-qualified MXID like `@pat:ariege.io`. diff --git a/journal/journal.py b/journal/journal.py index 63098dd..2a78fe3 100644 --- a/journal/journal.py +++ b/journal/journal.py @@ -25,12 +25,6 @@ async def upgrade_v1(conn: Connection) -> None: await conn.execute("CREATE INDEX entries_ts ON entries (ts DESC)") -@upgrade_table.register(description="Track source event id so edits can update entries") -async def upgrade_v2(conn: Connection) -> None: - await conn.execute("ALTER TABLE entries ADD COLUMN event_id TEXT") - await conn.execute("CREATE INDEX entries_event_id ON entries (event_id)") - - # Match `!journal` followed by any whitespace (space, tab, OR newline) # and capture everything after. Maubot's @command.new parser only treats # *space* as the command/args delimiter, so `!journal\n` gets @@ -66,18 +60,6 @@ class JournalBot(Plugin): async def journal(self, evt: MessageEvent, match) -> None: rest = (match[1] or "").strip() - # An edit arrives as a *fresh* m.room.message whose content mautrix - # swaps for `m.new_content` before we see it - # (mautrix/types/event/message.py:393-396), stripping the "* " - # fallback prefix — so it is indistinguishable from a new command - # and used to record a duplicate row per edit. get_edit() returns - # the *original* event id, which is what entries are keyed on, so - # the edit updates that row in place instead. - edit_of = evt.content.get_edit() - if edit_of: - await self._apply_edit(evt, edit_of, rest) - return - if not rest: await evt.reply(_USAGE) return @@ -119,39 +101,10 @@ class JournalBot(Plugin): # Default: record the full rest (multi-line preserved) await self.database.execute( - "INSERT INTO entries (user, room, ts, text, event_id)" - " VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO entries (user, room, ts, text) VALUES ($1, $2, $3, $4)", evt.sender, evt.room_id, evt.timestamp, rest, - evt.event_id, ) await evt.reply(f"📓 Logged for {evt.sender}.") - - async def _apply_edit(self, evt: MessageEvent, original_id, rest: str) -> None: - """Apply an edit of a previously recorded `!journal` message.""" - row = await self.database.fetchrow( - "SELECT id FROM entries WHERE event_id = $1", original_id - ) - if row is None: - # Edit of a message that never became an entry: a `show`/`today` - # query, or an entry recorded before v0.3.0 started tracking - # event_id. Nothing to update, and re-recording would duplicate. - return - if not rest: - # Editing the body away would blank the entry; leave it alone. - return - - # `ts` deliberately keeps the original send time — it records when - # the work was logged, not when a typo was fixed. - await self.database.execute( - "UPDATE entries SET text = $1 WHERE event_id = $2", rest, original_id - ) - try: - # Confirm on the original message rather than replying: a reply - # to an edit event renders as "This event could not be displayed" - # in Element, and a message per keystroke-save is noise. - await self.client.react(evt.room_id, original_id, "📝") - except Exception: - self.log.debug("Could not react to edited entry", exc_info=True) diff --git a/journal/maubot.yaml b/journal/maubot.yaml index 7084d99..74e2ae3 100644 --- a/journal/maubot.yaml +++ b/journal/maubot.yaml @@ -1,6 +1,6 @@ maubot: 0.1.0 id: dev.aiolabs.journal -version: 0.3.0 +version: 0.2.0 license: AGPL-3.0-or-later modules: - journal