A quest log for your repo and your agents.
Readable plans, explicit dependencies, and reviewable Git changes. Quest keeps your plans in your repository, so you and your agents read, claim, and finish the same work. Each quest is a Markdown file scoped to one pull request: its goal, what has been decided, and what stands in the way. A small TypeScript CLI checks the links and dependencies. Skills help Claude Code and Codex plan the work and set out on it.
It grew out of MoQ. This is the standalone version, being built for use in other repositories.
Early days: validation, readiness, init/uninstall, and the planning, execution, merge, and export skills are here. Release binaries are still being built.
# [S] Add a download button
## Goal
People can download their data as a CSV from the settings page.
## Plan
Use the export endpoint. Show progress and let the user retry a failed download.
## Required
- [CSV endpoint](/quest/m0/export.md) - the button needs something to callThat file sits in your quest log, a tree under quest/:
quest/
README.md # The roadmap
m0/
README.md # First milestone, in priority order
export.md # Ready
download.md # Required: export.md
Folders are epics. A README's Required lists its children in
priority order, beside any other dependencies. The README is the epic's
own quest, and it becomes ready once its children have merged. The top folders
(m0, m1, ...) are milestones that give the work a delivery horizon.
When a quest is complete, its PR removes the plan and the links that depended
on it, so the quests that required it can become ready. A PR that leaves work
rewrites the plan to what remains. Milestones are never
deleted, so a PR that finishes one (its README has no Required left) removes
the links to that milestone too; quest check flags any it misses. The
finished plan stays in Git history, beside the code that completed it.
Work is taken through a live runner lock or an open change on the forge.
kix.quest records each active job in its database and reassigns a job whose
runner goes offline. On GitHub, a draft PR
opened from the quest's branch, including from a fork, claims the work;
quest start opens one. These states stay out of the plan file.
Build from source with Bun (or enter the pinned shell with
nix develop), then link the quest command:
git clone https://github.com/kixelated/quest.git
cd quest
bun install --frozen-lockfile
bun run build
bun linkWith Nix, nix run github:kixelated/quest -- --help builds and runs it instead.
Native Windows needs no WSL for Quest: see installation and removal.
Windows ZIP artifacts are added by the next release containing this change;
older releases remain macOS/Linux-only.
The CSV export example is a small, working quest tree. From this checkout:
$ quest --root examples/export check
The quest log is in order: 4 documents checked.
$ quest --root examples/export unblocked
1 quest/m0/export.md
$ quest --root examples/export unblocked quest/m0/download.md
! [S] Add a download button quest/m0/download.md
Required:
[S] Add a CSV export endpoint quest/m0/export.mdWith no path, unblocked ranks every ready quest. Given a path, it shows what
blocks that quest. On a terminal, a yellow ! marks a ready quest and a grey
! a blocked one, and each size label takes its colour, like loot rarity: XS
grey, S green, M blue, L purple, and XL orange (your terminal's yellow). Set
NO_COLOR to keep the layout without colours.
Scripts and agents read the piped form, which is plain text:
$ quest --root examples/export unblocked quest/m0/download.md | cat
quest/m0/export.mdPiped, a path prints that quest's blockers, with an explanation on stderr; no blockers means no stdout. It exits zero for both ready and blocked quests; a nonzero exit means the command failed.
quest unblocked ranks ready quests by milestone priority, descending number
of distinct transitive dependents, then existing tree order. Counts include
later milestones without moving their candidates ahead of earlier milestones.
Structural root, milestone, and epic membership does not add to the count;
real dependencies on an epic still reach its remaining work. Other prerequisites
may remain for a dependent after the selected quest finishes.
$ quest --root examples/export unblocked
1 quest/m0/export.md
$ quest --root examples/export unblocked --json --limit 1
[{"path":"quest/m0/export.md","dependents":1}]Each plain output line contains the count, a tab, and the path. On a terminal,
a heading explains the count. --json prints an ordered array, including []
when no quests are ready; --limit <count> requires a positive integer.
Invalid trees fail with diagnostics and no ranking.
quest-spawn uses the ranking unless you explicitly select work in another order.
--unclaimed ranks the fetched upstream default branch's tree rather than
your checkout, so a stale checkout cannot offer a finished or deleted quest.
It then drops quests someone already holds: the quest's branch on the
upstream repository, or an open PR from that branch in any repository, forks
included. A branch whose tip is the head of a merged PR holds nothing, since
its work landed; a closed, unmerged PR's branch still holds its quest. It
lists each one it skips on stderr with its PR and how long ago that PR was
last active, so you can decide whether a stale claim should be taken over.
In a fork's clone, the upstream is the fork's parent, and the fork's own
branches and PRs count too; a remote that fetches from upstream and pushes to
a fork works the same way. --remote picks a remote other than origin.
Off GitHub, only branches are checked.
Neither form inspects kix.quest runner locks or outside conditions.
quest start claims a ready quest so every claim is visible on GitHub:
quest start quest/m0/export.md
quest start --release quest/m0/export.mdIt pushes an empty claim commit, built on the upstream default branch, to the
quest's branch (its path without .md), then opens a draft PR
from it right away and prints the PR's URL. It switches your checkout to the
claim on a new local branch, or fast-forwards that branch if it is already
checked out. A fork pushes to itself and opens the PR on its parent. It reads
readiness from the upstream default branch it builds on, not your checkout. It
fails on a tree quest check rejects, which can hide a blocker, and refuses a
blocked quest and one someone else holds; a rejected push means
another claimer got there first, and when two PRs race, the earliest open one
keeps the claim and the later one is closed. A branch left at a merged PR's
head is replaced, with a lease on that tip. A branch named after the claim
branch's directory, such as quest/m1/epic for quest/m1/epic/one, or inside
it, refuses the claim, since git cannot hold both; the refusal names it and
whether it is a merged PR's head, and leaves it in place. Off GitHub, it pushes
the branch without a PR.
--release deletes the branch, with a lease on its tip, and closes its PR, but
only while the tip is still an empty commit. Once work is on the branch, hand
the draft back instead: a ## Decisions needed section in its body marks it as
waiting on the maintainer.
quest start exits 0 when done, 3 when it refuses (blocked, held, a branch in
the way, or work on the branch), 2 for invalid arguments, and 1 for other
failures. On GitHub it
uses GH_TOKEN, GITHUB_TOKEN, or gh auth token.
Draft claims are placeholders, so CI can skip them. This repository runs its
checks only on PRs ready for review: the pull_request trigger lists
ready_for_review, and each job has the condition
github.event_name != 'pull_request' || github.event.pull_request.draft == false.
A PR from a quest's branch must leave that quest accurate. quest check --base <ref> runs the usual check, then compares the working tree with its merge base
with <ref>. When the branch is a quest's path without .md (an epic's is
quest/<path>/README) and that quest exists at the merge base, the diff must
delete or edit it. Any other branch passes. The branch defaults to the current one;
--branch <name> names the PR's head branch instead:
git fetch origin main
quest check --base origin/main --branch quest/m0/exportFetch the base first, since a stale one can predate the quest. The
quest-merge skill runs this before landing a PR. A repository with
quest installed in CI can also run it on pull requests, where the checkout is
detached, so pass the head branch:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- run: quest check --base "origin/$GITHUB_BASE_REF" --branch "$GITHUB_HEAD_REF"The check is a floor, not a review: any edit to the quest passes, so reviewers still judge whether a rewrite matches what remains.
quest mv and quest rm move or delete a quest or an epic with git mv and
git rm, then fix the links so the tree still passes quest check:
quest mv quest/m0/export.md quest/m1/export.md
quest mv quest/m0/drain quest/m1/drain
quest rm quest/m0/download.md
quest rm -r quest/m1/drainmv rewrites every root-absolute link to the moved path, and to everything
under a moved epic. rm drops each Required, Related, or Closes entry
that links to what it deleted, and the heading if that was the last entry.
It also drops every Required link to a finished milestone, including one it
just finished, but keeps the root's listing of the milestone.
Both stage the change, print one line per file (R from -> to, D path, or
M path), and report on stderr any other mention of the path, such as prose
or files outside quest/, without editing it. They run quest check on the
result and exit 1 if it fails, for example when a quest moved out of its epic.
An epic is its directory or README; removing one that holds other quests needs
-r. The root and milestones are never moved or removed. mv refuses a
destination that exists, and refuses to move a quest while it has a claim
branch or open PR on origin, or on the repository a GitHub fork came from,
since the branch would no longer name it.
Neither command touches branches or PRs.
Concurrent PRs edit the same link lists: the base branch deletes finished
quests' lines while a branch inserts a new quest beside them. quest init
routes quest/** through a merge driver in .gitattributes and registers it
in the clone's git config, so a local git merge resolves those conflicts.
Inside a conflicting Required, Related, or Closes list, quest resolve
keeps the entries neither side removed and adds the ones either side
inserted, ours before theirs at the same place, matching entries by their
opening link. Prose, headings, an entry both sides rewrote, or one side
removed while the other edited, keep the usual conflict markers. Any quest
command registers the driver in a fresh clone or worktree whose
.gitattributes names it. Without quest on the merge's PATH, the driver
falls back to git merge-file, and GitHub's merges ignore drivers, so a PR
shows as conflicting until a local merge from its base is pushed.
quest wait blocks until a GitHub PR has a review, comment, review-thread
reply, reaction to the PR or one of its reviews, push, merge, close, removal
from the merge queue, disabled auto-merge, or required CI result:
quest wait 101
quest wait https://github.com/kixelated/quest/pull/101 --since 2026-10-07T12:00:00Z --timeout 1800
quest wait 101 --since abcdef0123456789abcdef0123456789abcdef01A number uses the repository's origin; a URL works from any directory.
Authentication comes from GH_TOKEN, then GITHUB_TOKEN, then gh auth token.
Without --since, the point is the current head's push time, so an existing
review on that head is included. GitHub lists a push seconds to minutes after
it lands, so for the current head the wait rereads the listing every 15 seconds
until the push appears, warning once. Any other SHA must be a listed pushed tip;
otherwise pass an ISO timestamp instead. Forks use the head
repository's activity. Deleted head repositories require a timestamp.
The initial PR commit listing must match the resolved head; a racing push or
GitHub's 250-commit listing limit fails clearly instead of guessing membership.
Output is plain, one line per event: timestamp (or SHA for a newly observed
commit), type, actor, and URL. The last line adds a resume cursor. Pass its
last field as --since on the next call. Cursors preserve commit membership
and events sharing a timestamp; treat them as opaque, and use them only for the
same PR. Explicit timestamps include events at that instant. Historical plain
pushes come from repository activity; pushes observed while waiting come from new
commit SHAs, regardless of git author dates. Force pushes come from the PR
timeline, excluding the one that created the starting head. A merge through
the queue prints removed_from_merge_queue about a second before merged, so a
poll can catch the removal alone; check quest status before treating it as a
failed merge. A label change prints as labeled:<name> or unlabeled:<name>
(spaces as _), so a queue that dequeues by removing its label wakes the
waiter; the authenticated account's own label changes are ignored. Actors are
printed without deciding
whose review counts. Edits to existing comments or reviews are outside this
command's scope.
CI results are read for the current head. The first required check to fail
prints checks:failed right away. checks:passed prints once nothing on the
head is still going (no pending check and no unfinished GitHub Actions run, which
may still create jobs), every check and workflow the base branch names is
present, and every required check passed; it names the last check to finish,
when the head went idle. Each prints once per head, with the check's name
(spaces as _) in the actor field and its details URL. Results count as
quest status counts them, so neutral and skipped results pass. Each job of a
workflow a ruleset requires is required.
Requiredness comes from the base branch's protection and rulesets, readable
with read access; optional checks never wake the wait, unless the base branch
requires none, in which case every check is required. A result counts when it
was reached at or after the starting point, so a wait started after CI finished
still reports it; a push starts over for the new head, and the cursor records
what was reported, so resuming never replays it.
Review-thread replies and reactions never reach the PR timeline, so they are
read separately. A reply prints as replied; a thread's first comment is part
of a review and prints as reviewed. Reactions on the PR body or a review
print as reacted:<content>, so a clean review that only leaves a 👍 prints
reacted:thumbs_up. Each poll reads the newest 100 reactions on each, so a
burst of more than that still wakes the waiter but prints only the newest;
reactions on other comments are not read. Replies and
reactions by the authenticated account are ignored, so an agent's own replies
do not wake it; activity from a person sharing that account is ignored too.
The cursor keeps a separate position for each source, so a reply that becomes
visible after a newer reaction is still reported.
Each poll makes one conditional timeline request and one conditional
review-comment request every five minutes once it has read the existing pages,
following a new page when the current one fills, plus one GraphQL query each
for reactions and checks. It reads the base branch's required checks once per
call. It honors GitHub's rate-limit headers and fails loudly on invalid responses.
There is no deadline unless --timeout <seconds> is supplied. Activity exits 0,
timeout exits 3, invalid arguments exit 2, and other failures exit 1; interrupt
with Ctrl-C to stop waiting. A timeout prints a resume point as its only line:
a cursor, or the given --since (else the head SHA) if setup had not finished.
Retry with it, not the original command, so activity that landed during the
first wait is still reported.
quest status prints a GitHub PR's current state in one call, such as after
quest wait wakes. It accepts the same PR argument and authentication as
quest wait, and reports facts without deciding whether the PR is ready or
whose review counts:
$ quest status 128
url https://github.com/kixelated/quest/pull/128
state open
draft false
head 0ade9d977d54c6f48152410532c58e9ea78db3d0
mergeable conflicting
merge-state dirty
review-decision -
auto-merge -
merge-queue -
landing-stopped -
checks success passed=14 failing=0 pending=0
review stale commented kixelated 2026-10-16T06:14:04Z https://github.com/kixelated/quest/pull/128#pullrequestreview-5543745302
review current commented chatgpt-codex-connector 2026-10-16T17:57:16Z https://github.com/kixelated/quest/pull/128#pullrequestreview-5551703054
thread PRRT_kwDOUhuoys6tUUV2 current chatgpt-codex-connector https://github.com/kixelated/quest/pull/128#discussion_r4297708263 src/cli/gate.ts:115Each line starts with its record type; any free text, a check name or a path,
comes last, and a missing value prints as -:
| Line | Fields |
|---|---|
url, state, draft, head |
The PR URL; open, closed, or merged; true or false; the head SHA. |
mergeable, merge-state, review-decision |
GitHub's values, lowercased. |
auto-merge |
Method, who enabled it, and when. |
merge-queue |
Queue state, position, and when it was enqueued. |
landing-stopped |
GitHub's reason the latest merge-queue entry or auto-merge ended (such as merge_conflict, failed_checks, manual, manually_disabled, or merged), who ended it, and when. |
checks |
The head's rollup state and passed=, failing=, pending= counts. |
check |
failing or pending, required or optional, the result, details URL, and name. |
review |
current or stale against the head, state, reviewer, time, and URL of each reviewer's latest review. |
reacted:<content> |
Actor, time, and the PR or review reacted to, as quest wait names reactions. |
thread |
Node ID, current or outdated, last commenter, latest comment URL, and path:line of each unresolved thread. |
The node ID is what GraphQL's addPullRequestReviewThreadReply and
resolveReviewThread take. Neutral and skipped checks pass. A required check
is one branch protection or a ruleset requires for this PR. --json prints the
same facts as one object with the fields url, state, draft, head,
mergeable, mergeState, reviewDecision, autoMerge, mergeQueue,
landingStopped, checks (state, passed, failing, pending), reviews, reactions, and
threads. Long lists are paged; reactions are the newest 100 per PR or review,
as in quest wait. Any PR state exits 0, invalid arguments exit 2, and other
failures exit 1.
Before a session pushes to an existing PR, it takes the PR, so two sessions never iterate on or merge one PR at once:
quest iterate 128 # prints a hold token
quest ready 128 <token> # swap to quest:ready, printing a new token
quest ready 128 <token> --done # drop "Decisions needed" and mark it ready
quest merge 128 <token> # swap to quest:merge for the fix loop
quest merge 128 <token> --land <sha> # auto-merge bound to that head
quest release 128 <token> # hand it backA taken PR carries one label, quest:iterate, quest:ready, or
quest:merge. Sessions often share a GitHub account, so taking a label prints
a hold token: the ID of the timeline event that added it. Every command refuses
while a quest:* label it holds no token for is on the PR, including its own
state's; a draft its starter still holds, one without a ## Decisions needed
section; and a merged or closed PR, since a push after the merge brings the
branch back. The refusal names the holder's label, age, and token, so you can
decide whether to release a stale hold. When two sessions add different labels
at once, the first label added keeps the PR and the other backs off.
--land needs the full 40-character head SHA and refuses once the head moves,
before swapping labels, so the token you passed stays valid. If a step fails
after a swap, the error names the new token.
It enables auto-merge bound to that SHA, which enters GitHub's merge queue
when the base has one, or merges at once when nothing is left to wait for. It
makes a merge commit; --squash or --rebase picks another method, and a
merge queue applies its own. quest:merge stays on while the PR is queued or
auto-merging; once the PR leaves the queue unmerged, the next command removes
it. The commands accept the same PR argument and authentication as
quest wait, and exit 0 when done, 3 when refused, 2 for invalid arguments,
and 1 for other failures.
quest gate <command> [args...] holds an expensive command until the host has
room for it, across all of a user's repositories and worktrees:
quest gate bun run test
quest gate cargo build --releaseAdmission is sized from the host, with no configuration. Every platform waits while the host-wide 1-minute load average exceeds the host's CPU count, or while available memory is below one command's budget: 4 GiB, or a quarter of RAM on smaller hosts, where RAM is capped by any container memory limit. On Linux, available memory includes reclaimable host and cgroup file cache and is bounded by the remaining budget of each enclosing memory cgroup. Load and memory count work from every session and user. Windows reports no load average and macOS counts reclaimable cache as used memory, so those signals are skipped there.
Both measures lag new work, so each command admitted in the last minute counts
as one more unit of load and takes one budget from available memory. While the
host has room even counting those, commands start at once: a burst of gated
test runs on an idle host starts together. Once only the raw load and memory
have room, commands start one at a time, 5 seconds after the previous admission.
A user's admissions on one host are recorded under
~/.cache/quest/admissions/<hostname>, whatever the caller's environment or
login session. The home directory comes from the user database, not $HOME;
an account missing from it, as in arbitrary-UID containers, uses
/tmp/quest-<uid>/admissions/<hostname>, and refuses that directory unless only
its UID can write it.
Admission is the gate's only job: it holds no slot or lock while the command runs and does not track the command's descendants. A nested invocation runs immediately inside the admitted command. Internal compiler or test worker parallelism remains the command's responsibility.
This repository's just check, just test, just worker-check, and
just worker-test run through the gate.
Arguments, working directory, environment and terminal streams pass through to the executable, and waiting happens before it starts. A terminal's Ctrl-C reaches the command once, directly; SIGTERM and SIGHUP sent to Quest are forwarded. There is no fairness guarantee or timeout: a command waits as long as the host stays busy.
quest agent runs one headless agent turn per call, billed to one of the keys
in QUEST_API_KEYS, so /quest-spawn api launches every agent with one command
whose arguments name the whole launch:
quest agent start --harness claude --key 1 --model claude-opus-5-5 --effort high --permission-mode auto .worktrees/foo "/quest-start quest/m0/foo.md"
quest agent resume --harness claude --key 2 --model claude-opus-5-5 --effort high --permission-mode auto .worktrees/foo <session> "Take the recommendation."
quest agent stop .worktrees/foo
quest agent stop --allOptions go before the worktree, and the prompt or message follows it, so text
starting with - is never read as an option. --key names a key by its
1-based position. The first launch resolves every 1Password op:// entry in
one op inject and keeps the keys in a user-only
store under $XDG_RUNTIME_DIR (or the temporary directory), shared by the
repository's worktrees until quest agent stop --all deletes it. The agent
runs in the worktree with the user's environment minus the coordinating
session's harness variables (keeping CLAUDE_CONFIG_DIR and CODEX_HOME),
other keys, secret-manager tokens, and any quest gate admission, plus its one
key. --permission-mode is Claude Code's permission mode, or Codex's sandbox
mode; the agent cannot prompt.
It prints session <id> as soon as the id is known, then cost <dollars> when
the agent reports it, outcome finished|failed|exhausted|stopped, a blank line,
and the agent's final reply. It never prints a key. It exits 0 when the agent
finished, 3 when the provider refused the key for lack of credit, 2 for invalid
arguments, 128 plus the signal number when interrupted, and 1 otherwise. stop
signals the agent and waits until its start or resume has reported.
quest agent does not run on native Windows yet; use WSL.
quest runner turns a Linux or macOS machine with Nix into a
kix.quest runner, driving Claude Code or opencode with
that machine's own logins:
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 interruptedlogin 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 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 with the project's devShell, which the runner
builds with nix print-dev-env from the job's revision, honoring the flake's
nixConfig; a project without a flake gets a bootstrap job instead. On Claude
Code accounts, Claude Code's sandbox (bubblewrap on Linux, Seatbelt on macOS)
confines every command the agent runs, the devShell's shellHook included:
commands write only to the checkout and a private temporary directory that is
also their HOME, cannot read account homes, the runner's state, or shared
temporary directories, and have no Nix daemon. A trusted revision's commands
reach only the npm, crates.io, and PyPI package registries, so its checks can
install dependencies; an untrusted one's reach no network host. npm's registry
also accepts publishes, so a prompt-injected agent holding someone's npm token
could publish the checkout there. The agent's file tools edit only the checkout,
WebFetch is off, and the checkout's own .claude settings, hooks, and MCP
servers are never loaded. Claude Code itself runs outside the sandbox with a
minimal environment, so its login stays out of the commands' reach. This is an
OS sandbox, not a VM, and it narrows rather than removes the risk of running
agent commands on your machine. opencode has no sandbox yet: on opencode
accounts the agent's commands run unconfined on your machine, in the devShell
with a private HOME.
On Linux the sandbox needs bwrap and socat (the flake's runner package
pins both) and unprivileged user namespaces; on Ubuntu 23.10 and later,
AppArmor allows them only for programs with a profile granting userns.
The agent reports through kix tools: checkpoint summaries publish
work in progress to the job's branch, ask pauses the job on a decision for you,
and deliver publishes the result and its pull request, listing any suggested
follow-ups, which the inbox offers you to plan. A job on an issue or
pull request reads it through context, since the runner holds no GitHub
token; a new plan reads your idea and pasted context there. kix publishes as its GitHub App, so the runner never pushes. A job that hits an account's limit
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. Runners take only trusted revisions until they can build untrusted
heads' devShells in a VM, 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 (and on
Linux, bubblewrap and socat) 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.
The experimental quest worktree commands share one disk budget across this
user's repositories and release a checkout's disk by deleting its declared
scratch in place. They only operate on Git worktrees they create; use the
owning application for Codex, Claude, or other managed checkouts.
Set a shared capacity and an explicit reservation in bytes, covering the checkout and its expected build output. Measure representative jobs before choosing the budget. Reservations serialize creators and account for concurrent expected growth. They do not enforce filesystem quotas against arbitrary writers, and Git objects stored in the shared repository are not charged.
quest worktree capacity 20000000000
quest worktree create example --reserve 3000000000 --scratch node_modules dist
quest worktree run example -- sh -c 'just install && just test'
quest worktree statusCreation prints JSON with the ID and the detached, locked checkout path under
.worktrees/, and records the --scratch paths. run restores a released
checkout, runs the command there, and once it exits kills any descendants it
left behind. When no other run remains in the checkout, it releases it: the
declared scratch is deleted and the checkout keeps only its source. Put a whole
job, such as an agent session or a script that installs, builds, and tests, in
one run, since the next run starts without scratch.
run owns processes through a systemd user scope per run, so it needs Linux
5.14+ with a systemd 254+ user manager and unified cgroup v2; it fails on other
hosts.
Work handed to another service manager, such as systemd-run or a container
daemon, escapes the scope. If the supervisor itself is killed, its command keeps
running and its scratch is kept; the next quest worktree command drops the run
once its scope is empty and then releases the checkout. status lists each
run's scope, with the reason a run it cannot inspect is kept until an
inspection succeeds. quest worktree stop example kills what crashed
supervisors left in their scopes, waits until they are empty, and so releases
the checkout; it leaves runs whose supervisor is alive to that supervisor.
Release deletes only declared scratch, and refuses a path that Git does not
ignore, that holds tracked files or another Git worktree, or that is reached
through a symbolic link; links at or inside scratch are removed, never followed.
Missing paths are skipped, so release can be rerun. The checkout, its Git state,
and untracked files stay in place, and status charges what remains. A failed
automatic release is recorded on the checkout, which stays reserved. To manage a
checkout by hand, stop its unsupervised work first:
quest worktree release example
quest worktree restore examplerelease refuses while supervised runs are live. restore reserves the
checkout's disk again; then reinstall or rebuild its scratch, for example with
just install.
Once a checkout is finished, remove it and its record:
quest worktree remove exampleremove never forces. It refuses while supervised runs are live, and refuses
modified or untracked files and any commit, at HEAD or under the checkout's
own refs such as refs/bisect/, that no branch or remote-tracking ref
reaches. A squash-merged change leaves its commits unreached, so they count as
preserved only when origin's GitHub repository (a fork's parent) has a merged
PR whose head is exactly that commit; other commits need no forge or token.
It then prunes the scratch and refuses ignored files that remain, leaving the
checkout released, before git worktree remove deletes it. A checkout already
removed with git worktree remove only drops its record, as with release.
A failed creation stays charged until release drops it. Records live in
$QUEST_WORKTREE_HOME, or $XDG_STATE_HOME/quest/worktrees (default
~/.local/state/quest/worktrees). The registry lock of a process that exited on
this host is reclaimed; a live lock, or one held from another host, is never
stolen. This initial interface refuses unknown or changed checkout ownership.
Open this repository in Claude Code or Codex. The binary carries the skills;
a repository installs only a stub per skill that runs quest skill <name>, so
the version you pin decides what your agents follow:
| Skill | What it does |
|---|---|
quest-plan |
Ask the questions that turn an idea into scoped quests. |
quest-recurring |
Plan a quest whose finishing PR creates its next occurrence, and what gates it. |
quest-import |
Bring open GitHub issues into the planning conversation. |
quest-export |
Turn active quests into GitHub issues before leaving Quest. |
quest-audit |
Find conflicting, stale, or misprioritized quests, then resolve them. |
quest-start |
Claim a ready quest, complete it, and open a PR. |
quest-spawn |
Triage ready quests and send several agents out in parallel. |
quest-iterate |
Iterate on an open PR to settle its open decisions, fix CI and review findings, and push. |
quest-ready |
Mark a draft PR ready for review, wait for an automated review, then decide whether to merge. |
quest-merge |
Merge a quest's PR once CI and reviews pass, completing the quest. |
quest-complete |
Decide which open PRs to merge, then merge them in parallel. |
quest-afk |
Keep starting agreed work while you are away, leaving every decision for your return. |
quest-delete |
Abandon a quest, deleting it in its own PR. |
Use /quest-plan in Claude Code or $quest-plan in Codex. Skills coordinate
within your agent session. A started quest stops at a pull request; merging is
a separate invocation, so you review every change.
To bill spawned agents to API keys instead of your agent's account, export
QUEST_API_KEYS with one or more whitespace-separated keys and run
/quest-spawn api. Entries may be secret references such as 1Password op://
URIs, resolved in one op inject when the first agent starts; a 1Password
service account token (OP_SERVICE_ACCOUNT_TOKEN) avoids even that prompt. Each
quest runs through quest agent as a headless claude -p
or codex exec process on one key; a key whose credit runs out is dropped, and
spawning stops once every key is exhausted.
Disable auto-reload or set a spend limit in each provider console so draining a
key cannot charge a card. Agents run as your user, so they are not isolated from
the other keys.
To use Quest in your own repository, paste this into your agent:
Follow https://github.com/kixelated/quest/blob/main/SETUP.md to set up Quest here.
The getting started guide covers what happens next.
quest guide prints the format and workflow.
The first release should let you paste one line into your agent to set Quest up, bring in existing GitHub issues, work through quests, and leave again without losing your plans. The binary carries the skills, your tool manager (mise or nix) pins its version, and your repository's instructions stay yours. Quest supports macOS, Linux, and native Windows.
The hosted service is moving to kix.quest: maintainers plan issues, start ready quests, and act on GitHub PRs through a decision UI. Hosted work will use explicitly selected maintainer or donor API funding. The first milestone lists the quests that remain.
Quest is planned in the open with Quest itself: its quest log is the backlog, and contributors claim quests from the same log their agents read. To work on Quest:
nix develop
just install
just check
just test
just buildCI runs the same checks. See CONTRIBUTING.md for the details.
MIT or Apache-2.0, your choice. The CLI, initial skills, and development setup
were extracted from MoQ at 2bb3e6e.
Original notices are preserved in LICENSE-MIT and
LICENSE-APACHE.