eduard/cobra-boards

Cobra Boards, team task boards you can run from Claude or ChatGPT. FastAPI + DataK3 + appid + Dodil Models + MCP.

A remix template · 1 remixes · Remix eduard/cobra-boards

README.md

<p align="center"><img src="app/boards/doors/static/cobra-192.png" width="110" alt="Cobra Boards"></p>

Cobra Boards

Team task boards you can run from the web page, or straight from ChatGPT and Claude. Boards → lists → cards, with owner / member / viewer roles, a full change history, blockers, comments with @mentions, notifications, files on cards, a board that updates live, and an AI that suggests but never writes on its own. Everything runs on Dodil: DataK3 stores it (tables, vectors and the files), an appid pool signs people in, Ignite runs it, Dodil Models does the AI, and Dodil Git CI lints, tests and deploys it.

223 tests · 29 MCP action tools + 3 prompts · ruff clean · 98% coverage of the rules and doors · remix into a fresh org: tested

Someone says "I need a Trello". You say "remix this, it's yours":

dodil git remix eduard/cobra-boards --to-org <your-org> --name <your-repo>   # name ≤ 18 chars
Livehttps://cobra-boards-eduard-8080.ignite.dodil.cloud
MCPhttps://cobra-boards-eduard-8080.ignite.dodil.cloud/mcp
Sourcehttps://git.dodil.io/eduard/cobra-boards (MIT)

Cobra Boards: the board and a live ChatGPT session through the MCP endpoint

More screens: the board · sign-in (the appid pool, branded) · card detail · viewer, read only · My week · phone · themes Daylight and Venom


Judge it in 60 seconds

Demo team: sign in as each role

Open https://cobra-boards-eduard-8080.ignite.dodil.cloud and sign in with any of these. They share the boards Product Launch (all three) and Ops (owner and member). Password for all three: CobraDemo-2026!

RoleEmailTry this
Ownerowner@cobra-demo.comMembers: change a role, invite someone. Open #1 Ship v1 → What is blocking this?
Membermember@cobra-demo.comThe 🔔 has 8 notifications. Drag a card, comment with @owner, attach a file, archive a card.
Viewerviewer@cobra-demo.comRead only: cards don't drag, no comment box, no upload. Over MCP every write is refused.

Two windows, two roles: move a card as the member and watch the owner's board update by itself. MCP endpoint for ChatGPT / Claude: https://cobra-boards-eduard-8080.ignite.dodil.cloud/mcp (sign in with one of the accounts above). The demo data was written with scripts/seed_demo_team.py, through the app's own use cases.

Or start fresh with your own account

  1. Open the live URL and create an account (the pool allows sign-up). Your first visit gives you a private Welcome to Cobra 🐍 board, already populated: two overdue cards flagged in red, a chain of blockers, a half-done checklist, labels and history. Nobody else sees it.
  2. Drag a card to another list (a gold line shows where it lands), then open it: the History says who moved it, when, and what changed (To do → Doing).
  3. Open #1 Launch the new website → "What is blocking this?". It walks the chain (#1 ← #2 QA ← #3 login bug, and #4 security review) and says where to start.
  4. AI, on Dodil Models only: on a card, ✦ Suggest a checklist (nothing is saved until you pick the steps) and ✦ Find similar cards (vector search over DataK3); on the board, ✦ Daily briefing (the numbers are counted from the board; the model only words them).
  5. Talk on a card: write a comment with @name (the chips under the box fill it in). The person gets a notification under the 🔔; the card's assignee hears about it too. Attach a file (up to 10 MB, kept in this board's DataK3 bucket); images show a thumbnail.
  6. Live: open the same board in a second window as someone else and move a card there. Within seconds the first window updates itself and says who moved what (the green live dot).
  7. Archive, don't delete: Archive card takes it off the board with its number, history and comments; Archived → Restore puts it back. New boards start from a template (Sprint, Launch, Onboarding, Support, or the basic three lists).
  8. Roles: Members → invite a second email of yours as viewer and sign in with it: the board says read only, cards don't drag, there's no "add card", and every write through the API or MCP is refused (403 / tool error). A board you're not on is a 404, even by name.
  9. ChatGPT or Claude: add the MCP URL above as a custom connector (OAuth; the app's sign-in page appears), then ask:
    • "What's on my plate this week?"
    • "Add a card 'Send invoice' to Doing on the Welcome board, assigned to me, due tomorrow"
    • "Move card 3 to Done" · "What's overdue?" · "What is blocking the launch?"
    • "Comment on card 2: @bo the receipt email is the last blocker" · "Any notifications?"
    • the prompts menu: Weekly review, Stand-up, Plan a goal (it shows the plan and creates the cards only after you say yes)
    • signed in as a viewer: "Move card 1 to Done" → Not allowed: viewers can look but can't change the board.

Evidence (live app, real Dodil services)

v2, 3 Nov 2026 · watch in the browser, no sign-in: ChatGPT session · web walkthrough

  • 🎬 Web walkthrough v2 (50 s), filmed on the local preview with the demo team, two people at once: a comment with @leo and its notification, Leo moving a card in his own window while this board updates by itself, a file from DataK3 object storage, the archive, board templates, Leo's 🔔, and the same card as a read-only viewer.
  • 💬 ChatGPT, live through /mcp, after Refresh tools picked up the 29 tools: recording and the whole session as one image: a comment whose @member reached member@cobra-demo.com ("1 person notified"); ChatGPT asking before the invite; plan a goal shown as a draft, created only after "yes" (4 cards, 3 blockers); "what is blocking card 6?" → start with #3; and an archive (asked first: destructiveHint). When I first named a board this account doesn't have, it found only Launch Plan, said so, and changed nothing.

v1, 2 Nov 2026

ChatGPT creating a board and reading my week through the Cobra MCP endpoint, live


What's inside

AreaWhat it does
PeopleOwner invites by email, as member or viewer, changes roles, removes people. Removing someone unassigns their cards, with history. A board has exactly one owner.
BoardsBoards → lists → cards. Lists are the board's own (add, rename, reorder, mark as a done list). Cards: title, description, assignee, due date, labels, position.
Card numbersEvery card gets a short number on its board (#12), claimed by a plain INSERT so two people adding at once never share one. People and agents say "card 12".
HistoryEvery change is one event: who, when, and field: [before, after]. The card row and its event are written in one DataK3 transaction.
No lost updatesEach card carries a version. An edit sends the version it read; a stale one is 409 "changed since you opened it" and nothing is written (UPDATE … WHERE version = …).
OverduePast due and not in a done list. Shown in red on the board, in My week, Overdue, the sidebar count, and the daily briefing.
Blockers"#3 is blocked by #2", same board only, cycles refused. What is blocking? walks the whole chain and names the cards to start with.
ChecklistsManual steps, plus an AI suggestion that changes nothing until a person adds what they want.
Meaning searchCards are embedded with Dodil Models (arctic-embed-m-v2, 768-d) into a DataK3 VECTOR(768) column; cosine KNN finds cards that mean the same, only on boards you can see. Re-embedded only when the text changes.
Daily briefingCounts computed from the board (overdue, blocked, due today, finished), worded by kimi-k2.6 with a prompt that forbids inventing cards. Works without the AI, too.
Comments & @mentionsMembers comment on cards; @bo finds bo@… when only one person on the board starts with "bo" (two matches mention nobody: no guessing). Viewers read comments but can't write them.
NotificationsA 🔔 with what changed for you: you were @mentioned, a card was given to you, someone commented on your card. Never for your own actions, only from boards you're still on; mark one or all as read.
Files on cardsUp to 10 MB each, 20 per card, stored in the bucket's DataK3 object storage with the app's own service account. Streamed only to people on the board (a stranger gets 404 even with the file id); anything that could run in the page (HTML, SVG…) is served as a download with nosniff and a sandbox CSP.
Live boardThe open board polls one cheap query (the newest history line). When a teammate changes it, the board reloads in place and a toast says who did what. Works across replicas, no sockets.
Archive & templatesNo delete, by design: archived cards leave every view (board, My week, Overdue, search, blockers, meaning search), keep their number and history, and come back with Restore. New boards start from 5 templates.
MCP29 tools, all actions (move_card, add_comment, my_notifications, archive_card, what_is_blocking…), never table access, plus 3 prompts (weekly review, stand-up, plan a goal). Read tools carry readOnlyHint, writes don't. Answers are short text with card numbers, not JSON dumps.
WebA zero-build page (no framework, no third-party assets): drag and drop with a drop line, keyboard access, card detail, My week, Overdue, search by words or by meaning, three themes, a phone layout.

Asking instead of guessing

People name things loosely, and so do their agents. A board is found by id or title ("ops" finds Ops), a card by number on a board, a list by name ("doing" finds Doing), a date by words ("tomorrow", "+3d", "next week"), an assignee by email or "me". When two things match (two boards called Ops north / Ops south, card #1 on two of your boards), the tool answers with the candidates and asks. It never writes to a guess.


Architecture

Light domain-driven design. The web routes and the MCP tools are two thin doors onto the same use cases, so every rule lives in one place and can't be skipped by using the other door.

app/boards/
  domain/model.py          Board, BoardList, Card, Event, roles, the rules (no I/O)
  application/use_cases.py every action a person can take; resolvers that never guess
  application/sample.py    the first-visit sample board (built through the use cases)
  application/ports.py     what the use cases need: a repository, an object store, an assistant
  infrastructure/
    repository.py          DataK3 over the Postgres wire: claims, versioned updates, vectors,
                           comments, notifications, attachment records, the archive
    memory.py              the same contract in memory (tests, local preview)
    dodil.py               service-account token, a reused DataK3 connection, DataK3 object
                           storage for files, Dodil Models (a failed model call = a clear message)
  doors/
    web.py                 JSON API + the page (401 / 403 / 404 / 409 / 422)
    mcp.py                 the MCP tools, acting as the signed-in person
    text.py                how the board reads in a chat
    identity.py            who is calling: the gateway's X-Dodil-User, nothing else
    static/                the page: index.html, app.js, app.css, the logo
tests/                     223 tests: roles, visibility, conflicts, history, comments, files,
                           archive, live, MCP tools + prompts, web (CI: ruff + coverage >= 85%)
scripts/live_check.py      the same story against REAL DataK3 (tables, vectors, objects) and
                           Dodil Models, on a test bucket: 27 steps, "ALL GOOD" on 3 Nov 2026
scripts/preview.py         local preview with a demo team, no Dodil account
schema.sql                 12 tables, natural keys, created by the remix
.dodil/remix.yaml          what a remix creates; .dodil/deploy.yaml how CI deploys it

More: docs/DESIGN.md (the decisions and why).


Remix it into your organization

dodil auth login --web
dodil git remix plan eduard/cobra-boards --to-org <your-org> --name <your-repo>   # creates nothing
dodil git remix      eduard/cobra-boards --to-org <your-org> --name <your-repo>

It creates one bucket (<repo>-data, with the 12 tables from schema.sql), one appid pool (<repo>-users), one service account (k3.editor on that bucket + ignite.model-user), the repo, and deploys it. Credentials go straight into the new repo's CI secrets; nothing to copy by hand. Keep the repo name to 18 characters or less (the bucket name is limited to 25).

After the remix: let people sign in (the one documented step)

  1. Check the deploy log of the first run. Since dodil git 1.6.11 the deploy registers the app's sign-in callback on the pool itself ("already lists the sign-in callback"). If it says the callback was not registered, run the command printed on that line, once.
  2. Open the app's URL (dodil ignite app get <repo> → public_urls) and create an account. The pool allows sign-up by default; if you turned it off, invite people with dodil appid user create <repo>-users <email>.
  3. ChatGPT / Claude: add https://<app host>/mcp as a custom connector. ChatGPT registers through CIMD and is approved automatically. A client that arrives pending needs one approval: dodil appid client list <repo>-users, then dodil appid client approve <repo>-users <client-id>.
  4. Optional self-check: dodil ignite invoke <repo> hits POST /, which opens the bucket with the app's own credential and returns a row count per table.
  5. Optional look: a new pool's sign-in page carries the platform's default theme. To give it the Cobra look, put your app's host in logo_url in .dodil/branding.json, then dodil appid branding set <repo>-users @.dodil/branding.json and dodil appid pool update <repo>-users --display-name "Cobra Boards".

Tested: a remix into a fresh organization

On 3 Nov 2026, after v2 (12 tables), I remixed eduard/cobra-boards (template at f293bfa) into a second, empty organization (edrax) as cobra-fresh, and checked the copy from outside:

CheckResult
dodil git remix …repo, bucket, pool, service account created; run succeeded — deployed
dodil ignite app listedrax/cobra-fresh, deployed
dodil ignite invoke cobra-freshok: true, all 12 tables present, 0 rows (its own new bucket)
GET /healthz{"ok":true}
GET / signed out303 to its sign-in, then appid.dodil.io/edrax/cobra-fresh-users, its own pool
sign-in page200, the pool's hosted login with sign-up
GET /api/me, POST /mcp signed out401
/.well-known/oauth-protected-resourcepoints ChatGPT / Claude at the new pool

Nothing was copied by hand: the bucket, pool, service account, CI secrets and deploy all came from .dodil/remix.yaml. (The first copy, from 2 Nov with 8 tables, passed the same checks.)


Run it locally

python -m venv .venv && .venv/bin/pip install -r app/requirements.txt pytest
cd app && ../.venv/bin/python -m pytest -q ../tests        # 223 tests in ~8 s, no account needed
python scripts/preview.py                                   # http://localhost:8775
# act as the demo owner / member / viewer: http://localhost:8775/?as=viewer

Against a test bucket with real Dodil services (never the live -data bucket; the script refuses one):

BUCKET=<test-bucket> DODIL_SERVICE_ACCOUNT_ID=… DODIL_SERVICE_ACCOUNT_SECRET=… \
python scripts/live_check.py

What I'd improve next

  • Push instead of poll: the live board asks every 4 s for one row (cheap, and it works on every replica). Server-sent events would cut that to instant once there is a shared channel.
  • Invitations and notifications by email: today they live in the app (and in the agent's my_notifications); an email digest is the next step.
  • Board archiving (cards archive today; whole boards don't yet).
  • jina-embeddings-v4, the challenge's suggested model, timed out (> 120 s) every time I tried it on 2 Nov 2026, so meaning search uses arctic-embed-m-v2, also on Dodil Models. Switching back is one env var (EMBED_MODEL) plus the vector width in schema.sql.

Honest notes (what was hard)

  1. kimi-k2.6 rejects a request that carries temperature (500, "invalid for this model"); the app sends none.
  2. The embeddings model in the brief timed out, so I measured the alternatives and switched.
  3. A connection per query cost 1–3 s from Europe; one reused connection per worker (under the bucket's concurrency limit) made it ~3× faster.
  4. In the first live ChatGPT test the agent couldn't say "assigned to me", so it had to look up my email. Now "me" means the signed-in person, on the web and over MCP.
  5. Claude.ai inside a company organization only lets the org owner add a custom connector, so the live MCP proof above is from ChatGPT; Claude Desktop / Claude Code connect the same way.
  6. A free-plan org holds one appid pool, so remixing a second copy into the same org stops at "pool limit reached". Remix into a fresh org (as the judging does); that is the path I tested.
  7. Files: there's no upload helper for apps in the CLI guide, so I probed DataK3 object storage on a test bucket first. The app's own service account (k3.editor) can PUT, GET and DELETE objects in its bucket, so files need no extra grant and no public links.
  8. On 3 Nov an embeddings call returned 500 once. A model that doesn't answer now gives the person a clear "try again in a minute" (meaning search, checklist) while the board, plain search and the briefing's counted facts keep working.
  9. Re-testing the remix on 3 Nov, I deleted the old copy and remixed again with the same name straight away: the service-account step failed with a platform error (keycloak error: kc_apply_new_assignment), twice on resume. The same remix under a new name went through at once, so a just-deleted service-account name seems to stay taken for a while. A fresh org (as the judging uses) never meets this.
  10. A remix can't carry the sign-in page's branding (remix.yaml has no field for it), so a fresh copy first shows the platform's default login theme; step 5 above is one command.

Licence

MIT. Cobra Boards is my own name and design; it uses no Trello name, logo or artwork.

Dodil partner programme

Applied on 3 Nov 2026 (as EDRAX, Romania); under review.