Worktrees
Pohunek sessions can run in place or in a dedicated Git worktree. A worktree is
requested through pohunek session new with a repository or project plus a
branch. The base branch is taken from --base-branch, then the project’s
configured default, then the repository default. When an explicit base branch
is not present locally, Pohunek fetches that branch from origin before it
considers falling back to the repository default.
The base branch only applies to a branch the worktree creates. A --branch
that already exists locally is checked out as it is. When --base-branch was
given explicitly and that existing branch is at another commit than the base,
for example a local pull-request branch behind its origin copy, the
session.new result carries a base_branch_fallback warning whose detail
names both commits. The warning arrives with the created session, so a caller
that must not let the agent act on another commit starts the session without
--input, checks warnings, and only then sends the prompt with
session input.
A branch the worktree creates from a base has no upstream. The base is fetched
into a temporary ref, not a remote-tracking branch, and an upstream set to a
base with another name would make a plain git push target the base branch.
Set one when the branch is first pushed, with git push -u origin <branch>.
Worktrees are useful when an agent should make branch-scoped edits without touching the main checkout. They also make remote project sessions practical: the daemon resolves the project on the target host and creates the worktree in that host’s filesystem.
pohunek project show <project> --json lists the project’s worktrees. A
worktree Pohunek created carries owned: true and owner_session_id, the
session it was created for, which stays after that session stops; session_id
appears only while a live session runs in the worktree. The human output flags
them as owner=<id> and session=<id>.
Removing a project record never deletes the main repository. The project remove
command has a separate --prune-worktrees option for worktrees Pohunek created;
it does not remove unrelated worktrees.
A pohunek-owned worktree is removed with git worktree remove --force, which
deletes the checkout but leaves the branch and its commits in the repository.
pohunek session rm does that unconditionally, since it is an explicit operator
action. The automatic session retention sweep does not: it keeps any session
whose owned worktree has uncommitted or untracked changes, commits contained in
no other ref, or a state git cannot report (see the Sessions concept’s Retention
section). Cleanup is best-effort, so a checkout whose removal failed is reported
rather than silently counted as cleaned, and the leftover directory needs manual
cleanup.
pohunek session new writes a durable create record before it binds a
worktree, and the worktree binding before the checkout, so every worktree a
create makes can be traced back to its session. When the create fails after
the worktree was bound, the daemon removes that worktree only once the
session’s worker is proven ended, even if the client disconnected meanwhile,
so the branch is free for a retry: it retires the worker, then removes the
checkout and its binding, then deletes the create record. If the worker
supervisor cannot confirm the worker ended, the create fails with
runtime_supervision_unavailable, the session stays listed as reconnecting,
and its worktree is kept until reconciliation proves the worker ended. A
checkout that cannot be removed (for example a worktree locked with git worktree lock) keeps its binding and the create record: the create still
fails with its original error, the session stays listed as reconnecting with
reason create_compensation_pending, and the running daemon retries the
removal with the supervision backoff (1 s doubling to 60 s) until it succeeds,
so unlocking the worktree is enough; the next daemon start retries it too. A daemon that stops or crashes mid-create leaves
the same record, so the next start removes the half-created worktree; a
graceful stop first lets in-flight creates finish, including delivery of a
--input prompt, and refuses new ones with daemon_shutting_down. A create
whose prompt was not delivered before the daemon stopped is removed at the next
start, worktree included, since the prompt was never stored.
A session never starts outside its working directory. Before the worker spawns
the agent, it checks that the launch cwd is still a directory; when the worktree
was removed after binding, the launch is refused with session_cwd_missing,
nothing is started, and the failed create is compensated as above. The daemon
logs the bind, each removal with its reason, and a failed or timed-out
post-create or pre-remove hook at WARN, without the hook’s output. Every hook
run also logs hook started and hook finished at INFO with the session id,
the event, the layer (host or repository), the script, and its timeout,
duration (duration_ms) and outcome. session.new answers only after its
pre-create and post-create hooks finished, each layer bounded by 5 minutes,
so these lines show whether a slow create was waiting on a hook.
A failing pre-create or post-create hook leaves a hook warning on the
session. In the session.new result its detail also carries the last
hooks.stderr_tail_bytes bytes of the hook’s stderr (service.toml, default
4096, 0 disables), sanitized and marked when cut, so the caller can see why
setup failed. The stored session and the logs keep the warning without that
tail. Stdout is never kept, and a successful hook’s stderr is discarded.
session.new never waits for processes a hook leaves running in the
background; their stderr is drained and discarded. When such a process still
holds the stderr two seconds after the hook ended, the daemon hands the pipe to
a small drain process (/bin/cat reading it into /dev/null, in a process
group of its own) and logs that at DEBUG with the hook’s event and script and
the drain’s PID. The drain outlives a daemon restart as the process does, so its
stderr writes keep succeeding, and it ends when the process closes its stderr.
A restart while the hook runs, or within those two seconds, still leaves the
process with a closed pipe: its next stderr write raises SIGPIPE. If the
drain cannot start, the daemon keeps discarding and logs a WARN instead. A hook
that starts long-lived processes can still redirect their stderr
(>/dev/null 2>&1 or a log file); hooks.stderr_tail_bytes = 0 keeps hook
stderr on /dev/null.
Removing a worktree that a live session still uses is refused with the typed
worktree_in_use error; the same code is reserved for session.remove when
other active delegated tasks use the session’s worktree. Stop the sessions or
tasks that use the worktree, then retry the removal.
Assistant guidance should preserve this boundary: verify which checkout or worktree is active before editing, avoid deleting user-managed worktrees, and prefer explicit project or repository targeting for project work.