Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,22 +499,28 @@ signals the agent and waits until its `start` or `resume` has reported.
## Run kix jobs on your machines

`quest runner` turns a Linux or macOS machine with Nix into a
[kix.quest](https://kix.quest) runner, driving Claude Code with that machine's
own logins:
[kix.quest](https://kix.quest) runner, driving Claude Code or opencode with
that machine's own logins:

```sh
quest runner login --name laptop # prints a link to approve while signed in to kix
quest runner account add # Claude Code's own sign-in, in an isolated home
quest runner account add --console --label work
quest runner account add opencode # opencode's sign-in: OpenCode Go, ChatGPT, or an API key
quest runner account add opencode --model openai/gpt-5.5
quest runner account list
quest runner --slots 2 # runs jobs in the foreground until interrupted
```

`login` makes a token that never leaves the machine; the link carries only its
hash, and kix connects the runner once you approve it. Each account signs in
through `claude auth login` in its own `CLAUDE_CONFIG_DIR`, so kix stores only
an opaque account id, its kind, and a label. kix.quest/runners shows each
runner's presence and sets the order jobs use your accounts in.
through its CLI's own flow in its own home: `claude auth login` in a
`CLAUDE_CONFIG_DIR`, or `opencode auth login` in a set of XDG directories, so
kix stores only an opaque account id, its kind, and a label. An opencode
account uses only the providers it signed in to, with `--model` or opencode's
default. Use Claude Code for Claude subscriptions, since opencode cannot sign
in to them. kix.quest/runners shows each runner's presence and sets the order
jobs use your accounts in.

Every job runs in its own checkout inside the project's `nix develop` shell,
with the flake's `nixConfig` honored; a project without a flake gets a bootstrap
Expand All @@ -527,12 +533,14 @@ token; a new plan reads your idea and pasted context there. kix publishes as its
pauses for you to pick another account; the runner never switches on its own.
On SIGINT or SIGTERM, running work is published and its jobs move to another
runner. Until the job sandbox ships, runners take only trusted revisions,
agents run with `--permission-mode bypassPermissions`, and a job resumes with
another maintainer's answer only once you approve it in the inbox.

The flake's `runner` package is a `quest` whose Claude Code is pinned by this
repository's lock (`nix run github:kixelated/quest#runner -- runner`);
otherwise `claude` comes from `QUEST_RUNNER_CLAUDE` or `PATH`. State lives in `$QUEST_RUNNER_HOME`, by default
agents run with every tool allowed (Claude Code's `bypassPermissions`), and a
job resumes with another maintainer's answer only once you approve it in the
inbox. opencode runs as `opencode serve` on localhost behind a password made for each turn.

The flake's `runner` package is a `quest` whose Claude Code and opencode are
pinned by this repository's lock (`nix run github:kixelated/quest#runner -- runner`);
otherwise `claude` and `opencode` come from `QUEST_RUNNER_CLAUDE` and
`QUEST_RUNNER_OPENCODE`, or `PATH`. State lives in `$QUEST_RUNNER_HOME`, by default
`$XDG_DATA_HOME/quest/runner`; one `quest runner` runs in a home at a time. To keep a runner up, run `quest runner` as a user
service, such as a systemd user unit with `Restart=on-failure`.

Expand Down
15 changes: 15 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,17 @@
paths = [ quest ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/quest --set-default QUEST_RUNNER_CLAUDE ${pkgs.lib.getExe pkgs.claude-code}
wrapProgram $out/bin/quest \
--set-default QUEST_RUNNER_CLAUDE ${pkgs.lib.getExe pkgs.claude-code} \
--set-default QUEST_RUNNER_OPENCODE ${pkgs.lib.getExe pkgs.opencode}
'';
meta = quest.meta;
};
manifest = pkgs.lib.importJSON ./package.json;
# package.json's packageManager pins Bun for CI runners without Nix.
bunVersion = pkgs.lib.removePrefix "bun@" (pkgs.lib.importJSON ./package.json).packageManager;
bunVersion = pkgs.lib.removePrefix "bun@" manifest.packageManager;
# The runner drives the pinned opencode through the SDK of the same release.
opencodeSdk = manifest.dependencies."@opencode-ai/sdk";
in
{
packages = {
Expand Down Expand Up @@ -73,6 +78,13 @@
fi
touch "$out"
'';
opencode-sdk = pkgs.runCommand "opencode-sdk" { } ''
if [ "${pkgs.opencode.version}" != "${opencodeSdk}" ]; then
echo "package.json pins @opencode-ai/sdk ${opencodeSdk}, but the runner pins opencode ${pkgs.opencode.version}" >&2
exit 1
fi
touch "$out"
'';
quest-tree = pkgs.runCommand "quest-tree" { nativeBuildInputs = [ quest ]; } ''
quest --root ${self} check
touch "$out"
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"fix": "prettier --write ."
},
"dependencies": {
"@opencode-ai/sdk": "1.19.0",
"commander": "15.0.0",
"mdast-util-from-markdown": "2.1.0",
"mdast-util-gfm-strikethrough": "2.0.1",
Expand Down
8 changes: 7 additions & 1 deletion quest/m0/kix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,20 @@ fixes what it finds:
- A rejected `rate_limit_event` and its `resetsAt` arrive when a plan runs
out, so the account sits out until its reset instead of the one-hour
fallback.
- opencode accounts (shipped with stubbed servers and a free model) sign in
to an OpenCode Go plan and a ChatGPT plan, the latter through its headless
method on a remote runner, and run a job on opencode's default model or
`--model`. Their limits pause the account until the reset: OpenCode Go's
`account_rate_limit` retry, and ChatGPT's `usage_limit_reached` error with
`resets_at`. If ChatGPT plans fall short through opencode, plan the Codex
adapter the 2026-10-15 decision allows.

## Required

- [Deploy kix.quest](/quest/m0/kix/deploy.md) - the operator registers the App and deploys the Worker with GitHub sign-in at kix.quest
- [Operator setup](/quest/m0/kix/operator-setup.md) - fork namespace, pilot installations, and a verified test PR
- [Per-project forks](/quest/m0/kix/forks.md) - each project publishes through its own fork, recorded by repository ID
- [Job sandbox](/quest/m0/kix/sandbox.md) - OS-sandboxed commands, with untrusted PR devShells built in a VM on Linux runners
- [opencode adapter](/quest/m0/kix/opencode.md) - OpenCode Go, ChatGPT plans, and API-key providers through opencode
- [Merge jobs](/quest/m0/kix/merge-job.md) - a merge pick fixes conflicts, CI, and reviews on a runner, then lands the PR
- [Web execution](/quest/m0/kix/execution.md) - spawn and iterate through the UI, ending in contribution PRs
- [Seeded GitHub fake](/quest/m0/kix/github-fake.md) - the local server runs publication end to end, and tests share its fake
Expand Down
26 changes: 0 additions & 26 deletions quest/m0/kix/opencode.md

This file was deleted.

1 change: 0 additions & 1 deletion quest/m1/cloud/funding.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ cloud runners; donor allocations follow working cloud runners separately.
## Required

- [Job sandbox](/quest/m0/kix/sandbox.md) - the boundary keeping provider credentials outside job commands
- [opencode adapter](/quest/m0/kix/opencode.md) - the supported API-provider adapter

## Related

Expand Down
22 changes: 15 additions & 7 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { realpathSync } from "node:fs";
import { relative, resolve, sep } from "node:path";

import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
import { Argument, Command, CommanderError, InvalidArgumentError, Option } from "commander";

import { description, version } from "../../package.json";
import {
Expand All @@ -28,6 +28,7 @@ import {
removeAccount,
runRunner,
} from "../runner/cli";
import type { AdapterName } from "../runner/adapter";
import { held, reach, target, upstream, upstreamHead } from "./claims";
import { gate } from "./gate";
import { gitHubDependencies, GitHubInputError, mergedHead, pullRequest, remoteUrl } from "./github";
Expand Down Expand Up @@ -708,7 +709,7 @@ export function main(argv: string[], out: Output): number | Promise<number> {
"Turn this Linux or macOS machine into a kix runner. `quest runner login` connects it to your kix " +
"account, `quest runner account add` signs agent accounts in with their own CLIs, and `quest runner` " +
"runs routed jobs in the foreground until interrupted.\n\n" +
"Each account signs in through Claude Code's own flow in an isolated home (CLAUDE_CONFIG_DIR); kix " +
"Each account signs in through its CLI's own flow, Claude Code or opencode, in an isolated home; kix " +
"stores only an opaque account id, its kind, and its label, and never sees its tokens. Every job runs " +
"inside its project's `nix develop` shell. State lives in $QUEST_RUNNER_HOME, by default " +
"$XDG_DATA_HOME/quest/runner.",
Expand All @@ -733,11 +734,18 @@ export function main(argv: string[], out: Output): number | Promise<number> {
const account = runner.command("account").description("Add, list, and remove this runner's agent accounts");
account
.command("add")
.description("Sign a Claude Code account in with `claude auth login` in a new isolated home")
.option("--console", "Sign in with an Anthropic Console account (API billing) instead of a subscription")
.option("--label <label>", "The name kix shows for the account (default: its email and plan)")
.action((options: { console?: boolean; label?: string }) =>
runnerAction(() => addAccount(runnerIO(), { console: !!options.console, label: options.label })),
.description(
"Sign an account in with its CLI's own flow in a new isolated home: `claude auth login` for Claude " +
"Code, or `opencode auth login` for an OpenCode Go plan, a ChatGPT plan, or any provider's API key",
)
.addArgument(new Argument("[cli]", "The agent CLI").choices(["claude", "opencode"]).default("claude"))
.option("--console", "Claude Code: sign in with an Anthropic Console account (API billing)")
.option("--model <provider/model>", "opencode: the model this account's jobs use (default: opencode's)")
.option("--label <label>", "The name kix shows for the account (default: its email or providers)")
.action((cli: AdapterName, options: { console?: boolean; model?: string; label?: string }) =>
runnerAction(() =>
addAccount(runnerIO(), { cli, console: !!options.console, model: options.model, label: options.label }),
),
);
account
.command("list")
Expand Down
Loading
Loading