dmslanes/lanes

Lanes - team task boards with roles, history and an MCP endpoint for Claude and ChatGPT

A remix template · 0 remixes · Remix dmslanes/lanes

README.md

Lanes

Team task boards that run on Dodil and that you can drive from Claude or ChatGPT.

A workspace is a team. Its owner invites people as members (they edit) or viewers (they read). Boards hold lists, lists hold cards, and every change to a card is recorded: who, when, what changed. The same actions are available on a simple web page and through one MCP endpoint, so "what's on my plate this week?" or "move card 12 to Done" works from an assistant exactly as it does in the browser, and the assistant only sees what its person sees.

Demo: docs/demo.mp4, Claude Code connected to the live /mcp over OAuth: what's on my plate, move and label a card, read its history, search, add a card, list roles.

What it does

AreaIn Lanes
PeopleOwner invites by email as member or viewer, changes roles, removes people. Only the owner manages people.
BoardsBoards → lists → cards. Cards have a number (#12), title, description, assignee, due date and labels. Drag and drop between and within lists.
HistoryEvery card change is stored with actor, time and a field-by-field diff (list: Doing → Done).
MCPlist_boards, show_board, my_cards, overdue_cards, search_cards, card_history, add_card, move_card, assign_card, update_card, delete_card, create_board, list_members, invite_member, plus the extras below.
Dependencies"Card 4 is blocked by card 2" (block_card, unblock_card); "what is blocking the launch?" (what_is_blocking) walks the chain at any depth and skips cards already done. Loops are refused. Cards show ⛓ #2 on the board.
AI (Dodil Models)"Turn this card into a checklist" (card_to_checklist, appended to the description and kept in history), "find cards like this one" by meaning (find_similar_cards, embeddings), a short daily summary of a board (board_summary). Same buttons on the web page.
OverdueFlagged automatically: late cards turn red on the board, unless they sit in a Done list.
WebBoards, board view with drag and drop, card editor with history, "My plate" (due in 7 days), "Overdue", "People".

Rules that hold on both doors (and are tested on both): a viewer can't change anything; a board you are not on answers "not found", never "forbidden"; an assignee must be in the workspace; a stale edit is refused instead of silently overwriting someone else's change.

How it is built

app/
  domain/          Workspace, Membership, Board, BoardList, Card, Activity and the rules
  application/     BoardService: the use cases (move_card, assign_card, my_cards, overdue...)
                   ports.py: the Repository interface the use cases need
  infrastructure/  k3.py (DataK3 over the Postgres wire), models.py (Dodil Models), memory.py (tests), identity.py
  web/             FastAPI routes + one static page   -> call application/
  mcp/             MCP tools                          -> call application/
tests/             rules (in memory and against a real DataK3 bucket), both doors
schema.sql         the tables a remix creates
.dodil/remix.yaml  what a remix creates (bucket, user pool, service account)
.dodil/deploy.yaml how Dodil Git CI deploys it to Ignite

Design notes:

  • One place for every rule. Routes and tools are a few lines each: read the person from the gateway header, call a BoardService method, shape the answer. Permission checks live in Membership (require_edit, require_owner) and visibility in the use cases, so the two doors cannot disagree.
  • No login code. The Ignite gateway signs people in against the app's appid pool and injects X-Dodil-User; identity.py reads the email from it. Claude and ChatGPT reach /mcp through the same pool via OAuth (the app answers /.well-known/oauth-protected-resource).
  • Natural keys, no SERIAL. Ids are UUIDs chosen by the app. Card numbers are claimed with a plain INSERT into card_numbers, so two people adding a card at once never share a number.
  • Optimistic concurrency. Cards carry a version; an edit sends the version it read and a stale one gets 409 with nothing written.
  • AI behind a port. The use cases talk to an Assistant (complete, embed); models.py signs Dodil Models calls with the app's own service account (ignite.model-user in the remix), the tests use a fake. AI answers only see cards the person can see, and a viewer can ask for a summary or similar cards but not write a checklist. Embeddings use arctic-embed-m-v2: on short card titles it ranked by topic, where jina-embeddings-v4 scored "office chairs" as close to "send invoice" as "chase unpaid bills". The chat model (kimi-k2.6) reasons before it answers, so calls leave room for ~1.5k reasoning tokens and send no temperature (it refuses it).
  • Dependencies by card number. blocked_by holds numbers, so the history reads blocked_by: [] → [2] and Claude can say "card 4 waits on card 2".
  • Workspace-level membership. Seeing a board means being in its workspace. It keeps the rules simple; per-board sharing is listed under improvements.

Run it locally

python -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
DEV_ALLOW_ANON=1 LANES_STORAGE=memory .venv/bin/uvicorn app.main:app --reload
# open http://localhost:8000 ; add ?as=someone@x.io to act as another person
.venv/bin/pytest -q

DEV_ALLOW_ANON exists only for local runs; deployed, a request without the gateway's identity header is a 401. To run the rule tests against a real bucket: LANES_TEST_K3=1 K3_BUCKET=<a test bucket> DODIL_TOKEN=<token> pytest -q.

Remix it

dodil git remix dmslanes/lanes --to-org <your-org>

The remix creates the <repo>-data bucket with its tables, the <repo>-users pool and the <repo>-app service account (data + Models), puts the credentials in the new repo's CI secrets and deploys it.

One step after the remix (sign-in): turn on email + password sign-in for the new pool, once. The remix copies the pool and its callback URL, but a fresh pool starts with no sign-in connection (the page says "connection is not enabled for this pool").

dodil appid connection add <repo>-users local

If you renamed things and the sign-in page says the redirect is not allowed, check the callback too: dodil appid settings get <repo>-users must list https://<app host>/.dodil/auth/callback (copy the host from dodil ignite app get <repo>; settings set replaces the list, so keep what is there).

Then sign in on the web page and create your workspace: you are its owner. When you connect Claude or ChatGPT to https://<app host>/mcp, approve the client once: dodil appid client list <repo>-users then dodil appid client approve <repo>-users <client-id>.

What I would improve next

  • Per-board sharing on top of workspace membership.
  • Store card embeddings in a DataK3 vector column instead of process memory (fine up to a few thousand cards).
  • A scheduled daily summary posted to the board, instead of on request.
  • Comments and attachments on cards (attachments in the same bucket's object store).

Licence

MIT, see LICENSE. Built by Design Master Solutions for the Dodil Boards Challenge.