Skip to content
kixelatedPublic

About

A quest log for your repo and your agents: plans as Markdown files, explicit dependencies, and reviewable Git changes. For Claude Code and Codex.

Topics

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

240 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Quest

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.

Every plan is a file

# [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 call

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

Try it

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 link

With 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.md

With 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.md

Piped, 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.

Prioritize ready work

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.

Claim a quest

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

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

Check a quest's PR

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/export

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

Move and delete quests

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/drain

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

Merge quest lists

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.

Wait for pull request activity

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 abcdef0123456789abcdef0123456789abcdef01

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

Read a pull request's state

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

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

Own a pull request

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 back

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

Gate expensive commands

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 --release

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

Headless agents

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 --all

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

Run kix jobs on your machines

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

Prunable local checkouts

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 status

Creation 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 example

release 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 example

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

Work with your agents

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.

Where this is going

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.

Contributing

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 build

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

About

A quest log for your repo and your agents: plans as Markdown files, explicit dependencies, and reviewable Git changes. For Claude Code and Codex.

Topics

Resources

Contributing

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages