Repository navigation
feat(runner): run kix jobs on your own machines with quest runner - #188
Conversation
Quest-Claim: 7f8d2d05130c3865
Checkpoint: quest runner login/account/run, the Claude Code adapter over claude -p stream-json with kix tools as an HTTP MCP server, job checkouts and publication, raw output upload, and the Worker's runner page, login approval, and account preference order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Outcome: (written by Claude Opus 5.5) |
Delete quest/m0/kix/runner.md, which held only checks that need live machines, so the quests it blocked unblock. The kix epic's end-to-end rehearsal now covers them on a Linux and a macOS runner, and the sandbox quest records that it replaces bypassPermissions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (1)ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f79b90976
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # quest/m0/kix/runner.md # quest/m0/worktree-agents.md # src/cli/main.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61317ab3be
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: 61317ab3be
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
Until the job sandbox ships, a job resumes with an answer from a maintainer
other than its requester (the runner's owner) only once the requester approves
it in the inbox; the runner also refuses an answer without approvedAt.
- Publication: a stale head gets 409 { error, head }, and the runner continues
from that head, so a lost reply no longer fails the attempt.
- Plan and iterate jobs read their issue or pull request (reviews, unresolved
threads, checks) through a read-only kix context tool.
- Serialize operations on each repository's bare clone across slots.
- Clear the account-reset timer when another runner replaces this one.
- Point the agent to nested AGENTS.md files.
- Skip the login test on Windows, where runners do not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renumber the answer approval migration to 0017, after main's 0016_recovery, so the Worker applies both in order. The decision record follows main's retry chain and reads approved answers only. Start over records an approved answer, and the requester can start over in place of another maintainer's answer that awaits approval, as they can answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merging after main moved under this PR:
Enabling squash auto-merge on that head. (written by Claude Opus 5.5) |
Main's runner (#188) gives kix iterate jobs the iterate skill, which now takes and releases the PR with quest iterate and quest release. Runners hold no GitHub credentials and kix already owns the job's PR, so the system prompt tells the job to skip those steps, as it does for quest start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2cb9ac04f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| /** Each bare clone's queue: jobs on one repository share its config, objects, and worktree list. */ | ||
| const queues = new Map<string, Promise<void>>(); |
There was a problem hiding this comment.
Prevent runner processes from sharing one worktree
When a second quest runner starts with the same default runner home, the hub replaces the first connection and can immediately reassign its acknowledged jobs while the first process's agents are still stopping for up to STOP_GRACE_MS. This Map serializes repository operations only within one JavaScript process, so the two processes can concurrently reuse and modify the same job worktree and bare repository, mixing or losing the new attempt's changes. Enforce a process-wide lock for the runner home (or use an inter-process lock covering repository/worktree ownership) before accepting jobs.
Useful? React with 👍 / 👎.
| ? `issue(number: $number) { number title state body author { login } ${comments} }` | ||
| : `pullRequest(number: $number) { number title state body isDraft author { login } baseRefName headRefName headRefOid ${comments} | ||
| reviews(last: 50) { nodes { author { login } state body submittedAt } } | ||
| reviewThreads(first: 100) { nodes { isResolved isOutdated path line comments(first: 50) { nodes { author { login } body createdAt } } } } |
There was a problem hiding this comment.
Paginate unresolved review threads in runner context
For a pull request with more than 100 review threads, this fetches only the first page and filters unresolved threads afterward, so a newer unresolved thread beyond that page is silently omitted. Because context is the iteration agent's sole source of review feedback, it can deliver without addressing those comments; follow pageInfo cursors until all threads have been examined.
Useful? React with 👍 / 👎.
Implements quest/m0/kix/runner.md and deletes it. The job protocol, hub, and publisher were already on the Worker, but nothing ran jobs. This adds the runner and the Worker pieces it needs. The checks that need live machines move into the kix epic's end-to-end rehearsal, so sandbox, opencode, planning, merge-job, activity, and the m1 quests that only needed this code unblock.
Summary
quest runner(src/runner/, outside the core):quest runner loginmakes a token on the machine and prints a/runners?name=…&platform=…&key=<sha256>link. Once the signed-in owner approves it, the runner pollsGET /api/runnerand saves its login. The token itself never leaves the machine.quest runner account add [--console] [--label]runsclaude auth loginin a newCLAUDE_CONFIG_DIRunder the runner's home, then readsclaude auth status --jsonto get the account's kind and a default label. Account ids are random and stored on disk, so they stay the same across restarts. There are alsoaccount listandaccount remove.quest runner [--slots N]holds the WebSocket and reconnects with backoff. It sends a coalescedstatuswhenever anything changes, and an assignment is acknowledged by listing it. A repeatedassigncounts as one attempt. It honorscancelpushes and thestop/cancelactions in status replies. Requests that are unanswered when the connection drops are resent with the same id. If the hub closes with 4000 ("replaced"), the runner exits and clears its account-reset timer. On SIGINT or SIGTERM it publishes running work, then sendsattempts: []so those jobs move to another runner right away.nix develop --accept-flake-config --no-write-lock-file --command(bootstrap jobs run without a devShell). The checkout is kept between attempts while the branch head still matches, so a paused session resumes where it was.remote.origin.pushurlis set to an invalid value so a push from the checkout fails.202reply pauses the attempt. If kix replies409with a newerhead, as it does after an earlier reply was lost, the runner fetches that head and checks that it descends from the last head it knew. It then continues from that head and resends only what it lacks.claude -p --output-format stream-json --verbosewith--session-idor--resume. The kix tools (checkpoint,ask,deliver, andcontexton issue and pull request jobs) are served as a localhost HTTP MCP server with a bearer token per attempt. Its config file is written mode 0600 in the account's home, so the token stays out ofps.--strict-mcp-configand--disallowedTools AskUserQuestionare passed, so every question goes through kix. Account limits are read from a rejectedrate_limit_event, assistantrate_limit/billing_errorflags, or the text of the result. The adapter interface (src/runner/adapter.ts) is small enough for opencode to fit.deliveris the only path tocompleted.askand publication decisions pause the job without sending a result. A turn that ends without delivering isfailed, and work in progress is published first in every case.limitedkeeps the account out of its status reports until the reported reset (or one hour when none is reported), and the runner never switches accounts.cancelledpublishes the partial work. Untrusted jobs are refused, and so is an assignment carrying an answer withoutapprovedAt.AGENTS.mdand lists the tracked nestedAGENTS.mdfiles, which the agent reads for the directories it works in. A system prompt maps the parts of the workflow that assume push,gh, orquest startonto the kix tools. Plan-issue and iterate jobs are told to read their target withcontextfirst.outputchunks of at most 1 MiB with contiguousseq, split on character boundaries. Chunks are batched at 256 KiB or 60 s, and uploading stops ontruncated.runnerpackage wrapsquestwithQUEST_RUNNER_CLAUDEset to nixpkgs'claude-code. It is allowed through an unfree predicate for that one package. The defaultquestpackage is unchanged.Worker:
registerRunner(the same hash approved again returns the same runner; another owner's hash is refused),moveAccount,GET /api/runner, and a/runnerspage linked from the inbox. The page shows each runner's online state and accounts, the account preference order across runners with Up/Down controls (which re-dispatch), and the form that approves a login link. Form posts check the origin.GET /api/runner/context?job=…&attempt=…, fenced to the current attempt, reads the job's target live as the App with the requester's current authority. An issue returns its text and comments. A pull request returns its text, comments, reviews, unresolved review threads, and the checks at its head.409 { error, head }.job_questions.approvedAt(migration0017, after main's0016_recovery). The requester's own answers are approved as given. Another maintainer's answer leaves the jobwaitingand sends anapprovalpush. Only the requester canPOST …/jobs/<job>/approveit, or answer the question themselves instead. The inbox shows the requester the pending answer with an approve button above the usual form. Everyone else sees the job waiting. The planning decision record uses approved answers only.Validation
just checkandjust testpass (429 CLI tests with 2 skipped, and 330 Worker tests, after merging main).test/runner.test.tsruns a fakeclaudeandnixagainst a fake hub. It covers deliver, ask and resume, cancel, limits, failures, untrusted jobs, stop, a repeated assign, shutdown, and reconnect resends. This round adds tests for:contexttool on a planning job;AGENTS.mdlisting.Worker tests cover approval through the store and the inbox forms, the context endpoint for issues, pull requests, and quests, and the stale-head reply.
cloudflare/test/runners.test.tscovers the runner pages.The login test now skips on Windows, like the other runner tests, since
quest runnerrefuses to run there. That was the native Windows CI failure.Run against the real Claude Code 2.1.305 with a Max login, on 2026-10-21: the session listed only
mcp__kix__askandmcp__kix__checkpoint(no AskUserQuestion), called both, and a--resumewith the answer continued the session.rate_limit_eventarrives as{rate_limit_info: {status, resetsAt}}, which is what the parser expects. Not rerun this round.nix build .#runnerbuilds with claude-code 2.1.291. Nix also reads flakes from worktrees of a bare repository.Limitations
CLAUDE_CONFIG_DIRon macOS; whether GitHub serves a fork-network commit by SHA from the upstream URL, which a reassigned job and the lost-reply reconciliation rely on; a real rejectedrate_limit_event; thecontextGraphQL query against real GitHub; and the whole flow against kix.quest.--permission-mode bypassPermissionsand the user's environment, including the SSH agent and git credentials, minusGH_TOKEN,GITHUB_TOKEN, and harness variables. That holds for trusted revisions only, and only for answers the runner's owner gave or approved, until the job sandbox lands. Issue and pull request text fromcontextis still untrusted input to that agent; the sandbox quest already plans to contain prompt injection from it.quest runnerprocesses sharing one runner home are not supported: kix replaces the older connection.Decisions
The maintainer settled these on 2026-10-22, each as recommended:
/runners, rather than an RFC 8628 device-code flow with a server-side pending table. It needs no new table and the token never leaves the machine. A phished link connects the attacker's machine, which the page warns about; a device code has the same risk.askMCP tool with AskUserQuestion disabled, rather than intercepting AskUserQuestion through the SDK'scanUseToolcontrol protocol. A kix question pauses the job for hours, so the turn ends either way, and opencode can use the same tools.--permission-mode bypassPermissionsuntil the job sandbox ships and replaces it; the sandbox quest now records this. Routing permission prompts to kix would pause the job on every tool call.LIMIT_FALLBACK_MS), rather than being reported usable at once and taking the next job straight into the limit.runnerflake package, which lags upstream (2.1.291 against 2.1.305 today). Without Nix,claudecomes fromQUEST_RUNNER_CLAUDEorPATH.quest rmdeletesquest/m0/kix/runner.md, which held only live checks. The kix epic's end-to-end rehearsal now runs a Linux and a macOS runner and lists those checks (Keychain isolation, fetching a fork commit by SHA, a real rejectedrate_limit_event), so the quests it blocked unblock.Choices made while addressing the review, for the reviewer to check:
contexttool backed by a live kix read, rather than a snapshot sent with each assignment. A job resumed after a long pause sees new reviews and checks, nothing is stored in D1, and the assignment, which kix resends until acknowledged, stays small.AGENTS.md: listed by path for the agent to read, not inlined, to keep prompts bounded in large repositories.Suggested follow-ups
quest worktreein the job adapter, as kixelated/quest#186 had planned in the deleted quest:runfor supervision, scratch release, and disk admission,removeonce a job's checkout is finished (including jobs fenced out or never reassigned here), andstopfor a crashed supervisor's leftover processes.quest runner service installto write a systemd user unit or launchd agent.approvalpush. Today it goes to every project member with a device, like other decisions.🤖 Generated with Claude Code
(written by Claude Opus 5.5)