sea-lite
by rafe-walker
README.md
# sea-lite
**A work queue for parallel coding agents that can't collide.**
Run three agents against one backlog and the failure mode is not that they
are slow. It is that two of them pick up the same ticket, both "finish" it,
and the one that finishes second overwrites the first. Or one of them dies
holding a ticket and nobody else ever touches it.
sea-lite is a small MCP server where tickets are **fenced**:
- `sea_next` atomically picks the best eligible ticket **and** claims it. The
caller gets a one-time **claim token**; every write (`sea_update`,
`sea_transition`, `sea_link`) must present it. A write from anyone else is
refused and says who holds the ticket and until when.
- Claims are **leases** (≤ 600 s). An agent that dies stops refreshing; the
next reader sees the lapsed lease, treats the ticket as free, and records
that it did. No sweeper, no cron.
- Tickets carry **typed links** (`depends_on`, `blocks`, `parent_of`, …).
`sea_next` will not hand out a ticket whose predecessors are still open, and
the precedence graph is kept acyclic on every write.
- `sea_apply` lays down up to 50 tickets and 100 links in **one all-or-nothing
write**, so an epic with its children and dependency edges lands as a unit
or not at all.
- Every change is an event on a **hash chain** — the one
[kronicle-lite](https://github.com/rafe-walker/kronicle-lite) keeps. The
same SQLite file holds both. `sea_history` shows a ticket's record and
whether the chain still verifies; `sea-lite rebuild` replays the tables from
the chain.
One process, one file, no accounts. Runs over stdio next to Claude Code,
Cursor, Claude Desktop or any MCP client. Several server processes (one per
agent, or one shared) can point at the same file; SQLite's write lock and
`BEGIN IMMEDIATE` make claims exclusive across processes.
## Install
Requires Node ≥ 20.
```sh
claude mcp add sea-lite -- npx -y sea-lite serve
```
Or by hand (`.cursor/mcp.json`, `claude_desktop_config.json`):
```json
{ "mcpServers": { "sea-lite": { "command": "npx", "args": ["-y", "sea-lite", "serve", "--actor", "cursor"] } } }
```
Data lives in `~/.kronicle-lite/kronicle-lite.db` (or `--data-dir`, or
`$KRONICLE_LITE_DATA_DIR`), shared with kronicle-lite if you run that too.
Optional: copy `skills/work-queue/` into `.claude/skills/work-queue/` so the
agent knows the claim → work → release loop.
## Sixty-second tour
```sh
# a scratch store
export KRONICLE_LITE_DATA_DIR=/tmp/sea-demo
# the server, as an agent would see it (stdio; Ctrl-D to stop)
npx sea-lite serve --actor me
# look at the queue from a shell
npx sea-lite list
npx sea-lite show 0060ea49
npx sea-lite verify # recompute every chain hash, exit 1 on mismatch
npx sea-lite rebuild # drop + replay the sea tables from the chain
```
The agent's loop, in tool calls:
```
sea_next → { ticket, claim_token, expires_at }
sea_update (notes, tags) fenced by claim_token
sea_refresh if the work runs long (≤ 600 s per refresh)
sea_transition to=resolved terminal states release the claim
— or —
sea_release note="where I stopped"
```
## Tools
| Tool | What it does |
| --- | --- |
| `sea_create` | One ticket (`idea`, or `triaged` to make it dispatchable). Returns `possible_duplicates` (exact + full-text title match) but never blocks. `claim_seconds` claims it in the same call. |
| `sea_apply` | 1–50 tickets + ≤ 100 links between them, all-or-nothing. Rejects duplicate refs, foreign refs, self-loops, duplicate links and cycles with a list of every problem. `dry_run`. |
| `sea_get` | Ticket by id or ≥ 8-char prefix: fields, `row_version`, claim holder/expiry (never the token), active links with peer title/status, notes, `legal_next`. |
| `sea_list` | Filters (status, kind, priority, assignee `me`/`none`, tag, claimed `any`/`yes`/`no`/`mine`), FTS5 `query`, cursor paging, `compact`/`full`. |
| `sea_claim` | Lease one or a set of tickets (all-or-nothing, locked in id order). Returns a token per ticket. Re-claiming your own ticket rotates the token. |
| `sea_refresh` | Extend a lease to now + N (≤ 600); never shortens; holder-only. Not chained. |
| `sea_release` | Holder gives the ticket back (with a hand-off `note`). `force: true` for operators (`--role operator`), recorded as a force release. |
| `sea_update` | Title/body/kind/priority/assignee, tags add/remove, append a note. Fenced; `expected_version` (OCC); `idempotency_key`; `dry_run`. |
| `sea_transition` | Move status. Fenced. Rejections carry `legal_next`. `duplicate` needs `duplicate_of` and records a `duplicates` link. Terminal states release the claim. |
| `sea_link` | `add` / `remove` a typed link, fenced on `from`. `parent_of` / `depends_on` / `blocks` stay acyclic across existing links. Remove tombstones. |
| `sea_next` | Pick + claim the highest-priority unclaimed `triaged`/`in_progress` ticket assigned to me or unassigned whose predecessors are all terminal. |
| `sea_history` | Events for one ticket, or a `since_seq` feed of all sea events, with chain verification status. |
Plus a `sea://tickets/{id}` resource (same shape as `sea_get`).
Every error is `{ error: <code>, message, details }` with a closed code set
(`fence_invalid`, `fence_foreign_holder`, `fence_expired`, `already_claimed`,
`version_conflict`, `illegal_transition`, `cycle`, `batch_invalid`, …) so an
agent can branch on it.
## Lifecycle
```mermaid
stateDiagram-v2
[*] --> idea
idea --> triaged
triaged --> in_progress
in_progress --> resolved
state holding {
parked
blocked
needs_decision
}
idea --> holding
triaged --> holding
in_progress --> holding
holding --> idea
holding --> triaged
holding --> in_progress
state terminal {
resolved
wont_fix
duplicate
dropped
merged
}
idea --> terminal
triaged --> terminal
in_progress --> terminal
holding --> terminal
terminal --> [*]
```
Work track moves are one step forward at a time. Any active ticket may enter a
holding state or a terminal state; holding states resume only to `idea`,
`triaged` or `in_progress`; terminals have no exit. An optional design track
(`--tracks work,design`) adds `idea → in_design → ready → in_progress`.
Kinds: `feature research investigation refactor curiosity question other bug
chore incident security epic spike log docs decision`. Priorities: `urgent
high medium low parked` (`parked` is never dispatched by `sea_next`).
Link types: `parent_of relates_to inspired_by responds_to conflicts_with
supersedes blocks depends_on condenses_into branches_from references
duplicates`. Direction reads `from <type> to`.
## Composes with kronicle-lite
sea-lite does not have its own chain. It opens the kronicle-lite store (or
adopts an already-open one) and appends `sea_ticket.*` / `sea_link.*` /
`sea.batch_applied` events to the same hash chain, inside the same
transaction as the row change. That buys you:
- **One `get_system_state`.** An agent that orients with kronicle-lite sees
ticket creations, status changes and link changes in its delta. Routine
claim churn (`claimed`, `claim_released`, `claim_expired`) rolls up by
default so it does not crowd the briefing; a force release is enumerated.
- **Retractions.** A tombstoned link is classified as a safety retraction by
kronicle-lite's classifier, like a tombstoned block.
- **One verify, one export.** `kronicle-lite verify-chain`, `checkpoint` and
`export` cover sea events. After `kronicle-lite import`, run
`sea-lite rebuild` to re-project the tables (import rebuilds documents
only).
- **Shared idempotency.** `idempotency_key` lands in the chain's unique
column under a `sea:` prefix; a replay returns the first result.
You can run sea-lite without ever running kronicle-lite's server. The
dependency is the library.
## Limitations (read these)
- **Cooperative fencing.** A claim token stops a well-behaved agent from
writing a ticket it does not hold. It does not stop a process with the
SQLite file from editing rows directly. The chain makes that detectable
(`sea-lite verify`), not impossible.
- **Self-asserted actors.** `--actor` and per-call `actor_id` are labels the
caller chooses. The "different holder" check is defence in depth against a
leaked token, not authentication. See `docs/THREAT-MODEL.md`.
- **Lazy expiry.** A lapsed lease is noticed by the next read or write that
touches the ticket (or by `sea_list` / `sea_next`, which sweep). Nothing
fires on its own.
- **Local only.** stdio transport, one machine, one file.
## Roadmap
- Links from `sea_apply` to tickets that already exist (today links are
batch-local; use `sea_link` after apply).
- Bulk mutate (set priority / tags / assignee across a filter).
- Streamable HTTP transport alongside stdio.
- A read-only web board over the same file.
## Development
```sh
npm install
npm run build && npm test && npm run lint && npm run typecheck
```
During development `kronicle-lite` is a `file:../kronicle-lite` dependency;
`package.json` carries a TODO to switch it to the published range before
release.
## License
Apache-2.0. See `LICENSE` and `NOTICE`.
---
Built off-grid in Cochise County, AZ — https://theexiledentrepreneur.com
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues