Workers
A worker is a separate agent that one agent or conversation starts for a single brief.pan worker run starts it for an issue, waits, and prints its
report. The worker is a real Overdeck agent with the role worker:
- It has its own
~/.overdeck/agents/<id>/state.json, transcript, cost, and model routing (roles.worker.modelinconfig.yaml; the default is the same workhorse aswork). - It runs in a persistent pane on the host’s terminal backend (Herdr by
default, tmux when
terminal.backend: tmux). It is never a one-shotclaude -porcodex execrun. - It appears in the Agents Directory under the agent or conversation that started it and under its issue, and in the issue’s session tree as Worker <n>.
pan tell,pan kill, and the other lifecycle verbs work on it.
pan done,
pan review, or pan task done, and it does not push unless the brief says
to. Pipeline patrols that act on the work role never act on it.
--issue is required. Issue-less workers are not supported yet.
Workers or lanes?
Use a worker for issue-linked delegation inside the managed pipeline: it belongs to an issue and reports back to the agent that started it. Use a lane (pan lane start) for gauntlet fan-out outside the
pipeline: a lane is a conversation, so it runs any harness and model, nests
under its orchestrator on the Command Deck, and has a transcript view and a
composer. The Command Deck lists conversations only, which is why lanes are
conversations and not workers.
Commands
Worker ids are
agent-<issue>-worker-<n>, for example agent-pan-123-worker-2.
Exit codes
run and wait print the report body on stdout and every status line on
stderr.
A report’s status line names its sequence number and the next wait, for
example
worker agent-pan-123-worker-1 report 2: done; next: pan worker wait agent-pan-123-worker-1 --after 2.
The wait polls every two seconds and never messages the worker. A worker
that was never seen alive is not called dead during its first minute, while
the harness is still starting.
The report contract
The worker writes its report as Markdown to a file and runspan worker report <id> --file <path>. The command writes
~/.overdeck/agents/<id>/reports/<seq>.json:
seq is 1-based and zero-padded to four digits in the file name. Each report
is written to a temporary file and renamed into place, so a reader never sees
half a report. Overdeck never rewrites or deletes reports. The body limit is
1 MiB.
Reporting does not signal the pipeline. The directory derives a worker’s
done state from its newest report: an idle worker whose newest report was
written in the turn that just ended is done.
Working directory and read-only mode
Every worker that is not read-only also runs behind the default
git shim.
In its own worktree the shim refuses git commit, push, merge and
cherry-pick unless the worktree is on <feature-branch>-worker-<n>, and
refuses them on a detached HEAD.
--read-only puts a git shim first on the worker’s PATH. The shim
prevents accidental git writes to the issue’s repository: the primary
checkout, every worktree of it (the issue workspace included), and their
subdirectories, however the call reaches them (cd .., -C, --git-dir,
GIT_DIR, --work-tree). Inside that repository it lets only reads through:
status,diff,log,show,rev-parse,rev-list,ls-files,ls-tree,blame,cat-file,merge-base,describe,grep,shortlog,for-each-ref,show-ref;configonly when its first argument is--get,--get-all,--get-regexp,--list/-l(orget/list), and every later option is a read option such as--show-originor--global;remotebare or-v,remote show [-n]andremote get-url [--push|--all];branch --show-current,branch --list/-lwith patterns, a barebranchlisting, and exactly the listing options-a,-r,-v,-vv,--contains,--merged,--no-merged,--points-at,--sort=and--format=.
--unset or --set-upstream-t= is refused.
Everything else is refused there, including fetch, which writes refs.
The shim is not a sandbox. Calling git by its absolute path (/usr/bin/git)
bypasses it, so does setting OVERDECK_PAN_GIT_OP=1 (the switch Overdeck’s
own pan git operations use), and it does not restrict file writes or network
access at all.
It is there to stop a reviewing worker from changing the branch by mistake,
not to contain a worker that tries to.
Parent and pan tell
The parent is --parent, else $OVERDECK_AGENT_ID, else
$OVERDECK_CONVERSATION. Every conversation launcher exports
OVERDECK_CONVERSATION. When an agent (a caller whose stdin is not a
terminal) has none of these, run fails and asks for --parent. An
interactive operator shell may start a worker without a parent.
The parent is recorded in state.json (parentId) and stamped on the pane as
the parent token. The prompt guard lets a worker pane accept messages from:
- the issue’s
workagent, - its parent,
- an operator conversation.
pan tell <worker-id> "<message>" and then
pan worker wait <worker-id> --after <n>, where <n> is the last report you
read. Without --after you would get report <n> again.
Waiting from each harness
-
Claude Code: run
pan worker run …with the Bash tool’srun_in_background: true. The foreground limit is 10 minutes; a background command notifies the conversation when it exits, and its stdout is the report. -
Codex and other harnesses: run
pan worker run … --detach, then wait in a loop until the exit code is not3. A timed-out wait loses nothing: a report written between two waits is returned by the next one.
When the worktree goes away
Stopping a worker (pan kill, or --stop-after-report) keeps its
.swarm/worker-<n> worktree and its <feature-branch>-worker-<n> branch: the
parent may still need the work.
When the issue is torn down (close-out, close, approve) the worker worktrees
are removed only if the workspace itself is deleted (git worktree remove --force, then git worktree prune); a kept workspace keeps them, uncommitted
changes included. The closed-issue residue reaper removes them with the
workspace.
A worker branch exists only on this machine, so every cleanup path deletes it
only when its tip is already in the default branch (main, origin/main, or
whatever origin/HEAD names) or in the feature branch while that still
exists. Any other worker branch is kept, and the cleanup logs its name and its
unmerged commit count. A branch still checked out in a kept worktree is kept.
.swarm/ is in the repository’s .gitignore, so git add -A in a workspace
never commits an embedded worktree.
Where the files are
Externally spawned agents
An external agent is one another tool launched: Overdeck did not start it, has no pane for it, and cannot steer it. Overdeck can record it so the Agents Directory shows it under the agent or conversation that spawned it and under its issue, with its transcript when the harness writes one Overdeck can read (Claude Code, Codex, Pi, ACP, Kimi Code, Muse).Registering one: pan worker register
- Required:
--source([a-z0-9-], up to 32 characters;codex-pluginis reserved for the adapter below),--external-id, and--harness. - Optional:
--model,--cwd,--issue,--parent(an agent id, a conversation’s tmux session, orclaude-session:<uuid>),--label,--pid,--transcript,--session-id, and--json(prints{ "id", "created" }). - The id is
ext-<source>-<external-id>(a short hash is added when the external id is not already a lowercase slug). Registering the same source and external id again prints the existing id and writes no new registration; if the first attempt never linked its transcript, the repeat links it. --transcriptmust be a regular file under~/.claude/projects, the Codex sessions directory ($CODEX_HOME/sessions, default~/.codex/sessions) or~/.overdeck/agents, after following symlinks. Anything else (another directory, a symlink out of those roots, a FIFO, a directory) is refused with a message that names the allowed directories. The same check runs again before every read, so a transcript replaced later is simply not shown.
POST /api/workers/register with those fields as JSON (source,
externalId, harness, model, cwd, issue, parent, label, pid,
transcript, sessionId) and the x-overdeck-internal-token header. The
answer is 200 { "id", "created" }, or 400 { "error" } for a bad field or
a transcript path outside the allowed directories.
Codex-plugin jobs are registered automatically
The Codex plugin for Claude Code (codex:codex-rescue and friends) runs its
jobs as detached codex processes. The dashboard reads the plugin’s job
records (~/.claude/plugins/data/codex-openai-codex/state/*/jobs/*.json) every
15 seconds and registers each job the first time it sees it. It records the
parent then, because the plugin deletes its job records when the parent Claude
session ends:
- Parent: the conversation running that Claude session, else the agent
whose session index names it, else
claude-session:<uuid>(shown as Claude session and the first eight characters). - Issue: read from a
workspaces/feature-<issue>working directory. - Transcript: the Codex rollout of the job’s thread, linked once the thread id appears in the job record or its log.
~/.claude/plugins. It runs in the primary dashboard only. It will be retired
when plugins can call the registration door themselves (PAN-3940).
How its state is worked out
Nothing about an external agent’s state is stored. Each directory read derives it:- working — the recorded process is alive. The pid’s start time is recorded with it, so a reused pid does not count.
- done — otherwise, the transcript’s last turn finished.
- working — a registration with no pid whose transcript changed in the last two minutes.
- stopped — anything else.
Where the files are
pan sync’s agent-directory cleanup keeps ext-* directories, as it keeps
conversation directories.
See also: Harnesses — Use Overdeck workers instead of harness plugins.