feat(tracker): Phase 1 plugin — Matrix + SQLite, rules-only
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>
This commit is contained in:
parent
774cb44a4a
commit
b7a096a77a
6 changed files with 693 additions and 0 deletions
48
tracker/classify.py
Normal file
48
tracker/classify.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Rules-based classifier for !add freeform capture.
|
||||
|
||||
Level 1 of the spec's classifier conformance (§6.1). Maps obvious
|
||||
shapes to a kind+tags. Anything ambiguous returns (None, []) and the
|
||||
caller records the entry as `unclassified` for later sorting.
|
||||
|
||||
LLM fallback is Phase 2b and lives separately.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
_BUY_RE = re.compile(
|
||||
r"\b(buy|purchase|order|pick\s*up|grab|get\s+more)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_REMIND_PREFIX_RE = re.compile(
|
||||
r"\b(remind\s+me\s+to|don'?t\s+forget|remember\s+to)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_PAST_TENSE_RE = re.compile(
|
||||
r"\b("
|
||||
r"did|finished|completed|cleaned|watered|fed|opened|closed|"
|
||||
r"planted|harvested|fixed|moved|painted|swept|mucked|brushed|"
|
||||
r"installed|repaired|cooked|prepared"
|
||||
r")\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_TASK_INTENT_RE = re.compile(
|
||||
r"\b(need\s+to|should|have\s+to|must|todo|to\s+do)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def classify(body: str) -> tuple[str | None, list[str]]:
|
||||
"""Return (kind, tags). (None, []) means unclassified."""
|
||||
if _REMIND_PREFIX_RE.search(body):
|
||||
return ("remind", [])
|
||||
|
||||
if _BUY_RE.search(body):
|
||||
return ("task", ["buy"])
|
||||
|
||||
if _PAST_TENSE_RE.search(body):
|
||||
return ("journal", [])
|
||||
|
||||
if _TASK_INTENT_RE.search(body):
|
||||
return ("task", [])
|
||||
|
||||
return (None, [])
|
||||
Loading…
Add table
Add a link
Reference in a new issue