TL;DR — I gave a team of AI coding agents a real kanban board (Kanboard) and a real git server (Gitea), wired together by an MCP orchestrator (Marcus), and had them build a content management system one ticket at a time. I didn't write code. What I did do was read every diff before it merged and catch the decisions that mattered — a weak session store, a permission check that wasn't actually checking permissions, an XSS hole in comment rendering — while each ticket was still open. That's the whole point: the board turns "audit the black box after launch" into "steer the build while it's happening."
The thing that actually changes when agents write the code
Everyone's excited that an AI agent can take a ticket and produce a working feature. Fewer people talk about what you lose in that trade: visibility. A finished app that an agent built is a black box. It runs, the happy path works, and the hundred small decisions that determine whether it's secure, fast, and correct are buried in a diff nobody read.
The ticket said "add user login." It didn't say which password hash, whether the session cookie is HttpOnly, whether sessions survive a restart, or whether the "publish" button checks your role or just that you're logged in. The agent decided all of that — silently — and unless you look, you find out in production.
So the skill that matters shifts. You don't need to write the code anymore. You absolutely still need to understand it — enough to recognize an N+1 query in a diff, spot an unescaped user string, or notice that "viewers can't publish" isn't actually enforced. The job moves from author to reviewer. And a good reviewer needs to see the decisions while they can still be changed, not in a forensic post-mortem.
That "while they can still be changed" part is the entire reason I built on a board instead of a chat window.
The setup, in one breath
Four self-hosted pieces:
- Marcus — an MCP orchestrator. Hands out tickets, runs each one's lifecycle, is the only thing that holds the board's credentials.
-
Kanboard — the shared board. Columns are
Todo → Ready → In Progress → Waiting for Human → Done(plusBlocked). - Gitea — git server, one repo per project, one branch per ticket.
- MarcusDevEnv — a Kanboard plugin that puts live agent controls, per-ticket branch links, and one-click preview environments right in the board UI.
Agents never message each other — they coordinate entirely through the board. One command brings it all up:
./scripts/setup.sh
Then any MCP agent (Claude Code, Codex, …) connects and loops on a single tool call, marcus_work, pulling whatever Ready ticket is next. Crucially, when an agent says it's done, the ticket doesn't merge — it lands in Waiting for Human with a review comment, a live preview of that branch, and a diff for me to read. Nothing reaches main until I say so.
That review checkpoint is where all the interesting work happens. Let me show you, by building something real.
Let's build a CMS
I seeded a board with the backlog any content management system needs: user auth, roles and permissions, a post editor, media uploads, a comment system, and an admin dashboard. Then I pointed three agent sessions at it. Three unrelated tickets immediately went in flight on three branches, each with its own preview environment. I'll come back to the parallelism — first, the part that earns its keep.

The starting backlog. Six tickets, and every one already carries Marcus-generated acceptance criteria — posted as a ticket comment before any agent touches code, so "done" has a checkable definition from the first commit.
Showcase 1 — Seeing the decisions nobody wrote down
This is the one I care about most, because it's invisible by default. Call it silent-choice visibility: surfacing the implementation decisions a ticket never specified but an agent had to make anyway.
Take the auth ticket. Marcus auto-generated acceptance criteria for it — register, log in, log out, reject bad credentials, keep sessions across requests. An agent built exactly that, tests green. But the diff in review showed three things the ticket never mentioned:
- passwords hashed with bcrypt at cost 10,
- sessions stored in an in-process dictionary,
- the session cookie set with no
HttpOnly,Secure, orSameSiteflags.
None of that is in the acceptance criteria. All of it matters. I know that an in-process session store dies on the next restart and breaks the moment you run more than one worker; that a cookie without HttpOnly is one XSS bug away from account takeover; that bcrypt is fine but argon2id is the modern default.
Here's the thing that makes this catchable instead of archaeology: the agent doesn't wait for me to find these in a diff. It's prompted to flag them itself, mid-work, as a short comment on the ticket the moment it makes the call:

Two real progress-update comments from the same ticket. Each one leads with a "🏗️ Note:" — the agent's own prompt tells it to post exactly this, for exactly this reason, whenever it makes a call the ticket never specified. This is the log entry existing at all, not a diff I had to go spelunking through.
So I left a change request as a comment on the ticket:
@marcus changes:
- move sessions out of process memory (Redis or signed cookies)
- set HttpOnly + Secure + SameSite=Lax on the session cookie
- switch the hash to argon2id
The ticket dropped back to In Progress, the agent revised the branch, and I re-reviewed. The weak version never touched main. The board didn't make the agent smarter — it made its choices legible at the one moment I could still redirect them. That's the difference between auditing during and auditing after.
And these notes don't just sit buried in one ticket's comment thread, either — they compile onto a single page, sourced live across every ticket in the project:

Every flagged decision, from every ticket, in one place — no separate database, no "check each ticket's thread by hand." This is read live off the same ticket comments, not a parallel system that can drift out of sync with them.
Showcase 2 — Catching where "what got built" drifts from "what I asked"
The second auditability win is acceptance-criteria drift: the delivered behavior quietly diverging from what the ticket said it should do.
The roles ticket had explicit criteria — admins manage users, editors can publish, viewers can read but not publish. The agent implemented three roles, wired up the UI, and it looked right. But the publish endpoint only checked is this user authenticated, not is this user allowed to publish. A viewer could publish by hitting the endpoint directly.
Here's the gap at the code level, because "looked right" is exactly the trap. The endpoint for creating a post got the check right:
@app.post("/posts")
def create_post():
user = current_user()
if user is None or user["role"] not in ("editor", "admin"):
return jsonify(error="forbidden"), 403
The endpoint for publishing one didn't:
@app.post("/posts/<int:post_id>/publish")
def publish_post(post_id):
user = current_user()
if user is None:
return jsonify(error="unauthorized"), 401
# ...publishes immediately. No role check.
publish_post only asks "is anyone logged in" — not "is this the right role." A brand-new viewer account (the default role every registration gets) has a valid session, so user is None is False, it sails past the only guard the function has, and the post gets published. That's a direct contradiction of a criterion sitting right there in the ticket description.

Same ticket, annotated end to end: the review checkpoint (top), the acceptance-criteria line the code above actually violates (middle), and the argument spelled out (bottom) — this is intent drift, not a bug the agent hid. The criteria were set before any code was written; the shipped code quietly diverged from them, and the agent's own tests stayed green because none of them checked that specific line. That's precisely the failure mode the human-review stage exists to catch: the gap is invisible from inside the branch, and visible the moment someone checks delivered behavior against stated intent.
Because the acceptance criteria were written down and explicit, the drift was catchable — the review checklist and preview made "viewers cannot publish" a thing I could actually test, and it failed. (You can also turn on AI Verify, which sends the diff plus the acceptance criteria to a second model that returns a pass/fail with specific findings before anything merges — an automated first pass at exactly this drift check.) Back the ticket went with a specific ask: add the same check create_post already had, and add a test for the case that had been missing.
The fix that came back:
@app.post("/posts/<int:post_id>/publish")
def publish_post(post_id):
user = current_user()
if user is None or user["role"] not in ("editor", "admin"):
return jsonify(error="forbidden"), 403
# ...publishes.
def test_viewer_cannot_publish_post(client):
# editor creates a post, then a separate viewer account tries to publish it
...
r = client.post(f"/posts/{post_id}/publish")
assert r.status_code == 403
Same one-line check create_post had all along, now on publish_post too, plus the test that would have caught this in the first place if it had existed. Authorization enforced at the endpoint, re-reviewed, merged.
Stated intent versus delivered behavior is a gap that's trivially easy to miss in a black box and trivially easy to see when every ticket carries its criteria and every branch gets read against them.
Showcase 3 — Debugging is just more tickets
Reviewing the post-list preview, I noticed the page fired one query per post to fetch each author — a textbook N+1 that's fine with 10 posts and miserable with 10,000. And the comment section rendered raw user input straight into the page — stored XSS.
Neither is a feature. Both are bugs I found during the build. So I filed them as bug tickets on the same board:
[bug] Post list issues N+1 queries — eager-load authors in one query
[bug] Comments render unescaped HTML — sanitize/escape to close stored XSS

Boxed: the two bugs, filed as tickets in the same Ready column as the feature backlog. No separate bug tracker, no "I'll get to it" side channel — a bug found in review gets the exact same lifecycle as a feature: branch, preview, review, merge.
Marked them Ready, and agents picked them up like any other work — own branch, own preview, own review. The fixes are tracked, diffed, and auditable, sitting in the same history as the features. Bugs stop being things users discover after launch and become part of the visible build loop. Same mechanism, pointed at improvement instead of net-new.
Showcase 4 — Where the multi-agent speed actually comes from
Now the parallelism. Marcus keeps several tickets in flight at once (three by default), each agent on its own branch in its own private checkout, so they never step on each other. Dependencies are respected — a ticket that needs another sits Blocked until that one merges, so nothing gets built on unfinished work.
The chunky tickets decompose. The admin dashboard — user management, content moderation, site settings, an analytics view — had enough acceptance criteria (nineteen distinct items) that it got split into four independent sub-tickets several agents could grab simultaneously; the parent waits for every child, then comes to me for one sign-off on the assembled result.

One 19-criteria ticket (blue) became four independently workable ones (red) — two of them already picked up and running in parallel, in the same poll cycle other agents are working the bug tickets and the post editor.

Not four tickets that happen to share a name prefix — the parent tracks them as structural children, each with its own live status pulled straight from the board. Closing all four is what unblocks the parent.
A per-project stats page tracks tickets-to-Done per hour so I can tell whether the parallelism is turning into finished work or just motion.
The honest caveat: the ceiling on parallelism isn't the agents. It's how many Ready tickets exist, how the dependencies chain, and — the real bottleneck — how fast I can review. Which brings the whole thing back around.
The point
I shipped a working CMS and never wrote a function. But look at what actually kept it shippable: knowing that in-memory sessions don't survive a restart, that a publish route has to check role and not just identity, that unescaped user HTML is an XSS vector, that N author lookups for N posts is a performance bug waiting to happen.
I didn't need to write any of that. I needed to recognize it in a diff and a preview — and say something while the ticket was still open. That's the real shape of building software with agents: not "no engineers required," but a shift from writing implementations to reading decisions, applying the same judgment about where security, performance, and usability go wrong. The concepts still matter enormously. What changes is that you spend them on review instead of authorship.
A board-plus-git-server makes those decisions visible at the moment you can still change them — under the hood, during the build, instead of a forensic audit after launch. That's the feature. Everything else is plumbing.
Try it
git clone https://github.com/aak540114/marcus-kanboard-gitea
cd marcus-kanboard-gitea
./scripts/setup.sh
Repo (MIT): https://github.com/aak540114/marcus-kanboard-gitea
I wrote a longer, feature-by-feature walkthrough of the stack on Medium if you want the deep dive on how it all wires together.
https://medium.com/@aak540114/give-your-ai-agents-a-kanban-board-not-the-keys-to-production-81ffd19370f1
Your turn: if you're shipping agent-built code, where do you actually catch the risky decisions — in review, in CI, or after something breaks? I'm most curious which class of silent choice bites people hardest: security, performance, or plain drift from what the ticket asked for.
Top comments (0)