Code Sentinel
Orchestrator workflow
From a ready issue to a merged pull request, slot by slot.
The Code Sentinel orchestrator is a Claude session that runs a fleet of other Claude sessions. It reads the GitHub issues carrying the ready label, dispatches one worker per issue (or per slot of a planned wave) into its own git worktree, and merges each pull request once it goes green. This page follows one piece of work from idea to merge and shows what the orchestrator leaves on disk.
The orchestrator is the orchestrator skill of the plugin. Its skill is marked so Claude never starts it on its own: a person, or AgentDock, starts it.
The flow at a glance
/code-sentinel:spec → GitHub issue with acceptance criteria and a "## Parallel plan"
label cs:ready → the issue enters the queue
/code-sentinel:orchestrator start
round: read → ready set → decide → model per slot → board → dispatch
worker per slot → tmux cs-<slot> + worktree .wt-<repo>-<slot>, fenced by owns/never
checkpoints → .orchestrator-reply.md (picked up, plan ready, ... pull request open)
checks pass, PR green → orchestrator merges (autoMerge) and refills the slotFrom spec to queue
Work starts as a GitHub issue that says what done looks like: acceptance criteria for a feature, or a reproduction for a bug. The usual way to write one is /code-sentinel:spec, which can also add a ## Parallel plan that splits the work into slots.
An issue joins the queue when it carries the ready label (cs:ready unless readyLabel says otherwise). You can add it on GitHub, with /code-sentinel:issue --ready, by choosing Publish and queue at the end of a spec, or from AgentDock's Project → Queue → Create issue. Every route refuses the label for an issue with no acceptance criteria or reproduction.
The body can carry two more lines the orchestrator obeys:
Depends on #N: dispatch only after issue N was closed by a merged pull request.Gate: …: a person must clear this before merge. Green checks never clear a gate.
A round
Each round runs the same phases. The orchestrator never asks you to approve a round; it dispatches as soon as every computed condition holds.
Bearings
Checks the prerequisites, requires that its own session is on Remote Control (otherwise it asks you to run
/remote-controland start again), reads the config, findscs-*sessions and worktrees already running, runsgit fetch, and creates its labels if missing.Read
Collects ready issues,
cs:in-flightissues, open pull requests with their checks,git worktree listandtmux ls. Each worktree is classified as free, occupied, a dead slot to resume, or an aborted dispatch.Ready set
Gives each ready issue exactly one state:
IN FLIGHT,READY,BLOCKEDon work (an open dependency, or an open pull request touching a file it needs),BLOCKEDon a person (cs:needs-personor aGate:line) orNO SPEC. ANO SPECissue gets a comment, loses the ready label and is pointed at/code-sentinel:spec. An issue with a## Parallel planbecomes a wave, and dependent slots wait until the lead's pull request has merged.Decide
Takes as many
READYissues as there is free capacity undermaxSlots, ordered bypriority:label, then lowest issue number. Reads the usage footer and dispatches fewer if the allowance is nearly spent.Model per slot
Opus when the slot decides something (a schema or public API change, auth, payments or deletion, no precedent in the code, a vague issue, an unproven bug cause). Sonnet when it executes a written decision.
defaultModelapplies only when it genuinely cannot tell.Board
Writes the round board to
<git-common-dir>/cs-orchestrator/<YYYY-MM-DD>/round-<HHMM>.md: what it dispatches and why that model, what waits for a lead, what it does not dispatch and what would clear it, and what is already in flight.Dispatch
For each slot it adds
cs:in-flightto the issue, writes a brief, and runsdispatch.sh, which creates the worktree and starts the worker in tmux. Then it arms a watcher that wakes it on every change.
Workers, worktrees and the fence
A slot is named i<issue> (i42), or i42-api, i42-web for the slots of a wave. Its branch is feat/<issue>-<slug>, or fix/… for an issue labelled bug.
dispatch.sh does the launch:
- creates the worktree
<parent of repo>/.wt-<repo>-<slot>fromorigin/<base>; - copies the brief in as
.orchestrator-brief.md(excluded from git); - pre-accepts the trust and bypass-permissions dialogs so the session does not stop on them;
- wires
fence.pyas aPreToolUsehook through--settings, so nothing is added to your repository; - starts the worker in tmux session
cs-<slot>:
claude --remote-control cs-<slot> -n cs-<slot> --model <model> \
--permission-mode bypassPermissions --settings <fence hook> \
--append-system-prompt <fence prompt> '/code-sentinel:worker .orchestrator-brief.md'The brief lists what the slot owns and what it must never open, as globs:
## What this slot owns, and what it must not open
owns:
- src/api/facts/**
- .orchestrator-reply.md
never:
- db/schema/**fence.py re-reads the brief on every tool call and refuses Edit, Write and the shell writes it can parse that fall outside owns:. The orchestrator can widen a fence live by editing the brief in the worktree. Workers run in bypassPermissions mode because a worker that prompts on every test run is unusable unattended; the fence and the brief's "never merge" rule replace those prompts.
Slots run Claude Code today. Codex workers, and their different fence, are AgentDock milestone M4 and not built.
Checkpoints, checks and the pull request
The worker runs /code-sentinel:feature #<n> --worker: it studies the code, writes a plan, implements it step by step, runs the configured install and checks, and opens the pull request with Closes #<n>. It never skips or disables a test to get green.
It reports through .orchestrator-reply.md in its worktree, under fixed headings: picked up, plan ready, implementation done, pull request open, blocked, misclassified. At plan ready it waits for the orchestrator's go-ahead. After the pull request it writes a merge summary (what changed, checks, tests, gates, what it could not verify, risk) and stops. It stays alive to answer review comments but never merges.
The orchestrator answers worker questions from the issue and the code, and routes only real decisions to you. It never approves a permission prompt on a worker's behalf; it tells you which session to open. A worker that died with unmerged work is resumed in the same worktree with the same model and brief.
Merge
On every wake, the orchestrator merges each pull request that is green, MERGEABLE, CLEAN and has no open question from its worker, with gh pr merge <n> --<mergeMethod>. Then it kills the worker session, removes the worktree and cs:in-flight, and fills the free slot. It spaces merges a minute or two apart.
It does not merge a gated issue, a red, UNKNOWN or BEHIND pull request, or one whose commits or body carry a Claude attribution when the project forbids it.
autoMerge: false: instead of merging, the orchestrator tells you which pull requests are green.mergeApproval: true: green pull requests wait for an approval from a file, a GitHub review or thecs:approvedlabel.
mergeApproval exists only on the plugin's unmerged merge-approval branch (plugin #8). Released versions ignore it, so use autoMerge: false to stop automatic merges today. AgentDock's Approve button also writes an approval file in a format that branch does not accept yet (decision: "approved" and headSha versus "approve" and head). Until that is reconciled, approve with a GitHub review or the cs:approved label.
Labels
| Label | Meaning |
|---|---|
cs:ready (or your readyLabel) | In the queue |
cs:in-flight | A slot is working on it; removed after the merge |
cs:needs-person | Waits on a person: a gate, a credential or a decision. Not dispatched |
cs:blocked | Created with the others; the board names blockers per round |
size: XS to size: XXL | From /code-sentinel:estimate --post. XL or XXL without a parallel plan counts as NO SPEC |
priority: critical, high, medium | Dispatch order |
bug | Branch prefix fix/ instead of feat/ |
What it leaves on disk
Everything the orchestrator writes for itself lives inside .git, so it is never committed:
Path (under <git-common-dir>/cs-orchestrator/) | What |
|---|---|
<YYYY-MM-DD>/round-<HHMM>.md | The round board, one file per round |
<YYYY-MM-DD>/round-<HHMM>-<slot>.md | The brief for each slot |
events.jsonl, state.json | Structured event log and snapshot (plugin develop only) |
specs/ | Spec snapshots and clones when specDir points outside the repo (plugin develop only) |
approvals/ | Merge approval files (unmerged merge-approval branch only) |
In each worker's worktree: .orchestrator-brief.md, .orchestrator-reply.md and, when someone sent a message, .orchestrator-msg.md.
To look by hand:
tmux ls | grep '^cs-'
git worktree list
ls "$(git rev-parse --path-format=absolute --git-common-dir)/cs-orchestrator/"Start it by hand
Open Claude Code in the repository's main checkout (not a worktree), put the session on Remote Control, and start:
/remote-control
/code-sentinel:orchestrator start| Argument | What it does |
|---|---|
start | A full round, then the loop. The default with no argument |
next | A round on freed capacity. Identical to start |
status | Read-only: what runs, what each slot last reported, what merged, free capacity |
stop <slot> | Kills cs-<slot>. The worktree, branch and commits stay |
stop all | The same for every cs-* session |
Start it from AgentDock
Project → Fleet → Start… asks for the mode (start or next), the runtime profile, the model (default opus) and the permission mode (default auto; only an admin can choose bypassPermissions). The runner starts the profile's claude in a tmux session named agentdock-orch-<owner>-<repo>, with --remote-control on and the prompt /code-sentinel:orchestrator start (or next). The name deliberately avoids the cs- prefix, because the plugin's watcher treats every cs-* session as a slot.
Start is refused with a reason when the runner is offline, no profile is set, the orchestrator already runs, or a stop budget is exceeded. Stop ends only the orchestrator session; workers keep running. Schedules and inbound triggers can also start next rounds.
AgentDock never picks an issue, a slot or a model. The orchestrator decides.
Message a worker
The orchestrator talks to a worker through two files, never through Claude's peer messages (those are held for a person's approval). It writes .orchestrator-msg.md in the worktree and types a fixed line into the pane:
tmux send-keys -t cs-<slot> -l "Read ./.orchestrator-msg.md and reply into ./.orchestrator-reply.md"
tmux send-keys -t cs-<slot> EnterAgentDock's Message worker on a slot does exactly the same, with a From: <user> via AgentDock header and at most 16 KiB of text. It bypasses the orchestrator, so use it for nudges, not for new work. Stop slot kills the slot's session and keeps its worktree, branch and commits for the orchestrator to resume or clean up.
The kill guard
The plugin installs a PreToolUse hook that refuses pkill, killall and kill without literal PIDs in every session, workers and orchestrator included. A worker that started a dev server has to note its PID (cmd & echo $!) and stop it with kill <pid>. This keeps one worker from killing processes that belong to another worker or another project on the same machine.