Turn scattered work noise into "here's your one move."
Built for ADHD brains: reduce cognitive load, never add to it. It does not hoard raw feeds, and it does not invent busywork. It closes the loop.
ADHDecoder watches the places people quietly point things at you (chat, email, issues, calendar, CRM, calls, docs), decodes each new item into a short brief, and tracks the promises flowing in both directions: what others owe you (chase in) and what you owe them (radiate out). Then it surfaces the one time-sensitive thing you're most at risk of dropping. Every arrow out to you is a draft you approve - nothing auto-sends, auto-posts, or creates a task on its own.
flowchart LR
S["Your sources<br/>chat · email · issues<br/>calendar · CRM · calls · docs"] --> SW["sweep"]
SW --> RC["reconcile<br/>(verify vs the live source)"]
RC --> L[("ledger<br/>promises")]
L --> CI["chase-in<br/>nudge them"]
L --> RO["radiate-out<br/>tell them first"]
L --> DR["drift<br/>what's gone quiet"]
L --> PA["panic<br/>break the spiral"]
CI --> YOU(("You:<br/>approve + send"))
RO --> YOU
DR --> YOU
PA --> YOU
Each capability is a skill; together they close the follow-up loop:
| Skill | What it does | |
|---|---|---|
| 📓 | ledger | the promise store: who owes what, by when, which direction |
| ➕ | ledger | the promise store; also "add a task" -> a real task note |
| ⏱️ | set-the-clock | captures a promise the moment work flows in or out |
| 📣 | chase-in | surfaces slips as tiered, ready-to-send nudges |
| 🛰️ | radiate-out | publishes status so people stop chasing you |
| 🌫️ | drift | flags what has quietly gone stale |
| 🧭 | projects | declare a multi-week effort; it claims matching work |
| 🚨 | panic | mid-spiral, hands you the single next move |
| 🧹 | sweep | pulls promises in from your configured sources |
| 🔎 | reconcile | cross-checks against the live source before acting |
| 📊 | board | renders your ledger into a multi-tab HTML dashboard |
| 🧭 | setup | guided, conversational config builder (no hand-editing) |
| 💬 | help | orientation + the command cheat-sheet |
| 🩺 | doctor | read-only health check of your setup |
Plus an optional read-only Obsidian backend (run it against your existing
Obsidian notes - Markdown + YAML frontmatter - instead of a fresh store; ships in
adapters/obsidian/) and a schedulable daily-run routine that does one pass
and leaves you a board.
Each skill below is a paragraph, not the spec - full detail lives in each
skills/<name>/SKILL.md and the relevant reference/*.md. Promises point one
of two directions, set by set-the-clock: i-owe-them (you owe someone) or
they-owe-me (someone owes you).
Renders the ledger into the multi-tab HTML dashboard (Board / Shipped /
Waiting on Others / Tomorrow's Headlines / Projects / History). Triggered by
"show my board", "refresh the dashboard", "open the board", "render the
dashboard", or "regenerate my board". It calls scripts/render-board.py, a
pure function of config + ledger + clock that never reconciles on its own -
so board checks ledger freshness first (sweeping if lastSwept is stale) and
reconciles any card whose verifyStatus is null or TTL-stale before handing
off to the renderer. reference/dashboard.md explains a non-obvious fix: "done
today" and "ready to close" originally shared one visual style distinguished
only by a group label, and at a glance the board read as more finished than
it actually was - so ready-to-close got its own teal variant, because a
pending confirmation and genuinely finished work are not the same state.
Phase 3: turns slipping promises into tiered, ready-to-send nudges. Triggered
by "who do I need to chase", "what's slipping", "what should I follow up on",
"run my chases", "chase in", "who's overdue and what do I say", "draft a
nudge for <person>", or "what's falling through the cracks". It reads the
ledger only, recomputing overdue from expectBy vs. today, and never sweeps
a source directly - before drafting any nudge it hands the candidate to
reconcile, its only source cross-check. Tiering is by stakes (high-stakes
surfaces from due-soon onward; normal-stakes stays quiet until overdue), and
a slipping promise never reappears as a new item - it moves up an escalation
rung (friendly check-in → firmer → loop in a manager), which is the "no
flood" principle made concrete.
The scheduled, non-interactive routine: sweep → reconcile → update the
ledger → refresh the board → one-line recap, in a single pass. Triggered by
"run the decoder", "daily run", "do a scheduled run", "refresh the board", or
when wiring a cron/scheduled task. Because a scheduled run can never prompt
for approval, it never calls capture or promote - both require
--confirmed, an explicit human action - so an unattended pass can enrich
existing promises but never create a note. It also enforces the rule that
every enabled source is swept at least once per calendar day regardless of
weight or cadence, closing the gap where a quiet low-priority source
could otherwise go unswept indefinitely.
A read-only health check, triggered by "check my setup", "adhdecoder
doctor", "is this configured right", "diagnose ADHDecoder", or "why isn't
the decoder working". It reports each check as OK or a gap with a one-line
fix - runtime, config, backend, write mode, record-store integrity, schema,
connector presence, suppressions/sweep results - and never repairs anything
itself. Suppressions are the one place it still does the reading: the board
recap carries a bare count, so doctor is where each suppressed ref has to
account for its reason rather than becoming permanent by default. Connector
presence is deliberately reported as unchecked rather than a false "OK",
because a subprocess cannot see which MCP connectors the running session has
attached. Schema integrity exists because the schema used
to be prose only: different runs invented different field names for "this
note is malformed," and one of those inventions was write-only - a run
recorded a damaged note and nothing surfaced it for weeks.
Flags promises that look stalled, observationally rather than accusingly.
Triggered by "what's drifting", "what's stalled", "what have I not
touched", or "check for drift", and internally whenever panic runs its
drift check or a sweep does a quiet passive flag. It computes staleness from
ledger dates - days since lastVerified for overdue/due-soon-high-stakes
items with a real due date, plus a business-day fallback for open items with
none - then reconciles each surfaced candidate before showing it, the same
bounded-to-candidates discipline as chase-in. The no-due-date fallback
measures staleness from when a human last touched the item
(derived.lastTouched), not from lastVerified - measuring from
lastVerified was tried first and hid real rot, because a sweep refreshes
lastVerified on everything it looks at, so the automated pass meant to
catch stalled work was instead certifying it as fresh; on a live ledger,
three untouched items showed "0 days stale" under that scheme.
Orientation for someone who just installed ADHDecoder, triggered by "what
can ADHDecoder do", "get started with ADHDecoder", "how do I use
ADHDecoder", "ADHDecoder help", or "what are the commands". Read-only: a
two-line explanation of the loop-closer model (chase-in + radiate-out,
everything a draft) plus the command cheat-sheet, leading with a pointer to
setup if config is missing or thin. It's the entry point that routes
everywhere else - not set up, point to setup; set up but something looks
off, point to doctor.
Phase 1, the promise store itself: add a task, or read/write/query
promises. Triggered by "add a task", "remind me to <X>", "I need to <X>",
"put this on my list", "capture this", "add a task to <X> by <date>", and
read-side phrases like "what am I waiting on", "who owes me", "mark this
met/done", "what's overdue". "Add a task" is the most common path - it
writes a real note immediately, with no due date and no interrogation -
while a sweep-found "they-owe-me" stall goes through add, which does
enforce the full reality gate (named owner + concrete what + expectBy).
Every write, on any backend, goes through scripts/ledger_write.py and
every read through scripts/ledger_query.py - the single implementations
every other skill calls rather than re-deriving overdue/stakes/staleness
itself, because a second derivation of "overdue" is a second answer, and the
two disagree exactly where it hurts: an overridden deadline chased as if it
were hard, or a snoozed item resurfacing.
The reactive spiral-breaker, triggered by "panic", "SOS", "I'm freaking out", "I'm spiraling", "what's on fire", "I don't know where to start", or "I'm overwhelmed". It shows the top 2-3 most time-sensitive items (never the full board), a drift check, the one item likely being avoided (approximated as the most time-sensitive i-owe-them item), and one small next move - pure ephemeral, rendered in chat only, writing no promise data itself. It reconciles only the handful of items it's about to show, reusing drift's reconcile results within the same run so nothing is verified twice - the speed that makes it usable mid-spiral. The design principle stated plainly in its own description: it regulates, it does not aggregate; dumping the full ledger is the failure mode this skill exists to prevent.
Declares and tracks a multi-week effort that then claims matching work as it
arrives. Triggered by "start a new project", "I'd like to track X as a
project", "I've been assigned X", "add this to my <name> project", "how are
my projects", "check in on <project>", or "stop tracking <project>". It
interviews the user one question at a time and always previews what a
project's keyword/source rules would actually claim before writing, because
the words a user says are rarely the words in their ledger: measured against
a real 100+ promise ledger, "tech writing" and "documentation" both matched
zero items, while the real vocabulary in use was "doc," "docs," "Confluence
page," "playbook." A project is declared, never inferred - a customer is
never treated as one - and it only ever writes state.json, never a note,
because it's a lens over the ledger, not a second store.
Phase 4, the mirror of chase-in: composes a per-context "Where things stand"
status draft. Triggered by "give me a status update for <context>", "where
do things stand with <context>", "draft an update for <context>", "what
should I tell <context>", "post a status to <channel>", or reactively "any
update on <thing>". It reads the ledger through the same Query interface as
chase-in/drift/panic, groups by context, and - the hard gate - only includes
a promise in the outward draft once reconcile has confirmed it against its
live source; anything unverified moves to a separate internal "confirm
before sending" list instead. This gate replaced a plain freshness check
specifically because freshness alone missed a real mis-attributed item in
production - a promise tagged to the wrong customer read as "fresh" and
would have gone out regardless, which is why verification, not recency, is
the bar.
The cross-cutting verification skill: cross-checks a promise against its
live source before it's chased or published. Triggered explicitly by
"verify this", "double-check X against Jira/Salesforce/Slack", "is this
still open", "reconcile my chases", "confirm before we publish", and
internally by every other skill before it surfaces or acts on an item. It
dispatches by promise.source.type to a per-source adapter (issues, crm,
chat, email, calendar, docs, calls) and is read-only against every source
and against read-only backends - only the builtin state.json backend gets
verify-metadata writes. Its mis-attribution signal is the clearest example
of design-by-measurement in this repo: it started as a hard verdict
(automatically marking a promise mis-attributed whenever its owner
wasn't on that context's people roster), but tested against a real
31-promise ledger it fired on 8 of 10 checkable promises and was wrong
nearly every time, because real owner values are prose describing a
vendor, a team, or several people at once - not a single roster name. It was
downgraded to an advisory signal that only fires on actual cross-context
evidence, cutting false positives from 8 to 2.
Phase 2, captures the promised-by date at the moment work flows in or out.
Two triggers: (1) every decode reply ends with a clock-setting question; (2)
an "outbound watch" fires only on high-stakes threads when the user gives
info out with no return date - "let them know", "I'll send them X", "told
them I'd...", "just replied", "sent the update", "answered <person>". It
never auto-logs and never auto-sends - it prompts, the user confirms, and
the write is handed to the ledger skill's reality gate (named owner +
concrete what + a date, or explicit user confirmation). It gates first on
"is this even a promise": a task the user gives themselves ("add a task",
"remind me to X") has no counterparty and routes straight to ledger's
capture instead, because interrogating someone for an owner and deadline
that don't exist is worse than doing nothing - and the outbound-watch
trigger is deliberately scoped to high-stakes threads only so it doesn't
fire on every message that goes out (no flood).
The source-facing pass that populates the ledger. Triggered by "run a
sweep", "scan my sources", "what's stalled across my tools", "check for new
stalls", "sweep my chat/email/issues", or "refresh the ledger from my
sources". For each configured source it finds items pointed at the user,
applies the four-part stall signal (user owes the next move, genuinely still
open, gone quiet in business days, someone is waiting), and - the
sweep-to-reconcile relationship at the center of the whole system - runs
every candidate through the same per-source reconcile adapters the rest of
ADHDecoder uses before writing anything: sweep finds candidates, reconcile
verifies them, and only a verified-open result proceeds to dedup and
write. The three-pass chat adapter (mention search, a self-expanding
known-channel registry, silent-reply thread tracking) exists because of a
specific failure that justified building it: search alone was proven
unreliable against real data when a concise mention search showed two
customers' threads as unanswered when the user had actually already replied
and acted the same day - both false alarms that reading the full thread
would have caught. A sweep is also blocked from re-raising a ref you have
retired: suppress records a source ref that must never become a promise
again (with a required reason), and add refuses one outright, so the block
is enforced rather than remembered. That gate exists because of its own
specific failure - a ticket where the user was only a watcher was captured,
correctly marked met, and then reopened by a later scheduled sweep, costing
three rounds of attention for work that had already shipped.
Prerequisite: Python 3.8 or newer on your PATH as python3. Nothing to
install with it. A few operations (the ledger read, the board render, the
schema check) are committed scripts rather than prose, so they behave the same
every run. They use only the Python standard library - no pip install, no
virtualenv - so macOS (/usr/bin/python3) and any mainstream Linux already
satisfy this. On Windows, install Python and make sure python3 resolves.
- Add the marketplace - from GitHub,
/plugin marketplace add <owner>/adhdecoder, or from a local clone,/plugin marketplace add /path/to/adhdecoder. - Install the plugin:
/plugin install adhdecoder@adhdecoder. - Reload:
/reload-plugins. - Check it: run
doctor. Its first check confirmspython3resolves and the scripts run, so a missing runtime is diagnosed up front rather than mid-task.
No JSON to hand-edit - setup builds your config for you (next).
help- two-line orientation + the command cheat-sheet.setup- a guided conversation that builds yourconfig.jsonand initializesstate.json(sources, identity, backend, schedule, optional context discovery). Ask it "set me up."- "what's slipping" - your first
chase-inboard.
Not sure your setup is sound? Ask doctor ("check my setup").
setup writes these; this is what each field means. Full template:
config/decoder.config.example.json.
| Field | Meaning |
|---|---|
identity.name / identity.email |
you |
identity.handles.chat / .crm |
your user id per source (e.g. Slack member id) |
storage.adapter |
"filesystem" (the v0.1 adapter) |
storage.instancePath |
absolute path to your instance folder (holds config.json + state.json) - outside this repo |
storage.knowledgePath |
absolute path to your knowledge vault |
storage.overrides.* |
filenames/dirs: stateFile, radarFile, archiveFile, tasksDir, dashboardFile |
ledger.backend |
"builtin" (default, writes state.json) or a note-backed adapter name |
ledger.writeMode |
"readonly" (default) or "readwrite" (post-cutover only; see below) |
ledger.cutover.singleWriterConfirmed |
your explicit confirmation that nothing else writes the note store; required for readwrite |
watchlist.customers / .people |
priority entities that raise stakes |
projects (in state.json, not config) |
declared multi-week efforts; see reference/projects.md |
contacts |
per-context channels + people (used by reconcile / sweep) |
sources[].type |
issues | chat | email | calendar | crm | docs | calls |
sources[].enabled |
whether this source is swept |
sources[].category |
the ~~category placeholder (see CONNECTORS.md) |
sources[].weight |
high | medium | low - sweep order/depth + surfacing tiebreak |
sources[].cadence |
every-run | daily | hourly |
sources[].tz / .noise |
optional: the source's own timezone; a known bulk-sync noise pattern to ignore |
schedule.pivots |
run times, e.g. ["08:30","12:30","16:00"] |
schedule.timezone |
IANA timezone |
schedule.boardPath |
where the refreshed board is written each run (unset = chat only) |
lastSwept |
managed by the plugin; leave null |
The plugin talks to storage through an adapter, not a hardcoded location.
- Filesystem adapter (built now): for storage that keeps real files
physically present on the machine that runs sweeps (local disk, Obsidian
Sync, Syncthing, git, or pinned Dropbox/OneDrive). You set
instancePathandknowledgePath. - Connector adapters (spec'd, not yet shipped): for cloud-native stores
that serve placeholders instead of real files, reading/writing via the
service API. Contract in
reference/connector-adapters.md.
Ledger backend is a separate axis (ledger.backend):
builtin(default): the promise store isstate.json- always writable.- The Obsidian adapter (optional): overlay your existing Obsidian notes.
Set
ledger.backend: "obsidian"(any valueXresolves to aledger-Xskill). Read-only by default: ADHDecoder never mutates the notes, and its own metadata (snooze, verify results) goes to astate.jsoncompanion. After a deliberate cutover (reference/cutover.md: retire your old writer, confirm single-writer, flipwriteMode: "readwrite") it also applies your approved actions - mark met, updates, promotions - directly to the notes. Ships inadapters/obsidian/; seereference/ledger-backend-interface.md. Long-lived sweep-found promises can be promoted into real notes, draft first, always with your approval (reference/promotion.md).
Two rules that matter: 👀 No hidden files (everything written is visible, never dot-prefixed) and ✍️ Single writer (run sweeps on one machine at a time, or synced state can conflict).
- "what's slipping" / "who do I chase" → chase-in
- "what's drifting / gone quiet" → drift
- "panic" / "I'm overwhelmed" → panic
- "where do things stand for <context>" → radiate-out
- "show my board" / "refresh the dashboard" → board
- "is this still open / reconcile this" → reconcile
- "add a task" / "remind me to <X>" → ledger (a real task note, no due date needed)
- "<someone> owes me <X> by <date>" / replying to an ask → set-the-clock
- "run a sweep" / "daily run" → sweep / daily-run
- "help" / "set me up" / "check my setup" → help / setup / doctor
ADHDecoder can run on a schedule via the daily-run routine, which does one
non-interactive pass: sweep the configured sources (ordered by weight,
honoring cadence, every enabled source at least once a day) → reconcile the
about-to-surface items → update the ledger → refresh a read-only board file →
print a one-line recap. It drafts and updates only; it never auto-sends or
auto-posts. Full detail in reference/scheduling.md.
The board is a multi-tab HTML dashboard (Board / Shipped / Waiting on Others /
Tomorrow's Headlines / Projects / History), regenerated from the ledger each run. The repo
ships a data-free template at assets/dashboard-template.html; only the rendered
output at your boardPath holds your data. Ask "show my board" or "refresh
the dashboard" any time to re-render it on demand (see reference/dashboard.md).
- Trigger the routine at your
pivots. The plugin describes the routine; your host scheduler runs it. Add a scheduled task at each time inschedule.pivotsthat invokes thedaily-runskill. - "Early and often" (optional). Add extra light runs (e.g. hourly) that sweep
only
every-run/ high-weightsources, so chat stays fresh. - Set
schedule.boardPathto a durable file (e.g. in your vault). Each run overwrites it with the current board; if unset, a run only prints to chat.
weight and cadence shape emphasis and frequency, never urgency: ranking is
always stakes > time > weight, so a genuine emergency from a low-weight
source still surfaces first.
The non-negotiables, enforced everywhere:
- Never auto-send, never auto-post. Every reply and status is a draft you approve.
- Never auto-create tasks. Promotion is always deliberate.
- Never flood. Dedup hard; overdue items get more prominent, not more numerous.
- Verified before surfaced. Nothing is presented as a settled fact or action without a fresh reconcile verdict; verified-only goes to any customer-facing surface, and no internal links land in customer-facing copy.
- Flag the sensitive and the "bigger than it looks."
ADHDecoder is split into three buckets so it can follow you anywhere - a new job, a job hunt, home life:
- The plugin (this repo) = the method. Skills, formats, guardrails. Zero personal or company data. Portable, versioned, install anywhere. Keep it forever.
- The instance layer = your config + state. A
config.jsonandstate.jsonat a path you choose, outside this repo. Company-bound and disposable. - The knowledge base = your files. Radar, tasks, dashboard, as plain Markdown/HTML in a folder you point at (e.g. an Obsidian vault).
Your data never lives in the plugin. A .gitignore guards against accidentally
committing a config.json/state.json even if you point instancePath into the
repo. Leaving a context? Drop the instance, keep the method.
The durable method lives in reference/method.md; onboarding detail in
reference/onboarding.md; per-capability specs (sweep, reconciliation,
verification discipline, scheduling, source-links) live alongside them in
reference/; optional backend adapters live in adapters/ (e.g. the Obsidian
adapter in adapters/obsidian/). Repo/maintainer orientation is in CLAUDE.md.