CHANGELOG / WHAT SHIPPED

What each release changed.

Every release of hm writes a note, and this page publishes those notes as they were written. Nothing here is a summary of one. Where a note is technical it stays technical — it was written for whoever is about to upgrade, and shortening it for a visitor would mean deciding on their behalf which half mattered.

Two parts of each note are left out on purpose. Install and Release mechanics (after land) are addressed to whoever installs or cuts the release rather than to a reader, and how to get hm is answered on the download page, against the release profile rather than against one note. Entries carry no date because the notes carry none; the order is the order they shipped in.

v0.1.38the site looks like the app, and a finished round says how it ends

This is the first published release since v0.1.33. The four cuts between them were local nightlies with no GitHub assets, so upgrading from v0.1.33 brings their work too: the Superintendent agent runtime and its panel, the skill corpus with retrieval and evolution, and an MCP policy that survives the database.

CLI and desktop stay one version. The tarballs and the DMG on this release are 0.1.38. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH. The published macOS app talks to production; staging is an opt-in bake (make desktop-dmg-staging) and it is not the GitHub Release asset.

Highlights

The desktop is a download

  • hm_0.1.38_aarch64.dmg is published beside the tarballs: an unsigned macOS application, Apple silicon, macOS 11 or later. The release job builds it on the same runner that builds the CLI, SHA256SUMS covers it, and the job refuses to publish a DMG whose version disagrees with the tag.
  • The app carries its own hm. An application launched from Finder inherits launchd's PATH and never sees ~/.local/bin, so the binary ships inside the bundle and the app prefers it over any CLI on PATH. Nothing to install first, and an hm you already have is left alone.
  • It is unsigned, and opening it takes one detour: System Settings → Privacy & Security → Open Anyway, once per copy. Not Control-click → Open, which macOS 15 removed. The macOS section of the install runbook is the walkthrough; signing and notarization are not scheduled.

hmcortex.com is the application at web scale

The site and the app were two designs. The site now spells its colours, its typefaces and its chrome in the application's own vocabulary: one light scheme with no second one declared anywhere, Inter and JetBrains Mono self-hosted from the same four subsetted files the desktop ships, the icon tile beside the name as the brand mark, and red returned to meaning refusal and nothing else — a numbered index, an in-sentence link and a pending slot were all wearing it.

The front door is the application's sign-in shape, and the workbench beside it is the shipped shell rather than an illustration of one: a rail of Works, a board of Ready, In flight, Dispatched and Landed, cards written in hm's own id grammar, a still connection dot, a docked launcher. Eight keyframes became one fade.

The seven secondary pages — download, pricing, docs, changelog, the three trust pages and contact — were restyled onto the same tokens, and the alias block that had been holding them legible in the meantime is retired. A new test reads the desktop's stylesheet off disk and holds all twenty-one names the two share to the same value, so the two surfaces cannot drift apart quietly again.

No third-party origin is contacted for a font or anything else, and the privacy page says that rather than naming two addresses it no longer uses.

A finished skill round is visible, and says how it ends

A skill round succeeds by closing every candidate — promote and decline both end one with a cancel — which left the finished round with a roster of nothing but cancelled tickets, and no card. The Principal's own completed work went invisible at the moment it was done.

A non-empty roster now always earns a card; only a ticketless manifest has none. The round also says how it closes: the brief names the closing act, and hm skill promote and hm skill decline echo it when a verdict closes the last open candidate. Both are prints. Nothing lands on their account, and nothing waits on a round.

Needs land asks the land gate rather than guessing. A card carries the land policy, the readiness verdict and the reason it is not ready, and the cycle reads the same call — the third opinion about what is landable stops existing. Under a per-ticket policy the column is the union of the gate's verdict and every member being terminal, so a unit the gate refuses keeps its card, because it still needs landing.

A card names where its unit came from, and doctor names a landable one

A manifest card carries the source it was opened from as a kind and a slug, so a storefront can label a unit's origin without parsing its id. It is an origin and not a placement: a round and an ordinary unit in the same state land in the same column, and a test holds that.

hm works doctor now reports every Active unit the Works' own land policy would accept, and hm patrol run carries the same line as advice. It is the exact complement of the existing warning about a complete ticket that never landed, and it asks the land gate rather than deriving a fourth opinion. hm manifest status prints that verdict for a single unit someone already thought to name. All of it is advisory: it never moves the doctor's verdict, it raises nothing stuck, and a --fix sweep will not land on its own — held by a test, because landing a unit is a fact a Principal has to be in the loop for.

What ships

Two CLI tarballs — hm-0.1.38-aarch64-apple-darwin.tar.gz and hm-0.1.38-x86_64-unknown-linux-gnu.tar.gz — the macOS desktop DMG hm_0.1.38_aarch64.dmg (production account plane), and a SHA256SUMS covering all three.

v0.1.37-nightlythe policy a preview can be read from

This cut is a local nightly on the production account plane. It is not a GitHub Release. There is no tag on origin and no downloadable tarball.

The macOS app talks to production: auth.hmcortex.com, api.hmcortex.com and account.hmcortex.com. Staging is an opt-in bake (make desktop-dmg-staging) and it is not this image.

CLI and desktop stay one version. The DMG on this cut is hm_0.1.37-nightly_aarch64.dmg. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH.

Highlights

A policy set before its column existed is replayed into it

The last cut gave tickets and manifests an mcp_policy column, but a migration only reaches writes made after it lands. A policy set before it lives on in its ticket.mcp_policy_set event and nowhere else, so the row reads back empty forever: the seat-wired connector resolves to optional, and a locked credential store refuses the run the operator had explicitly excluded it from. Two live tickets in this Works were in exactly that state.

Migration 0008 replays the latest such event into any column that has nothing for it. A migration rather than a script, so every Works self-heals the next time it is opened. Latest-wins, so a policy cleared after the column landed stays cleared; rows written since are untouched; a payload that does not parse is skipped rather than stored.

A dry run names the policy it resolved, not just the servers

hm run dispatch --dry-run printed the resolved requirement and left the declaration out. A ticket that declared nothing still printed mcp required: hm — the seat's mandatory bridge, injected structurally — which is also what an hm-only policy prints, so the two were indistinguishable. The optional set that a locked store actually gates on never appeared at all.

The preview now leads with what the ticket declared, and says none declared (seat wiring only) when it declared nothing, then prints the optional servers beside the required and disabled ones. That is the line that separates a connector being excluded from a connector simply not being required.

What ships

The local macOS desktop DMG hm_0.1.37-nightly_aarch64.dmg (production account plane), built from 960a217. No CLI tarballs, and no GitHub assets.

Authoring an MCP policy at manifest scope is still open: there is no set_manifest_mcp_policy and no CLI flag for it, so a manifest column can only be filled by the backfill this cut adds, never by a hand.

v0.1.36-nightlythe MCP policy survives the database

This cut is a local nightly on the production account plane. It is not a GitHub Release. There is no tag on origin and no downloadable tarball.

The macOS app talks to production: auth.hmcortex.com, api.hmcortex.com and account.hmcortex.com. Staging is an opt-in bake (make desktop-dmg-staging) and it is not this image.

CLI and desktop stay one version. The DMG on this cut is hm_0.1.36-nightly_aarch64.dmg. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH.

Highlights

A ticket's MCP policy is read back the way it was written

The ticket-scoped MCP contract shipped in the last cut, but only half of it reached SQLite: the insert named eleven columns and the policy was not one of them, and the read path handed back a default. A ticket created with --mcp-disable linear therefore arrived at dispatch carrying nothing, the connector fell back to optional, and a locked credential store refused the very run the operator had excluded it from. Dual mode lost it in the same place — it wrote a correct file and then read the lossy row.

Migration 0007 gives tickets and manifests a mcp_policy column, JSON-encoded and defaulting to empty, and the save and read paths carry it the way depends_on and touchpoints already do. A blank column reads as "no policy" rather than making the row unreadable, so an existing Works upgrades without a backfill.

A dry run predicts the refusal it was going to hit

hm run dispatch --dry-run ran every preflight except the scoped secrets gate, which is the one a locked store actually trips. A preview could be clean and the live run refuse a second later, with nothing in between to explain it. The gate now runs on the same resolved policy the live path uses, against the same store, so the preview and the run agree — including the case where a disabled server is the reason there is nothing to unlock.

What ships

The local macOS desktop DMG hm_0.1.36-nightly_aarch64.dmg (production account plane), built from b958b88. No CLI tarballs, and no GitHub assets.

Authoring an MCP policy at manifest scope is still open: there is no set_manifest_mcp_policy and no CLI flag for it, and the persistence this cut adds is what that work will land on.

v0.1.35-nightlyMCP reach, on a local production desktop

This cut is a local nightly on the production account plane. It is not a GitHub Release. There is no tag on origin and no downloadable tarball.

The macOS app talks to production: auth.hmcortex.com, api.hmcortex.com and account.hmcortex.com. Staging is an opt-in bake (make desktop-dmg-staging) and it is not this image.

CLI and desktop stay one version. The DMG on this cut is hm_0.1.35-nightly_aarch64.dmg. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH.

Highlights

A seat reaches the tools its ticket allows, and no others

MCP is now something a Works configures rather than something a hand inherits. hm mcp registers, inspects and removes a seat's servers; a ticket carries an MCP policy, and dispatch resolves the effective connector set before it spawns anything — the secrets preflight is scoped to the servers that are actually enabled, and a disabled server is filtered out of the executor's config rather than left for the agent to decline. Onboarding belts a new MCP-capable seat at wire time, and hm works doctor reports the seats it has not belted.

The account register: paths, never values

An account is declared once in hm.toml — label, provider, owner, the paths its credentials live at, and what it reaches. SeatExecutor.auth follows the same rule: it names where a credential is, not what it is. Nothing in the ledger holds a secret.

A Line can be retired

hm org line remove drops a Line and every seat on it, fail-closed: every blocker is reported in one refusal, each naming the command that clears it. There is no force flag, and no bench tree is ever deleted.

A named refusal for remote MCP

OAuth and remote-HTTP MCP servers are not supported in this cut, and the refusal now says so in those words rather than failing somewhere further in. The design for the home they will get is written down beside it.

What ships

The local macOS desktop DMG hm_0.1.35-nightly_aarch64.dmg (production account plane), built from 8a0e8d0. No CLI tarballs, and no GitHub assets.

v0.1.34-nightlylocal production desktop, no published assets

This cut is a local nightly on the production account plane. It is not a GitHub Release. There is no tag on origin and no downloadable tarball.

The macOS app talks to production: auth.hmcortex.com, api.hmcortex.com and account.hmcortex.com. Staging is an opt-in bake (make desktop-dmg-staging) and it is not this image.

CLI and desktop stay one version. The DMG on this cut is hm_0.1.34-nightly_aarch64.dmg. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH.

Highlights

A production-plane desktop built on the machine that runs it

GitHub Actions is not cutting this release. The image is built locally with make desktop-dmg and installed on this Mac. When a numbered release can publish again, it will. Rebuilt from 1810ac3 so hm secrets unlock / status / lock and check --dispatch ship; the version stays 0.1.34-nightly.

What ships

The local macOS desktop DMG hm_0.1.34-nightly_aarch64.dmg (production account plane). No CLI tarballs, and no GitHub assets.

v0.1.33Create or join a Works is a Super-shaped conversation

Highlights

The setup pane talks the way the Superintendent does

Create or join a Works used to be a question machine wearing a chat costume: fixed order, closed answer sets as buttons, and hm's own refusal sentences as the only correction loop. The Superintendent panel already did free prose, streaming turns, markdown, interrupt, and an approval card. A Principal who lives in that panel met the setup pane and found a form that answered back.

The pane now hosts the same agent-session engine, one step before there is a Works for any of it to be about. An agent probes the prospective path, proposes one hm works init or hm works onboard, and waits. The Principal approves. The first event in the new company's ledger is works.initialized by principal — nobody on a seat, because there is no seat yet.

CLI and desktop stay one version. The tarballs and the DMG on this release are 0.1.33. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH. The published macOS app talks to production; staging is an opt-in bake (make desktop-dmg-staging) and it is not the GitHub Release asset.

The toolbelt is a seat-less bridge, not the whole catalog

The conversation's tools arrive through hm mcp serve --pre-works. --seat is refused, an ambient HM_SEAT is stripped from every child, and the class gate is on by construction. Against a directory that is not a Works yet, the catalog is six rows: init, onboard, doctor, workflow show, and the two probes that answer create-or-join before an argv is proposed. The other hundred-odd rows would be a wall of not a Works errors, and offering them would be the shell looking more capable than it is.

Widening that list is editing a reviewed table, not flipping a flag. Nothing in the environment admits a row.

The session itself is a twin of the Superintendent's, not a fork: the same stream lifecycle, turn events, interrupt, and class-gate handshake, with three substitutions. The working directory is the prospective path. The runtime comes from the device plane, never org/executors.toml. The record is written on that same plane, never under the directory whose seal may never come.

Only Claude and Codex may host it. Three of the five agent hosts register hm mcp serve by writing a config file into the workspace they run in, and that workspace is a directory that has to be left byte-for-byte as it was found if the Principal walks away. A pick this build cannot host is refused by name.

The seal is a card the Principal actually reads

A held works init or works onboard used to be a hold like any other: an invocation string and an Approve button. That is the right card for hm ticket ready. It is the wrong card for the one write that makes a company, because the fact that matters most — this path already holds a Works, so this joins one rather than making one — is not visible in the argv at all.

The plan machine now runs backwards. A held argv is replayed through the same setup plan the question machine already knew, and the hold carries create versus join, which of the two seals, the Line, the North Star, and the sentences that must have been on the card. The argv still runs verbatim. The surface renders what the gate derived rather than deriving a second answer of its own.

A refused argv is a sentence, not a card. --interactive, --north-star-draft, an unknown Line, a repository that is not a root, a relative path, a --north-star at a Works that keeps its own — each comes back as an error carrying hm's own words, no hold is written, and nothing runs. The agent sees the refusal and the next turn is the correction.

The only write door on the live pane is that hold-approve. The webview reaches no hm subprocess; the bridge runs the argv after the Principal says yes.

The question machine is still there, as the floor

A machine that cannot host the conversation still has to set a Works up. The question machine is kept whole and moved rather than rewritten: same five questions, same plan card, same subprocess seal. It is the second of three rungs. A live agent that never comes up, or that dies before it announces itself, degrades onto that walk with an honest line and the Principal's own first sentence counted once. A host that did come up and then failed keeps its named cause rather than being torn down over a guessed one.

The Settings → General form is the last rung, named rather than opened. It still asks for all four values at once, including a Line a brand-new Works can only have one of.

A conversation may resume until the directory is a Works

Walking away mid-setup used to lose the thread. Opening the same directory again, on the same runtime, inside ninety minutes, resumes onto the same record. The pane replays what was said and names why: remembered, or new, because the path is a Works now, because it went cold, or because the runtime changed.

The key dies the instant the directory holds an hm.toml. The seal is what puts that file there, so the transcript is cleared at seal without a flag threaded back from the screen. A Claude session id is not a Codex thread, and one host is never handed the other's resume.

Composer chips fill a sentence and hand the caret back. They do not send, and they spell no flag. A click is not an answer hm is handed.

What ships

Two CLI tarballs — hm-0.1.33-aarch64-apple-darwin.tar.gz and hm-0.1.33-x86_64-unknown-linux-gnu.tar.gz — the macOS desktop DMG hm_0.1.33_aarch64.dmg (production account plane), and a SHA256SUMS covering all three.

v0.1.32the desktop talks to production, and a ticket carries its own note and its own changes

Highlights

This is the production desktop cutoff

The published macOS app talks to production. auth.hmcortex.com, api.hmcortex.com and account.hmcortex.com are the plane a bundle reaches when nobody named one at build time — including every bundle the release job makes, which passes no profile at all. Staging is an opt-in bake (make desktop-dmg-staging) and it is not the GitHub Release asset.

CLI and desktop stay one version. The tarballs and the DMG on this release are 0.1.32. Installing the app neither requires nor disturbs a CLI install; the app still prefers the hm it carries over anything on PATH.

A ticket and a manifest can hold a note

hm already drafted a ticket description and dropped it. Intent lived in acceptance, and three manifests off one brief drew three identical board cards because the card quoted the brief, which is not a unit.

Notes are documents now. ticket_note and manifest_note are kinds in content.db. Two templates ship compiled-in and are overridden through hm.toml, not an agent plugin. hm ticket note and hm manifest note write, show and fill over one surface: the body arrives on stdin or --file, never on argv. A write whose sections match the resolved template records that template; anything else is free-hand. fill writes the guidance-stripped skeleton and refuses to replace an authored note without --force.

A ticket that is still being drafted keeps its ## Plan as the ticket note instead of throwing the body away. The unit Detail description reads the manifest note first — sentence, then the brief's first prose, then an empty slot — so six manifests that share a brief no longer describe themselves with one shared sentence. The board card does not read the note. A filled-but-unwritten template yields no prose rather than speaking in the Principal's voice.

Resources and activity stay derived. There is no comment stream beside mail.

A ticket's Changes is that ticket's commit range

The delta under a ticket used to be the whole manifest branch wearing a ticket's name. Under per-ticket land that was wrong twice: ticket N showed t1 through tN, and once a ticket landed the per-ticket reset left it with nothing to show at all.

The range is now the ticket's own ledger anchors. The tip is the land merge's two parents (land merges --no-ff, so the merge is a git identity) or the integrated verified sha, else the worktree HEAD. The base is the first rung that is still an ancestor of that tip — the ticket's own earliest dispatch base, the manifest's cut, a sibling's landed tip. Uncommitted work is a separately labelled group, never folded into the range. When no anchor survives, the read says so rather than showing a plausible wrong range.

The commits never went anywhere when a worktree was reclaimed. Gatehouse land merges in the bench, not in the worktree, so a landed ticket's Changes is read from the bench after the worktree is gone. An unlanded ticket is not offered the landing branch as a tip to borrow — that fabrication would have shown every sibling manifest's files under this ticket's name. hm ticket changes is the CLI of the same read the desktop draws.

Notes and Activity live on the Ticket screen

Four panes that were ticket-grained — Mentions, the Hand run, Mail, the Hand Session — had been mounted on the unit because they landed before the Ticket screen existed. They move whole: same ids, same reads, same four channels, and every one of them is about the ticket the screen is open on, never a sibling the board card was drilled into.

Notes is the screen's first prose, above the spine: the whole markdown body, read-only. Writing is hm ticket note write, gated in hm-core on who may write and on the ticket still being open. Absence is one answer for three states — no note, no content.db, and a template nobody wrote into.

The empty "no hand has run" slot retires into the run card, which already said the same fact under the same state. Changes keeps its commit-range read and is renamed Resources on the head; the element, the command and the hm-core call keep their names. No second resolver, no artifacts field, no third prose channel.

Query can see a note, and a manifest's activity includes its tickets

The free-text q searches a ticket's note body. Title, acceptance and completion note were the fold justified by hm having no description; the note is the description column arriving, and a search that could not see it would have stopped seeing the field the storefront made primary.

hm query events --manifest rolls a manifest's activity up from its tickets. The events read used to filter exactly one entity, so a manifest's stream was only what was written against the manifest id. The manifest and every ticket bound to it now bind into one filter over the same sequence order. Closed tickets stay in the set. --entity and --manifest are two spellings of one axis; both at once is a parse error, not an empty stream that reads like a quiet manifest.

What ships

Two CLI tarballs — hm-0.1.32-aarch64-apple-darwin.tar.gz and hm-0.1.32-x86_64-unknown-linux-gnu.tar.gz — the macOS desktop DMG hm_0.1.32_aarch64.dmg (production account plane), and a SHA256SUMS covering all three.

v0.1.31a landed ticket moves its own manifest on, and the gatehouse gets a column to wait in

Highlights

The manifest moves itself on after a ticket lands

The wait that cost 123 of 241 hours of handoff sat strictly after the merge: the gates were clear, and nothing dispatched the next ticket until a human came back to type it. Detection was never the gap. hm.app is running, its ledger tail polls every second, and the merge was already visible to it — nobody in the process had the job of acting on it.

run::continue_next now answers one tick: which manifests a delta named, whether their gates are clear, and whether this ticket already had its dispatch. hm run continue --since is the follower's command, and the desktop tail runs it over the subprocess seam, because a latch and a dispatch are writes and the shell holds no write handle. The cursor rule moved into hm-core (follow_start / follow_events) now that this tail is no longer the ledger's only follower, and every act records run.continued with the triggering event's kind and sequence.

It is off by default for one release, behind a per-Works hm.toml switch. It never lands and never pushes, and it notifies no Superintendent: a finished manifest is held at ready-to-land, where the board's new Needs land column is already showing it to the Principal.

Three gates were added to the decider on the way, two of them found by the first live run. A ticket still in grooming, and a seat already holding a latch on the manifest, are named waits rather than latches the write path refuses on contact, and the read-only dispatch preview now runs inside the tick. Both of the follower's own event kinds are excluded from triggering it, and the idempotence predicate keys on any dispatch attempt rather than on run.dispatched alone — that event is written before the spawn, so a dispatched-only test triggered by a spawn failure would look past its own attempt and retry once per second.

A cycle says why it is standing still

next_cycle_step could say what the next mechanical move was but never why a cycle stood still: a ticket it called Dispatch might be one a Hand was already running, or one the dispatch gates would refuse on contact. Those are different answers and they wanted different words.

CycleStep::Await now means only the first — work in flight. The second is CycleStep::WaitingOn, whose gate carries the remedy in every variant, crossed with a read-only gate state: land policy, unmet depends_on, the Hand's run state, preflight refusals. The decider is pure, so the transition table is a table test, and collection sits below a marked I/O line and only reads.

Gate order mirrors plan_dispatch with the Hand check ahead of it, because a ticket in flight is not a ticket to re-gate. Live dispatch behaviour is unchanged down to the message: the executing cycle raises the identical error from the same gate at the I/O boundary. What changed is hm run cycle --dry-run, which now reports the gate it hits and the one the plan walks into further down, where it used to abort on a blocked ticket or print "ready to land" over a stopped plan. The dispatch preview also predicts the coupling the real dispatch would inherit, so the rehearsal and the run agree about one more thing.

The default Hand comes from the manifest's own Line

Auto-latch asked the roster for "a Hand" and got the first one, which on this Works is the product-engineering Hand for every Line — so a manifest bound to operations would have been latched to another Line's Hand the moment the follower started latching on its own.

Resolution is Line-first now. A manifest names its Line by explicit relation: a recorded bench binding, then a ticket already latched to a seat. The Line then names its own Hand — the seeded one for that Line, else a Hand seated on it — and resolution never leaves the Line it resolved. A Hand-less Line yields the seat the Works has to add, so the latch fails naming it rather than quietly handing the work to a neighbour. The cycle's default seat became an option, because "unset" was the state it could not hold: the old hard-coded default was indistinguishable from an operator's --seat, so no caller could resolve it.

The gatehouse gets a column, and the board scrolls to reach it

A unit whose whole roster was finished but whose phase was still active sat in Ready, whose stage line reads "Work · awaiting a seat". It is awaiting the gatehouse, not a seat — and on the dogfood Works 33 of the 43 Ready cards were that shape. Under per-ticket land it is also refusing the next dispatch on its own manifest, so Ready was filing a blocker as work nobody had started.

The fold gained a fifth group and the grid a fifth column between Dispatched and Landed: head "Needs land", stage line "Gatehouse · awaiting land", empty "Nothing waiting to land.", in the amber a held Hand already wears. No new token — the column is a fifth state of the same lifecycle. Precedence is Dispatched, In flight, Ready, Needs land, Landed, at manifest grain only: the ticket partition keeps its four keys, so the cadence ladder, the Overview insights and the Detail chip are untouched, and the ladder gets no Gatehouse rung because every rung on it counts tickets and Needs land is a fact about a unit.

Five columns do not fit the default window at the width a card was fitted to, so the panel buys the track with sideways scroll rather than with card width. The floor is the width a column had when four split the default window, so the four that were already right keep their size and the fifth is reached sideways; past about 1300 pixels the flexible half takes over and the scrollbar disappears. Taking fifty pixels off every card at every width was the alternative, and was not taken.

The two key sets are now one type — the renderer's group IS the fold's column key rather than a union written out a second time — and the board walks the column registry instead of naming one render call per column, so a column registered with the grid is painted the moment it is registered.

Every column paints its ticket mix

The card's bead row was only painted in Dispatched. Ready and In flight are exactly where mixed states are invisible: none-started against some-complete, and a live Hand on a latched unit. Both now carry the same yellow, green, red and grey row, and Needs-land cards keep it too — green-for-complete is the tell that a unit is finished rather than stuck. A unit already through the land gate still gets no row, because its column has said the only thing the mix could add.

Board to Unit to Ticket

The design's third destination returns as a screen and not as its data. A ticket row in the unit's Tickets list now navigates instead of expanding in place, and the touchpoint spine moved onto the screen with it, so it is still drawn in exactly one place. The back chip is the unit's key, one level, not the board's.

Every field is the same work unit read the unit view already made, so the screen costs no read of its own and repaints on the same ledger tick. The empty state is the design's own sentence — "No hand has run on this ticket yet." — shown on hm-core's never-dispatched state and no other, so a run that happened but offers this pane no channel is never called a run that did not.

Not drawn: the subline's Line crumb. This read carries no Line, and hm-core attributes a unit to one by explicit relation, a rule the webview must not keep a second copy of.

What a hand changed, opened in something that can read it

The Ticket screen's empty artifact slot is filled with the one thing hm actually has: the ticket's bench worktree delta. The read was written and tested in hm-core all along and has had no surface since the design-v2 layout removed the Changes-primary detail. It is back on the desktop's read allowlist, minus its patch, because this screen draws no inline diff and a patch on every ledger tick would be the cost of a view nobody scoped.

The granularity is the ticket. A ticket resolves to one bench, so it has one delta — the design draws artifacts per touchpoint, hm has no per-touchpoint change, and none is invented. Unavailable and clean stay two different sentences: a reclaimed bench is not a bench with nothing in it, and neither is a row.

Open with is a guarded command, not a permission. The webview sends an index into the list it drew; the shell re-reads the delta, resolves the index, canonicalizes, and re-checks that the result is still under that ticket's own worktree — and the application must be one this machine just offered, because an unchecked bundle path would make the command run anything on anything and the path guard worth nothing. The launch is the opener the account link already rides, called in exactly one place, and the webview is granted no opener permission.

Zed first for code and for the worktree is hm's preference and not a registry read: on this laptop the default for a Rust file is whatever claimed plain text, and a TypeScript file offers a video player because the extension is also an MPEG transport stream. The machine's own default is still offered, and marked as the default. Classification is hm-core's, beside the diff — the extension plus git's own text or binary verdict, where the verdict can only demote — and it answers one question, whether hm hoists a code editor, so "other" is not a failure, it is no preference.

Probed rather than assumed: Zed declares it opens folders, so handing it the worktree is real. What is not real is a native "open as a diff" verb — nothing reachable through the system's launch services asks an editor to show a branch delta — so the worktree opens as a folder and a single file opens as a file, and that is the whole of what ships. Opening a change in a remote app is a boundary crossing, and stays a connector, later, if ever.

The roster list groups by Line

The Roster screen ran the seats out flat. The file it is read from does hold one relation — a Line names seats — so the list now draws that level and only that level: one group per Line, in the file's order, seats unsorted inside it, and a trailing group for the seats whose Line the roster no longer holds. That group says in words that the roster has drifted rather than inventing a Line to file those seats under, and a Line seating nobody says that too.

Roster is not a graph, and this does not make it one. A group is a filter over the single roster answer already in hand; no seat points at another seat, no group holds another group, and the leaf row is unchanged with exactly one call site. The group hangs on the Lines screen's spine and wears its head chrome, so one Line reads the same on both screens.

What ships

Two CLI tarballs — hm-0.1.31-aarch64-apple-darwin.tar.gz and hm-0.1.31-x86_64-unknown-linux-gnu.tar.gz — the macOS desktop DMG hm_0.1.31_aarch64.dmg, and a SHA256SUMS covering all three.

v0.1.30the board ranks what is happening now, and a card says which of its tickets is doing it

Highlights

Dispatched is a run state, not a run file

The Cadence board decided a unit was Dispatched by asking whether a run log existed. list_hand_runs returns every log a Works has ever written — 672 of them on the dogfood Works — because the Detail Activity pane still reads ended transcripts, and the only thing that ever took a log back out of the column was a ticket completion inside the digest's eight-event window. So a unit's column was a function of how busy the Works had been since it finished rather than of its own state. On the dogfood Works, 175 of 177 units that had already been through the land gate were painted in Dispatched, and the Landed column read zero.

The gate is now the run's state — live or paused — which is the field the read already derives from the PID probe plus the ledger's last lifecycle event. An ended run earns no column. Paused stays Dispatched, because SIGSTOP holds the process and there is a Hand to resume; ended-but-still-latched falls through to In flight, where the latch is the remaining fact.

No read changed to do it. Every log still comes back, ended ones included, and Activity, the run badge and the run-state selector are untouched — the filter is one predicate inside the pure fold. Landed still means the manifest's own phase written by the land, and the units that had been stolen from it return by falling through the hottest-work precedence rather than by retargeting the column.

Latest-first, and the chips that flip it

The read behind the board answers id-sorted and the fold preserved that, so the columns ranked units by the order their ids happened to be minted. That put the release which was actually running at rank 143, under 142 units finished weeks earlier. The order was never chosen — it was inherited from a sort that exists so the payload is stable, not so the board is legible.

The board now paints latest-first, with a two-chip control in the board head that switches to oldest-first. Each card carries the one clock every unit has, its manifest creation instant, off the list the read already loads: no new read, no ledger event. Each column then ranks by the freshest instant it can honestly name — Dispatched takes the run log's modification time, Landed takes the newest ticket completion in the digest window, and Ready, In flight, or either of the other two when its richer instant is missing, take the creation instant.

Instants are parsed rather than string-compared, so a source that ever carries an offset cannot silently re-rank the board, and one that will not parse sorts last in both directions: that card is not the oldest thing on the board, it is the one the board cannot place. Ties break on id, so the same partition paints the same column twice rather than reshuffling equal-timestamped cards on every ledger tick.

Ordering lives in the paint, not in the partition. The partition is a fact about the Works; the order is the Principal's choice, and every column takes the same one — a board ranking Landed by time and Ready by id would be two boards. The chips reuse the Chamber filter's pill geometry and active tint at board scale, carry no counts because the columns already do, and cost no read: pressing one repaints from the last digest and cards rather than re-reading the Works, so no refresh can land between the press and the paint.

A card paints its ticket mix on the bead it already had

A folded manifest card showed one dot. A live Hand, a sibling waiting on the land gate and a blocked one all looked the same, which is the question the fold created when it stopped painting one card per ticket.

The card now carries one bead per showable member, in roster order, inside the meta row the subtitle already occupies — not a fourth copy slot, and not a size change. A member is yellow when the board places it in Dispatched, because the run outranks whatever the ledger still says; red when it is blocked, or when its run has ended while the ticket is still latched, which is the closest thing the ledger records to a failure since hm has no failed ticket state; green when it is complete or landed, waiting on the unit's own gate; and the dormant grey the bead already was for everything else. Open, and a latch with no Hand on it, are not finished waiting, and painting them green would make never-started and complete-but-not-landed the same colour.

The bead is a fact about the member and the column a fact about the unit, so the class is read from the ticket partition and never re-derived from the manifest phase. Cancelled members are skipped exactly as the placement fold skips them. A unit already through the gate gets no row at all, because its column has said the only thing the mix could add. The row caps at eight beads: a bead plus the meta row's gap is thirteen pixels a member against a column that still has to hold the subtitle, and past the cap the leftover members go unpainted rather than earning a second row that would grow the card.

The roster list wears the v4 card

The design's Roster screen is an org chart of cards — a root, standing columns, connectors, indented reports. Only the card travels here; the list stays the flat seat list the loader answers with.

Each row gained the Line's colour as a left spine and a square dot beside the id, reusing the Lines screen's hashed swatch rather than inventing a second palette, and the Line label moved under the head in mono to match the card's meta row. The row also gained a status word, and what that word says is narrower than the design's: the design coloured a runtime liveness that this read cannot prove, because a seat is four fields in a roster file and health is keyed by snapshot agent ids rather than seat ids. So the word is what the roster does rule on — seated when the seat's Line resolves, and line unresolved when it points at a Line this roster does not hold, which is real drift a hand-edited file produces. Inventing a colour per seat would have read as a fact.

Explicitly not ported, and now gated rather than forgotten: the parent pointer, the kind, the connectors and reports indent, create, retire and promote, one-off dispatch, New roster, and per-seat spend and cap.

A wired seat stops being called unconfigured

Asking whether a seat can be reached by mail resolved the seat's executor provider before looking at its toolbelt. Providers are feature-gated, so the answer depended on which ones the asking binary had been compiled with: the desktop, which compiles the pilot hands, and the default-feature CLI printed two different reasons for the same seat on the same queue write, and the CLI's was that the seat had no entry in the executors file — a sentence about the Principal's Works that was not true of it.

Reachability never needed the provider resolved, so it no longer resolves one. It reads the seat's own entry and reports what the [seat.mcp] declaration says, which means the two binaries now agree about the Works in front of them, and a seat that genuinely declares no toolbelt is told that rather than told it does not exist. Found by dogfooding @ addressing in the Superintendent and in the Detail Mail pane against this Works.

The v4 Cadence Tracker HTML is the design authority

The design authority moved to the v4 Cadence Tracker HTML, pinned by digest and byte count so a fork cannot quietly become the thing cuts are measured against, and the earlier note that named the v3 file as the right visual variation was corrected — the Roster screen is not a graph. Alongside it, the delta document now records the full parity between the canonical HTML and what is shipped, so a product cut keeps, re-scopes or drops each item against the authority rather than against a copy of it, names the v3 git pin, records that the Superintendent typeahead's one-off entries are deliberately not ported, and keeps class-gated dispatch on hold. The three-class bead mapping the board now paints was written down as research before any of it was built, against the partition that existed at the time.

v0.1.29the board shows the work, not the ticket rows the work was cut into

Highlights

A manifest is one card, and the board stopped painting one per ticket

A five-ticket manifest was five cards on the Cadence board, each titled with a ticket name the Principal never wrote, each sitting in whichever column its own ticket had reached. The unit of work is the manifest — it is what a brief opens, what a petition argues for, and what a land closes — and the board was the one surface that could not say so.

It could not say so because the read under it cannot answer it. The digest the board renders from carries a ticket line with no manifest id on it; its in-flight states are Open, Latched and Blocked, so every terminal ticket falls off the board entirely; its Landed column is a ticket completion caught inside an eight-event window; and its list of active manifests filters the landed ones out. Every one of those is right for the ladder and the insight counts that read them. None of them is a manifest.

So there is a second read beside the digest rather than a change to it. manifest_board returns every manifest with its phase, its whole ticket roster — Complete, Cancelled and Landed included — and two lines of copy taken off the unit's own brief or petition. It is composed in the desktop shell out of four reads the core already published, and it opens no ledger handle, writes no event and moves no schema: a manifest arrives on the board because the ledger already says it exists, not because a projection was taught to keep a second copy of it.

The column a card lands in is the hottest work on the unit

Folding a roster into one card means choosing which member speaks for it, and ticket-level placement has the precedence backwards. On a ticket, Landed wins, because a finished ticket is finished. On a manifest, Landed losing is the whole point: a completed sibling must never pull a live dispatch into the Landed column and take a run the Principal is watching off the board.

So the fold reads Dispatched, then In flight, then Ready, then Landed, and the first one with a member on it is the column. Cancelled members earn no column at all, and a unit whose entire roster is cancelled has no card rather than an empty one. The Landed column now means what the gate means — the manifest's own phase, written by the land — and not a ticket completion that happened to fall inside a window of recent events. A unit whose tickets are all complete but which has not landed yet sits in Ready, which is the true statement about it: the work is done and the land has not run.

The grouping key is the manifest id the ledger stores on the ticket. Ticket ids are never parsed for it. That manifest-tN looks like a join is a convention the writer follows, and a view that reversed it would be reading a naming scheme as if it were a foreign key.

The ticket partition is an input to the fold and not a casualty of it. The cadence ladder, the chips and the Overview insight counts keep reading it, keep counting tickets, and keep their meaning — the fold is added beside them.

A card carries the words the unit was opened with

The three slots on a card were an id, a title and a meta line, and the title was a ticket's. Now the id is the manifest's, the title is the first heading of the unit's brief or petition with the house prefix stripped, and the meta line is the first prose sentence under that heading, past the label rows.

Both are clipped where they are read, at 48 and 36 characters, because the card has no line clamp and no maximum height — clipping in CSS would be the size change this work is not allowed to make. An artifact that is missing, or that has no heading, degrades to its own source slug read as words with an empty second line. It never falls back to the manifest slug, which is a copy of the id the card already prints, and it never invents a count line to fill the space. One unreadable brief costs one card its copy; it does not fail the read.

Clicking a card still drills a ticket, because that is what the Detail view resolves — the member that earned the column is the one it opens on, and the highlight follows it back.

The panel that opens cannot retitle the unit

The work-unit Detail titled itself from the manifest slug and described itself with a tally: the phase, a ticket count, a touchpoint count. Three facts already visible in the tree underneath, standing in for the one thing the panel could not say, which was what the unit is.

It now carries the same two lines the card printed, read through the same copy function, so the board and the panel cannot disagree about what a unit is called. Where the artifact offers no sentence the description is empty rather than refilled with the count line it replaced.

The grouping rule was written down before any of it was built. The research note under docs/reference/research/ seals the precedence and the copy derivation against the partition that existed at the time, with no runtime change in it, so the four changes above implement a decision rather than discover one.

The cursor a spawn resolves is the Cursor CLI

A shim on this machine's PATH at ~/.local/bin/cursor execs a different vendor's agent. Resolution took the first cursor it found, which meant a Cursor seat could spawn a runtime nobody asked for — and the failure was invisible from inside hm, because the program name on the left of the spawn is the one that was asked for either way.

Resolution now skips that wrapper and prefers the CLI the Cursor application installs, falling back to cursor-agent when that is the real one. The hand's argv is unchanged: cursor agent with -f, as it was. (#348)

A dry run refuses what the live land refuses

hm gatehouse land --git --dry-run previewed a whole-manifest land under a per-ticket policy — the exact shape the live land declines. A preview that says yes where the real thing says no is worse than no preview: it is the one an operator trusts.

Both paths now go through one policy check, so a --git --dry-run without a --ticket under a per-ticket policy refuses as a policy mismatch, in the same words and for the same reason. A dry run that names its ticket still previews, and the ledger-only dry run is untouched. (#349)

A failed check keeps the table that explains it

hm works doctor and hm executor probe earn their keep by printing a table: which seat, which runtime, what answered. Over MCP, a non-zero exit turned that into an error result and the table went with it, so the Superintendent showed the bail and nothing about why — on the two commands whose entire output is the diagnosis.

The error result now carries the command's own output rather than replacing it. The bridge puts the table in the content text as well as in the structured result, the desktop host fills its output from whichever of the two is there, and the panel's send path joins both streams. A probe still exits non-zero when a seat fails; it just no longer takes the reason with it. (#350)

v0.1.28the panel stops keeping second copies, and the belt it runs on is enforced

Highlights

The Superintendent's six action chips are gone, and nothing they did went with them

The floating panel carried a row of six chips — Toolbelt, Manifests, Draft Manifest, Assign seat, Pause run, Terminate run. They were honest when they shipped: each was a typed route into the live runtime rather than a label, two firing a real hm call on click and four staging a command the runtime must not invent an argument for. What they were not was the catalog. They were a hand-written list of six commands sitting above a menu that reads the whole catalog from the session itself.

The / menu is tools/list — the same list hm workflow show renders, grouped by its own lifecycle phases and described by its own guidance. Every command a chip named is in it (workflow show, manifest list, manifest create, latch assign, run pause, run terminate), and so is every command no chip ever named. Mid-run steer has a better home than either: the run card for a dispatched ticket, which sends the id it is already drilled into instead of asking the Principal to type one back. Its Hold and Terminate are unchanged.

So the row is deleted — actions.ts, ActionChips.svelte and their test — and the panel is left with the surfaces that cannot fall behind the runtime: a composer, a catalog menu that grows on its own, and a run card that knows which run it is looking at. (#330)

The slot the chips left holds the agent's own configuration

The header chip has always named which runtime hosts the chat. It never said how that runtime was configured — and in this Works it was already running --model claude-opus-5 off the Superintendent seat, with nothing on the panel saying so. Under the approval cards there is now a row that does: Model, Level where the runtime has a flag for one, Posture, and a quiet Save as seat default.

It is the second axis of the same switcher, not a second switcher. The chip picks the CLI; the row picks how that CLI is configured, and it re-reads when the chip changes — each runtime has its own model namespace and its own knobs, and Cursor has no Level control at all because its level rides inside the model slug.

A pick is a session override, remembered per runtime so a relaunch comes back on it, and it never touches org/executors.toml — that seat is shared with decompose, and one model string cannot hold five namespaces. Claude takes a model or posture change hot, on the same stdin the stop button already writes to, so the conversation is not restarted to change what is answering it. A level is a spawn flag, so it re-opens on the same conversation and says so before it does.

Posture is hm's own, not a vendor's permission list: it is the class gate that decides whether a write raises a card, the bridge re-reads it on every held call, and it applies on every runtime rather than only on the one that takes a flag for it.

Every value on the row is what the host reported it resolved, never the pick that was asked for — a change that failed leaves the live value standing. (#331)

The row is now true on every runtime, not just the two that read a model

Codex, Pi and Opencode each spawned the Superintendent on a model written into their host module, and read neither the row above them nor the seat below it. A Model pick there was a label — so the row disabled it and said why. That is gone, because the reason is: all three hosts now build their argv from the one resolution the panel writes to, on the open path and the per-turn path alike.

A level travels with it, in each CLI's own vocabulary rather than a word hm invented — Claude --effort, Codex model_reasoning_effort, Pi --thinking, Opencode --variant. It is passed only when one is set: an unset level means the CLI's own default answers, never hm's idea of a middle setting. Cursor still has no Level control, for the reason it never had one — its level rides inside the model slug.

The seat row grew the matching homes, so a session pick has somewhere durable to be promoted to on every runtime: effort under [seat.claude] and variant under [seat.opencode] join the model_reasoning_effort and thinking their Codex and Pi siblings already carried. Unset stays unwritten — a seat that names no level materializes no table.

None of it widens a sandbox. Codex still runs read-only with approvals never, Pi still excludes the three mutating built-ins, Opencode still ships its posture in opencode.json, and the guards that assert so are unchanged. (#332)

Every Line's Superintendent arrives with a row to be addressed at

Save as seat default writes hm executor set --seat superintendent-<line> with no --provider in it, because the row it is updating is supposed to already be there. Only hm works wire ever authored that pair, so a Line that arrived any other way answered the Principal's Model pick with an error about a flag they were never shown.

The pair is now seeded with the Line itself: works init, works line add, and works onboard for any Line the roster carries and the executors file does not. Seeding is an address and not a wiring — the script provider and nothing else — and it is additive, so a seat already answering on claude keeps its model, posture and toolbelt when a Line is added beside it. org apply-template stays roster-only on purpose, and the runbook says so rather than leaving it as an inconsistency to rediscover. Doctor's line.executor_mismatch was the same gap read from the other side, and it goes quiet for a seeded Line. (#333)

A ticket's Mail box takes an @, and the panel's @ knows what is open

The Detail's Mail composer now spends the same sigil the Superintendent's does, on the same authority — list_agent_mentions for the rows and address_agent_mentions, with this ticket as the mention's work unit, before the body travels. The write underneath is the one it already had: a single hm ticket mail through the write hand, QUEUED until the hand pulls it. The trigger, the rows in flight, the highlight and the dismissal moved into one module, so the two menus cannot disagree about what @ means. (#335)

The floating panel needed the other half of that. It resolved a mention with no work unit at all, which let the seat's own latch answer precisely when the Principal was looking at a ticket that seat is not latched to. The shell now says what is open and the store hands it along — leaving the Detail takes the Given unit away — and both mention paths, the pre-send guard and the rewrite that travels, go through one address. With nothing open, the latch fills the target exactly as before. (#336)

Where a mention resolves onto a ticket, the panel can carry the whole write itself, because a mail body rides argv where a call's body rides stdin. So it does, and it stages it: the composer pre-fills the command and the Principal reads it and sends. Naming a seat queues nothing by itself. Where there is no ticket, nothing is staged and the panel says which miss it was — several latches, no latch, a manifest with no queue at its address, or two names and two queues — and the line travels to the agent instead, which reaches the same command over MCP. Neither route paints a dispatch: QUEUED is the ceiling both stop at. (#337)

The menu draws its superintendent-… rows rather than filtering them out, and says what they are: mail lands on a ticket, and that ticket's queue is read by the hand latched to it. Hiding was the worse half of the trade — the resolve rules on the roster and not on this menu, so a hidden row is a name that still resolves when it is typed, with the explanation taken away. (#338)

The ticket says who has worked it, and the rail's Roster row opens

The composer answers "who am I naming". Nothing on screen answered the other half: who has already worked this ticket, and could any of them be reached about it now. hm-core has had that answer since hm listen — the latch holder plus every seat whose runs left events on the ticket, each resolved through the same router the composer reads — and no surface asked it. One read is added and one row under the Tickets tree draws it, in the host's order and with the host's own sentence per row. Three reach states rather than two, because whether anything is listening now and whether there is anywhere to listen at are different facts, and one grey chip over both would be the lie the row exists to avoid. Display-only, and that is a decision: un-reaching a seat is not a verb hm has. (#339)

The nav rail has carried a disabled Roster row since the shell's first draft, with a tooltip saying it would land with its own row of work. This is that row: one read of the roster, and one screen that draws what it answers. A seat is an id, a Line, a role, a mandate and the hats it stacks — so there are no standing or one-off chips, no per-seat spend bar and no create or retire control, because none of those are objects the model has. Delivery and spend go where they live: two header links open Overview's own agent health and spend regions rather than restating a figure per row, since those rows are keyed by a different id and that panel is a Works-wide aggregate. The one affordance a row carries hands the composer the catalog's own latch assign line with the seat named and the ticket left for the Principal. The list is the roster's, whole and in its order. (#340)

The Superintendent's belt is MCP-only, and the runtime now enforces it

Super is an hm entry point rather than a shell, and that was held by convention. It is now held by the argv: a Claude Super session filters Bash and Agent out of the tools it registers, a Super seat refuses --toolbelt instead of quietly accepting a second one, and Opencode's webfetch is an explicit off rather than an unstated default. (#342)

Reads stop asking. The class gate already knew which verbs only read, so Super's reads travel without raising a card — the Principal was approving a list and a show one at a time on the way to the call that mattered. The wait on a card is bounded, so a deny that never comes back names the argv it was holding instead of hanging with nothing on screen. (#343)

Burst work has a path that does not invent a seat. A Super that needs a Hand to take a repair opens a maintenance manifest and dispatches one Principal card — a Repair title and its acceptance — and run status lists every live Hand on the Line, so the choice is made against what is actually running. The Works-side MCP wiring a Hand needs in order to pull its mail is recorded in the runbook. (#344)

Super reads the public web host by host, and none of it opens a file write: Claude registers WebFetch and WebSearch behind the same belt filter, and Opencode sets webfetch true explicitly. Cursor, Codex and Pi stay documented and unverified, and are named as such. (#345)

A completed ticket's leftover latched_seat is history, not a live latch. Mention, listen and mail read it the same way now, so all three agree the Hand is gone rather than one of them offering to reach a seat whose work is over. (#346)

v0.1.27the switch opens, and the runtime that refused it says why

v0.1.26 gave the Cursor Superintendent permission to call the hand it registered. That fix wrote the permission file — and wrote it in a shape cursor-agent's own schema rejects, so switching Claude → Cursor opened a panel onto a runtime that had already exited. It exited loudly, on stderr, into a pipe the host inherited and a Finder-launched desktop points at /dev/null. The 0.1.26 rule holds and gets its second half here: a runtime that died must not arrive wearing the agent's voice — and when it said why, the Principal reads its sentence.

That is the switch working. The rest of this release is what a working switch turned out to need behind it: a conversation that survives the switch instead of restarting on the far side of it, a hand that finds its CLI when the desktop spawned it, a seat whose dontAsk is honoured by the thing that kept asking, a tree that dispatch can branch without a checkout error, and a main thread that is not a command queue.

Highlights

The file that admits Cursor's tool calls now satisfies Cursor's config schema

cursor-agent validates <workspace>/.cursor/cli.json against a schema that requires permissions.deny as an array. hm wrote permissions.allow alone, so the file the 0.1.26 fix materialized was the file that killed the runtime: "Invalid project config … path [permissions, deny] … Required", exit 1, before the agent said a word. The merge now writes deny beside allow — the documented shape — so a switch to Cursor opens on a config the CLI loads.

It widens nothing: an empty deny list denies nothing extra and grants nothing extra, there is still no Shell(...) entry and no --force/--yolo, and the merge is still a merge — an operator who keeps their own denies keeps exactly their list, a re-run adds nothing, and a file that does not parse is named rather than replaced. (#324)

A Cursor turn that dies before it speaks carries the vendor's own sentence

The Cursor host inherited its child's stderr, discarded its exit status, and turned an empty stdout into (the agent returned no output) — an Ok turn, zero events, and the panel reading the shape correctly as the agent runtime did not start with nothing to say about why. The one place the reason existed was the pipe the host had thrown away.

Stderr is now piped and drained (on its own thread, so a child cannot deadlock filling it), and a turn that exits non-zero or never streams a single event fails with the last of that stderr as its sentence — through the same failed terminal a refused Claude resume goes through. The placeholder survives only where it is honest: a child that ran, streamed, exited clean, and left no narration. The desktop hint table reads Invalid project config as stale/invalid .cursor/cli.json, ahead of the permission rule that would otherwise send the operator hunting for a grant over a sentence that is really about a file's shape. (#324)

One Works conversation, and the runtime it was last held by

The Superintendent had a conversation per runtime and no memory of which one the Principal was last in. Switching Claude → Cursor opened an agent that had heard nothing, a Cmd-Q dropped a resume key that close was the only writer of, and a terminal hm superintendent rotated the identity the desktop's live bubble was still holding.

The session cache now remembers last_provider — the runtime that last held the conversation, written when it resolves a resume key rather than when it is picked, so a runtime that died without speaking never becomes the one a relaunch opens on. The resume key is persisted the moment the host resolves it, and the desktop's exit hook is the orderly teardown rather than the only save, because close is not a promise a Cmd-Q keeps.

What a new runtime is owed is composed rather than copied. render_handoff_brief reads the one shared conversation log every runtime already records into — there is no second store — and SessionSeed decides per open: a fresh chat takes turn-zero plus the handoff, a resume takes the handoff alone, and a runtime resuming its own conversation takes nothing. Claude receives it through --append-system-prompt; the one-shot dialects (Cursor, Codex, Opencode) prepend it to the first turn, which is the only ingress they have; Pi carries it in argv. A session with no pick opens on the remembered runtime, filtered through the hosts this build actually compiled — and hm superintendent re-attaches by default, with --new as the rotation. (#328)

A hand spawned from the desktop resolves the agent CLI the way its host does

ledger_poll's three spawn paths and StdProbeHost::probe_binary handed the bare name to Command::new. A hand dispatched from the desktop's MCP sidecar inherits Finder's PATH/usr/bin:/bin:/usr/sbin:/sbin — so it reached for a claude or a cursor that is not on it, and a probe of that same seat failed a dispatch which would have run. Both now go through the resolve_agent_cli the hosts already use (explicit override, well-known install prefixes, login-shell PATH), and a spawn that still fails reports every candidate it tried rather than the bare name it could not find. (#325)

A seat told not to ask runs its writes, traced

The Superintendent's live path held every write-class hm call for an approval card — including a --dry-run dispatch, which dispatches nothing — and read the acting seat's permission_mode only to keep dispatch held. So a Principal who had already set that seat to dontAsk was still approving each petition, ticket and bind by hand.

class_gate now maps write-class under dontAsk and bypassPermissions to an allow by seat policy: the call runs, and the same resolution row an approval would have written is appended anyway, carrying the seat and the mode that granted it — so a write with no card is still on the transcript and in the journal. Dispatch stays Hold under every mode, and an unwired seat, an unset mode or an unreadable org file all stay Hold: failing the read must not be the way to be asked less.

Read verbs are classified off the tool's command path, where no user prose can reach, and --help, -h and --dry-run off the argv hm has already built, which is the only place they appear. The four Superintendent hosts resolve the acting seat rather than pinning a constant, so hm executor set --seat superintendent-<line> --permission-mode dontAsk is a control with an effect instead of one with neither effect nor error. (#326)

hm works settle — the tree dispatch was about to branch

Dispatch wrote the seat's .hm/prime.md into the worktree before cutting hm/<manifest>, so in a repo that tracks the prime, git refused the cut: "Your local changes to the following files would be overwritten by checkout". The prime now moves after the cut, and every branch arm first restores hm's own regenerable output (.hm/*, .cursor/*.json, .pi/mcp.json) — operator work is refused by name, with the settle line, instead of through a raw git error.

hm works settle is the standing remedy. Its safe tier restores hm-generated dirt and fast-forwards clean benches, --stash parks operator work in a labelled entry, and --discard needs a typed confirmation. hm works doctor gains bench.dirty_worktree — ERROR for hm dirt on a repo bench, WARN otherwise — and a --json shape to read it from. (#327)

The Superintendent commands come off Tauri's main thread

The eleven Superintendent-family commands are now #[tauri::command(async)] — bodies, channel, registry and event vocabulary unchanged. A synchronous command is dispatched on the main thread, so a turn taking a minute held the thread every other command arrives on: the interrupt meant to stop that turn queued behind the turn it was meant to stop, and the tail tick stopped for the length of the reply. (#322)

v0.1.26the Superintendent opens, and says so when it does not

v0.1.25 shipped the live Superintendent; this release is about the moment it is launched. Every fix here is on the seam between the panel and a vendor CLI — which conversation gets resumed, what a refused one looks like on the wire, what a Cursor turn is actually allowed to call, where the pi binary is, and what the Principal reads when the process never started at all. The rule underneath them is one rule: a runtime that died must not arrive wearing the agent's voice.

Tip: 1931a21 (E-super-launch, #314).

Highlights

A resume id belongs to the runtime that minted it

The Works session cache held one agent_session_id for the whole Works, but a resume id is a dialect's id — a Claude uuid, a Cursor chatId, a Pi session file. After a Cursor or Pi conversation, opening Claude handed claude --resume a foreign id it rejects, and the panel opened dead.

The cache now holds agent_sessions, keyed by the ExecutorProvider name. Open reads its own runtime's slot, respawn re-reads it, close writes that slot alone — so a switched runtime opens fresh rather than resuming someone else's conversation, and never overwrites a key it cannot use. A 0.1.25 cache still parses; it reads as no resumable session, because nothing on disk says which runtime minted that lone id. (#315)

A failed Claude terminal is a failure, not silence

The CLI closes a refused --resume with an ordinary-looking terminal: subtype: error_during_execution, is_error: true, the sentence in errors[] and nothing in result. The adapter read only result/text, so a session that never opened came back as the Superintendent having nothing to say — and the panel then retried the same bad id.

That terminal now closes the turn as failed, carrying the CLI's own sentence, classified StaleResume when the conversation was refused and AgentHostError otherwise. is_error alone is still a reply: a turn that narrates its own error is a turn. The Works session recovers it the way it recovers a dropped pipe, with the one difference that matters — the respawn goes out without the refused id, dropped from both the cache and the panel's resumed claim. A reconnect that fails again closes the stream with the vendor's line rather than a generic dropped-session status, and the desktop hint table reads no conversation found as a stale resume id, not a missing binary. (#316)

Cursor may call the hand it registered

Registering hm mcp serve in .cursor/mcp.json and approving it with --approve-mcps is not permission to call it: a headless -p turn auto-rejects every individual tools/call, so a Superintendent that looked wired reached the catalog and was refused at every tool.

The missing half is now materialized beside it — Mcp(hm:*) merged into the workspace's .cursor/cli.json permissions.allow, on both paths a turn arrives by (the one-shot run and the always-alive open spawner). It is a merge, not a write: the operator's own keys, allows and denies are written back untouched, a re-run adds nothing, and a file that does not parse is named rather than replaced. It widens nothing else — no Shell(...) entry, no --force/--yolo — and the write/dispatch half of the catalog is still held by hm mcp serve --class-gate for Principal approval.

pi is resolved the way a login shell would resolve it

A Finder-launched desktop inherits /usr/bin:/bin:/usr/sbin:/sbin, and pi sits off that list. The Pi host was the last sibling handing a bare name to the OS. Both its paths now go through resolve_agent_cli — the HM_PI_BIN override, the well-known install prefixes, the login-shell PATH, then the process PATH — resolved at open and per turn. And when there is nothing to find, the failure names the binary and every candidate tried, instead of the old io error at <works root>, which pointed at a working directory that was never the problem.

A turn that died before its host spoke reads as a spawn failure

A CLI that dies at spawn does not always say so in a classifiable way: it closes the turn with is_error set, no words, and nothing in errors[]. The empty-result fallback then reported the host's no-output placeholder — a dead runtime wearing the Superintendent's voice, and the one thing a Principal cannot debug from.

The panel now reads the shape. The store latches whether the host has ever announced itself (the CLI's system line, any word or work from the agent, the no-stream notice), and a wordless turn while that is still false lands as a Runtime notice naming the death, with the same operator hint a failed tool result gets — PATH / HM_BIN, stale resume id, permission, MCP unreachable. The failed-before-init half arrives as a throw and is reframed identically: the seam's sentence as the body, under the runtime's name instead of the agent's.

Three boundaries keep it honest. The latch is per conversation, not per turn — the always-alive host announces itself once and every turn after rides the same child — and it resets wherever a host is spawned. A host that does not stream is never read this way. And a cancelled or failed terminal is left alone: the first is the Principal stopping the turn, the second is the vendor's own sentence and cause, both more specific than a shape read off the stream. projectRecord applies the identical rule, so a reload says what the Principal was told the first time. (#319)

hm superintendent is the digest, and now says so

The terminal command reads the ledger, prints the Today digest and the toolbelt, rotates a cache under runs/superintendent/, and exits. Every sentence around it said otherwise — the help called it "the Principal's sole comms entry", the catalog row called it an "always-alive orchestrator session", the run opened with "Superintendent session started". A Principal who believed those sat at a prompt waiting for a conversation that was never coming, and read the silence as a broken agent.

The help, the catalog row, and the banner now carry one claim: this is the digest and the cache, it spawns nothing, and the live Superintendent is the desktop app's bubble — which starts its runtime over hm mcp serve against the same cache --resume re-attaches to. The banner says it on the run, not only in --help, because the misreading happens at the prompt. SEAM.md carried the inverse of the same claim from before the panel landed; it now names the floating bubble as the live seam and the CLI as the digest read beside it. (#320)

Release plumbing

The first release to carry the app found the artifact one directory out of reach: upload-artifact roots the archive at the fixed prefix of its path, so releases/*/macos/*.dmg shipped <version>/macos/ inside the artifact. It got that far because sha256sum … | tee reports tee's status, so an unmatched glob was a green step with an incomplete SHA256SUMS behind it. The DMG is now staged flat, and a missing artifact fails where it happens.

v0.1.25reaching a prior worker, prose while it is written, and the desktop as an app you install

This release ships three packs that arrived together: hm listen, the explicit way to reach a worker who already touched a work unit; Superintendent happening-prose, which streams a Claude or Cursor turn as it is written rather than as confetti at the end; and the macOS desktop bundle, which puts hm inside the app so a downloaded .app never depends on the login shell's PATH. Underneath all three, hm_core grew four named layers — comms, host, policy, stream — so the streaming surface has one owner instead of several.

Everything that landed on main after v0.1.24 is here: 216 commits, including the council bodies, the run card, billing, and the staging plane's move to Nomad. The CLI remains the mutation source of truth on every surface below.

Highlights

hm listen — a target is built, never parsed

  • hm listen list --ticket|--manifest prints everyone a work unit has a record of, each with the reach it actually has. Nothing is filtered: a seat that worked the unit and is gone comes back listed and unreachable, with the sentence saying which, and a unit nobody has touched is an honest empty rather than a fallback to the whole roster.
  • hm listen show --mention '@name' is the resolve that happens before a call — the roster rules on the name, and what comes back is the explicit --seat / --ticket argv to call with. A name the roster does not hold prints as that answer, with the near misses, and still exits zero.
  • hm listen call --seat … --ticket … says one thing to one explicit target. The body is read from stdin, never argv: a message that happens to read as a verb would otherwise classify the whole invocation read-class and skip the approval card the Principal expects.
  • The same pair reaches an agent runtime over the MCP bridge as list_reachable / call_agent, generated from the hm workflow show catalog rather than hand-registered — the catalog row is the registration.
  • Delivery is comms' queue for the ticket the target holds the latch on, so a call is mail that commits; a live Claude seat is nudged after the mail lands.

The rule the surface is written against: hm may look a name up, but the lookup is the registry's and it happens before the call. The moment a vendor CLI is asked to find an hm seat in its own mention typeahead, the answer stops being the roster's. Free-text @ in an arbitrary hm subcommand is therefore not the CLI path — the explicit listen subcommands are.

Superintendent prose, as it is written

  • A live Claude or Cursor turn streams its narration while it is still being produced, and a long think is said to be a think while it is one, instead of a blank working fold.
  • The closing-summary contract carries onto the live session, and an empty result falls back to the accumulated narration rather than replaying the blank behind it — the Principal reads what the agent actually said.
  • The last turn's work stays on screen while the next one starts, and the effort a turn is spending is visible rather than inferred.
  • Live MCP writes and dispatch are held for operator approval instead of running silently; tool failures surface instead of disappearing.
  • A headless Cursor turn trusts its workspace so it does not stall, and is read through --stream-partial-output.

The desktop is an app you install

  • The macOS bundle is on: hm ships inside hm.app as a Tauri sidecar, so an app launched from Finder — which inherits launchd's PATH, never a login shell's — can still run every write. make desktop-dmg builds it; make desktop-dmg-staging builds the same bundle against the staging account plane, under releases/<version>/macos-staging/.
  • The account plane is baked at build time, and the session says which one issued it. A runtime environment variable cannot point a downloaded bundle somewhere else.
  • Sign-in is composed around the device grant — no password field, no provider buttons, no gate — and the account session is a row you can sign out of, which means gone.
  • The Works you had open is a square in the rail; window geometry, recent Works, and per-Works cursors persist; the theme is read before there is a window.
  • Every webview binding declares when it stops being true.

hm_core, in four named layers

comms (the conversation layer, its queue, and one router that answers where a reply goes), host (one owner per agent dialect, so a host can hold a pipe without reading it), policy (the gate, the classifier, the allowlist), and stream (one streaming surface, whether a pipe held it or a file did). The reclassify dropped twelve addresses and shrank lib.rs onto five names.

Councils, petitions, and the run card

  • One timeline row shared by every council body; the petition remembers who filed it, deliberates, and ends in a list that ends. Briefs are listable, the council session is reachable, and the Clerk records the body.
  • Activity, Mail and Hand Session became one run card, with the turn rows, the focused drawer, a status dot over four states, and an action row over the verbs that actually exist.
  • hm ticket mail is a queue a dispatched hand pulls; a per-ticket Hand Session resumes the hand rather than dispatching it.

Billing, feedback, and the staging plane

  • Stripe test-mode checkout, portal, and signed webhooks for staging, over a plan catalogue and entitlement read model, with Team billing UI on account-web and desktop Settings.
  • hm feedback send is Works-agnostic, with a labelled Feedback row in the nav-footer; the relay is POST /feedback on the account plane, which accepts then delivers to Discord from a server-side webhook, never a client artifact. Discord egress lives in hm-connectors behind a non-default feature.
  • Staging deploy is a Nomad submit over hm-*-staging jobspecs and GHCR images, with the planned-prod systemd path split away from it. Outbound mail goes through Pingram, and hmcortex.io is retired — hmcortex.com is the name.

v0.1.24E0-align: a deploy seat, the credentials it never reads, and Reckonings

This release ships the E0-align pack: a named deploy seat, written toolbelts, a path policy that refuses to pre-approve a secret read, Works-level secret providers, and seat-scoped MCP with the credential held in the server's process rather than in the hand's environment. It also carries the Reckonings pack and everything else that landed on main after v0.1.23. The CLI remains the mutation source of truth on every surface below.

Highlights

The deployer seat, and a toolbelt you can write

  • docs/reference/templates/deployer-seat.toml stands up an Operations Line with superintendent-operations, hand-operations, and a dedicated deployer-operations seat, applied through hm org apply-template --file so org/roster.toml is never hand-edited. The id sits outside the hand- prefix on purpose — no hand inherits the deploy toolbelt by being a hand — and its role stays hand, so the doctor auth probe and the charter excerpt in a prime still cover it.
  • hm executor set --seat <id> writes one seat's executor entry — provider, script, model, permission mode, allowed tools, timeout, budget — with --dry-run and --json. Only the fields passed move, the seat must exist in the roster, and every write records executor.seat_set.
  • hm executor toolbelts ships the three documented belts: build-hand, deployer (build-hand plus gh and rsync), and superintendent (MCP-only). The compiled default stays narrow; widening is a per-seat opt-in.

Route, don't read

  • A path policy judges an allowed_tools list entry by entry. Refused on the write path: a pre-approved secret store (gopass, pass, vault, op, sops), an environment dump (env, printenv, export), a nested shell, sudo, and a bare Bash or Bash(*). Warned: Bash(ssh *), Bash(scp *) and Bash(curl *) — authorable shapes that want a narrower one.
  • The gradient is deliberate: refuse on write, warn on read. hm executor set names every offending entry and writes nothing; hm executor probe and hm works doctor (executor.tool_policy) report a list that was hand-edited into org/executors.toml, so the finding appears wherever the belt came from.
  • Said plainly in every place the policy appears: an allow list is a permission list, not a tool filter, and none of it is containment. Containment is --strict-mcp-config plus an MCP-held credential, which is the next two entries.

Secret providers, and keys injected server-side

  • [secrets] in hm.toml names the provider as a property of the Works: gopass, onepassword, env for CI, or none — the default, which resolves nothing rather than probing whatever store happens to be installed. Paths follow <prefix>/<env>/<KEY>, a grammar narrow enough that a pasted credential cannot parse as one.
  • Keys reach the MCP server's process and nowhere else. The seat says which servers it gets; the Works says how those servers' keys resolve. A seat can never widen its own credential reach, and injection is refused outright for a seat that has not asked for a strict MCP surface.
  • Nothing discloses a value. hm secrets list shows paths plus the exact command hm would run, hm secrets check shows presence and a truncated sha256 and exits non-zero when a key is missing, and there is no reveal flag on any surface. hm works doctor gains warn-only secrets.* findings and never calls the provider — a health check must not block on a passphrase.

Seat MCP on the Claude hand

  • [seat.mcp] resolves a seat's MCP servers into the hand it dispatches (--mcp-config and --strict-mcp-config). A seat with no table passes neither, which leaves the hand on the operator's ambient MCP surface rather than on none. Both Claude superintendent argv builders now pass --strict-mcp-config, so the registered catalog is the whole MCP surface instead of an addition to it.
  • hm mcp serve --seat <id> pins HM_SEAT on every child command, so an MCP-routed hm ticket complete is attributed by contract rather than by process-environment inheritance.
  • hm mcp proxy --seat <id> --server <name> spawns a declared server with the resolved credentials in that server's own environment, stdio inherited, exiting with the server's status. Every credentialed server a seat declares is re-pointed at the proxy before the hand's config is built, so the hand's argv carries names and a path and no value. One mcp.proxy.launched event per spawn records the seat and the environment variable names only, and hm mcp stays out of the tool catalog so an agent cannot call the launcher as a tool. Verified end to end against Porkbun's own DNS server: a real handshake through the proxy lists its tools, and a live call reaches the API with the injected keys.
  • hm works doctor gains secrets.seat_not_strict, which catches at check time the seat that would fail at spawn.

Reckonings

  • The recordhm reckoning run allocates an R-041-shaped per-Works key and requires an authored --title. Nothing derives a title from the feedback paragraph beside it.
  • The read halfhm reckoning list with inclusive --since and --until bounds, and hm reckoning pending for landed manifests still owed a ruling, longest-waiting first. A backwards window is refused rather than answered with an empty list that reads like nothing happened.
  • The decompress pipeline[reckonings] decompress_window in hm.toml is a fortnight by default, bucketed from a fixed epoch Monday so a window has the same identity on every machine. The store lives at .hm/reckonings/decompress.db, beside the ledger and never inside it, and is safe to delete. hm reckoning decompress show reads it and never recomputes.
  • The desktop screen — the timeline spine reads the record, the notes column reads the store, one range picker resolves both, and a drawer turns the landed-but-unreckoned list into a ruling by running the same command the CLI runs. Browsing a retained window issues no advisor calls at all.

Also on main since v0.1.23

Shipped before the pack above and released here for the first time: Lines as a Works-wide unit-of-work roll-up (hm line list) and the desktop Lines screen; near-real-time Overview, where a burst of ledger events costs one board read rather than fifty; the account plane — sign-in, organisations, invitations and settings sync, with its API rewritten in Rust; and the marketing site, whose changelog publishes this note as written.

Upgrade notes

  • Workspace version is 0.1.24; no config migration is required.
  • [secrets] and [seat.mcp] are both absent-by-default. A Works that adds neither behaves exactly as it did on v0.1.23.
  • A hand-edited allowed_tools list is still supported. If it holds something the path policy refuses, hm works doctor now says so — the entry is reported, not rewritten.
  • Run hm works doctor after upgrading to pick up the new secrets.*, executor.tool_policy and executor.unattended_auth findings.

v0.1.23Epic 13: Overview desktop + agentsview projection

This release ships the Epic 13 Overview pack on the packaged hm binary: hm overview (agentsview Option B aggregates + Option C projection cache), Cadence desktop Overview with design-faithful chrome, and E13·11 polish for actionable first snapshots and centralized Agent data status UX. CLI remains the mutation source of truth; Overview is a read surface over hm-core::overview.

Highlights

hm overview (CLI)

  • hm overview — Works-scoped U-row over agentsview's public aggregate API (stats, usage, health); never opens sessions.db, never auto-starts the agentsview daemon.
  • --refresh — drop/rebuild the hm-owned projection cache under .hm/overview/projection.db so Cadence paints fast/offline after a cold pull.
  • --json / --since — machine-readable report and window tuning; freshness ladder fresh | stale | source-cold | unavailable (see docs/runbooks/agentsview-cadence-ops.md).

Cadence desktop Overview

  • Nav destination — Overview is a built Cadence screen (not a Board morph); reads only through hm overview / hm-core::overview.
  • Regions — Insights, Agent health, Reckoning cloud, Recently landed, spend / session volume / model mix / activity / tooling extension panels; honest empty when the projection or archive is cold.
  • E13·11 — actionable first snapshot CTA, centralized Agent data status, compact previews with disclosure, Pulse/Mixes grids, synced_at-only recency.

Install / release smoke

  • tests/install/smoke.sh asserts hm overview --help documents --refresh and hm overview --refresh succeeds on a fresh Works (unavailable is ok).

Upgrade notes

  • Workspace version is 0.1.23; no config migration is required.
  • Set AGENTSVIEW_DATA_DIR when agentsview's archive is not at ~/.agentsview.
  • Run hm overview --refresh after agentsview ingests new sessions to rebuild the projection cache.

v0.1.22Epic 9: desktop shell, Superintendent runtime, and MCP pack

This release lands the Epic 9 Interface-tier pack: a Principal-facing desktop shell layered over the same hm-core public API the CLI uses, a floating Superintendent runtime that drives Works only through hm, mid-run steer as an execution gate beside Gatehouse land, structured Hand transcripts (Pi + Claude) in Detail Activity, and a catalog-driven stdio MCP bridge. Epic 9 does not fork domain logic — CLI remains the mutation source of truth; desktop and MCP are additional shells. Plan record: docs/plans/e9-*-*.md.

Highlights

Desktop shell layering

  • Three-region Cadence shell — nav · center board · Detail/right inspector over a live Works, with no mock ledger data. Reads go through hm-core public APIs; writes spawn hm --works <path> … (E9·1–E9·4, E9·6–E9·7).
  • Floating Superintendent — a global floating agent panel, not a right-pane tab. Opening context is the same hm herald brief --session projection the CLI prints; every send/action chip runs through hm_desktop::runtime::run_hm_tool (E9·7 in-app runtime + UI refinement).
  • Detail / Changes / Activity — board drill-in, bench diff, and Hand monitor stay read surfaces; the Superintendent panel never owns Hand tokens or a private ledger bus. Seam: crates/hm-desktop/SEAM.md.

Mid-run steer (execution gate)

  • hm run pause <ticket> — SIGSTOP on the recorded liveness PID; PID file kept; run.paused audited; ticket stays latched.
  • hm run terminate <ticket> — SIGTERM then SIGKILL; PID dropped; run.terminated audited; redirect is ordinary hm run dispatch.
  • Desktop exposes the same gate from floating-panel chips and Detail Properties (E9·8). Preserve #4: execution gate lives alongside Gatehouse land, not merged with it.

Hand transcript (Pi + Claude)

  • Detail Activity parses runs/<ticket>.log into structured turns (tool calls, results, assistant text, run summary) when the log is Pi JSONL or Claude stream-json, and falls back to the opaque tail otherwise (E9·9 / E9·9b). Presentation read only — no second capture bus, never fed into the Superintendent panel.

MCP bridge (B1 / rmcp)

  • hm workflow show --json — machine-readable projection of the binary-shipped workflow catalog (schema_version, tools with dotted ids, coarse read_only hints). One catalog source; no hand-mapped tool table (preserve #6).
  • hm mcp serve — stdio MCP server (rmcp, dynamic router) that registers tools from that catalog, filters out hm mcp itself, captures child hm stdout/stderr so JSON-RPC stdout stays pure, and audits each call as mcp.tool.called (actor mcp). Mutating tools still append normal domain events through CLI → hm-core (preserve #7).
  • Architecture authority: research bench docs/reference/research/paperclip/hm-mcp-bridge.md (product plan: e9-mcp-bridge.md).

Operator surfaces & docs

  • hm workflow show catalog refreshed for Epic 9 CLI rows (hm mcp serve, hm run pause / terminate, hm herald brief --session, hm superintendent, workflow show --json).
  • ARCHITECTURE.md Interface tier documents desktop + MCP shells, steer, and transcript seams; design authority remains docs/design/README.md + Cadence HTML.

Still open

  • E9·5 / #69 — no-fork contract test that desktop invokes only hm-core public API — still open. The seam and runtime already follow that rule; the automated contract test is not part of this cut.

Upgrade notes

  • Workspace version is 0.1.22; no config migration is required.
  • Desktop app version inherits the workspace crate version (no hardcoded tauri.conf.json version).
  • New CLI: hm mcp serve, hm run pause / terminate, hm herald brief --session, hm workflow show --json. Existing Works continue unchanged.

v0.1.21Epic 11: org/workspace structural parity pack

This release lands the Epic 11 family of Paperclip parity ports: the org/workspace read surfaces (hm line show, hm org tree, hm bench slots) that make hm's single-tenant Works → Line → slot → binding → worktree axis inspectable, plus the negative guards that keep the fold onto that axis honest. Epic 11 is a structural completeness epic — it proves Paperclip's org nesting (company → project → project_workspace → execution_workspace) is fully accounted for as a blend/fold with a cited verdict per row, not a verbatim schema port. Every backlog row is green or deferred-with-reason under docs/plans/e11-epic-completeness-audit.md and the nine docs/plans/e11-*-parity.md seam docs.

Highlights

Org/workspace read surfaces

  • hm line show <line> — read-only Line workspace boundary: the Line's landing branch, its registered bench slots (each with landing branch + workspace status), and manifest→slot bindings (--json for machine output). This is Paperclip's project_workspace_id FK folded onto the Line seam (E11·2).
  • hm org tree — read-only flattened org tree (Works → Lines → bench slots → manifest bindings, --json), each Line node the hm line show boundary. Folds Paperclip's nested company → project → project workspace navigation onto hm's single-tenant org (E11·3).
  • hm bench slots — reads the declarative slot registry ([[benches.slots]] / [[benches.bindings]] in hm.toml, Works-wide or --line-scoped, --json) with no git walk or mutation — distinct from hm bench list, which walks the bench tree on disk. Paperclip's per-run realizeExecutionWorkspace folded to a declarative registry (E11·8).

Structural guards (fold honesty)

  • Single-tenant boundaryhm works doctorworks.tenancy_leak [ERROR] fires if a folded multi-tenant/path column (company_id, project_id, repo_url, persisted cwd) ever leaks onto disk. hm's Works is single-tenant; those columns are deliberately unported (E11·1).
  • Cross-repo routing guardhm works doctorworks.cross_repo_route [ERROR] fires if a bench_ref escapes its Line subtree. One path family; cwd is derived via resolve_bench_path, never a persisted per-row repo_url (E11·5).
  • Column-fold audithm works doctorbench.column_fold [INFO] enumerates every one of Paperclip's 24 execution_workspaces columns and the Epic 8 absorption ledger (#84–#88 absorbed; #89 land queue explicit non-absorb), so the disposition of each column is witnessable live (E11·9).

Workspace lifecycle & strategy

  • Lifecycle transitions — the isolation-slot workspace record now carries the idle / in_review / cleanup_failed transitions on top of the Epic 8 active → archived baseline, surfaced through hm latch release, hm bench push, hm bench list, and the bench.workspace_cleanup_failed [WARN] doctor finding (E11·6).
  • Provision strategygit_worktree vs project_primary is derived from slot shape in provision_slot_workspace (a named slot worktrees; the Line-default is the shared primary), not a persisted strategy column; visible via hm bench list / hm line show (E11·7).

Touchpoint keying

  • Touchpoint precedence — hm keys workspaces by ticket-step touchpoint, where Paperclip keys by issue. A touchpoint's bench binding wins over the manifest binding at resolution, surfaced in the hm query board BENCH column (E11·4).

Operator surfaces & docs

  • hm workflow show — the hm works doctor catalog row now lists the three Epic 11 guard findings (works.tenancy_leak, works.cross_repo_route, bench.column_fold) alongside the Epic 8 bench/workspace findings; the golden snapshot is refreshed.
  • ARCHITECTURE.md — the workspaces/isolation section gains an Epic 11 structural-parity note (read surfaces + guards), the doctor findings list picks up the three E11 guards, and the parity-plans index adds the Epic 11 family.

Upgrade notes

  • Workspace version is 0.1.21; no config migration is required.
  • The new Epic 11 doctor findings surface automatically on the next hm works doctor. works.tenancy_leak and works.cross_repo_route are [ERROR] and flip report.ok; bench.column_fold is [INFO] only. All three are read-only audits over existing state — nothing new is persisted.

v0.1.20Epic 8: Paperclip parity upgrade pack

This release lands the Epic 8 family of Paperclip parity ports: the workspace runtime that lets several Manifests run parallel hands on one Line without colliding, plus the content revision-history, audit-trail, and work-graph read surfaces that upstream Paperclip already had. Every seam is audited fold-by-fold under docs/plans/e8-*-parity.md.

Highlights

Parallel hands, isolated by slot

  • Per-manifest workspace isolation — each Manifest binds to a named isolation slot from the Line's [benches] registry; dispatch cwd, prime bundle, and gatehouse land all resolve through one bench path, so two Manifests on the same Line never share on-disk state.
  • Durable workspace records — a named slot now carries a lifecycle record (active → idle / in_review / archived / cleanup_failed), surfaced as workspace.opened / workspace.archived events and bench.workspace doctor findings. The Line-default bench keeps no record (matches upstream).
  • Branch-coherence guard — a latched Manifest's worktree must stay on hm/<manifest>. A drifted worktree now fails hm works doctor (bench.branch_mismatch [ERROR]) and is refused at dispatch preflight before any hand spawns. --no-cut-branch dispatch is exempt.

Landing & reclaim

  • Per-slot landing branch — each slot declares its landing branch (slot → manifest → Works → main cascade), seeded on wire. Drifted slots can be backfilled with hm works wire --reseed-landing --bench <ref>; doctor warns on bench.landing_branch_stale.
  • FIFO land queue — concurrent lands onto a shared landing branch serialize behind a per-scope ticket lock, granted in land-request order. A queued land whose producing bench moved is invalidated (Superseded) and re-dispatched rather than pushed stale.
  • Dead-executor slot reclaim — when a detached executor dies without completing, hm patrol run --fix now frees the isolation slot it held and stale-marks its workspace record, so a dead run can't silently exhaust the slot pool. Manifest complete/land archives the slot's record, gated by a close-readiness verdict.

Content, audit & read surfaces

  • Revision history & restore — content writes append a revision only when the content hash changes; the chain is readable and restorable (list / load / restore_document_revision). A restore appends a document.restored audit event to the works.db ledger.
  • Work-graph readhm query board --q <substr> --limit <n> and hm ticket list --q <substr> do a case-insensitive substring match over title + acceptance (+ completion note); hm query events can filter the new restore audit via --kind document.restored or --entity <kind>/<slug>. Limits are clamped (board [1,1000], events [1,500]).

Operator surfaces & docs

  • hm workflow show refreshed for the Epic 8 catalog — manifest status and ticket-progress lines, dispatch --no-cut-branch, and the new Epic 8 doctor flags.
  • DocsARCHITECTURE.md and the build-walkthrough teaching HTML now cover the Epic 8 workspace runtime; the bench-reclaim runbook documents the reclaim/close-readiness flow.

Upgrade notes

  • Workspace version is 0.1.20; no config migration is required.
  • New doctor findings (bench.workspace*, bench.branch_mismatch, bench.landing_branch_stale, bench.landing_aliased) surface automatically on the next hm works doctor. bench.branch_mismatch is the only one that flips report.ok.