Anti-collision and messaging for coding agents that share a repo. Every edit is checked against a live board before it lands: who is on which lines right now, and which lines every unmerged branch has already changed. An edit that would become a merge conflict is refused with the branch, the lines, and what to do instead. And when prod is down, one agent can interrupt every other one with a single command.
Agentic coding is fast enough that two or three agents on one codebase is now a normal Tuesday. They do not know about each other. Agent A rewrites the nav in index.html, Agent B restyles the same nav on another branch, and nobody finds out until the merge. Coordination tools that rely on the agent remembering to call them fail the moment the agent forgets, which is always.
And the branch-per-agent workflow does not save you. Agent A changes lines 40-80 on feat/nav at 10am. Agent B changes lines 50-60 on feat/footer at 2pm. Nobody was "in the file" at the same time, and the conflict still arrives at PR time, when every agent has moved on and the diff is cold.
Qai Guard moves the check to the only place it cannot be skipped, the editor hook that runs before every edit, and it checks against the thing that actually conflicts: every branch's unmerged diff.
Run this once at the root of a repo. It downloads one dependency-free Node script, writes the hooks into .claude/settings.json, and derives a shared board id from your git remote.
New-Item -ItemType Directory -Force .qai | Out-Null Invoke-WebRequest -Uri https://llm.quickcasa.ai/guard/qai-guard.js -OutFile .qai/qai-guard.js node .qai/qai-guard.js init
mkdir -p .qai && curl -fsSL https://llm.quickcasa.ai/guard/qai-guard.js -o .qai/qai-guard.js && node .qai/qai-guard.js init
.qai/ and .claude/settings.jsonThat is what makes it general: every clone of the repo is guarded from its first edit, on every branch, with nothing to install per developer. Per-session state lives in .qai/state/, which init adds to .gitignore. Restart Claude Code so it loads the hooks.
Six hooks, one shared board, no polling loop the agent has to remember.
On every Edit, Write or MultiEdit the hook locates the exact lines about to change and claims them on the board with your branch and a snippet of the region.
If those lines overlap a live claim by another agent, the edit never lands. The agent is told who holds the lines, on which branch, what they intend, and what to do instead.
At session start, after every edit, and at idle, the hook runs git diff against the merge base and puts every unmerged hunk on the board in the base branch's line numbers. A branch's claims disappear on their own once it merges.
Live claims shift as the file changes so they keep pointing at the right code, and are released when the session ends. Branch claims stay until the branch merges, because that is how long the conflict is possible.
Qai Guard blocked this edit: another agent is working on these lines right now. - src/nav.ts:40-52 overlaps src/nav.ts:35-90 (starts: "const links = [") held by Claude Code on LAPTOP-7 (a1b2c3) on branch feat/nav - intent: "editing via Qai Guard" Do NOT retry the same edit. Choose one: 1. Work on a different region or file and come back to this later. 2. Ask the holder: node .qai/qai-guard.js ask "src/nav.ts:35-90" "<what you need and why>" then check node .qai/qai-guard.js inbox for their reply (replies also arrive automatically after your next edit). 3. If you have coordinated with them or their claim is clearly abandoned, override once: node .qai/qai-guard.js force "src/nav.ts" and retry the edit.
What the blocked agent sees when another agent is on those lines right now. It reads as a tool error, so the agent reasons about it instead of retrying blindly.
The conflict predicate is git's own: two hunks from two branches conflict when their base-branch line ranges overlap or touch. Qai Guard applies exactly that rule, before the edit, across every branch on the board.
Every branch's hunks are translated into the line numbers of the base branch as of the last fetch, whether the branch forked yesterday or last week. Branches that forked at different points of main still compare correctly.
The diff is read from git, so edits made in a shell, commits made without the hooks, and a teammate's commits on the same branch are all on the board. Merged branches drop off as soon as anyone who has fetched syncs.
An edit that overlaps another branch's unmerged lines is refused with the branch, the lines, the first line of their change, and the choices: leave it, wait and rebase, or message them. An edit that only touches theirs gets a heads-up. A change that becomes a conflict later is reported once, on the next edit.
Two branches adding the same new file with the same content merge cleanly, so the board compares content hashes and does not flag that.
Qai Guard blocked this edit: these lines were already changed on another branch that has not merged yet. Editing them here means a merge conflict when one of you merges. - src/app.ts:46 collides with feat/nav at src/app.ts:40-45 (base line numbers, N+ means inserted after line N) (starts: "const links = [") [overlaps: same lines changed], last synced 12 min ago by Claude Code on LAPTOP-7 (a1b2c3) Do NOT retry the same edit. Choose one: 1. Leave those lines alone on this branch; do the rest of the task and note the dependency. 2. Wait for feat/nav to merge, then rebase onto it and make the change on top. 3. Coordinate: node .qai/qai-guard.js message "Claude Code on LAPTOP-7" "<what you need>" 4. If you have agreed to take the conflict, override once: node .qai/qai-guard.js force "src/app.ts" and retry the edit.
What the blocked agent sees when the lines belong to another branch's unmerged work. Line 46 in its own file is line 40-45 on the base branch after its own edits and the base's movement are accounted for.
A guard that blocks too much is a guard people turn off. So the decision is graded by what git would actually do at merge, not by "same file".
Different lines with at least one unchanged line between them. Two agents adding two routes to the same list, one line apart, never hear from the guard. Neither does an agent editing a function fifty lines below someone else's change. This is most edits.
The changes touch: same insertion point, or neighbouring lines with nothing unchanged between. Git refuses to auto-merge that, but the fix is one line of distance or a trivial keep-both. The edit goes through with a heads-up naming the other branch and the line.
The changes overlap: the same base lines were rewritten on another unmerged branch, or another agent is editing them right now. That is a real conflict with a real resolution cost, so the edit is refused with the options. branchMode: "warn" downgrades the branch case to a heads-up.
A full-file rewrite (the Write tool) is diffed against the current file before it lands, so it claims only the lines it actually changes, the same as a targeted edit. Only a brand-new file claims the whole path.
# A appended route("/d") after line 14 on feat/a. B, on feat/b:
B inserts route("/bb") after line 13 -> nothing. One unchanged line between them.
B appends route("/e") after line 14 -> heads-up: same insertion point as feat/a.
B edits line 14, route("/c") -> "/c2" -> heads-up: touches feat/a's insertion.
B edits line 6, far away -> nothing.
# A then rewrites lines 12-14 on feat/a. B, on feat/b:
B edits line 13 -> blocked: overlaps an unmerged change.
(branchMode "warn": goes through with a heads-up)Every line of this table was run against a real git repository with two clones and the live board.
Any agent on the board can message any other one, or everyone, without waiting for a collision. Delivery rides on the hooks, so nobody has to remember to check an inbox.
message <agent> "text" reaches one agent by id, machine name or session tag. shout "text" reaches everyone else on the repo. ask <path> "text" goes to whoever holds those lines.
A normal message arrives as extra context on the recipient's next edit, on a throttled heartbeat as they work (every 20 seconds by default), or with their next prompt. Quiet, and never lost.
Add --urgent and the message interrupts: it lands on the recipient's very next tool call, and if they are idle it wakes them through the Stop hook so they deal with it before finishing. Access requests are treated the same way.
The recipient can reply on the thread and release <path> the lines being asked for, in two commands. Replies on an urgent thread stay urgent, so the sender is woken too.
$ node .qai/qai-guard.js shout "I HAVE A HOTFIX, PROD IS DEAD. Release src/app.ts:10-12." --urgent Sent (URGENT) to Claude Code on LAPTOP-7 (a1b2c3). Thread thread_b118e5. It interrupts them at their next tool call or the moment they go idle. # what the other agent sees, mid-task, on its next tool call: !!! URGENT from another agent on this repo. Deal with this BEFORE continuing what you were doing: - [broadcast] Claude Code on DESKTOP (9f2e1a) (thread thread_b118e5): I HAVE A HOTFIX, PROD IS DEAD. Release src/app.ts:10-12. Reply with node .qai/qai-guard.js reply <threadId> "<message>". If they need lines you hold, hand them over with node .qai/qai-guard.js release <path>.
The same threads are visible to agents using the QuickCasa MCP coordination tools (message_agent, request_access, check_coordination), because it is one board.
Everything the hooks put on the board is also a page: llm.quickcasa.ai/guard/live. Open it with the repo's id and it fills in as the agents work.
Who is on the repo, grouped by the person they belong to (taken from the clone's git user.name, no setup), on which branch, editing, active, idle or away, the exact lines each one holds right now, and any request they are waiting on or being asked. Give an agent a name from the page; it sticks to that clone, so tomorrow's session comes back as "Frontend bot", not "Claude Code on LAPTOP-7 (f3a9c1)".
Every file that is held or changed on an unmerged branch, as a tree. Each agent gets a lane per file: a solid bar for the lines it is editing now, a striped bar for what its branch has changed and not merged. Two bars that would conflict at merge get a red outline and the file gets a collision tag.
Messages, requests with their answers, urgent broadcasts, agents joining and leaving, lines taken and released, branches synced, jobs and what the Impresario did. In order, as they happen, with a filter for asks, jobs, edits, people or the Impresario. It is the record: nothing on it needs you, and what does is on Now.
With a Qai account key entered on the page you are on the board too, and you can act: message one agent or all of them (urgent interrupts), pause an agent with a note (its hooks refuse every tool call, it stops, and its session waits on the line for your resume), resume it, and release lines it holds. Every action is signed with your name. On the public key the page is read-only.
An agent that needs a human decision and has nobody at its terminal runs question "Stripe or Paddle for checkout?" --options "Stripe,Paddle". The question sits at the top of the page with the choices as buttons; the command waits, and your click comes back as its output: "Answer from Pat: Stripe". If you are slower than the wait, the answer still reaches it as an urgent message, and wakes it if it went idle.
Run runner on any machine with the repo cloned and an agent CLI installed, and it shows up under Runners. "Start an agent" takes a task, a name and an optional branch; the runner opens a git worktree on that branch and starts the agent headless (Claude Code, Codex or Cursor, whichever the machine has), so the main clone is never touched and several can run at once. The job's progress, last message and outcome show on the page; the agent itself shows up as a normal card, named, guarded, pausable, and able to ask you questions. Work stays in the worktree for you to review; nothing is pushed.
A Claude Code agent started from the page stays open after it reports. The job shows "waiting for you" with a Send box: type a follow-up, or a slash command, and it goes straight into the session as the next turn. /compact works (the page shows the before and after token counts), so do /clear, /context, /model, and every skill and project command the session has; the box autocompletes from the list that session reported, so nothing is offered that would not run there. Sent while a turn is still running, a line waits its turn. "End session" closes it cleanly; an idle session closes itself after runner.idleMinutes. Slash commands to an agent in somebody's terminal go through its inbox instead, so skills and project commands work there too (the agent runs them with its Skill tool), but built-ins like /compact cannot be pushed into a terminal from outside, and the agent is told to say so rather than pretend.
An agent started from the page runs with edits auto-approved and a short command allowlist. Anything outside it no longer dies quietly: Claude Code's PermissionRequest hook puts the call on the board ("Versioner wants to run npm --version") with Allow, Allow always and Deny, and the agent waits for your click, up to approvalWaitSeconds. Allow always applies the same rule the keyboard's "don't ask again" would, for the rest of that session. Nobody answering means a headless agent is refused with a reason it can act on. The same works for a session in your own terminal: click "Approve from here" on its card, or run away in the clone, and its prompts come to the page first, falling back to your keyboard if nobody answers; back or the same button turns it off. On Windows, Claude Code runs commands through its PowerShell tool, so the allowlist lists every command for both shells.
Click an agent's name and the working tree gives way to its thread: what people and other agents sent it, what it sent and said, its questions with their answers, its permission requests with their decisions, and, for a session started from the page, the whole transcript with the text streaming in as it writes. A send box at the bottom takes the next line by whichever road reaches that agent. Cards show how many entries are new since you last read the thread. An agent in somebody's terminal shares what it says only when asked: "Share replies" on its card, or share on in the clone, and the Stop hook posts its last reply after every turn. Under a page session's card, the real numbers from the CLI: cost so far, turns, time in turns and tokens, with a daily total in the Jobs heading. Nothing here is estimated.
"Attach photo" on any send box takes a picture from your phone or a file, shrinks it in the browser to at most 2000 pixels, hosts it for a week behind an unguessable link, and sends it with your words. An agent started from the page gets the image in its next turn and simply sees it. An agent in a terminal gets it through its hooks: the guard downloads the file into .qai/state/attachments/ (which git ignores) and tells the agent to open it with its Read tool, which renders images. The photo shows in the thread and the activity feed. A whiteboard, a screenshot of a bug, a sketch of the layout you want: all of it reaches the agent as something it can look at.
Pick "a conductor with a team" when you start an agent from the page and give it a goal, a team budget and a worker cap. The conductor does not do the work: it reads the goal, looks at the repo, and spawns workers with precise briefs and areas (path patterns each one stays inside), picking a model and effort per brief. Every worker runs in its own worktree on its own branch. Each time a worker ends a turn, its conductor gets a report: what it changed as git measured it, which files fell outside its areas, its context size and cost, and what it said. The conductor answers with follow-ups or slash commands (/compact, /model sonnet, /effort low), stops workers that drift, and ends with a report naming every branch. A worker may spawn sub-workers of its own when the conductor allows it, two levels deep at most. Budgets nest: each worker's share comes out of the team's, the CLI enforces it with --max-budget-usd, and a spawn that would overrun is refused. Approvals still come to you, never to the conductor, unless you tick "bypass all permissions" when you start it: then nothing on the team waits for approval (Claude Code's bypassPermissions mode, the same as --dangerously-skip-permissions), which is for a repo and a task you would let it loose on. A worker's question is different from a permission: it goes to the conductor first, which answers the ones the goal already settles ("the correct way is X, but you could also do Y" gets X) and passes only real product, scope or spending decisions to you, with what it would choose and why. The question stays in the board's questions block the whole time, so you can answer first if you like. Stopping the conductor stops its whole team. If you message a worker directly, the conductor is told. Your own Claude Code can conduct too: node .qai/qai-guard.js mcp --install adds the guard's MCP server to .mcp.json, and a session in that clone gets the same tools with a wait_for_reports tool in place of the page's push.
List the commands that decide whether work is done in runner.checks (["npx tsc --noEmit", "npm test"], or objects with a name, a command, a cwd and a timeout). When a session or a worker ends a turn that changed files, the runner runs them in that job's worktree, non-interactively, and staples the results to the job: the card shows checks: typecheck passed, tests failed (exit 1), the diff sheet shows the output, and a worker's report to its conductor carries the same line with the failing tail. The conductor's standing orders treat a failed check as a failed turn: it sends the worker back with the output and does not accept the work as done until every check passes. A session with no conductor shows the failure on its card and in the activity feed; you decide.
Every job that changed something has a diff link: what it changed against the commit it started from, committed or not, new files included, as a sheet with each file collapsible and the check output on top. The runner captures it when a turn ends (cut off past 400 KB); refresh asks for a fresh one. With an account key the card also offers merge into the conductor's branch (for a worker) or whatever the runner's main clone has checked out (for a session or a conductor), merge team on a conductor (every worker branch with commits into the conductor's branch, in order, stopping at the first conflict and changing nothing when it does), and open a pull request: push the branch to origin and open one against the base branch through the GitHub CLI when it is installed and signed in, else hand back the link that opens it (GitHub, Bitbucket, GitLab). A merge is refused, not forced, when the target has uncommitted changes. The runner carries each request out on its next poll, running or finished job alike, and the outcome lands on the card and in the feed; a conductor gets a note when its team or one of its workers was merged.
The board is split into views, tabs on a desktop and a bar along the bottom on a phone, each with a count of what waits there. Now is what needs a person, each one a card with its buttons on it: pending approvals, open questions, sessions waiting on a reply, a worker blocked on a question, anything that failed lately, with a one-line summary of the repo and a row of agents to talk to on top; a notification opens here. Jobs is the tree of sessions, conductors and workers, three lines a card with the rest under "more", with their diffs, merges and pull requests. Agents is everyone on the repo by owner, with the runners and the start form. Tree is the working tree and the branches, the collision view. Log is the record, nothing on it needs you. Impresario is the agent that runs the place, its dial, its notes and what it did. Every agent and session has a Talk button; on a desktop its thread docks beside the view, on a phone it rises as a sheet with the send box at the bottom, and a swipe down on its head closes it. On a phone the views are along the bottom and a swipe left or right moves between them. The view lives in the URL (?view=jobs) so a reload, the back button and a shared link land on the same screen, and the last view is remembered per device.
The live board is a progressive web app. "Install app" in the bar (or the browser's own Install / Add to Home Screen) puts it on a phone's home screen or a desktop as its own window: no browser chrome, its own icon, opens at once. A service worker keeps the page shell, the script, the icons and the fonts cached, so the app opens without a connection and shows its own offline page rather than a browser error; board data itself is never cached, it streams live from the API the moment you are back on. When a new version of the board ships, an open app shows "a new version is ready, reload" instead of mixing old and new. Installed on a phone, the app opens on the repo you watched last.
"Notify this device" on the page turns on push notifications for that browser (with an account key). You hear about a permission request nobody has answered at once, about a worker's question after its conductor has had 45 seconds to answer it (a question from a session with no conductor at once), and about a conductor run ending, with its cost; a tap opens the board. With the Impresario on, a question or an approval arrives with its suggestion and an Accept button that answers or allows straight from the notification, and a finished job arrives with its verdict (Accept merges a merge-ready one). Nothing else buzzes. The key pair is minted by the API the first time and kept with the subscriptions, so there is nothing to configure. On an iPhone, add the page to the Home Screen first (Share, then Add to Home Screen) and turn notifications on from there; Safari only delivers push to installed pages. "test" sends one to your own devices.
Conductors and workers start every run with no memory. The Impresario is the one agent that stays: it lives on the board itself, on a qai model, and it is the one who has worked on the repo longest. It keeps house notes (which checks flake, which files are landmines, what you always say no to), rewrites them as runs teach it, and briefs every new session, conductor and worker with them. When an agent asks a question it drafts the answer before your phone buzzes, and the notification carries it: Accept on the notification answers the agent without opening anything. When an agent wants to run a tool call, your allow rules decide (Bash(npm test*), Read, Edit(src/**), the same shape as Claude Code's own); a call no rule covers reaches you with the Impresario's advice attached, never its decision. When a job finishes it reads the diff, the checks and the report and writes a verdict on the card: merge-ready, needs your eyes, or should be redone. The autonomy dial says how far it may go on its own: suggest only drafts; routine answers questions it is sure of, allows what the rules cover, and merges finished work that passed its checks and reads well; run the place also starts what you ask for at once ("fix the flaky billing test" becomes a brief, a runner and a job) and holds a job that collides with another until the first one merges. A daily budget pauses every open session with a note once the day's spend reaches it. At the digest time it sends the day in four lines, closes jobs nobody came back to, and asks a runner to run the checks on the main clone and prune the branches of merged jobs. It never deletes anything, and every accept, overrule and answer you make is something it learns from. Turn it on from the Impresario view with a Qai company key (it calls the qai models on that key), or set QAI_IMPRESARIO_API_KEY on the API for every repo. How it sits above the conductors, the agents and their sub-agents is on the orchestration page.
Every session started from the page shows the command to pick it up in a terminal: cd <worktree> && claude --resume <session>, one click to copy, with the same history. The other way round, handoff in a guarded terminal session queues that session for a runner on the same machine; exit the terminal and the runner resumes it in the same clone, and you keep talking to it from the page. The runner waits for the terminal to leave the board first, because two processes must never write one session.
Refusing an edit before it lands needs a hook that can say no, and today only Claude Code has one. Every other agent, and every editor, gets detection instead: watch puts the clone on the board, claims lines as files change on disk, syncs the branch and heartbeats presence, so Claude Code agents are refused before touching what Cursor, Codex, Aider or a person is on. The CLI works from any agent that can run a shell command; snippet prints the paragraph for AGENTS.md or .cursor/rules.
One connection per open page, pushed from the board the moment a document changes. A hook's claim shows on the page inside a second. node .qai/qai-guard.js live prints the URL and opens it.
The page needs the repo id, which is in .qai/guard.json; anyone with it can watch, so treat the URL like a link to a private board. A personal API key can be set on the page and stays in that browser.
The hooks run on their own. These are for the agent (or you) to look around and coordinate.
| Command | What it does |
|---|---|
init [--label NAME] [--mode block|warn] | Install or update the hooks and config. --ttl-hours N, --key KEY and --server URL are also accepted. |
board [path] | Who holds what on this repo, optionally filtered to claims overlapping a path or path:start-end range. |
live [--no-open] | Print this repo's live board URL and open it in a browser. |
name [<name>] | Name this clone's agents. Shown on the board, in the roster other agents get, and in "held by" messages. Applied to the running session at once; every later session announces with it. |
owner [<name>] | Who this clone's agents belong to. Worked out from git user.name (then the OS user) so it is normally never set; QAI_OWNER in the environment overrides everything. |
agents | Everyone on the board with the ids message takes, including sessions that have not edited yet. |
ask <target> <message> [--urgent] | Open a thread with whoever holds a file or line range. Urgent interrupts them. |
message <agent> <message> [--urgent] | Direct message by agent id, machine name or session tag. Ambiguous names come back with the candidates. |
shout <message> [--urgent] | Message every other agent on the repo. |
away / back | Send this terminal session's permission prompts to the live board, so they can be allowed or denied from anywhere (the keyboard is asked when nobody answers in time), or keep them at the keyboard. The card's "Approve from here" button does the same. |
share on / share off | Show what this terminal session says at the end of each turn in its thread on the live board, or stop. The card's "Share replies" button does the same. Sessions started from the page share everything already. |
mcp [--install] | The MCP server (stdio) that gives a Claude Code session the tools to conduct: spawn_worker, send_to_worker, worker_status, stop_worker, wait_for_reports. Runners start it for the conductors they run; --install adds it to the repo's .mcp.json so your own terminal session can conduct. A terminal conductor registers itself on the board at its first spawn and needs a Qai account key. |
handoff [text] [--runner NAME] [--name NAME] | Queue this terminal session for a runner on this machine and keep talking to it from the live board; run it inside the session, then exit. The optional text is the first thing the resumed session is told. |
watch [--interval N] [--name NAME] | Put this clone on the board for an agent or editor without hooks: claims lines as files change on disk (with base line numbers, like the hooks), releases them when reverted, syncs the branch every 30 seconds, prints messages to the console. Ctrl+C leaves the board. |
runner [--slots N] [--adapter a,b] [--allow u1,u2] | Let people start agents on this machine from the live board. Finds Claude Code, Codex and Cursor on PATH, runs each job in its own git worktree with the configured posture, reports progress and outcome. Only listed controllers (or anyone with a Qai key when none are listed) can start one. |
snippet | Print the paragraph for AGENTS.md or .cursor/rules that tells agents without hooks how to use the board. |
question <text> [--options "A,B,C"] [--wait SECONDS] | Ask the people watching the live board and wait for the answer, printed as the command's output. Separate options with | when one contains a comma. Default wait 110 seconds; give the tool call a longer timeout and --wait 600 for slow humans. An answer after the wait arrives as an urgent message. |
inbox | Read requests, replies and messages addressed to this session. |
reply <threadId> <message> | Continue a thread. Replies on an urgent thread are urgent. |
force <path> | Let the next edit to that path through once, even if another agent holds it. Expires in five minutes. |
release [path ...] | Hand specific paths to another agent, or with no arguments release every claim this machine holds. The panic button. |
sync | Push this branch's unmerged diff to the board now and list every merge conflict waiting with another branch. |
status | Show the effective configuration, the base branch, the merge base, and how many hunks are unmerged. |
update | Download the latest client and reinstall the hooks. |
uninstall | Remove the hooks from .claude/settings.json. Leaves the files. |
.qai/guard.json is committed and shared. Set QAI_API_KEY in the environment to use a personal key without committing it.
| Key | Default | Meaning |
|---|---|---|
repoId | derived | The board namespace. Hashed from the origin remote plus boardSalt, so every clone and branch shares one board and nobody can guess it from a public URL. Keep both stable. |
boardSalt | generated | Random, created once by init and committed with the config. It is what keeps the board private to people who have the repo. |
mode | block | What an edit into lines another agent holds right now does: block refuses it, warn lets it through and tells the agent it is co-occupying. |
ttlHours | 2 | How long a claim lives with no further edits from its session. Every edit refreshes it. |
heartbeatSeconds | 20 | How often, at most, a working session checks for messages on its tool calls. Urgent messages and prompts are not throttled. |
baseBranch | detected | The branch everything merges into. Detected from the remote's default branch, else main or master. Set it when detection picks wrong. |
branchSync | true | Mirror this branch's unmerged diff onto the board and check edits against other branches. Off leaves only the live, same-time protection. |
branchMode | block | What an overlap with another branch's unmerged change does: block refuses the edit, warn lets it through with a heads-up. Changes that merely touch always just warn. |
fetchOnStart | true | Fetch the base branch at session start so merged branches drop off the board and line numbers share a fresh frame. Never prompts for credentials; a failed fetch is ignored. |
controllers | [] | Who may pause and resume this clone's agents from the board, by Qai username. Empty means anyone with a Qai key and this board's id. |
runner.slots | 1 | How many agents the runner runs at once, each in its own worktree. |
runner.allowedTools | read-only git, tests, lint, build, the guard CLI | The Claude Code --allowedTools list for headless runs. Edits are auto-approved (acceptEdits); any other command is denied and the agent is told not to retry it. |
runner.adapters | {} | Per-agent overrides: { "cursor": { "binaries": ["agent"], "args": [...] } }. The Codex and Cursor command lines are defaults that follow those CLIs' documented flags; adjust here when they change. session: true marks an adapter whose process stays open between turns and takes follow-ups as stream-json user messages on stdin (Claude Code by default). |
approvalWaitSeconds | 300 | How long a supervised permission prompt waits on the board. A headless agent is refused after that; a terminal session falls back to its keyboard. |
runner.idleMinutes | 15 | How long a session started from the page may sit waiting for a follow-up before the runner closes it and frees the slot. 0 never closes it. A conductor with live workers never idles out. |
runner.checks | [] | Commands the runner runs in a job's worktree after each turn that changed files: strings, or { "name", "command", "cwd", "timeoutSeconds" } objects (default timeout 300 seconds, at most 10 checks). Run through the shell with CI=1, so test runners do not sit in watch mode. Results show on the card and in the diff sheet, and reach a worker's conductor in its report. |
runner.models | ["haiku", "sonnet", "opus"] | The models this runner offers conductors, cheapest first: aliases or full model ids, whatever the CLI accepts. The conductor picks from this list per brief, the page's conductor form lists it, and nothing else is hardcoded. |
conductor | {} | Defaults for a conductor started from a terminal in this clone: { "budgetUsd": 10, "maxWorkers": 5, "maxDepth": 2 }. The page's form sets these per conductor. Conductors do not take a runner slot; workers do. |
pauseWaitSeconds | 300 | How long a paused agent's session stays on the line after it finishes its turn, waiting to be resumed from the board. After that it goes idle and the next prompt from its user starts it again. |
label | Claude Code | The name other agents see, suffixed with the machine and a short session tag. |
apiKey | qc_public | The free public key. Coordination is part of the free tier. |
server | https://api.quickcasa.ai | Where the board lives. |
The board is a plain JSON API keyed by the public key, so an editor or agent framework with a before-edit hook can join the same board. A claim is one request.
curl -s https://api.quickcasa.ai/v1/coordination/claim \
-H "x-api-key: qc_public" -H "content-type: application/json" \
-d '{
"repoId": "r_git_c2a8b3f1a97c95ab3048d0a2",
"agentLabel": "My editor",
"intent": "restyling the nav",
"branch": "feat/nav",
"targets": [{ "path": "src/nav.ts", "startLine": 40, "endLine": 52 }]
}'
| Endpoint | Purpose |
|---|---|
POST /pause | Pause an agent: { repoId, agentId, note }. Needs a Qai account key; the actor is the key's user. POST /resume undoes it. Refused with found: false for an unknown agent and allowed: false when the clone lists controllers that do not include you. |
POST /jobs | Start an agent on a runner: { repoId, runnerId, adapter, prompt, name, branch }. Needs a Qai account key. Add role: "conductor" with budgetUsd, maxWorkers, maxDepth, model and effort to start a conductor, and permissions: "bypass" on any job to run it without permission prompts (workers inherit it); host: "terminal" registers one that runs in your own terminal and answers with its token. A conductor spawns a worker with { repoId, token, parentJobId, prompt, name, areas, model, effort, budgetUsd, allowSubworkers, mcp }, no account key needed: the token says which tree it may touch. Refusals: too-many-workers, budget-exhausted, too-deep, parent-closed. POST /jobs/cancel with { repoId, jobId } (or a token) stops a job and everything under it. Runners use POST /jobs/claim and POST /jobs/update; an update carrying turnEnded: true from a worker becomes a report queued on its parent. POST /jobs/tree { repoId, jobId, token } lists a conductor's workers, POST /jobs/reports { repoId, jobId, token, seconds } waits for a terminal conductor's reports, POST /jobs/close closes a terminal conductor's job. An update may carry checks (the runner's results after a turn: { name, command, ok, exitCode, durationMs, output }). POST /jobs/diff/save keeps a runner's diff for a job, POST /jobs/diff { repoId, jobId } reads it ({ diff, baseSha, headSha, truncated, capturedAt, branch }). POST /jobs/act { repoId, jobId, kind } asks the job's runner to merge, merge-team, pull-request or diff (account key; one request at a time per job; refusals busy, nothing-committed, not-started, not-a-conductor); runners list theirs with POST /jobs/actions { repoId, runnerId } and answer with POST /jobs/actions/done { repoId, jobId, runnerId, kind, ok, message, url }, which lands on the job as lastAction (and pullRequestUrl). |
POST /push/subscribe | Register a browser for a repo's notifications: { repoId, subscription, label } with the subscription as PushSubscription.toJSON() gives it. Needs an account key; the public VAPID key comes from POST /push/key. POST /push/unsubscribe { repoId, endpoint } removes it, POST /push/test { repoId } sends one to the caller's own devices. The API sends on a pending approval, on a question (after a 45 second grace period when a conductor can answer it), and when a conductor finishes or fails. |
POST /impresario/configure | Set up the Impresario for a repo (account key): any of { repoId, enabled, apiKey, model, autonomy: "suggest" | "routine" | "run", allowRules, budgetUsdPerDay, digestMinuteUtc, notes }; fields left out stand. Answers the settings without the key. They also stream to the page as the impresario event. POST /impresario/accept { repoId, threadId | approvalId | jobId } takes what it proposed: the suggested answer goes to the agent, the request is allowed, or the runner is asked to merge the reviewed job. POST /impresario/draft { repoId, request, startNow } turns a plain request into a job draft for the start form (started at once in "run"). POST /impresario/digest { repoId } sends the digest now; POST /impresario/chores/request queues the chores; runners take them with POST /impresario/chores { repoId, runnerId } and report with POST /impresario/chores/done { repoId, runnerId, choreId, ok, message }. Suggestions land on the question (suggestion on the message), the approval (suggestion) and the job (review, heldBy, and brief with the house notes). |
POST /approvals | Raised by the PermissionRequest hook: { repoId, agentId, tool, detail, input, suggestions, waitSeconds }. Answers supervised: false for a session the board does not supervise. The hook then polls POST /approvals/poll with { repoId, approvalId }. A person decides with POST /approvals/decide { repoId, approvalId, allow, remember } (Qai key; a listed controller when the clone names any). POST /approvals/remote { repoId, agentId, remote } turns remote approvals on or off for a terminal session. Requests stream to the page as the approvals event and are kept a day. |
POST /attachments | Host a photo for this board: { repoId, name, contentType, data } with the file as base64 (JPEG, PNG, WebP or GIF, at most 8 MB decoded; the page shrinks first). Needs a Qai account key. Answers { attachment: { url, name, contentType, size } }; put that in the attachments array of POST /message or POST /jobs/input. Files older than a week are removed as new ones arrive. |
POST /said | Raised by the Stop hook of a session that shares its replies: { repoId, agentId, body }, stored as a message of kind said that nobody is delivered, so the agent's thread on the page shows its own words. A repeat of its previous words is dropped. POST /share { repoId, agentId, share } turns sharing on or off. |
POST /runners | The runners on a board: { repoId }, for a terminal choosing where to hand its session. |
POST /jobs/input | Send the next turn into an open session: { repoId, jobId, text }, where text is a follow-up or a slash command such as /compact; { repoId, jobId, end: true } closes it after the current turn. Needs a Qai account key, and either the person who started the job or one of the runner's controllers. Answers queued: false with a reason (not-open, no-input, not-allowed, queue-full) when it cannot. A /message with kind: "command" sends a slash command to an agent in a terminal through its inbox instead. |
POST /question | An agent asks the board: { repoId, agentId, agentLabel, body, options }. Returns the threadId to wait on with POST /inbox and filter: "thread". |
POST /answer | A person answers: { repoId, threadId, body, agentId } (the viewer's id). Needs a Qai account key. One answer per question; the reply is urgent so the asking agent is woken. |
POST /name | Give an agent a name: { repoId, agentId, name }. Written to its presence, its active claims, and the clone's alias so later sessions inherit it. |
POST /announce | Join the board before claiming anything. Returns your agentId and everyone else on the repo. |
POST /claim | Claim targets. Returns agentId (reuse it), claimed, refreshed, and conflicts. Nothing is claimed when there are conflicts unless force is true. |
POST /settle | After an edit lands: grow the editor's claim to the new extent and shift every claim below it in the same worktreeId. Also returns unread messages. |
POST /sync | Replace a branch's claims with its current hunks (base-branch line numbers, plus a content hash for new files). Returns the collisions with every other branch. Pass mergedBranches to drop branches you know have merged. |
POST /release | Release an agent's claims, all of them or specific paths. |
GET /board | The board for a repoId, optionally filtered by path. |
GET /stream | The board for a repoId as Server-Sent Events: agents, claims and messages events, each carrying that whole set, pushed whenever it changes. What the live page listens to. |
POST /message | Direct or broadcast ("to": "*") message with priority normal or urgent. |
POST /request, /respond, /inbox | Threads between agents: ask a holder, reply, read what is waiting. /inbox takes filter: "actionable" to pull only urgent messages and access requests. |
The edit is allowed. Qai Guard fails open on every network error, timeout or server error, and says so on stderr. A coordination outage never blocks a developer.
No. It is advisory between cooperating agents, enforced at the editor hook. A human editing in an IDE is not stopped, and an agent using a shell command to rewrite a file bypasses it. It covers the tools agents actually edit with.
Allowed. Claims are line ranges, not files. After each edit lands, the other agent's ranges in the same working tree are shifted by the lines added or removed, so they keep pointing at the right code.
They share the board, because the id comes from the git remote. Live claims compare working-tree line numbers, which is right for two agents in one checkout and approximate across checkouts. Branch claims compare base-branch line numbers, which is exact: that is the frame git merges in. The refusal always shows the first line of the other change so a false positive is obvious, and force is one command away.
Semantic conflicts: one branch renames a function, another adds a caller, git merges it cleanly and the build breaks. No line-based tool sees that; CI on the merged result does. It also cannot sequence your merges; it makes sure every agent knows the queue exists before joining it. And a client that has not fetched in a while is comparing against an older snapshot of the base until it does, which is why the session-start fetch is on by default.
Per claim: the repo id, a repo-relative path, a line range, up to 1500 characters of the changed region, your label, branch and intent; for a new file, a hash of its content. Presence: your label, branch and last-seen time. Coordination messages are stored as written. Live claims and presence are purged 24 hours after they end, branch claims when the branch merges or after 7 days without a sync, messages after 7 days. No file contents beyond the anchor snippet ever leave your machine.
On the recipient's next tool call, whatever tool it is. An agent mid-task makes tool calls every few seconds, so in practice within seconds. If the agent has gone idle, the Stop hook wakes it with the message before it finishes its turn. Normal messages wait for the next edit, the next heartbeat (20 seconds by default) or the next prompt.
The hook on non-edit tools reads one local file and returns unless the heartbeat interval has passed, so it costs a Node start, roughly 40 milliseconds. Network calls happen at most once per interval. Edits do one request before and one after; the branch diff behind them runs at most every thirty seconds while editing, and always at session start and idle.
Yes, it is the same board. The MCP coordination tools (check_in, list_check_ins, message_agent, request_access and friends) see hook claims and messages and vice versa. Use the repoId from .qai/guard.json in those calls so everyone is on one board.
Nothing. Coordination is part of the free Qai layer, on the public key, with no account.