Skip to content
AgentDock

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

From idea to merge
/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 slot

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

  1. Bearings

    Checks the prerequisites, requires that its own session is on Remote Control (otherwise it asks you to run /remote-control and start again), reads the config, finds cs-* sessions and worktrees already running, runs git fetch, and creates its labels if missing.

  2. Read

    Collects ready issues, cs:in-flight issues, open pull requests with their checks, git worktree list and tmux ls. Each worktree is classified as free, occupied, a dead slot to resume, or an aborted dispatch.

  3. Ready set

    Gives each ready issue exactly one state: IN FLIGHT, READY, BLOCKED on work (an open dependency, or an open pull request touching a file it needs), BLOCKED on a person (cs:needs-person or a Gate: line) or NO SPEC. A NO SPEC issue gets a comment, loses the ready label and is pointed at /code-sentinel:spec. An issue with a ## Parallel plan becomes a wave, and dependent slots wait until the lead's pull request has merged.

  4. Decide

    Takes as many READY issues as there is free capacity under maxSlots, ordered by priority: label, then lowest issue number. Reads the usage footer and dispatches fewer if the allowance is nearly spent.

  5. 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. defaultModel applies only when it genuinely cannot tell.

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

  7. Dispatch

    For each slot it adds cs:in-flight to the issue, writes a brief, and runs dispatch.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> from origin/<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.py as a PreToolUse hook through --settings, so nothing is added to your repository;
  • starts the worker in tmux session cs-<slot>:
What dispatch.sh runs
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:

.orchestrator-brief.md (excerpt)
## 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.

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 the cs:approved label.

Labels

LabelMeaning
cs:ready (or your readyLabel)In the queue
cs:in-flightA slot is working on it; removed after the merge
cs:needs-personWaits on a person: a gate, a credential or a decision. Not dispatched
cs:blockedCreated with the others; the board names blockers per round
size: XS to size: XXLFrom /code-sentinel:estimate --post. XL or XXL without a parallel plan counts as NO SPEC
priority: critical, high, mediumDispatch order
bugBranch 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>.mdThe round board, one file per round
<YYYY-MM-DD>/round-<HHMM>-<slot>.mdThe brief for each slot
events.jsonl, state.jsonStructured 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:

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

Claude Code
/remote-control
/code-sentinel:orchestrator start
ArgumentWhat it does
startA full round, then the loop. The default with no argument
nextA round on freed capacity. Identical to start
statusRead-only: what runs, what each slot last reported, what merged, free capacity
stop <slot>Kills cs-<slot>. The worktree, branch and commits stay
stop allThe 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:

The poke
tmux send-keys -t cs-<slot> -l "Read ./.orchestrator-msg.md and reply into ./.orchestrator-reply.md"
tmux send-keys -t cs-<slot> Enter

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

Next steps