API reference
All API endpoints require authentication via either a browser session or a Bearer token (KANON_API_TOKEN).
Discovery
POST /api/discover
Run the taxonomy discovery pipeline.
Body:
{
"repoSlug": "your-org/your-app",
"localPath": "/path/to/repo", // optional if GitHub source configured
"shaped": true, // optional: conform to approved structure
"crawl": false // optional: also crawl the running app
}Response: { runId, domains, features, refreshed, signals }
GET /api/taxonomy?repoSlug=…
Fetch the taxonomy state (approved and proposed nodes).
Response: { exists, approved, proposed, spaces }
POST /api/taxonomy
Bulk-approve proposed nodes.
Body: { repoSlug, nodeIds? } — omit nodeIds to approve all.
Response: { approved, total, errors? }
Scanning
POST /api/scan
Deep-scan one feature.
Body:
{
"repoSlug": "your-org/your-app",
"localPath": "/path/to/repo", // optional
"featureId": "your-org/your-app::domain::feature",
"guide": true, // optional: generate the full guide
"targetCommitSha": "abc123..." // optional: pin the run to a commit
}Response: { runId, status }
POST /api/scan-all
Scan all approved features with bounded parallelism.
Body:
{
"repoSlug": "your-org/your-app",
"fleetConcurrency": 3, // optional
"guide": true // optional
}Response (202): { accepted, features, skipped }
GET /api/scan-all?repoSlug=…
Poll fleet status.
Ingestion
POST /api/ingest
Upload a discovery bundle (from the plugin).
Body: { repoSlug?, bundle } (max 25MB)
Response: { runId, crawlRunId, domains, features, refreshed, reviewUrl }
GET /api/ingest?repoSlug=…
Recent ingest runs.
Changes
GET /api/changes?repoSlug=…&status=open,implementing&linearId=ENG-42&limit=10
List spec changes.
Response: { changes: [{ id, title, summary, status, direction, source, linearIdentifier, linearUrl, featureId, claims }] }
POST /api/changes
Update a change's status.
Body: { repoSlug, changeId, status: "implementing" | "resolved" | "dismissed" }
Worker
The worker daemon's queue endpoints. Machine-token only, and the token must be workspace-scoped (from /kanon:setup) — browser sessions and the legacy instance-wide env token are refused. Normally only the daemon calls these; see The worker daemon.
POST /api/worker/claim
One poll: heartbeat the daemon's presence, expire stale leases, atomically claim at most one queued task.
Body: { workerId, hostname?, version?, repoSlugs: [string] }
Response: { task: { id, repoSlug, type, payload, status, attempt, cancelRequested, specChangeId, leaseExpiresAt, createdAt } | null, retryAfterSeconds, leaseSeconds, deniedRepoSlugs }
type is work_change, discover, or scan; the payload shape follows it — {changeId, branch, sliceN?, planOnly?}, {mode: "code_only"}, or {feature?}.
POST /api/worker/tasks/{taskId}/events
Heartbeat + activity batch (an empty batch is a pure heartbeat). Re-sending a batch is safe — (taskId, seq) deduplicates. 409 means the lease was lost: stop the child process and stop reporting.
Body: { repoSlug, workerId, activities: [{ seq, kind: "status" | "thought" | "tool" | "text" | "error", label, detail? }] }
Response: { cancelRequested, leaseExpiresAt }
POST /api/worker/tasks/{taskId}/complete
The lease holder's terminal report. On success with a PR, the URL is written through to the change's prUrl/prNumber.
Body: { repoSlug, workerId, outcome: "succeeded" | "failed" | "cancelled", result: { prUrl?, prNumber?, branch?, summary?, errorMessage? } }
GitHub
GitHub is connected through an OAuth round trip in the browser, not through a machine API. These are session-only — a bearer token cannot use them.
GET /api/github/oauth/start
Redirects to GitHub to authorize. Sets an httpOnly state nonce that the
callback compares, so a crafted callback cannot bind someone else's account.
GET /api/github/oauth/callback
GitHub returns here. Exchanges the code and stores the token encrypted at rest.
GET /api/github/repos
The repository picker's list for the connected account.
Authentication
POST /api/device/start
Start device authorization (for the plugin).
Response: { userCode, deviceCode, verifyUrl, expiresInSeconds, pollIntervalSeconds }
GET /api/device/poll?deviceCode=…
Poll for device approval. Unauthenticated: possession of the 256-bit device code
IS the credential. Always answers 200 with a status body; unknown, consumed and
raced codes all read as expired, so the endpoint cannot be used to probe which
codes exist. The approved response carries the raw token exactly once.
Events & Tickets
POST /api/events/create-tickets
Create Linear tickets for uncovered analytics events.
Body:
{
"repoId": "...",
"routes": [{ "method": "POST", "route": "/api/foo", "featureName": "..." }],
"mode": "batch" // or "individual"
}POST /api/chat/create-ticket
Create a Linear ticket from a chat draft.
Body: { repoId, threadId, messageId, widgetId, title, description }
MCP tools (plugin)
| Tool | Description |
|---|---|
kanon_setup_begin | Start device auth |
kanon_setup_poll | Poll for device approval |
kanon_whoami | Check auth status |
kanon_validate_bundle | Validate a bundle file |
kanon_assemble_bundle | Assemble a discovery bundle |
kanon_push_bundle | Upload a bundle |
kanon_get_taxonomy | Fetch taxonomy state |
kanon_ingest_status | Recent ingest runs |
kanon_approve_all | Bulk-approve proposals |
kanon_list_changes | List pending spec changes |
kanon_update_change | Update change status |
kanon_select_files | Select closure files for a feature |
kanon_collect_facts | Collect mechanical facts |
kanon_assemble_guide | Build guide from collected data |
kanon_push_guide | Upload a guide |
kanon_get_guide_status | Check guide freshness |