Repository navigation
Charkh — the wheel: what the owner still does by hand becomes machinery. 0.5.0 shipped two eras
and, in shipping them, showed that the weakest thing left in this project was the era-close itself.
KS12.3 was recorded as performed and was one-seventh performed: master was moved; the tag, the
CHANGELOG rename, the doc move and the backfill were not, and the edge era sat shipped-but-untagged
for a week while nothing anywhere said so. The next era's runbook — hand-written in the same shape a
fortnight later — discovered that by accident. The failure is structural, not careless: in prose,
"this one is yours to do" and "nobody did this one" are the same sentence, and a document cannot
notice an act it never mentioned. This era turns the derivable half of the close into verbs that
measure, perform and refuse by name, and leaves the judgement — which number an era is, whether a
run joins the published corpus — exactly where it was. It went after the two other places this
project was quiet about being wrong: a phase gate that passed 23 checkpoints while CI's windows leg
was red on every commit of the era with nothing comparing them, and the demo GIF at the top of the
README, recorded against a Face that predated two eras, which no gate could see had gone stale.
Added
conductor release preflight— the era-close checklist gets a failure mode. Six preconditions,
one named verdict each, none of them read off a document: merge (is--ff-onlya fast-forward
of the local base and oforigin/<base>— different questions — plus a clean tree),
changelog (tools/changelog-section.shrun for real, the same scriptrelease.ymlruns as the
first job of a tag build; a section that exists but says "nothing yet" is red, because that
body is the release notes), processes (the sameUpdateSafetydetectorupdaterefuses on,
withCONDUCTOR_PIDnamed among the live engines), migration (schema skew between the tree and
the installed engine — the skew that killstask,noteandbugmid-era), courier (would
it survive the reinstall: token persisted where a logon-triggered task can see it, chats,
allowlist, scheduler state — never dialling Telegram, because onegetUpdatesconsumer owns the
token), and backfill (which finished run has no GitHub record, by full run id). It writes
nothing, merges nothing, tags nothing, installs nothing. Exit 0 all green · 1 red · 2
nothing red but something is a judgement only the owner makes.conductor release perform— perform what is mechanical, name what is judgement. Four
mechanical acts, each landing as its own commit so the close is reviewable an act at a time and
each safe to ask twice: the CHANGELOG rename (refused over a placeholder body), the--ff-only
merge (gated onpreflight's own merge verdict rather than a second opinion), the annotated tag
(refused until the section it will publish exists; an existing tag is never moved), and the doc
move with the plan'stracker,planDocandreadOrderrepointed in the same act — a move
without the repoint means the next session reads nothing. It stops at the first refusal instead of
performing the rest onto a half-done sequence. Five owner acts are printed on every run,
whatever the state, each with the command to type: the version number, single-vs-split, the
published corpus, the reinstall and the push. Dry run is the default;--yesperforms; everything
is local, and it refuses outright while a run is live in the plan's own state directory.conductor release runbook— the runbook is generated, not written. The document two eras
wrote by hand, produced instead from the same two entry points the other verbs use: the six
probespreflightruns and the act plannerperformuses in dry run. Preconditions with their
verdicts and detail, the mechanical acts with what each would do right now, the owner acts each
with its command, and the order. Rendering is pure and the timestamp is an input, so the same facts
render byte-identical output and a regenerated runbook diffs to exactly what moved. It performs
nothing and — unlikeperform— does not refuse a live run: mid-era is when the close is most
worth reading.conductor github ci— what CI said about this commit. Every active workflow the
repository has, then that workflow's latest run on this branch, compared toHEAD— not the
commit's check-runs, which list only the workflows that commit triggered, so a schedule-only or
dispatch-only workflow is invisible there and a branch reads CLEAN while a broken one sits red.
Measured on this repository:gh pr checkslisted only the Vercel checks on a PR whose gates
workflow had never run on the new head, andmergeStateStatussaid CLEAN.- The two batteries can no longer differ in silence.
github ciwrites a dated observation
carrying the sha it was about to<stateDir>/ci-status.json; theREPORT.mdheader, the owner
queue anddoctorderiveci-battery(does CI run the same commandsplan.gatesdoes) and
ci-verdictfrom it and from.github/workflows, so a green cannot outlive the commit it was
for. - The demo GIF now fails the build when the product moves past it.
docs/assets/demo.manifest.json
records what the GIF was recorded from — its bytes and sha, the tape'sCOMMANDhash, the fleet
fixture's hash, the pixel geometry — and a gate refuses the merge when the recording no longer
matches the thing it was taken of. The pattern is ported from payesh's social-card check. - A re-recorded tour.
docs/assets/demo.gifis the first thing under the README's H1 and it
predated Divan and Karvansara. The tape now visits the courier, the inbox and the run switcher,
recorded end to end by the shipped recorder against the live binary — container pull, linux
cross-compile, render — not assembled from stills. - The published CLI surface is diffed against a binary, not read and agreed with.
tools/ch3/dump-help.ps1dumps a binary's whole verb and sub-verb surface and
tools/ch3/docs-surface-diff.pydiffs it againstREADME.mdand thedocs/pages in both
directions — undocumented verbs and documented ones the binary does not have.
Fixed
- The board page is one document whatever the checkout did.
BoardSnapshotHtml's inline CSS is a
C# raw string literal, and a raw string literal inherits the line endings of its source file.
Every other line of that renderer appends an explicitLF, so on a CRLF checkout — every Windows
clone, CI included — the CSS block alone carried CRLF andRender()emitted a mixed document. - A plan in this repo loads on a fresh clone, not just on one machine.
PlanConfig.Load
validates before it returns and refuses a repo that does not exist; every plan underplans/
carried an absolute machine path, eleven of them naming a directory that had since been renamed,
so they loaded on exactly one machine on earth. - A flush that says an event is durable now means it. A narrow event-flush race that CI caught
twice on one branch and this machine never reproduced, chased with a per-instance perturbation seam
and a negative control rather than retried away. - The analyzer suppression ceiling was made, not moved. CI reported 35 suppressions against a
ceiling of 31. None of the 35 came from this era — they had been hidden behind adotnet testthat
failed first for the whole of the previous era. Four pragmas were measured out; the ceiling stays
at 31. - A backfill can no longer vandalise another run's board.
GithubBoardSynclisted a repository's
issues whole and closed every task-marked one the run being synced did not declare, so each era
transition retired its predecessor's checkpoints on the real board — measured: all 23 of the
previous era's checkpoint issues, plus every one before them, wearingconductor:retired. The
retire sweep is now scoped to the run being synced, and a backfill that would retire another run's
checkpoints is refused with what it would have closed.
Changed
- A path is rewritten only when something still reads it. The rule for moved documents, decided
once and applied: what is live — the plan in flight, the docs itsreadOrdernames, the
published surface, the index, a path a test prints in a failure message — is repointed in the same
checkpoint as the move; what is record — a closed era's plan, contracts and tracker,
docs/history/— is reported and left alone.docs/dev/README.mdcarries the derived
where-it-went table. ARCHITECTURE.mdsays what this era added and what it did not. Recounted rather than asserted:
src/Conductor.Corestill declares exactly thirteen public seams — the tenRelease/files and
the fourCi*files introduced none, because every type in both takes its facts as a parameter.docs/dev/TOKEN-BUDGET-TUNING.md§14 carries this era's own measurement and the caveat that
moves it: a session that times out records no cost rows at all, so an era total computed from
conductor budgetis a floor, not a total. Re-runconductor budgetandconductor money
when this heading is renamed — the numbers in a release section are a measurement with a date on
it, and the section is what the world reads.
Peyk — the courier stands on its own. The courier was a thread inside the engine, and it had been
dying silently for weeks: courier status answered from a pid, a process that exited took the
messenger with it, and the only thing that noticed was the owner, later, wondering why the phone had
gone quiet. This era makes the messenger a separate process with its own binary, its own install
directory, its own supervision and its own protocol, and then it goes and finds out what was
actually killing it. It also stops a run from having to know how to talk to Telegram at all: one
transport, one verb, and a room — chats plus a voice — that a plan points at by name.
Added
conductor-courier.exeis its own process (src/Conductor.Courier, D1). The listener and the
run verb's composition root moved out of the engine; the daemon stayed inCore/Courier/.
conductor courier runis now an alias that execs the new binary, andArchitectureBoundaryTests
carries the rule — Courier → Core only — with a seeded violation proving it can fail.tools/install.ps1installs the engine around a live courier, and gains-CourierOnly:
publishconductor-courier.exealone, re-register the scheduled task at it, restart it, and never
touchconductor.exe. The file-lock coupling that made every engine install stop the messenger is
gone; the courier has its own install directory.- The courier has a pulse and a death record (D2).
courier.run.jsoncarrieslastPollUtc
written every poll;conductor courier statusanswers alive / stale (last poll N min ago) /
dead (last seen T) instead of pid liveness. A presence file still on disk at startup means the
previous instance did not clear it, and the new one journals previous courier pid N died silently,
last poll T, task last-run result R — R read from the scheduler, not guessed. - A five-minute keep-alive trigger, and a run that restarts its own messenger.
RestartOnFailure
does not fire on exit 0 — and did not fire on exit 1 either (#93) — so the task XML carries a
repeating calendar trigger withIgnoreNew.ProcessExitand unhandled-exception handlers journal
before dying. A run checks the heartbeat at every session boundary and, when it is stale, starts the
task itself and says so in the log and the owner queue. - Protocol 3 (D5):
/send,/react,/deleteand/chats, message ids returned to the caller,
andmessages.jsonlas the courier's own record. Protocol 2 pushes are still accepted — that is
what the version number is for. conductor say— one verb for every outward message, with the direct fallback (D3): a send
no courier takes goes out with the run's own token, sayssent directlyin the log, and shows up on
the health line.say --dry-runprints the exact bytes and the resolved chat; ceilings are refused
by name.- A run names its own project (D4). The first session boundary
POST /hellos the courier, which
files the allowlist entry by run id — a fresh plan in an allowed repo no longer needs
courier allowtyped for it. - Rooms (D6).
rooms/<slug>.jsonin the courier home,conductor room add|show|list|import, and
a one-time import of the old~/.claude/telegram/*/config.json. A room is chats plus a voice; the
private voice files stay where they are and are pointed at, never copied. A plan with no room and
an old-shapetelegramblock behaves byte-identically. - The checkpoint card (D7).
conductor task --done --tell "<words>"hands the claim's words to
the engine, which composes a card at the verdict — not at the claim — and posts exactly one to
the room's observer chat. A red claim posts nothing and the words ride into the next session's
prompt instead of being lost. - The room's voice rides the prompt (D8).
RoomVoiceBatteryputs the people a card will be read
by into the session that writes it, under the prompt's byte cap. - An inbound note has a sender (D9).
InboxNotecarries the four identity fields,inbox list
shows sender and message id, the acknowledgement is a reaction rather than a reply, and
conductor say --reply-to <note>answers the message it came from. Notes without the fields still
list. - The figure verbs are answered by the courier (D10), from the project's newest
run.db,
read-only, whether or not a run is live:/money,/tokens,/progress,/status,
/evidencegive the same figures the CLI prints, with a test asserting no store write on that path. - ADR-0009 amends ADR-0008 for D3, D4 and D5 — it restates 0008's four conditions and adds the two
new ones.
Fixed
- Bug #100 — an
HttpClienttimeout no longer kills the courier.CourierDaemoncaught
OperationCanceledExceptiononly when its own token was cancelled, and the next clause excluded that
type: the 65-secondgetUpdatestimeout fell between the two clauses and escaped the poll loop
as an unhandled exception. The catch is widened, andPK6_1CourierPollSurvivalTestsis a property
over eight things a poll can throw plus the negative control that our own cancellation still ends
the loop silently. Controlled both ways: the old filter restored fails exactly the four
OperationCanceledExceptioncases. conductor sayreaches shell completion — the exhaustive verb test is what noticed it missing.
Changed
docs/operating.mdgains "The courier died — reading out why", written from the read-out of a
real 24-hour window on the owner's machine (PK6.1, 2026-09-17 → 2026-09-18) and correcting three
things the docs had wrong: an unhandled-exception death leaves no death record, because the
unwind runs thefinallythat clears the presence file; a death by signal reads as "died
silently" and the signal line is the truth; andcourier process exit:has never once been
written. A scheduled task'sLast Result 0x800710E0while it is Running isIgnoreNewrefusing
the keep-alive — that is the supervision working, not a failure.ARCHITECTURE.md's courier section is rewritten for a separate binary, and the seam count is
re-counted rather than asserted:src/Conductor.Coredeclares fifteen public seams, thirteen
before this era. Peyk added two, and neither is a wire we do not own, so each is justified in its own
row.- The
telegram-notifyskill is two pages aboutconductor sayand--tell, and nothing else:
send.ps1,walk-ids.ps1andlib/are deleted,watch-liveis re-pointed at the verbs, and a
grep of the skill folder finds no Bot API URL. The wire is the engine's now. - What this era cost, measured from its own ledger (
conductor money, 2026-09-18): Peyk, 12
sessions, 191.2M tokens at 98.1% cache, $164.00 across 16 checkpoints — $10.25 a checkpoint.
Charkh, in the same release, was 9 sessions, 178.9M tokens, $129.20 across 13. Both are floors:
a session that times out records no cost rows at all. Re-runconductor budgetandconductor money
when this heading is renamed.
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.5.0...v0.6.0