Skip to main content
Glama
arkaigrowth

waiting-on

by arkaigrowth
README.md
# waiting-on

A local-first, tenant-separated open-loops ledger for agentic workflows: it
tracks who has the ball, per domain, across email, agent sessions, and other
channels.

An "open loop" is anything waiting on a decision or a reply. waiting-on reads
the places those loops accumulate (an email account, your Claude Code and Codex
session transcripts, a voice queue, manually entered leads), normalizes each one
to a single record type, and stores it in a per-domain local SQLite database. It
then answers one question from several surfaces: what is still waiting on you,
and what are you waiting on someone else for.

The ledger core reads and tunes your data; it never sends anything. The one
write it can make to an outside system is an explicit, opt-in draft-creation
tool: `waiting_on_draft_reply` (and its `draft --create-gmail-draft` CLI
equivalent) can create a Gmail *draft* through an external Gmail MCP server, and
that is the only write path off the local store. There is no send path anywhere
in the code. waiting-on holds no credentials of its own and stores snippets
rather than full message bodies.

## Why it is built this way

The interesting parts are the guarantees, not the CRUD.

- **Fail-closed tenant walls.** Each domain maps to its own SQLite database
  file, with a fail-closed profile allow-list enforced in application code (the
  `Ledger` facade), not an OS-level sandbox. A session runs under a profile that names the domains it
  may open; an unknown or unmapped profile resolves to no domains rather than to
  everything. The `Ledger` facade raises `TenantWallError` when a caller reaches
  for a domain outside its profile, and `AmbiguousDomainError` when a bare
  `line_id` exists in more than one allowed domain, so it refuses to guess which
  tenant you meant instead of silently writing the wrong one.

- **Optimistic-concurrency writes with named failure modes.** Lead writes are
  versioned compare-and-swap operations against a WAL-mode database. A losing
  write raises `LeadWriteConflict`, and a lock-contention retry-exhaustion raises
  `LeadWriteBusy` (a subclass). The failure each one guards against is spelled
  out in its docstring, for example a completed lead being silently reopened by a
  stale writer.

- **Pluggable adapters over one contract.** Email, agent-threads, voice, and
  manual-lead collectors each normalize their source into a single
  `LineObservation`. Adding a channel means writing one adapter, not touching the
  ledger. The deterministic file-based collectors (agent-threads, voice, manual
  leads) degrade to empty results when their source is absent: they yield nothing
  rather than raising. The email path is the exception: it shells out to an
  external Gmail MCP server, and a failure of that subprocess surfaces as a
  `GmailMcpError` rather than being swallowed.

- **Deterministic core, zero-LLM collectors.** The agent-threads and voice
  adapters classify state with deterministic heuristics and replay, not a model
  call, so their output is reproducible and testable. An injected clock makes all
  age math deterministic under test.

- **Four surfaces over one ledger.** An MCP server (for agent tool use), a CLI, an
  fzf-driven tuning TUI, and a static self-contained HTML board all read the same
  store.

The test suite covers the tenant walls, the concurrency failure modes, each
adapter's normalization and fail-soft behavior, and the render surfaces. Run it
with `python3 -m pytest` from the repo root; it is 156 tests today.

## Architecture

```
email account         -> GmailMcpClient       -+
Claude/Codex sessions -> AgentThreadsAdapter   -+-> LineObservation -> per-domain
voice queue (JSONL)   -> VoiceAdapter          -+     (one contract)     SQLite DB
manual leads (JSON)   -> ManualLeadAdapter     -+                            |
                                                                            v
                                        MCP server | CLI | TUI | HTML board
```

The tenant boundary is enforced where roots and accounts resolve to a domain, so
an adapter can only ever read the sources authorized for the domain it is
collecting.

## Install

Requires Python 3.11 or newer. No required third-party dependencies for the core.

```
python3 -m pip install -e .
```

Optional extras: `.[mcp]` for the MCP server, `.[parquet]` for Parquet export.
The tuning TUI additionally needs `textual`, and the fzf TUI needs `fzf`.

You can also run straight from a checkout without installing, via the launchers
in `bin/`.

## Quickstart

The repository ships a synthetic `examples/demo-leads.json` so you can see the
shape without any configuration. With no config file, waiting-on uses a single
default domain.

```
# Import the demo leads into the local ledger
bin/waiting-on lead seed examples/demo-leads.json

# See who has the ball (your side first)
bin/waiting-on lead list

# A compact open-loops pane, suitable for a terminal or a cmux column
bin/waiting-on pane

# Render a static, self-contained HTML board from the current leads
python3 scripts/render_lead_board.py --out board.html
```

To wire up real sources (email accounts, agent-session roots, domains, and
profiles), copy `config.example.toml` to `~/.config/waiting-on/config.toml` and
edit it. The example file documents the tenant-wall and profile model inline.

## Documentation

- `docs/open-loops-ledger-design.md`: the design and phased plan, including the
  tenant-wall model.
- `docs/agent-threads-adapter-spec.md`: how the agent-session collector decides a
  session is a forgotten open loop.
- `docs/cli-contract.md`: the machine-readable CLI contract and exit codes.
- `docs/security.md`: what is and is not stored, and why there is no send path.
- `docs/studio-spec.md` and `docs/calibration-and-panes.md`: the TUI and the
  live-tuning panes.
- `docs/agent-integration.md`: driving waiting-on from an agent.

## Status

Early and actively developed: this repository was extracted from a working personal deployment, so the interfaces are real but may still shift, and the demo leads shipped here are synthetic.

Known rough edges:

- The agent-threads classifier is a deterministic tail heuristic over transcript
  events. It is conservative and tunable (idle-hours and max-age dials), but it
  can still misjudge an unusual session; treat its `waiting_on_me` calls as
  strong hints, not verdicts.
- The email adapter expects an external Gmail MCP server to be configured (its
  command is set in config). waiting-on deliberately does not hold Gmail
  credentials itself, so email refresh does nothing until that is wired up.
- Additional channels (for example iMessage) and an optional LLM enrichment pass
  are described in the design doc but not yet built. Today the collectors are
  deterministic and zero-LLM.
- The config example is intentionally minimal. Multi-domain setups work, but you
  configure the domains, accounts, and profiles by hand.

## License

MIT. See [LICENSE](LICENSE).

TDQS

C2.8/5.0

Scored across 12 tools

Disambiguation3/5

The alias between waiting_on_loops and waiting_on_list creates direct ambiguity, as they are the same operation. The three refresh tools are distinct but share the same verb prefix, which could cause misselection. Other tools are sufficiently differentiated.

Naming Consistency3/5

All tools share the consistent 'waiting_on_' prefix, but the suffixes are a mix of nouns (loops, health, export) and verb phrases (draft_reply, lead_add, lead_bump), with no clear pattern. The alias also undermines naming distinctness.

Tool Count5/5

With 12 tools, the server covers its domain without being overwhelming or insufficient. Each tool serves a distinct function except for the alias, and the count supports a focused scope.

Completeness3/5

The toolset covers listing, showing, refreshing, and basic lead management, but lacks operations to close or delete threads, send drafts, or remove leads. This leaves obvious gaps for a tracking system, limiting agent autonomy.

Maintenance

ActivitySlowing
ResponsivenessNo issues