Projects and worktrees
Agent work has to happen somewhere on disk. A project is keelson’s name for that somewhere: a named pointer to a directory that conversations and workflow runs attach to, so you bind work to a directory once instead of retyping a path every turn. This page covers what a project is, the different ways chat and runs attach to one, and the git-worktree isolation that lets a run change code without touching your working tree.
What a project is
Section titled “What a project is”A project is a small, durable record: a name and a root path (plus an id
and a creation time), owned by the server and stored in SQLite next to your
conversations and runs. keelson project add <name> <path>
creates or registers one, and keelson project list and keelson project remove manage the
set. A fresh home carries a default project, so work has somewhere to land
before you register your own.
The root path is the only thing a project asserts about the world: this directory is where work for this name happens. Three things hang off that binding:
- Conversations attach to a project, so a chat turn runs with the project root as its working directory.
- Workflow runs resolve
--project <name>to the root path and run there. - Recall and the notebook are scoped by project: memory recall filters to the project, and the project notebook (standing markdown) is injected into every chat turn for that project. See Memory and state.
Creating or registering a project
Section titled “Creating or registering a project”The host’s project service backs keelson project add, HTTP creation, and the
optional rib methods createProject and cloneProject.
HTTP and ribs can create with a name alone. The destination defaults to
<workspaceRoot>/<name>: KEELSON_WORKSPACE, or ~/keelson when unset. The CLI
still requires an explicit path. Returned project records carry a resolved
absolute root.
| Target before creation | Host behavior | Ready for worktrees |
|---|---|---|
| Missing directory | Create directories, initialize Git, make one empty Initialize project commit. | Yes. |
| Empty directory | Initialize Git and make the same empty commit. | Yes. |
| Existing Git repository | Register without changing files, index, config, or history. | If it has a commit. |
| Populated non-Git folder | Register as-is, without Git initialization or a commit. | No. |
Host-created repositories use your configured Git identity and touch no project
files. If the initial commit fails, creation fails with a clear error naming
missing user.name or user.email config. Set those Git keys and retry. Cleanup
removes only state the operation created, not pre-existing folders or unexpected
user content.
A non-Git project cannot host Write agents until it is a Git repository with a commit. Existing repositories with an unborn HEAD also need a commit. Repositories initialized by the host already have the empty commit, so their first Write agent can branch into a worktree immediately.
Names and exact canonical roots must be unique; symlink aliases count as the same root. A project beneath the default project’s root is allowed. Cloning creates a project beneath the workspace root and refuses any existing destination, even an empty folder. See the HTTP project API for request shapes and errors.
Two ways to attach
Section titled “Two ways to attach”Chat and workflows both bind to a project, but they treat the working tree differently, and that difference is the most important thing on this page.
| Chat | Workflow run | |
|---|---|---|
| Binds to a project by | /project <name> in interactive chat, or --project on a one-shot | --project <name> on keelson workflow run |
| When the binding is set | At conversation creation, like the provider | Per run |
| Where it works | In place, in the project root | The project root, or an isolated git worktree |
| Effect of changes | Edits land in your working tree directly | An isolated run lands on a branch, leaving your working tree untouched |
So /project is a chat command. It binds a new conversation to a project,
the same way a conversation’s provider is fixed at creation. It does not move an
existing chat between projects, and it is not how a workflow picks one: that is
the --project flag.
A chat agent edits the project directory in place. There is no isolation: when the model writes a file, the file changes under you, exactly as if you had edited it. For interactive work that is usually what you want.
Live-checkout mutation lock
Section titled “Live-checkout mutation lock”Keelson also guards its own unleased writes to a project’s live checkout
(Project.rootPath). When a mutating workflow runs without worktree isolation,
the server holds a per-project mutation lock until that run settles. A second
live-checkout mutator on the same project fails fast and names the current holder
and purpose, so the next run does not quietly clobber the first one’s checkout.
The default lock mode is exclusive. A workflow with locking: shared can run
alongside other shared readers, but it still conflicts with an exclusive mutator
in either order. This is the right mode for a read-only workflow that inspects the
live checkout. mutates_checkout: false takes no lock and can overlap a mutating
run, so reserve it for workflows that do not depend on a stable live checkout.
Worktree-isolated runs do not take this lock. They already run in a separate checkout, and workspace leases represent real on-disk worktrees that can be inspected or pruned later. The mutation lock is different: it is process-local, has no on-disk artifact, and is empty after a server restart. Boot therefore clears stale live locks by construction.
Ribs that mutate a project root directly should use
acquireMutationLock
or lease a worktree instead. KEELSON_DISABLE_MUTATION_LOCK disables enforcement
for emergency operator recovery. Treat it as a bypass, not normal coordination.
Worktree isolation for runs
Section titled “Worktree isolation for runs”A workflow run that mutates code is a different story. You often want the run to edit, build, and commit without disturbing whatever you have checked out. Keelson does this with a git worktree: a second working tree of the same repository, on its own branch, in its own directory.
When a run is isolated, keelson:
-
Branches. Creates a branch named
keelson/<workflow>/<short-run-id>from the project’s current checkout. -
Checks out a worktree. Adds a git worktree for that branch at
<root>/.worktrees/<branch-leaf>/, giving the run a full, separate copy of the tree without re-cloning. -
Installs dependencies. For a Bun project with a lockfile it runs
bun install --frozen-lockfilein the worktree, sincenode_modulesis gitignored and a fresh checkout has none. Repos with no manifest or lockfile are left alone. -
Runs there. Every node executes with the worktree as its working directory. Edits, commits, and validation all happen on the branch.
-
Leaves the result on the branch. Your project’s main working tree is never touched. The worktree path is recorded on the run row so you can inspect it or clean it up later.
Isolation is opt-in as workflow policy, but once requested it is a launch requirement. Keelson never turns an isolation setup error into in-place execution:
- A workflow declares it in YAML with
worktree.enabled: true. The bundled workflows that change code (fix-issue,plan-act-evaluate,resolve-pr) set it. --worktreeforces it on and--no-worktreeforces it off, each overriding the workflow’s default.- With neither flag, the workflow’s own default decides; a workflow that does not declare isolation runs in place.
Before the first node starts, keelson confirms the source is a Git repository, prepares the worktree, and persists its identity. A failed repository probe, worktree creation, or identity write fails the run with a setup error. No node executes in the source checkout. The server-down CLI path uses the same rule.
Run status preserves the distinction after the live warning stream is gone.
isolationEnabled records required, disabled, or unknown legacy intent;
worktreeEstablished latches true once setup succeeds; and worktreePath is the
retained checkout, if one remains. workingDir is the requested source, not an
effective execution directory for a required run whose setup failed. After
cleanup, worktreeEstablished remains true while worktreePath becomes null.
Resume also fails closed. A known-required setup failure with no completed nodes can retry setup. A required run with node history but no retained worktree is refused, because those outputs cannot safely move to another checkout. A legacy run whose isolation intent is unknown and whose checkout is gone is also refused. Start a fresh isolated run when status reports that isolation is unavailable.
An advanced caller that already owns a workspace lease can deliberately avoid a
nested worktree. Pass the lease path as the working directory and set the API
override to isolation: "none", or use --working-dir <lease-path> --no-worktree. The workflow then runs in place from its perspective. Keep the
lease alive across pauses and resume, and release it yourself in finally;
workflow completion, cancellation, and cleanup do not release a caller-owned
lease.
A failed or cancelled run keeps its worktree so you can inspect it. Once the run
is finished, keelson worktree prune removes the worktree and its keelson/…
branch (--dry-run lists candidates; --force also removes live runs’ worktrees
and ones with uncommitted changes). The server checks that no run is using the
worktree and blocks resume during removal. New runs targeting the same path wait
until pruning finishes before preparing their checkout. Candidate discovery needs
a running server, even with --force; an unavailable server means nothing is
discovered or removed. An older server without prune coordination leaves all
recorded worktrees alone, even with --force. Recorded worktrees with broken Git metadata
also require --force. A run whose worktree has been removed cannot be resumed.
The database retains that identity even if another run recreates the same path.
Pruning records the marker before deletion, so an interrupted deletion also blocks
resume; rerun prune to finish cleanup. A normal Git refusal to remove a dirty
worktree leaves that run resumable. The repository and branch cleanup intent is
also persisted before removal, so rerunning prune retries unfinished branch
deletion even when the worktree directory is already gone.
Directories
maps where they live.
Where to go next
Section titled “Where to go next”- Chat: the in-place agent loop a project binds.
- Workflows: the runs that can isolate into a worktree.
- Memory and state: how recall and the notebook scope by project.
- The CLI reference lists every
keelson projectandkeelson worktreecommand, and Directories maps the.worktrees/layout.