Kanon
Plugin

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 card

One 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.

Prerequisites

  • The plugin installed and signed in (/kanon:setup) in the repo the worker will serve — the daemon reads .kanon/config.json for the repo slug and ~/.kanon/credentials.json for the token.
  • claude — the daemon spawns headless Claude Code sessions; this machine's subscription pays for them.
  • gh authenticated (gh auth status) — the worker opens PRs with it.
  • A clean clone with an origin remote it can fetch from.

Start it

From a Claude Code session in the repo:

/kanon:worker

Or directly from a shell (what the skill runs for you):

node <plugin-root>/mcp-server/dist/cli.js daemon

The 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:

StageWhereWhat it queues
DiscoverReview page → "Run discovery with worker" (or "Re-discover")/kanon:discover --non-interactive, code-only — proposals land on the review page
ReviewReview pageAlways UI — approve, edit, reject; the human gate never moves
ScanReview page → "Scan all with worker" (appears once review is complete)/kanon:scan --non-interactive — every approved feature, with the freshness pre-skip
WorkChanges 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:

  1. Queues a task and derives the branch name server-side (fix/…, improve/…, feat/… — the same convention /kanon:work uses), so the card, the session view, and the PR all agree before work starts.
  2. 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.
  3. 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 for debugging.

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 machineNever 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 numberDiffs, code, test output bodies
Task status + error summariesYour 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.plist

Linux (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.target
systemctl enable --now kanon-worker

Whatever 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_MINUTES overrides) 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.json on first run; revoking the API token in Settings → Tokens cuts it off immediately.

Troubleshooting

SymptomCause / fix
"Run with worker" doesn't appearNo 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/403Token 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/<task-id> for inspection; re-queue when ready.
PR opened but the card shows no linkThe 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.

On this page