# kingshand (kingshand.emgeelabs.in): full text > One session doing all of it, without wasting context. You talk to one Claude Code session; it dispatches each worker into its own git worktree and brings back what needs deciding. Every page of kingshand.emgeelabs.in inlined, in the order a reader would take them. The short index is at https://kingshand.emgeelabs.in/llms.txt ------------------------------------------------------------------------------ # kingshand - run many Claude Code sessions at once on Windows Source: https://kingshand.emgeelabs.in/ One session doing all of it, without wasting context. kingshand is the single Claude Code session you talk to. Describe a piece of work and it writes the brief, waits for your word, and runs the worker itself - in its own copy of the repository, so several things can be in flight without treading on each other. Windows, PowerShell 7, MIT licensed. - Start using it - See it run - Source on GitHub What the one session does Running one Claude Code session is easy. The trouble starts at three: a fix in one repository, an investigation in another, a flaky test somebody has to sit with. You become a tab-juggler - three terminals, three sets of context in your head, and no way of knowing which session went quiet an hour ago because it is waiting on a question nobody is there to answer. kingshand makes one session the only one you talk to. It dispatches the others, watches their screens rather than a status word, and comes back to you when there is something to decide. Six things make that worth doing, and the heading at the top of this page is what they add up to. Replies you can actually read Next action first, numbered steps, lists capped at five, no preamble - not a wall of terminal output pasted into the chat. The herald skill owns that shape and it is on in every session without being asked for; say you want the long version and it steps out of the way. Anything worth comparing gets rendered, not scrolled past A diff, a list of requirements, a review gate, anything you have to decide on: it opens as a surface in your browser instead of going by in chat, because a choice buried in a paragraph is a choice you have to dig back out. That surface is lavish-axi , one of the six tools the installer fetches or names. A review gate in front of the remote, when you ask for one Register a project no-mistakes and every change made in it goes through automated review, tests, lint and documentation before it can reach a remote. It is one of three delivery postures a project can be registered with, and it is opt-in: a project registered local-only stops on a finished branch and never calls the gate at all. The work is written straight through You describe it once. The session writes the brief, makes the isolated copy of the repository, starts the worker, watches it, and brings back what it found - there is no point at which you stop and hand the next step to another session. What it will not do is decide for you: nothing is dispatched before you approve the brief, and nothing lands before you approve that, unless you have registered that project to proceed without asking. It writes down what it learns, as it learns it Every durable file it keeps for you is listed in an index as it is written, so a decision you already made cannot sit in a file nobody opens. Operational facts go to a memory file, each dated and backed by the session that produced it, rather than being re-discovered next week. Being straight about the limit: the pass that curates that memory is a command you run - /chronicle - and nothing runs it on your behalf. And that memory is kept to a budget The two memory files are measured against what a session start can afford to load, and the session-start digest says when they have outgrown it. So the thing loaded at the top of every session cannot grow forever, or crowd out the standing instructions you stated yourself - which are a separate file the tool is not permitted to edit. Curating it back down is the same /chronicle pass. See it run The check kingshand runs against its own toolchain, as it came off a real machine. It is the transcript itself typing - no video, no recording and nothing scripting it, which is the same thing the headline on emgeelabs.in is doing a few hundred lines further up the same stylesheet. PS C:\repos\kingshand> .\bin\Test-CrewPrereqs.ps1 Checking crew prerequisites OK claude C:\Users\you\AppData\Roaming\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe OK git C:\Program Files\Git\cmd\git.exe OK lavish-axi C:\Users\you\AppData\Roaming\npm\lavish-axi.ps1 OK tasks-axi C:\Users\you\AppData\Roaming\npm\tasks-axi.ps1 OK gh C:\Program Files\GitHub CLI\gh.exe OK claude.exe C:\Users\you\AppData\Roaming\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe OK herdr C:\repos\kingshand\tools\herdr\herdr.exe OK no-mistakes C:\repos\kingshand\tools\no-mistakes\no-mistakes.exe OK Pester 6.1.0 OK lavish port 4388 OK gitignore C:/Users/you/.gitignore_global All prerequisites satisfied. kingshand checking its own prerequisites, printed as it came out of a real run and typed back out here line by line. Only the home directory and the clone path were changed. The script verifies what kingshand requires, the optional extras it will use when they are present - those are reported as notes rather than failures - and two machine settings it reads. It runs in PowerShell 7, so the shell it needs prints no line of its own. The check installs nothing: it reports what it found and where, and a missing tool is named with the command that fixes it. Ask for reduced motion, or print the page, and the same fifteen lines are simply there. Getting started Six things worth knowing before you begin, each with the thing you would actually type. The install commands are not repeated here - they are in the repository's README, beside the code they install, which is the copy that stays right. Open it, and say anything at all The screen is empty when the session opens. That is normal and not a failed install: the session-start digest is handed to the session as context rather than printed, so nothing is shown until you type. Say hello . The first reply opens by telling you what is registered, what is running and what is waiting. Register a repository before you give it work /annex D:\code\your-repo - any path, on any drive. It records what that project is allowed to do and it never clones anything. Nothing is dispatched into a repository you have not registered, so this is the step that comes first. Ask for the work in one sentence fix the flaky login test in your-repo . What comes back is a brief for you to approve, not a running worker - and the same sentence is all that is needed whether the work is a bug, an investigation or a change. Say how you want to be worked with, once Standing instructions live in instructions.md . It is read at the start of every session and never written to, which is enforced by the permission layer rather than asked for in prose. A line out of the template that ships with it: Default a new project to local-only until I say otherwise. Ask where everything stands /survey for the current picture - what is registered, what is live, what is queued. /audience for what happened in this session while you were looking elsewhere. /afk when you are stepping away, which holds the fleet and batches everything that does not need you. Curate its memory before a context reset /chronicle . It sweeps the session for anything durable that so far exists only in the conversation, files each finding where it belongs, and prunes the memory files back inside their budget. What decides whether an install works - It is Windows only. Every script targets PowerShell 7. There is no macOS or Linux build and there is not going to be one; on those the tool to use is firstmate . - You clone it. There is no hosted service, no account and no installer to download - the repository is the distribution, and it is MIT licensed. - There is no one-line remote installer. A page offering irm ... | iex would be offering something that does not work: install.ps1 resolves its own location and imports PowerShell modules from the bin\ directory beside it, so it only runs from inside a clone. - Nothing is installed unless you ask for it. By default the installer reports each missing tool with the exact command that fixes it and installs none of them. Nothing self-elevates, and every value it writes outside the clone is named as it is written. The commands themselves, and every requirement with the command that installs it, are in the repository's README - the copy that ships beside the code. This subdomain's llms.txt carries them as plain text for anything reading without a browser. How a piece of work moves through it - You describe the work in the one session you talk to. - It writes a brief and waits for your word. Nothing is dispatched before you approve it. - It creates a git worktree inside the target repository, writes the worker's settings and marks the worktree trusted, so an unattended worker does not stop on a folder-trust dialog. - The worker runs as an ordinary interactive Claude Code session in a terminal kingshand owns, which is what lets it read a worker's screen while it runs and send text to one that has gone astray. - The worker leaves a report.md . That reaches the landing gate, and the landing gate reaches you. What it is not - It does not do the project work itself. It dispatches and supervises; the workers write the code. - It is not the tool to reach for on macOS or Linux. That one is firstmate by Kun Chen, which kingshand is a Windows-native rebuild of ideas from, and which does some things kingshand cannot. - It is not a hosted service, and there is no release to download. The clone is the release: nothing is tagged, nothing is packaged, there is nothing to sign up for and no account. A Pester suite ships with it. Permissions, stated before you install kingshand ships with Claude Code's permission prompts turned off for itself and for every worker it starts: neither will ask you to approve a tool call. That is deliberate - the session reads diffs, runs git and merges locally hundreds of times in a session - but it is a real decision, and better made knowingly. What still constrains it: - Workers only ever run inside their own git worktree, never in your checkout. - Nothing is dispatched into a repository you have not registered. - Nothing pushes unless that project is registered with a push-capable posture, and nothing is merged for you at all - a pull request is where the work stops. - Reads of ~\.ssh , AWS credentials and kingshand's own credentials directory are blocked, as are edits to your instructions.md . If you would rather approve each call, the setting is one line in .claude\settings.json and removing it restores the prompts. Where everything is - Getting started - six pointers with the thing you would type, and the four facts that decide whether an install works. - The repository's README - the install commands and every requirement, beside the code they install. - Design decisions - the records kept about how workers are run and controlled, mirrored from the repository. - llms.txt and llms-full.txt - this subdomain as plain text. - emgee-labs/kingshand - the source, the tests, and the MIT licence. ------------------------------------------------------------------------------ # Design decisions Source: https://kingshand.emgeelabs.in/docs kingshand design decisions These are the records kingshand keeps about how it runs and controls workers: what was decided, what prompted it, what it costs, and what would reverse it. They are mirrored from the repository when this site is built, so they say what the repository says. The records Worker control plane: stay on claude --bg This record's own revisit trigger fired. It is kept because the reasoning is still the reason kingshand refused a heavier control plane for as long as it did, and because the trade table below is what the replacement... Worker control plane: herdr 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... The data index - an index, not a classification The data index - an index, not a classification: a design decision recorded in the kingshand repository and mirrored here. Read first is declared, not parsed Read first is declared, not parsed: a design decision recorded in the kingshand repository and mirrored here. Noticing a worker that has stopped making progress Two failures on the same night, both costing real time, both invisible until the King asked. Text in a worker's input box that nobody sent A worker was found sitting idle with a well-formed instruction in its input box - show me the getting started section rendered. Nothing had sent it. It appears in no transcript, no brief and no report.md, because... Where these come from - Source: github.com/emgee-labs/kingshand/tree/main/docs - Mirrored from commit a7f9e180 , dated 2026-09-02. - Anything not here - the README, the skills, the tests - is in the repository itself . What the tool these records describe actually is, and how to get going with it, is on the overview . ------------------------------------------------------------------------------ # Worker control plane: stay on claude --bg - design record Source: https://kingshand.emgeelabs.in/docs/2026-08-28-worker-control-plane-decision Date: 2026-08-28 Status: **superseded on 2026-08-29 by `2026-08-29-herdr-worker-control-plane.md`** **This record's own revisit trigger fired.** It is kept because the reasoning is still the reason kingshand refused a heavier control plane for as long as it did, and because the trade table below is what the replacement was measured against. Do not follow its instructions - workers are no longer spawned this way, and a running worker can now be steered. ## The decision Kingshand keeps spawning workers with `claude --bg --worktree` and accepts that it cannot steer a running worker. It does not adopt the ConPTY model firstmate uses on Windows. Revisit only under the conditions at the end of this file. ## What prompted it Porting the `rally` procedure from the tool kingshand derives from exposed that three of its five escalation steps - answer a question in one line, interrupt and redirect a confused worker, relaunch carrying a progress note - have no mechanism in kingshand. The verified control surface is `claude agents --json`, `logs`, `stop`, `rm` and `attach`. There is no `claude send`. `attach` is interactive and belongs to the user, not to the Hand. ## Why a ConPTY harness can steer and kingshand cannot This is architectural, not a Windows limitation. A harness that hosts the agent inside a pseudoconsole it creates itself owns that ConPTY, and can therefore write into the agent's stdin: send the literal text, then send the Enter key to submit it. Liveness comes from OSC 133 prompt marks emitted into the same stream. A working implementation of exactly this exists, so the capability is real rather than hypothetical. Steering requires owning the process's terminal. Kingshand's workers are Claude Code background sessions, so Claude Code's supervisor owns them and there is no pseudoconsole to write into. No amount of work inside kingshand changes that; only replacing the spawn mechanism would. ## The trade | | `claude --bg` (chosen) | ConPTY daemon | |---|---|---| | Infrastructure | none | a Node daemon, ~5 JS files, a native dependency, its own liveness machinery | | Steer a live worker | no | yes | | Interrupt and redirect | no | yes | | Relaunch carrying a note | no | yes | | Stuck-worker options | stop and re-dispatch, or hand to the user via `claude attach` | full five-step escalation | ## Why the cheap option wins here A worker costs almost nothing to re-dispatch: the brief is on disk, the worktree persists through `claude stop`, and a replacement starts from the same brief plus a progress note in its text. The escalation steps that are lost are the ones that matter least when restarting is cheap. Against that, the ConPTY model adds a daemon and a native dependency to a tool whose whole appeal is having no moving parts. That is a poor trade today. ## What this costs, stated plainly - A worker waiting on a question its brief already answers cannot be answered by the Hand. It is stopped and re-dispatched with a better brief, or handed to the user with `claude attach`. - `Dispatch-Worker.ps1` always creates a fresh worktree, so a replacement is a second copy by construction. This sits in tension with firstmate's "never two worktrees for one task", and `rally` names that tension rather than hiding it: the stuck worktree's work must be committed and carried into the replacement's base before the replacement runs. ## What would change the decision - Claude Code gains a scriptable way to send input to a background session. That removes the whole reason to own a terminal, and the ConPTY model stays unnecessary. - Re-dispatching stops being cheap - long-running workers, expensive setup, or work that cannot be resumed from its brief. - Stuck workers become frequent enough that stop-and-re-dispatch is a real cost rather than a rare annoyance. Nothing so far suggests this; no worker has hung yet. **The third one fired the next day.** A worker opened an interactive prompt and sat on it for five to six hours with nothing watching, and the control plane above had no way to see that state at all. The successor record explains what replaced this and what that cost. ------------------------------------------------------------------------------ # Worker control plane: herdr - design record Source: https://kingshand.emgeelabs.in/docs/2026-08-29-herdr-worker-control-plane 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. - A genuinely busy worker read `working` for the full duration of a 75-second turn and settled to `idle` when it ended. The feared "reports everything as dead" failure did not occur. - State does not come from the process name. It comes from priority-ordered regex rules in a per-agent manifest run against the rendered terminal, and the manifest for Claude Code is current and fetched from herdr's own server. - A worker made to open an `AskUserQuestion` menu read `blocked` twelve seconds later, and a wait armed on `blocked` beforehand fired with it. **This held in the trial and does not hold generally** - see the correction below, which was measured later on the same machine. - Sending that worker a prompt while blocked was refused outright, with a distinct error, rather than silently swallowed. - A clean `/exit` proved the worker stopped in 1.7 seconds. A hard kill was detected in 662 ms. - Text arrives intact: a 3,374-character prompt reached the worker whole. ## 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: - `agent explain` reported `state: idle`, matched by rule `live_prompt_box` at priority 950. Rule `live_blocked_form` at priority 980 - the one that fired in the trial - evaluated and did not match. Every blocked rule failed. - Minutes later the same still-blocked worker reported `done`, while a genuinely finished worker reported `idle`. The two states effectively inverted. **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: - `Test-HerdrAgentAwaitingInput` reads the LIVE VIEWPORT - `agent read --source visible` - and answers whether an interactive prompt is on screen now. The live viewport is load-bearing: `recent` and `recent-unwrapped` include scrollback, so a worker that answered a menu earlier still holds that text in history and would read as blocked forever. - `Get-HerdrAgentState` is herdr's state corrected by that screen, returning `blocked` whenever a prompt is showing whatever herdr said. It is the only state function anything downstream calls, and `bin\Get-CrewStatus.ps1` routes through it, so nothing above reads `agent_status` raw. - `Wait-HerdrAgentSettled` is the guarded wake. It wraps the wait, re-checks the screen before reporting a state, and distinguishes "not settled" from a state rather than inventing an outcome on a timeout. 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 - Claude Code gains a first-class scriptable way to run, watch and steer a background session. That would make owning a terminal unnecessary and this whole layer removable. - State classification degrades further. The blocked case has already gone, and the answer was to read the screen directly rather than to abandon herdr - the terminal it owns is what makes that possible at all. What would genuinely reopen the decision is losing `working`, or the screen read itself becoming unreliable, because then nothing distinguishes a busy worker from a stuck one and the case for this layer is gone. - herdr's argument handling is fixed on Windows. That does not undo the decision, but it would let configuration go back to being explicit rather than written into a settings file first. ## What must not be undone - The wait is an event, not a poll. Reintroducing a sleep-and-check loop rebuilds the thing this replaced and re-opens the silence it was meant to close. - `blocked` reaches the user immediately, and is never answered on their behalf. It is the defect this port exists to fix. - The screen check stays, and it reads the live viewport. Deleting it, or pointing it at `recent` or `recent-unwrapped`, restores a control plane that reports a worker waiting on a person as finished. No state is ever proof of completion on its own. - One file knows herdr's command line. Two would mean the next migration touches everything again. ------------------------------------------------------------------------------ # The data index - an index, not a classification - design record Source: https://kingshand.emgeelabs.in/docs/2026-08-30-data-index 2026-08-30 ## What went wrong On 2026-08-29 the whole Emgee Labs brand was settled in one session: logo, favicon, tagline, both theme palettes, the homepage headline verbatim, and a list of things never to put on the page. It was written to `data\emgee-brand.md`, whose own first line says "This is the input to the website brief." On 2026-08-30 the site was built and shipped live with none of it. No logo, no favicon, no tagline, none of the settled palette. It was found by looking at the page. Nothing was lost and nothing was overruled. The file was simply never read, because nothing made anyone read it. A worker sees exactly one thing - its brief - and no brief named the file. ## The design that was rejected The first answer was a category: define "a settled decision that a future brief must carry", give it a home at `data\decisions\`, list it in the digest, and require brief-writing to look for one. It was rejected, correctly. Classifying a file as important at the moment it is written means guessing what some future task will need. That guess is wrong regularly, and a wrong guess is silent - the file is simply not in the special place, so nothing looks for it and nothing reports that anything is missing. That is the same failure this whole change exists to fix, wearing a different hat. ## The design that shipped An index. Every durable file under `data\` gets one line recording where it is and what it is. Nothing is judged important or unimportant at write time; everything is listed. The judgement moves to read time, where the reader knows exactly what they are working on and opens the handful of files their own task touches. Four things make it work, and removing any one of them puts the failure back. **It is a table of contents, never content.** Path, one line, date. A reader pays one line per file, not one file per file. `Add-IndexEntry` refuses a summary over 160 characters rather than truncating it, because a silently cut summary is a summary that lies, and because a cap is the only thing that keeps a table of contents from growing into a second copy of the data. **It is scoped per project.** `data\index\.md` holds one project's files; `data\index.md` at the root holds kingshand's own operational files. The project is the cut because the project is the unit a brief is written against - an index spanning every project hands a website worker the aegis reports, and an index nobody can scan is an index nobody reads. The root index is a file beside the directory rather than a reserved name inside it, so no project name can ever collide with it. *Amended 2026-09-01.* In practice the settled files this design exists to deliver land in the ROOT index, not in a project one: `chronicle`, `annex` and `survey` all index with no project, so an unscoped `data\.md` goes to `data\index.md`, while `data\index\.md` holds little beyond briefs and reports. One look at the live installation is the evidence - `data\index\` did not exist at all while `data\index.md` held real entries - so the dispatch gate added that day could never have fired while it consulted the project index alone, and it reads both. `bin\Dispatch-Worker.ps1`'s header owns that gate's rules. Nothing about how the index is written or scoped changed; this is a correction to the record. **Indexing is part of writing, not a separate act of virtue.** `Write-DataFile` writes the file and indexes it in one call, so the two cannot come apart. Where another tool owns the write - `tasks-axi` writing the backlog, a worker writing its own `report.md` - `Add-IndexEntry` records it at the first moment somebody has both the file and the context to describe it, which is why `muster` indexes a brief in the step that writes it and a report in the step that reads it. A rule that says "remember to add it afterwards" is exactly the rule that was forgotten last time. **The gap is visible.** `Get-IndexDrift` counts the files under `data\` that no index lists, and the session-start digest prints that count. This is the part that makes the whole thing self-checking, and it is the reason an index beats a classification: "this file is listed nowhere" is a fact a machine can notice, where "somebody should have realised this mattered" never was. **And it has to be clearable from both ends.** The digest also prints `STALE:`, the entries whose file is gone, and that half had no remover at first - a dated `/survey file` artefact is written to be deleted, so every deletion cost one permanent line of noise and took the whole number with it. `Remove-IndexEntry -Missing -All` prunes them. The same argument that says a drift count growing by one per dispatch is a count nobody reads says a count nothing can take to zero is not a signal. ## What is deliberately not in it No exclusion is a judgement about a file's worth. The test each one passes is derivation: the file is produced from something else that is itself listed, so an entry would record the same fact twice. An index does not index itself. A rendered `*.html` review surface is regenerated from state rather than read as a source. A `read-first\` copy is the snapshot dispatch takes of a file that already has its own entry at its own path, so that its worker can reach it at all. The count is not the rule; the test is. Guarding a number would have taken the drift signal down instead: `read-first\` arrived after this note was first written, and with only "there are exactly two" to go on, the choice was between a drift count that grows by one per dispatch forever - a count nobody reads - and indexing each copy, which would put a duplicate entry in the table of contents for a file already listed. An exclusion that cannot answer "derived from what?" is the rejected classification creeping back in one file at a time, and that is what must not be added. The digest prints counts and the location, never a file list and never a file's contents. The startup-memory budget accounts for `king.md` and `learnings.md` only; the index is not printed in full and is not accounted, which is what keeps a growing index from becoming a growing session-start cost. ## What still has to move `data\emgee-brand.md` is where it was. It is live evidence of the failure and was left alone deliberately. It wants an entry in `data\index\emgeelabs-site.md`, and until it has one the digest counts it as drift - which is the mechanism working, not a defect. ------------------------------------------------------------------------------ # Read first is declared, not parsed - design record Source: https://kingshand.emgeelabs.in/docs/2026-08-31-read-first-declared-not-parsed 2026-08-31 ## What this replaces `bin\Dispatch-Worker.ps1` used to read the brief's `## Read first` section, pull file paths out of that prose, and compare the set it found against the paths handed to `-ReadPath`. The intent was sound: the section and the parameter are written in two different steps, and prose was the only thing tying them together, so a brief could name a copy nothing staged or stage a copy no line named, and either way the worker ended up holding a path it could not open. The mechanism was the problem. Over one run it took six consecutive review rounds, in this order: refuse paths outside the worker's grant, read every path form rather than only drive-letter paths, read whole paths rather than truncated ones, tighten the parsing, refuse spaced file names instead of guessing where a path ended, and refuse spaced mentions while searching every index. Every one of those findings was correct and every one of those fixes was right. That is the point: a path written in prose can be absolute or relative, forward or back slashed, quoted or bare, contain spaces, sit inside a sentence, or wrap across a line, so there is no round after which the parser is finished. Two of those rounds had already produced the failure mode a parser eventually reaches: refusing a correct brief with a message naming a path nobody had written. One read a staged original as running on into the words after it and lost its exemption; another read the leaf ` in` out of the phrase "under read-first\ in this directory". A guard that refuses good work is not a safer guard than none. ## What replaced it The paths arrive structurally. `-ReadPath` takes the list, the dispatcher copies each file into `data\\read-first\`, and nothing reads the section's text for a path at all. The reason this is not a weakening is that there was never a second source to reconcile against. The Hand writes the brief and the Hand calls the dispatcher, in the same step, holding the same list. Parsing the prose was the dispatcher asking the Hand a question the Hand had just answered, and then trying to understand the answer in English. **Do not reintroduce a parser here.** A future round that notices the section and `-ReadPath` can still disagree has noticed something true; the answer is that muster writes them together, not that the dispatcher should start reading English again. ## What still refuses, and why each one needs no prose Five checks, all before a worktree or a branch exists, so a refused dispatch leaves no debris. **The `## Read first` section must exist.** This is the check that closes the originating failure described in `2026-08-30-data-index.md`: a brief that names no settled file at all, which passed every set comparison because both sets were empty. A brief with nothing to read says so in one line; a brief missing the slot says nothing, and only the first is a decision somebody made. It is a regex against a heading - no path, no ambiguity. Fenced blocks are skipped, because a brief for a task on muster's own template quotes that template and the quoted heading satisfied the check for a brief that had no section of its own. **An indexed dispatch was either handed a file or says the index was checked.** Added 2026-09-01, because the section being present says a slot was filled in and says nothing about the index behind it - an index of pointers nobody is obliged to follow is the same settled-spec failure at a larger scale, and worse for looking solved. When any index that could cover the dispatch lists something, it is refused unless at least one `-ReadPath` was passed or one line of the section states the index was checked and nothing in it applies. Reading that one stated line is not the parser this document forbids: it counts `-ReadPath` entries that arrived structurally, asks `Index.psm1` what each index lists, and looks for a statement - never a file name - in the prose. *Amended 2026-09-02.* One `-ReadPath` no longer counts towards it - the project's standing-criteria file at `data\done-.md`, which `muster` passes on every brief for a project that has one. A path passed by rote is no evidence the index was read for this task, and counting it would have made this refusal unreachable from the first criteria file onwards. That is still not a parser: the path is compared as a path, against one the dispatcher composes itself. `bin\Dispatch-Worker.ps1`'s header owns the exact rule, as `2026-08-30-data-index.md` already says. **Every `-ReadPath` entry exists on disk.** A brief naming a file that is not there is a brief the worker cannot carry out. **No `-ReadPath` entry is a directory.** A directory would copy whatever happened to be in it. **No two `-ReadPath` entries share a leaf name.** They would land on top of each other in the staging directory and the worker would read whichever was copied last, with nothing to say the other was ever named. The refusal says to pass one or copy it under a distinct name, and deliberately does not say to rename: two reports really are both called `report.md`, and that is the name every index entry pointing at them already uses. Each of those knows its path exactly, because the caller handed it over. That is the difference between this list and the one it replaces. ## What is unchanged The staging copy itself, the single grant it protects, the exclusion of `read-first\` from the drift count, and the whole index design in `2026-08-30-data-index.md`. Nothing here reopens any of it. What came out is one cross-check, not the mechanism it was guarding. ------------------------------------------------------------------------------ # Noticing a worker that has stopped making progress - design record Source: https://kingshand.emgeelabs.in/docs/2026-09-01-stall-detection Date: 2026-09-01 Status: **current** ## What happened Two failures on the same night, both costing real time, both invisible until the King asked. **A review gate waited an hour for CI that could never arrive.** A pipeline run on the kingshand repository reached its `ci` step and sat there for over an hour. This repository has no CI at all: no workflow files, nothing configured on GitHub, zero check runs on the commit. The gate cannot tell "checks have not started yet" from "checks will never exist", so its wait had no end. **The Hand's own wait was watching the wrong thing.** `Wait-HerdrAgentSettled` asks whether a worker is alive. A worker that handed its work to a background pipeline and returned to its prompt read `done` immediately, so the wait fired within seconds and reported a completion that had not happened. The same night the opposite also happened: a worker whose work was genuinely finished read `working`, because stray text was sitting in its input box. The common thread is that liveness and progress are different questions, and only one of them is the one anybody cares about. ## The decisions ### CI is established before a worker is promised a wait for it `bin\Ci.psm1` answers one question - can anything report a check on this repository's pull requests - and `muster` Step 1b asks it before a `no-mistakes` task is dispatched. The answer picks which of the two `no-mistakes` Done-means blocks the brief carries, so a repository with no CI produces a brief that says to stop at the pull request rather than one that says to wait for green. **Two signals, and the second one is why this is not a directory test.** A `.github\workflows` directory is the obvious signal and it is insufficient in both directions: `emgeelabs-site` has no workflow file anywhere in the repository and gets Cloudflare Pages check runs on every commit, while a repository that deleted its last workflow keeps the empty directory. So the stronger signal is the checks GitHub actually reported on recent commits of the default branch, and it is consulted whenever nothing the repository itself configures could report on a pull request. That last clause is the third way a file listing lies: a workflow triggered only by `schedule` or `workflow_dispatch` exists, runs, and still never puts a check on a pull request, so its triggers are read rather than its presence counted. It is also the fourth: on a GitHub remote, another provider's config file - a dormant `.travis.yml`, a `.gitlab-ci.yml` from a mirror - says nothing about the GitHub pull request the worker will actually open, so GitHub is asked instead of the file being believed. Where there is no GitHub to ask, that same file stays a positive signal, because discarding it would be guessing in the expensive direction. A workflow whose `on:` block cannot be read is kept for that reason too. Both were verified against the three real cases on this machine: kingshand answers `no-ci`, emgeelabs-site answers `has-ci` from check runs alone, and a repository that cannot be reached answers `unknown` with the HTTP error in the detail. **The third answer is the point.** `unknown` is the refusal to guess when the question could not be settled - no `gh`, a remote that is not GitHub, an unauthenticated machine, a network that did not answer. Nothing in that module converts a failed lookup into either answer, because both wrong answers are expensive: a false `no-ci` throws away a real green check, and a false `has-ci` restores the hour-long wait. `unknown` takes the same terminating brief line as `no-ci`, because under uncertainty stopping at the pull request loses at most a wait for a check the user can see on the forge anyway. The shared line says checks *may* not report rather than that they are not expected to: the instruction is what ends the wait, and a line that asserted the absence as fact would have a worker report a repository as CI-less on the strength of an expired token. ### The progress signal is the worker's own screen, normalised Three signals were available and the screen was chosen. - **The worker's screen changing** needs no knowledge of what the worker was sent to do. - **A pipeline run's step, head commit or last-activity timestamp** is a much stronger signal, and only where the work is a review-gate run. - **The worktree's `git log` and working tree** is a third, and it moves only at commit boundaries - a worker can work correctly for forty minutes without touching it. The screen won because the Hand waits on investigations, audits and plain edits as well as pipeline runs, and a watcher that only understands pipelines is blind to every other kind of work. It also subsumes the pipeline signal in practice: a review gate prints its own step transitions into the worker's terminal, so a step advancing **is** a screen change, with nothing here having to know that a pipeline exists. That also sidesteps the trap that `no-mistakes axi status` with no `--run` returns the most recent run in the repository rather than the one being watched - a watcher started before its own run registered once read a different, already completed run and reported success immediately. Not knowing about runs at all cannot get the run id wrong. **Normalisation is what makes the screen usable.** Claude Code repaints an elapsed timer and a token counter every second while it works, so the raw screen is never twice the same and a naive hash would report a worker frozen for an hour as making steady progress. Only those volatile shapes are removed - durations, token counts, spinner glyphs, trailing space. Digits in general are left alone deliberately: a counter like `142/300` is real progress and must survive. The bias that leaves is toward missing a stall rather than inventing one, which is the correct direction, because a false alarm reaching the King is worse than a silent one and a missed stall is only as bad as today. ### Twenty minutes, and it is a parameter A review pass on this project has legitimately taken 38 minutes, though it printed progress throughout. Under about fifteen minutes, slow steps start reporting as stalls; an hour is plainly too long, since that is roughly what the incident cost. Twenty minutes is the default on `Wait-HerdrAgentProgress` and `-StallMinutes` overrides it per call. ### What the wake can and cannot promise The stall half answers the second failure: a worker that has stopped advancing is reported whatever state word it carries. The first failure has no watcher's answer at all. A worker that has genuinely finished and one that handed its work to a background pipeline and went quiet are indistinguishable from outside - both read `done` within seconds, and no amount of screen reading separates them, because both screens show a prompt. So the wake does not claim to. It carries the worker's final screen out with the settled answer, and `muster` says in as many words that `state=done` is a state word rather than a delivery: read what the screen last said, then check `report.md`. Reporting the screen from before the wait would have been worse than reporting none, so an unreadable final screen answers `signalReadable = $false` rather than falling back to a stale line. ### Reporting is the whole deliverable `Wait-HerdrAgentProgress` never recovers anything: no steer, no answered prompt, no relaunch, no stop. It returns a stall with the evidence needed to act - how long, what the worker was last seen doing, whether its screen could be read at all - and `rally` owns the response. A wrong automatic action on a stalled worker is worse than a late human one. ## What must not be undone - **`Wait-HerdrAgentSettled` keeps its behaviour.** The progress wait was added beside it, not over it. Other callers depend on it and its guarded screen read is correct for what it does. - **The normalisation stays, and stays narrow.** Delete it and every frozen worker reports as progressing. Widen it to strip digits wholesale and every counter-printing job reports as stalled. - **An unreadable screen never becomes a stall.** `signalReadable` being `$false` means the watch was blind, and a caller that reads it as "unchanged" reports a healthy worker as stuck. It is the same pane-width defect that inverts herdr's own classification, arriving at a different function. - **`unknown` is never rewritten into an answer.** The value of the CI preflight is that a failed lookup stays visibly a failed lookup. - **The wait stays an event.** herdr's own wait is the blocking primitive and the sample interval is only that wait's timeout, so a finished worker still wakes the caller instantly. Sampling exists only for the stall half, because "nothing happened" is not an event anything can push, and it is bounded so that a herdr answering instantly with an error cannot turn the wait into a spin. The bound counts only returns that came back without consuming their slice: counting every iteration left about three of margin, so one server restart on a healthy worker ended the watch. - **Nothing on a wake is assumed.** A worker herdr does not name is read twice before it is called gone, and the server is checked before that answer is given at all - a server that is down answers nothing for every worker, so reporting it as `gone` would send the Hand to reconcile a worktree still being written to. A stall reports the state and `awaitingInput` it read, because a worker sitting on an unmatched dialog looks exactly like a stalled one and needs the user rather than a hunt for a stuck step. And the default timeout is computed from the stall threshold, since a watch that ends first can never reach it. - **A read that failed is not a screen.** `agent read` is invoked with `2>&1`, so herdr's own error arrives as ordinary text: non-empty, constant, and therefore a stable fingerprint that reports a stall twenty minutes later with the error message as evidence of what the worker was last seen doing. One function owns that read and the question of whether it succeeded, because the pane guard `rally` cross-checks with reads the same viewport and would otherwise confirm the story. ## Revisit when - herdr grows a progress or activity signal of its own, at which point the screen fingerprint may become redundant. - Claude Code changes its status line enough that the normalisation stops matching. The symptom is silent: stalls simply stop being reported. `Get-HerdrAgentProgressSignal` on a busy worker, sampled twice a minute apart, is the check. ------------------------------------------------------------------------------ # Text in a worker's input box that nobody sent - design record Source: https://kingshand.emgeelabs.in/docs/2026-09-02-prompt-box-safety Date: 2026-09-02 Status: **current** ## What happened A worker was found sitting idle with a well-formed instruction in its input box - `show me the getting started section rendered`. Nothing had sent it. It appears in no transcript, no brief and no `report.md`, because nothing ever submitted it. It is the harness generating the prompt a person would plausibly type next and rendering it into the empty box between turns. Three sightings, all found by chance while looking at something else. That is a cosmetic render right up to the moment anything presses Enter at that worker. Then it is an instruction the Hand never wrote, submitted under the Hand's name. ## The decisions ### The guard sits on the send paths, not on the input side The suggestion is application state rather than input. It cannot concatenate onto anything - the first typed character displaces it - so text arriving at the box is never at risk of being mixed with it. A bare Enter is the whole hazard: it **accepts** what is rendered, and the harness records a submission whose text equals the suggestion as accepted by `enter`. So `Send-HerdrKeys -Keys @('enter')` at an idle worker submits a generated instruction as though the Hand had written it, and `rally` step 3 sends exactly that call to answer a menu. `Send-HerdrPrompt` and `Send-HerdrKeys` in `bin\Herdr.psm1` therefore read the box before they act, and refuse when it is not empty. **Refuse, never clear.** Clearing destroys the evidence of the one event worth noticing, and the caller may be about to send something that must not be mixed with whatever is already there. The escape hatch is `-AllowNonEmptyBox`, which is the escalation: `rally` quotes the text to the King and passes the switch only once they have seen it. Teardown is the one standing exemption. `/exit` displaces whatever is rendered harmlessly, and a worker that cannot be stopped because of a cosmetic render is a worse failure than the one the guard prevents - so `Stop-HerdrAgent` passes the switch deliberately, through the guarded path rather than around it. ### The discriminator is the caret plus a no-break space The box line is drawn as the caret `❯` (U+276F) followed by U+00A0. The highlighted row of a numbered option menu is drawn with the **same caret** followed by a plain space. On a rendered screen that one glyph is the only thing separating them, and the detector returns its first match top-down, so a caret-only rule reads a menu row above the box as box content. That is not a cosmetic wrong answer. A worker blocked on a menu is answered by a bare Enter and there is no other route to it - the panes are headless. A detector that refused that Enter would leave the worker stuck with nobody able to deliver the answer the King had already given. A plain `>` is never matched at all: a worker's own output lines start with one, and treating those as a box would refuse every send to a perfectly healthy worker. This is the captured render the fixtures in `tests\Herdr.Tests.ps1` are built from - a bare U+2500 rule, the caret and U+00A0 and the text at column 0, then another rule. There are no U+2502 side borders on a real screen. `` below stands for the no-break space that is really there, because an invisible character in a document gets tidied into a plain space by the next editor to touch it: ``` ──────────────────────────────────────────────────────────────── ❯show me the getting started section rendered ──────────────────────────────────────────────────────────────── ? for shortcuts ``` The fixtures escape it as `` `u{00A0} `` for the same reason. ### The placeholder exclusion is a named list, not "the value is empty" The harness draws its own dim placeholder into the **empty** box at exactly the position box content occupies, so position alone cannot tell one from the other. Refusing on one makes a worker unsteerable on a hint the harness printed itself. So `Get-HerdrPromptBoxText` carries a list of the placeholder strings, read out of the shipped binary rather than paraphrased, and a match returns empty. Where a placeholder has a variable part it is anchored on its invariant prefix and suffix, with the variable part required to be non-empty so the anchor cannot swallow an arbitrary line that merely opens the same way. **It is deliberately not the general rule "the underlying value is empty, so Enter submits nothing".** That rule is false for the generated suggestion, which is the entire hazard: its value is empty too, and the harness still records a submission equal to it as accepted by `enter`. Only the named placeholders are excluded. The list is bound to a harness version. A placeholder a later version adds is not on it, falls through to the refusal, and `-AllowNonEmptyBox` covers the gap until it is added. Refusing an unknown string is the safe direction; letting one through is not. ### This detector fails open, and the stall signal fails closed They are opposites on purpose, and it is not an inconsistency. `Get-HerdrAgentProgressSignal` fails closed: an unreadable screen answers `signalReadable = $false` and no stall is claimed, because a caller that reads "could not see it" as "nothing changed" reports a healthy worker as stuck - a false alarm reaching the King. `Get-HerdrAgentPromptBox` fails open: an unreadable screen answers empty and the send proceeds. The cost of guessing here is a teardown or a corrective steer that cannot be delivered to a worker that may already be wedged. A pane too narrow to render must not make a worker unsteerable. ### The feature is switched off on worker panes as well A worker has no human at its prompt, so a suggestion of what that human should type next has no purpose at all and only creates the hazard. `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=0` is set on the pane, at both the server start and the pane creation in `bin\Herdr.psm1`. **The environment check is the first branch of the harness's resolver**, so it wins over the remote flag and over any setting file. It is preferred to the equivalent settings key because that key is read at user scope: writing it into a worktree's `settings.local.json` may do nothing at all. The variable is passed at pane creation as well as at server start for the same reason `CLAUDE_CODE_CHILD_SESSION` is - the server scrub covers panes that server launched, and a herdr server kingshand did not start carries whatever environment it inherited. ## What must not be undone - **The guards stay even though the feature is off.** They also cover a herdr server kingshand did not start, and typing over a box the Hand did not fill was a defect before this feature existed. - **The caret and the no-break space are matched as a pair.** Match the caret alone and every worker blocked on a menu becomes unanswerable. Match a plain `>` and every healthy worker becomes unsendable. - **A refusal is never turned into a clear.** The quoted text is the only record that the event happened. - **The placeholder exclusion stays a named list.** Generalising it to "the value is empty" reopens the exact hole the guard exists to close. - **The box is reported, not classified.** `promptBox` is a field on the progress signal and on every `Wait-HerdrAgentProgress` report - settled, stalled, gone and timeout alike - and it is not folded into `Test-HerdrAgentAwaitingInput`. A box with text in it is not an interactive prompt, and treating it as one would read every finished worker as `blocked`. Reporting it on a wake the Hand already handles is what stops the fourth sighting also being found by chance. ## Revisit when - The harness changes how it draws the input box, at which point the caret-plus-no-break-space pair stops matching. The symptom is silent in the safe direction: the guard simply stops firing. `Get-HerdrAgentPromptBox` against a worker with something visibly in its box is the check. - The harness adds or reworks a placeholder string. The symptom is a worker that cannot be sent to, with the refusal quoting a string that is plainly the harness's own hint. - `CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION` is renamed or its resolver reordered, at which point the suggestion comes back on worker panes and only the guards are left.