The worker daemon
The worker turns Kanon's UI into an execution surface: anyone on your team clicks Run with worker on a pending change, and a PR comes back — no one opens Claude Code, no one copies a command. The daemon runs on a machine you control, uses your Claude subscription, and your code never leaves that machine. Kanon receives one-line activity labels and the PR URL, nothing else.
How it works
Kanon UI: "Run with worker" on a change
→ task queued (visible immediately as a chip on the change card)
→ your daemon polls, claims the task
→ creates a git worktree on a server-derived branch
→ runs /kanon:work <change> --non-interactive headlessly
→ streams a live activity trace to the session view
→ opens a draft-or-ready PR via gh
→ reports the PR URL back to the change cardOne task runs at a time, each in its own worktree under .kanon/worktrees/,
so a running task never touches your checkout or another task's files.
A run that dies resumes where it stopped
Worktrees are named after the task's lineage — the work being done — not the individual task id. Every attempt at the same work therefore reattaches the same directory, whether that is an automatic retry seconds later or a re-queue from the UI days later. Three things carry over, in ascending order of how much they survive:
| What | Where it lives | Survives |
|---|---|---|
Commits, uncommitted edits, .kanon/ crawl state | the reattached worktree | anything short of deleting the directory |
| The Claude session (what was read, tried, decided) | ~/.kanon/sessions.json → claude --resume | the same machine only — a transcript is local |
| Which slices are done | Kanon (the change's plan) | a wiped worktree, a different machine, a different daemon |
Reuse never moves HEAD: fetching is safe, but a checkout or reset would
discard the very work being preserved. A worktree is removed only on success —
a failure keeps it precisely so the next attempt can pick it up.
For a multi-slice feature this is what stops a re-queue from redoing merged
work: /kanon:work reports each slice to Kanon as its PR opens, so the next run
starts at the first unfinished slice rather than re-planning from scratch.
Prerequisites
- The plugin installed and signed in (
/kanon:setup) in the repo the worker will serve — the daemon reads.kanon/config.jsonfor the repo slug and~/.kanon/credentials.jsonfor the token. claude— the daemon spawns headless Claude Code sessions; this machine's subscription pays for them.ghauthenticated (gh auth status) — the worker opens PRs with it.- A clean clone with an
originremote it can fetch from.
Start it
From a Claude Code session in the repo:
/kanon:workerOr directly from a shell (what the skill runs for you):
node <plugin-root>/mcp-server/dist/cli.js daemonThe daemon fails loudly at startup if claude, gh, or git aren't ready —
it will not claim a task it can't finish. Once polling, the Worker card on
/connect shows it online, and Run with worker buttons light up on the
Changes page within ~10 seconds.
Stop it with Ctrl-C. A task in flight fails honestly via lease expiry (~2 minutes) and can be re-queued — nothing silently re-runs.
The whole pipeline, from the UI
With a worker online, every stage of the loop runs from the browser:
| Stage | Where | What it queues |
|---|---|---|
| Discover | Review page → "Run discovery with worker" (or "Re-discover") | /kanon:discover --non-interactive, code-only — proposals land on the review page |
| Review | Review page | Always UI — approve, edit, reject; the human gate never moves |
| Scan | Review page → "Scan all with worker" (appears once review is complete) | /kanon:scan --non-interactive — every approved feature, with the freshness pre-skip |
| Work | Changes page → "Run with worker" | /kanon:work <change> --non-interactive — a review-ready PR |
Discover and scan tasks run in a detached worktree (they read code and push bundles; no branch, no PR) and every task type gets the same live session view. One discovery, one sweep, and one task per change can be in flight at a time — double-clicks land on the already-running session.
Queue work from the UI
On Changes (/knowledge-base/{repo-id}/changes), every open change shows
Run with worker while a worker is online. Clicking it:
- Queues a task and derives the branch name server-side (
fix/…,improve/…,feat/…— the same convention/kanon:workuses), so the card, the session view, and the PR all agree before work starts. - The change card shows a live chip (
worker: queued → running) linking to the session view — a step-by-step trace of what the worker is doing. - On success, the PR link lands on the change and in the session view. On failure, the session view shows what went wrong and the worker keeps the worktree — re-queueing resumes in it rather than starting over.
Cancel from the session view: a queued task cancels instantly; a running one is asked to stop and acknowledges within a heartbeat (~2s).
Only one task per change can be in flight — double-clicks and racing teammates get the existing task, never a duplicate.
What the worker sends to Kanon (and what it never sends)
| Leaves your machine | Never leaves |
|---|---|
| One-line activity labels ("Read: src/cart.ts", "Bash: pnpm test") | File contents — tool inputs are whitelisted to paths/patterns only |
| The PR URL and number | Diffs, code, test output bodies |
| Task status + error summaries | Your Claude or GitHub credentials |
The task payload going the other way is structured too: the daemon only ever interpolates validated ids and enum flags into a command — free text typed into the Kanon UI cannot reach a shell on your machine.
Run it always-on
A worker on a laptop stops when the lid closes; queued tasks wait until it's back. For a team, run it on a box that stays up.
macOS (launchd) — ~/Library/LaunchAgents/com.kanon.worker.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.kanon.worker</string>
<key>WorkingDirectory</key><string>/path/to/your/repo</string>
<key>ProgramArguments</key><array>
<string>/usr/local/bin/node</string>
<string>/path/to/plugin/mcp-server/dist/cli.js</string>
<string>daemon</string>
</array>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/kanon-worker.log</string>
<key>StandardErrorPath</key><string>/tmp/kanon-worker.log</string>
</dict></plist>launchctl load ~/Library/LaunchAgents/com.kanon.worker.plistLinux (systemd) — /etc/systemd/system/kanon-worker.service:
[Unit]
Description=Kanon worker daemon
After=network-online.target
[Service]
User=ci
WorkingDirectory=/path/to/your/repo
ExecStart=/usr/bin/node /path/to/plugin/mcp-server/dist/cli.js daemon
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.targetsystemctl enable --now kanon-workerWhatever runs the service needs the same auth as an interactive run: a
signed-in ~/.kanon/credentials.json (or KANON_API_TOKEN), gh auth, and
Claude Code auth for that user.
Behavior details
- Timeout: a task is killed after 30 minutes (
KANON_TASK_TIMEOUT_MINUTESoverrides) and reported as failed. - Draft by default: in non-interactive mode the PR is a draft unless typecheck, lint, and tests all passed — a human promotes it.
- Nothing auto-merges. Ever.
- Identity: the daemon mints a stable worker id into
~/.kanon/worker.jsonon first run; revoking the API token in Settings → Tokens cuts it off immediately.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "Run with worker" doesn't appear | No fresh heartbeat: the daemon isn't running, or its token maps to a different workspace. Check the Worker card on /connect. |
| Daemon exits immediately with 401/403 | Token revoked or workspace-scoped token missing — re-run /kanon:setup. The daemon deliberately refuses to retry auth failures. |
| Task stuck "queued" | Worker offline (lid closed?) — tasks wait; start the daemon and it claims within one poll (~5s). |
| Task failed with "worker lease expired" | The daemon died mid-task (crash, network, shutdown). The worktree is kept under .kanon/worktrees/<lineage>; re-queueing reattaches it and continues from there. |
| Session view says "the recorded session could not be resumed" | The local Claude transcript was pruned or the run moved machines. Harmless — the id is forgotten and the retry starts a fresh session in the same worktree, keeping the files and the slice progress. |
| A re-queued feature re-plans instead of resuming | The plan was never pushed to Kanon (an older plugin, or the run died before step 3d). Check kanon_get_plan for the change; once a plan is stored, slice progress is machine-independent. |
| PR opened but the card shows no link | The transcript didn't include the URL and gh pr view failed in the worktree — check gh auth status on the worker box. |
worker id is registered to another workspace | ~/.kanon/worker.json was copied between machines signed into different workspaces — delete it; a fresh id is minted on next start. |