kingshand

Worker control plane: herdr

Where this came from

Source
docs/2026-08-29-herdr-worker-control-plane.md in emgee-labs/kingshand
Mirrored from
commit 617dca36, dated 2026-08-30

This page is a mirror. The text below is the document as it stands in the repository; edits belong there, and reach this page the next time the site is built. Nothing is rewritten for the web on the way here, so the headings, tables and code below are the ones the repository holds, and a link that points at another record points at the copy served here. Back to all records, or to the install page for the tool these records describe.

The document

Date: 2026-08-29 Status: decided Supersedes: 2026-08-28-worker-control-plane-decision.md

The decision

Kingshand spawns and controls workers through herdr. Each worker is an ordinary interactive Claude Code session running in a herdr pane, in a git worktree kingshand creates itself. claude --bg --worktree is gone, and so is the whole claude agents --json / logs / stop / rm / attach surface it came with.

bin\Herdr.psm1 is the only place that knows herdr's command line. Nothing else composes one.

What prompted it

Yesterday's record said the cheap option won because no worker had hung yet. One did, the next day. It called AskUserQuestion, drew a menu nobody could see, and waited five to six hours. The old control plane could not tell that state apart from working: a hung worker and a busy worker looked identical, so the hang was found by the user asking rather than by anything noticing.

That was the trigger this record's predecessor wrote down for itself, and it fired sooner than the predecessor expected.

Why herdr answers it

herdr hosts the agent in a terminal it owns, so it can both write into it and read what is on it. Two capabilities follow, and they are the whole reason for the move.

It classifies state. idle, working, blocked, done - and blocked is a first-class state, detected from what the agent actually rendered. In the trial, the five-hour hang was a state herdr reported within seconds of the menu appearing. That classification did not hold in production, and the correction is recorded below - what survives is the capability underneath it, that herdr can be asked what is on a worker's screen.

It waits on state. A wait blocks inside herdr and returns the moment the worker reaches one of those states. The Hand's dispatch arms one, and its completion is the wake. There is no polling loop left anywhere, which is not just tidier - a poll on a 20-second interval was a promise the Hand had to keep making to itself, and the defect it was written to fix was the Hand not keeping it.

Steering came along with it. A running worker can be sent text, and a blocked one can be answered a key at a time. That was the capability the predecessor record wrote off as not worth a daemon; it turns out to be free once something owns the terminal.

The evidence

All of it observed on this machine against herdr 0.8.2, protocol 20, on native Windows. Nothing below is inferred.

Correction: blocked detection does not hold

Added 2026-08-29, after a live end-to-end run of this layer. The trial evidence above is real and stays; this is what happened when the same thing was measured again in production conditions.

herdr 0.8.2 with agent-detection manifest 2026.08.21.1 does not reliably classify a Claude Code worker sitting on an unanswered AskUserQuestion menu. With the menu visibly on screen:

Correction, established later the same day: the cause was terminal width, not the manifest. Those panes were 3 to 6 columns wide, rendering one character per line, because dispatch split an existing pane for each new worker and every split halved the survivors. herdr's rules are regexes over the rendered screen, so they cannot match a UI that never renders - and neither can kingshand's own guard. Re-tested at 94 columns, herdr classified the same blocked worker correctly, and so did the guard. Dispatch now gives every worker its own workspace; four created in a row measured 93-94 columns with no degradation.

So the accurate statement is not "herdr's blocked detection does not hold". It is that detection of any kind needs a readable terminal, and kingshand was destroying the terminal it depended on. The guard stays anyway: one correct classification is not proof across a Claude Code interface change, herdr's rules are a network-fetched artifact that can lag one, and a screen read costs almost nothing against a worker silently reported as finished.

The consequence is worse than the hang this port was built for. Wait-HerdrAgent with no -Until matches idle, done or blocked, so a worker waiting on a human wakes the Hand claiming completion: previously the worker went silent, now it is actively reported as finished, and whatever is downstream tears it down and reports work nobody did.

Kingshand therefore no longer relies on herdr's classification for the blocked case. The worker's screen is the authority, and bin\Herdr.psm1 owns the guard in three functions:

The classification is still used for working and for waking at all, which is what herdr is needed for. What is not used is its word for whether a stopped worker is finished or waiting on a person, and muster no longer treats any state alone as proof of completion - a worker is done when it settled, is not awaiting input, and left the report.md its brief required.

What this costs, stated plainly

No arguments can be passed to a worker, at all. herdr's argument form is broken on Windows for Claude Code - it launches through a mechanism that cannot run a PowerShell entry point - so the only working shape is the bare launch. Everything that used to be a flag now lives in the worktree's own .claude\settings.local.json, written before the worker starts. A future requirement that genuinely needs a command-line argument has nowhere to go.

A fresh worktree hits the folder-trust dialog. The old spawn path inherited trust and never needed its own entry; this one does, so trust is pre-seeded before the worker starts. Skip that and the worker starts up blocked on a dialog instead of working.

A force-killed worker costs its pane permanently. It never sends its terminal-mode reset, so the pane echoes every later keystroke as literal junk and no herdr command recovers it. The worktree is never harmed - it is only a directory - but the pane must be discarded and a new one made. Hence: always /exit, never a kill.

Answering a prompt is a wrong-answer risk, not just a fiddly one. Sending an arrow key and Enter in one call selects the wrong option and reports success while doing it. Keys go one per call, with the screen read back in between.

Workers run with transcript saving off. The Hand is itself a Claude Code session, so the herdr server and every pane inherit CLAUDE_CODE_CHILD_SESSION. Both are scrubbed where kingshand starts them, but a herdr server it did not start carries whatever it inherited. The practical consequence is that the session transcript is not a reliable fallback for a worker's findings any more. report.md is not a nicety; it is the only durable record.

herdr keeps state outside the repository, in %APPDATA%\herdr and %LOCALAPPDATA%\herdr, and there is no way to redirect it. Its server also fetches detection manifests over the network at start, falling back to bundled ones when offline.

The fragility moved rather than went away, and it has already cost us. State classification is coupled to Claude Code's rendered interface and to herdr shipping a manifest that matches it. This was written as a future risk; it was live at the time of writing. Manifest 2026.08.21.1 does not classify the blocked case, and it degraded exactly as predicted - silently, with a confident wrong answer rather than an error. A versioned, network-fetched artifact can be fixed without waiting for a release, which is the good half; the bad half is that nothing announces the degradation, so kingshand carries its own screen check rather than waiting for a manifest to be right.

What would change the decision

What must not be undone