Two eras — gates that cannot be gamed, and the chancellery. 0.4.1 opened the door; this release
is about what happens once someone walks through it and finds they need to trust what the run tells
them, and to be reachable while it works. It carries two eras built back to back and tagged together
because they are one story. Karvansara edge went after the sentence "the gates are green", which
until now meant only this command exited 0: a holdout gate runs at the phase gate with its name
redacted everywhere a session can see, a regression gate fails when a check that used to pass has
quietly stopped existing, a mutation gate fails a suite that runs and asserts nothing — and a second
model may now review the work, but no code path lets its score flip a gate verdict. Divan — the
chancellery — gave the run a mouth and an ear. One bot that outlives the run owns the Telegram token
and files what you say into the project it is about, from an explicit allowlist; a filed note arrives
as context a session actually reads rather than as a message it missed; and what the run learned
finally leaves the machine as durable GitHub issues, a Projects v2 board, code-scanning alerts and a
single page you can open on a phone. Built the way the last four were: conductor driving itself
against this repository, unattended, every checkpoint confirmed by an independent gate battery rather
than by the agent that claimed it.
Their own score, produced by the tools this project ships (conductor money and conductor budget,
measured at the tag): karvansara-edge — 23 checkpoints, 414.3M tokens, $324.01; 18.0M and $14.09
each; Divan — 23 checkpoints, 417.0M tokens, $320.38; 18.1M and $13.93 each; both at 98.4–98.5%
cache reads. Divan's measured window took one rollover in eighteen costed sessions, and its
cooperative wrap-up rail converted six of the seven sessions it nudged. The per-checkpoint estimate
has now come in low two eras running — edge +4.0% on tokens, Divan +7.7% — which is why
docs/dev/TOKEN-BUDGET-TUNING.md §13 tells the next plan to compile against 19M / $14.50 per
checkpoint and 42M / 0.9, rather than round the last measurement down.
Added
- The courier — one bot, always awake. Until now the Telegram poll loop lived exactly as long as
the run that owned it, so a note sent to a machine with nothing running reached nobody.conductor courier installregisters a per-user scheduled task (logon trigger, restart-on-failure, no admin
rights) for a machine-level daemon that owns the bot token, polls whether or not a run is live, and
files each note into the project it is about — from an explicit allowlist, never the whole
state catalogue.status,run,restart,stop,uninstall,allow,deny,chat,
unchatcomplete the lifecycle. The poll offset is durable, so a courier killed between receiving
and acknowledging files the note exactly once on restart instead of replaying everything Telegram
still holds. Its handover port is loopback-only on a fixed named port with a per-install shared
secret, and it accepts notes — never run state. A run that speaks a newer protocol refuses a stale
courier by name, namingconductor courier restart. The honest limit is documented rather than
hidden: Telegram discards an undelivered update after 24 hours, so the courier turns "no run is
live" into "the machine is on" and cannot do better from one machine. See ADR-0008. - The inbox — what the owner said, as context a session reads. A filed note lands in that
project's.conductor/inbox/and reaches the next session as the last block of its prompt:
the engine's own knowledge first, the human's words last and framed. Only what actually fit is
marked seen — the remainder is counted in one line and reaches the session after that, so a
long-lived project's battery cannot grow without bound and nothing is skipped.conductor inbox
reads it (list,show,add,transcribe,parked,prune);pruneis the only thing in
conductor that deletes a note, and it refuses to run without being told which. Nothing in the
inbox moves a run — promotion into a followup or a task is a deliberate act, one button or one
verb. Transcripts are never committed:.conductor/.gitignoreis deny-by-default and the inbox
gets no allowlist entry, which on a public repo is the whole difference. - Voice notes become words, locally.
courier.transcribe.command(or
CONDUCTOR_TRANSCRIBE_COMMANDfor a machine with no plan in front of it) shells out to a command
you choose —tools/transcribe/whisper-json.pyis a local faster-whisper wrapper this repo ships,
so audio never leaves the machine. Per-segment confidence belowconfidenceFloor(default 0.45)
is marked in the stored note. With no command configured the note still files and the audio is
still kept; the reply says it was not transcribed rather than dropping it silently. - A dead-letter box. A note for a project that has moved, been deleted or was never allowed is
parked under the state home and the sender is told by name.conductor inbox parkedlists them.
Nothing is discarded on a routing miss. /cloud— a session on Anthropic's infrastructure, owner-only. Following up on an existing
cloud session is headless, so conductor drives it and brings the answer back to the chat; creating
one is interactive-only on today's CLI and is refused with the platform's own words plus the exact
command to type, rather than by faking a TTY. Creating preflights git first — a cloud agent clones
from the remote — with six verdicts (nothing to clone, detached head, dirty tree with the
count and the files, no upstream, branch missing on the remote, remote tip differing from local
HEAD), each quoting the state that produced it, in the chat. Cost is always reported as a word
and never as a number, because there is no per-turn telemetry out there.- An opt-in cloud review lane. The
cloudplan block runs a per-session review out there
alongside the local analysis lanes. Off by default, with deliberately no environment override
to switch it on by accident, bounded bytimeoutMinutes(default 30, 1–240). The referee never
moves: every gate still runs on your machine and nothing the cloud says confirms a checkpoint. - Tracked bugs and followups become durable GitHub issues. They get their own labels and markers,
are created only while open, and are closed by the ledger with a comment rather than by the run
ending. The daily digest gains the ledger line. - Projects v2 columns.
conductor github sync --project <n>drives a Projects v2 board's columns
from the same fold that writes the issues. It needs a token carrying theprojectscope and says
so by name when it does not have one —gh auth refresh -s projectis the one-command fix. conductor github sarif— bugs as code-scanning alerts. Every open tracked bug that names a
file and a line becomes one SARIF run uploaded to GitHub code scanning, so a defect the run already
knows about appears where a reviewer is looking. Free on a public repository; a private one needs
GitHub Advanced Security, and without it the upload is refused by name, quoting the repository
it read, rather than failing blind.--outwrites the SARIF and uploads nothing.- The board as one page for a phone.
board.htmlis a self-contained snapshot rendered from the
control plane's own contracts at each boundary and pushed as a Telegram document. It states its own
staleness at the top. Nothing inbound. - Chat profiles —
adminandobserver.telegram.chatsgives each chat a profile, so a
stakeholder can be put in a chat the bot serves without also being handed/injectand the
control verbs. A plan carrying onlyallowedChatIdsbehaves exactly as before; an unknown profile
string fails plan load by name rather than defaulting to admin. The observer surface is a closed
list enforced at one gate, and every verb is checked against both profiles by an exhaustive matrix
test rather than a sample. - Onboarding. Every configured chat is told what this run is (plan, stage map, budget ceiling),
what will arrive and when, and exactly what it may ask — before the run's first word, again after
a plan reload adds a chat mid-run, and on/start, which until now answered one static sentence.
The message is composed per profile, and the "what you can ask" list is derived from the same
catalogue the gate enforces, so the promise cannot drift from the permission. - Gates that cannot be gamed — three new gate classes. A gate that exits 0 has said one thing:
this command succeeded. Three failure modes hid inside that, and each is now aclassor a
visibilityyou declare on the gate in the plan, not a new kind of code.visibility: "holdout"— the gate runs at the phase gate only, and its name is redacted
everywhere a session can see: the progress line, the fix brief, the failure tail. A session
cannot tune to a bar whose name it never learns.class: "regression"— reads what still passes rather than what failed. A check that passed
earlier in the run and no longer does fails the gate even though the command exited 0, so
deleting a test to get green is a gate failure rather than a smaller test count.class: "mutation"— reads a mutation report the gate produced and fails on a score shortfall:
the suite that runs and asserts nothing. An unreadable report is reported as unreadable, never
as a pass.
All three say their verdict in the class's own words. "A gate failed" is wrong twice over for a
classed failure — the gate exited 0, and what is broken is the checks rather than the code under
them — and a fix session told the wrong thing goes looking for an assertion that does not exist.
- A second model may review the work; it may not score it. An optional review command runs after
a session and its verdict joins the evidence set as an advisory row, beside the gates and the
claims, reaching the fix prompt and the record. No code path lets a judge's score flip a gate
verdict, and a test asserts that rather than a comment promising it. conductor mcp-observe— a read-only MCP surface. Serves this machine's run catalogue to any
MCP client as resources (conductor://history,conductor://runs/{run}/status,
conductor://runs/{run}/money) and no tools at all:tools/listis empty andtools/callis
refused. Control operations are excluded by design rather than by a flag, and the store is opened
Mode=ReadOnly, so SQLite refuses a write before any policy check would. The reasoning, including
MCP's 2026 attack record, is in ADR-0007.conductor history export <run> --atif. A finished run leaves as an ATIF-v1.7 trajectory — the
Harbor / Terminal-Bench interchange format — with-o <FILE>for one and--all -o <DIR>for the
whole catalogue. Each session is one agent step; the gate battery, the checkpoints it confirmed
and the commits it landed are that step's observation. Billed dollars only: conductor has no price
table, so ATIF's own cost derivation is not applied.conductor worktree. What attempt worktrees are on disk, which run made each, and which are
orphans from a run that died.--reapremoves the orphans and never touches a live run's or one
you made yourself. The engine runs the same sweep at startup.conductor otel. A run's spans in OpenTelemetry's own vocabulary, mirroring thegen_ai.*
names, rendered from the event log — so a run can be read in a collector rather than only in the
Face.conductor initwritesAGENTS.md. Plus aCLAUDE.mdthat imports it, clobbering neither if
either already exists — one file of guidance, honoured by every agent that reads either name.
Changed
- The bot token moves to the courier, where one is installed. Telegram allows exactly one
getUpdatesconsumer per token, so a run polling alongside a courier would fight it for updates.
Where a courier is configured, in-run polling refuses to start and names the courier; the run
pushes through it instead. A machine with no courier behaves byte-identically to before, pinned
by golden replay rather than asserted. tools/install.ps1stops the courier and puts it back. A running courier holds the published
exe open, so the publish would fail on a file lock — and a courier left down is a bot that stops
answering, while a courier not restarted keeps running the old engine indefinitely, precisely
because it is built to outlive everything else. The installer now owns both halves and warns
loudly if the restart did not take.- The prompt batteries are bounded, and say so.
batteries.ledgerMaxEntriesand
batteries.maxBytescap what the knowledge ledger contributes to a prompt. An unbounded ledger was
measured starving the open-bugs battery out of the prompt entirely, which is a run whose sessions
cannot see the defects they are meant to avoid. - The messenger seam. Message composition, chat profiles and the command surface are now defined
without knowing which messenger will carry them (Conductor.Core.Integrations.Messaging:
IMessageChannel,MessageComposer,CommandRouter,RemoteSurface);TelegramServiceis the
transport adapter behind that seam. Nothing a chat receives changed — fifteen goldens generated by
the previous engine pass byte-identical through the new one. Internally,ITelegramServiceis now
IRunNotifierandRunContext.TelegramisRunContext.Messenger, so the run loop no longer names
a messenger it does not depend on. - Every push now reads headline / proof / telemetry. What landed, then what proves it (the gate
verdict and the evidence artifact, together on one line), then the numbers — progress, money
against the cap and tokens — in monospace. A session-end push reads standalone: previously it was
a status line plus clipped result text, with the artifact buried under the gaps and the cost below
the prose where a phone cuts it off. Ownernotify/templates written against the old fact names
(progress,gates,cost) still render; the new facts areproofandtelemetry. - A confirmation keyboard is only sent to admin chats. An observer still gets the news that the
run is asking for a decision — it is the text half of the same push — but is not offered a button
it would be refused for pressing. - Telegram readiness counts every configured chat.
doctor,/telegram/statusand the reload
message countedallowedChatIdsalone, so a plan configuring its chats the new way reported
"push-only to nobody" while delivering perfectly. - Hooks are the record of what a session did, not the transcript. Tool events arrive by hook and
the transcript is the fallback, so the digest counts the call the agent made rather than the one
it printed. A hook-less agent still works, on the fallback path. - Per-turn usage carries the cache split. The cache-read half used to vanish from the per-turn
view even though it is ~98% of what a session costs; it is now parsed from the stream and
reconciles with the context curve. - A fix or audit session forks the session it is fixing where the agent CLI supports it, instead
of resuming cold. Measured, because "forking is cheaper" deserved a number rather than a belief:
the carried context lands as a cache read instead of a fresh input, at 0.15% more tokens and
fractionally less money than a resume — so the saving is not the fork, it is not re-discovering
the context at all. - Gate output no longer floods the prompt. A failure's full text goes to an evidence file and
the prompt carries a bounded tail plus the path. Two new prompt batteries — a repo map, and a
definition-of-done recap — cost less than what the spill saves. - The analyzer ruleset is curated, and the debt only ratchets down. Roughly twenty-five
design-shaped rules are errors, everything else is explicitly off, and each adoption carries a
one-line reason. A separate bar counts analyzer debt in every spelling it takes — pragmas,
SuppressMessage,NoWarn, severity downgrades — against the minimum this branch's own history
has achieved, so no single commit can move its own bar. Complexity budgets (CA1502/1505/1506) are
enforced per project on the same terms. - The verdict left the loop. The evidence-to-verdict taxonomy is now a total, deterministic
function: same evidence in, same decision out, on any machine, with no run in progress. Every
branch of it used to be reachable only by standing up a run context, a store, a git repository and
an agent process, which is why it was the least-tested part of the engine. docs/cli.mdnow names every long option a shipped verb declares, enforced by a test that
reflects the options off the commands rather than trusting the page. Forty-one were missing when
it was first run, includingtask --evidenceandtask --blocked-until.
Fixed
- A prompt battery could be truncated to nothing.
BatteryGroup.Renderclipped the
concatenation rather than each block, so whichever battery happened to sort last vanished
silently; and the open-bugs battery dropped every bug past the twelfth with no line saying it had.
Both are the same failure — a session that cannot see what it was given, and no signal that
anything was withheld. - Budget counters restarted at zero on every engine process start, so a resumed run measured
itself against a ceiling it had already spent. - A stage-boundary squash could silently rewind the branch.
- A rate-limit storm could burn a stage's whole attempt budget in minutes, turning a transient
429 into an exhausted stage. - Two engines on one bot token looped on 409 conflicts. Closed by the courier owning the token —
one consumer per token, by construction. - A composed prompt over roughly 8,191 characters silently stopped a
cmd.exeagent, and nothing
warned when a plan's packs pushed it over the argv ceiling;doctor's argv lint under-measured the
real spawn because it did not count the batteries. - Telegram diagnostics on an empty chat list. The startup line counted the wrong collection, and
POST /telegram/testindexed the first configured chat without checking there was one. report push failed:logged with an empty reason, so a repeated failure said nothing about
itself.- A brand-new
run.dblogged a foreign-key constraint failure on first write. - An unattended run under a restricted permission posture could silently lose its own claim path.
Probed against the shipped agent CLI rather than assumed: an allowlist profile cannot replace
--dangerously-skip-permissionsfor this workload, and the finding is filed with the exact refusal
rather than worked around with a guessed flag. Refusals are now telemetered, so the next attempt
starts from evidence. - The attempt diff no longer counts the engine's own commits — tracker regeneration and report
writes — as the session's work. - Orphaned attempt worktrees are swept at startup, so a run that died does not leave a branch
that the nextgitoperation trips over.
Install
Download the archive for your platform, unpack it, and put the two binaries somewhere on
your PATH. They belong next to each other — the engine finds the Face by looking
beside itself.
Then, to see it work without credentials or spend:
conductor demo
Confirm what you unpacked with conductor version — it answers with this release's tag,
the commit it was built from, and the build date.
No .NET or Go install is needed: the engine is self-contained and the Face is a static
Go binary.
macOS: these binaries are not signed or notarised. On first run:
xattr -d com.apple.quarantine conductor conductor-face
Building from source instead: tools/install.sh (macOS/Linux) or
tools\install.ps1 (Windows).
Full Changelog: v0.4.1...v0.5.0