Skip to content

fix(tui,web): call an execution of a flow an epic in every label and message (#447) - #448

Merged
futrime merged 20 commits into
release/0.2from
chore/backport-main-to-release-0.2
Oct 14, 2026
Merged

futrime merged 20 commits into
release/0.2from
chore/backport-main-to-release-0.2

Conversation

@futrime

@futrime futrime commented Oct 14, 2026

Copy link
Copy Markdown
Member

Cherry-picks what landed on main since release/0.2 was last brought up to date, for 0.2.0-beta.3. #387, #392, #395, #398, #399, #400, #402 and #403 add nothing to the changelog, since #396 and #404 brought them already. Afterwards release/0.2 holds the same code as main, and only the version files, CHANGELOG.md and release-please-config.json differ.

feat: chat like Claude, no all-sessions log, Exomyth fixes, split profiling, TUI parity (#422)
feat: many chats, each its own epic beside the workspace's flow, folding away when idle (#427)
feat(runtime): let docker and swarm runtimes share a host: name prefix, exec user, running container (#426)
fix(coganchor): let turns working in one mirror at once see each other's work (#423)
fix(flows): keep and carry on supervised sessions, whose CLI runs here in a mirror (#424)
fix(telemetry): scrub reports as bundles are, timings and profiles too, only where started (#439)
fix(coganchor): keep the turns of the shared-mirror test in the mirror at once (#442)
fix(agents): retry Codex's dropped streams and start every harness in a virtual config home (#425)
feat: rewrite specs as api/ and features/, pick the btw agent per epic, refuse cross-machine workspace hooks (#444)
fix(coganchor): build a harness's virtual home on its own machine under afar (#445)

BEGIN_NESTED_COMMIT
feat(runtime)!: say where a harness runs with one setting, not four placements (#446)

BREAKING CHANGE: runtimes take harness instead of affinity, and work on another machine with no setting no longer drops to a harness here when the CLI is missing or a gating hook is hung: set the runtime's harness to local. A saved affinity is still read, as its first entry with self as env.
END_NESTED_COMMIT

🤖 Generated with Claude Code

futrime and others added 20 commits October 14, 2026 16:57
… pre-release (#387)

release-please takes the last release to be the one its manifest names, so 0.1.0's notes
said only what changed since 0.1.0-beta.2. release.yml now opens the release pull request
through release-please's API, held to a lockfile, telling it the line's last release, or for
a `.0` none, so a release's notes cover all its pre-releases did.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d35b69a)
The composer's Options chip stood 22px tall beside an 18px role summary,
fields' rows centred on their labels rather than their controls, and
selects, segmented controls, icon buttons and small buttons each had a
height of their own. Two tokens, --control-h and --control-h-sm, now set
how tall a control stands; rows mixing type sizes sit on one baseline;
and two sizes a later .icon-btn rule silently overrode are restored.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dd78794)
…n main (#398)

release.yml looked it up by its `autorelease: pending` label, which release-please had not
yet added for 0.2.0-beta.2: the lookup came back empty and CI was dispatched on main, leaving
the release pull request without `ci-ok`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ce90040)
… row on one column (#399)

* fix(web): hang every sidebar row on one column

The sidebar's brand, its three links, the workspace folders and the epics each
started at an x of their own: icons at 16, 17, 19 and 29px, words at 36, 39,
53 and 60px. As ChatGPT and Claude lay theirs out, every row's icon now stands
centred in one slot (--side-in, --side-slot) and every row's words start at
--side-text just past it; an epic opened out runs its line under the slot's
middle and its agents' icons where the epic's words start.

A workspace's folder and an epic's mark stand in that slot themselves, giving
way to the chevron that folds them where the row is pointed at or the chevron
has the focus, rather than the chevron taking a column of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(desktop): centre the traffic lights on the sidebar's top row

Tauri's `y` is where the title bar is made to end, and AppKit keeps its
buttons 9 points above that, so `y: 21` stood the lights' centre 19 down,
8 above the logo, the bell and the fold at 27 and the bar's 28. They now
stand at `y: 30`, centred 28 down, and at `x: 19`, the close button centred
on the sidebar's column of icons. The sidebar's top row is as tall as the
bar over the view and centred on the same line, in the app and the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fc0588f)
…ing one (#400)

A dispatched run's ci-ok does not satisfy the branch rule on 0.2.0-beta.2's pull request; the
pull_request run, held for approval as it is on a pull request a workflow opened, does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a4b64a3)
…top row (#402)

The lights' position the app set never took: they stood where macOS puts
them, in the window's top 32 points, with the sidebar's top row beside them
and centred lower. The app now leaves them there, and the sidebar keeps
that strip for them alone, drawing its top row under it, from its left
edge as every row below it is.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 01d54c4)
…403)

The sidebar kept a strip for the lights, but folded away the bar over the
view stood beside them, centred lower, as it did before. The strip now runs
across the whole window: the view is drawn under it too, and moves the
window where it is dragged, and a dialog that fills a narrow window starts
under it. Nothing is pushed right past the lights any more.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dcf9d02)
…filing, TUI parity (#422)

* fix(web): drop Exomyth's arrows and keep its panel's text from overlapping

Exomyth no longer draws an arrow from a span to the process it started,
nor offers the Arrows toggle; Perfetto draws nothing like them.

The panel's facts overlapped in WebKit: a process named over several lines
sits in its fact as a button, an inline-block whose baseline is its last
line, so the facts' baseline alignment drew it up over the fact above.
It is top-aligned now. The chart's tooltip, kept only inside the window,
also lay over the panel after a click; it is kept over the chart now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): tell nothing of the epic you are on, and mark unread epics in the sidebar

A notification of the epic the page is on, while it is looked at, is read as it comes: no
card and no system notification. An epic with one unread has a mark in the sidebar, as does
its workspace while folded; opening the epic reads them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): drop the log of every session and the sidebar's Exomyth row

An epic's sidebar tree lists only its agents and their sessions: the
"Log" row every agent and person appeared on, and the "Exomyth" row, are
gone, as is "Open log" from the epic's menu. Exomyth opens from the
epic's menu or the chart in its bar. `#/epics/<name>/logs` with no agent
now opens the epic's graph; per-agent and per-person logs and sessions
stay. The transcript's shared mode, which only that log used, goes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: profile the flow and its environment apart in the web page and the TUI

The New epic form's Options carries two boxes, Profile flow and Profile
environment, and the /flow roles page two switches, profile flow and
profile environment, each remembered per flow, carried on resume and sent
with the start as profile_flow and profile_environment. The epic panel
says which of the two an epic was profiled for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): open nothing on Flowverses until chosen, and list no local or user

The Flowverses tab no longer shows the Add flowverse form by default: a
flowverse's detail, or the form adding one, appears only once its card is
chosen, and choosing it again closes it. A `?verse=` address still opens
that flowverse. `local` and `user` are places read where they stand rather
than indexes, so they are no longer listed among flowverses; they remain
under Installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: format the flowverses unit test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): say a chat running only while its agent answers, and each turn done as a notice

A chat epic's row and detail say how=running while a turn is open and
done otherwise (stopped, left or over, short of failing). The host's
status and run standing carry turn: running|done for chat, done where it
asks what to say next. Notices tell each finished chat turn as done and
never a chat stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): find /epics by status, flow, workspace and task, a page at a time

/epics now has filter rows above the list (status, flow, and workspace on
this machine), each a dropdown with counts, beside the `/` search, and pages
of 50 behind Newer and Older buttons, as the web's search dialog has.

How an epic went and whether a search finds it move into hmz.runtime.epic
(`ending`, `found`, `ENDINGS`), which the web's /api/runs and the TUI share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): read a chat as Claude Code reads one conversation

A chat is one agent and one conversation, so the TUI drops everything that
is about a flow of several agents while one runs: no monitor on left, no
views to round on tab/shift+tab, no outworlder line above the prompt, no
Exomyth for its epic, and nothing about stopping. The status line says
running while the assistant answers and done once it waits for you.

ctrl+c clears the line and, on an empty one, twice leaves, as in Claude
Code; /stop is refused; /clear ends the chat so the next line starts
another; /exit and ctrl+q leave without asking, a host keeping a chat it
holds.

Tests that exercised flow-wide behaviour through chat now run chat's own
code under another name (TALKS), which the interface reads as any flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): usage page, download from backups, smoother import/export, flow-update count

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): give Exomyth in the terminal what the web's has

Exomyth in the TUI now does what the web page's Exomyth does, all from the
keyboard:

- `v` picks a range from one span to another. Press it again to hold the
  range and a third time to drop it. The pane shows the range's spans, time
  and tokens by category, and `f` zooms to the range.
- `p` opens a pane on the process of the row under the cursor.
- The flame is built like the web's: one stack per agent, flow or program,
  then its spans as they nest. `m` puts sessions apart or merges them.
  `t` cycles the weight through time, tokens in, tokens out and count.
- A Slices tab lists every span name with its count, total, self, avg,
  p50, p95, max, in and out. `o` changes the sort column, `O` reverses
  it, and `enter` finds the slice on the timeline.
- `tab` and `shift+tab` go round the three views.

Lanes of any process are drawn and weighed the same way, so a lane of
nested slices, such as a sampled flow, shows without special code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web): draw a chat as its conversation alone, as a chat app does

A chat epic is opened on its conversation wherever it is opened: no graph, no
agents or sessions under its sidebar row, no Exomyth, Log or People, and no
Stop. It is said only as running while its agent answers and done otherwise,
from the live `turn` where the server says it. A new epic started in its
workspace ends the chat going there, as a new chat leaves the last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): open the epic from the monitor, and stop or force it there

The monitor gains a `◫ epic` node, under the all-agents one, saying how the
epic stands and its budget against what has gone. Enter or a click opens its
page, the web's epic panel in the terminal: figures (time, spend, tokens),
the budget limit by limit with a meter, what it was set up with (flow, task,
agents, environments, params, whether profiled) and the flows called, live.

For every flow but chat, the page has a `Stop` button while the epic runs and
`Force stop` while it is stopping, each asked first in a box and only acting
on the epic it was asked about. ctrl+c and /stop work as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(web): a chat on another machine tells each turn done, not its stopping

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): tell of this machine's epics as notifications, and dot them in /epics

The TUI now tells, as the web interface does, of every epic on this machine
that ends, asks, or waits for its next chat turn -- a toast and, unless the
system is turned off, the terminal bell and an OSC 9 system notification --
never of the epic on the screen. Each epic told of is dotted in /epics until
it is opened there, and a Notifications page in /settings switches the same
kinds and system setting the web interface's does.

What is news is worked out once: hmz.web.notices moves to
hmz.runtime.watching.notices, with the notification preferences, so both
interfaces share it; the web routes for /api/notices move to
hmz.web.notifications.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): drop the all-agents log; land a run on its first conversation

No view shows every agent's lines at once any more. The monitor's first row
is a line of how far the epic has got rather than a node, and tab/shift+tab
round only the conversations running and the outworlders. A run lands on the
agent of its first conversation as it opens, with its task put there; before
then the interface's own (home) transcript is up. A question part of no
conversation is shown, and answered, on the view being read; /stop and /afk
work from any view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tui): download one epic at a time, mark here by directory, read usage of its days only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): read an epic back as its graph, sessions and transcripts

`/epics` rows now say an epic's agents, spend, duration and backup
targets, as the web lists them. An epic's sheet gains a `graph` row: its
agents drawn as the monitor draws them, turns and handovers read back
from its logs, its sessions under each agent, and the tree of flows it
called; enter on a session reads what it did, searchable. The graph's
button and the monitor's `ctrl+e` (or a click on `Exomyth` above it)
open Exomyth, never for a chat.

The transcript reader moves from `hmz.web.runs` into
`Epics.transcripts`, which the web and the terminal both ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): daemons page, /epics @daemon and an m menu on every list

The TUI reaches other machines' daemons as the web page does: a Daemons
page in /settings lists Local and every SSH daemon with how it stands,
adds, edits, imports, connects and removes them; `/epics @<name>` (or the
list's Daemon… button) lists that daemon's epics through its own web
interface, each exported here, backed up there or copied. `m` on a row of
epics, flows, flowverses or daemons opens that row's menu, the keyboard
equivalent of the web page's right-click menu. A chat epic offers no
Exomyth.

What the daemons are and how one is reached moves from hmz.web into
hmz.runtime.doing.daemons (Hmz.daemons), so both frontends share it; the
web routes keep their behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: profile a flow's own Python apart from the programs its agents start

`--profile` is split into `--profile-flow` and `--profile-environment`, and
`profile` into `profile_flow` and `profile_environment` on `hmz.run`,
`Runner`, `Epic`, the daemon's `start`, run status, `Ran`, settings and the
web API.

The environment is the psutil profile it was (`profile.jsonl`). The flow is
sampled with pyinstrument on the thread and task it runs in, written as
`flow-profile.jsonl` (`t`, `dt`, `stack` per stretch), and drawn as a
`python` process under the epic: one `flow` track of `sample` slices, each
frame nested in what called it, in the Exomyth timeline and the Chrome trace.

The TUI's single switch now drives `profile_environment` until its own
controls land; the web page is left to its own change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: sort imports after merging the profiling frontends

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: end the web walkthrough's chat with /clear, and name all nine settings pages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tui): batch apply settings to other daemons, and start or follow an epic on one

The daemons page offers Batch apply, writing this machine's general, fallback, runtimes and
backup settings to the daemons picked as the web page's Batch apply does. /epics @<name>
starts a new epic there with n, and follows one going there from its menu, through that
machine's web interface (hmz.daemon.link.remote over Remotes.followed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(daemon): a remote link asks that interface and is told its stream

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: type the stand-in URL striker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(tui): reach batch apply and refusals through the layers the terminal may name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(tui): read a stopped epic's row whichever line it wraps on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): find a chat between turns as the epic going, not only one running

A chat whose turn is done is listed done, so #/live looked for a running epic and never named
it; it now takes the epic a host holds first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: settle what review found in the batch

- a new epic started on another daemon from the terminal carries the profiling and backup
  that directory remembers, rather than writing them off
- the terminal reaches other daemons through one reach, however many threads ask first
- a download from a backup lets go of its sheet whatever goes wrong
- a new chat from the web replaces the chat a host holds, not merely the newest epic, and
  starts once that one has unwound
- the epic's page says which of its parts are profiled, and nothing of the old flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(tui): a chat lands on its conversation, read without a reading line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: let the docs build, and read a pasted path whichever line it wraps onto

A line of the backup download's reference began with <target>, which VitePress took for an
HTML block; a long temporary path wrapped the pasted one on CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit d863c64)
…ing away when idle (#427)

Every chat now runs on a host of its own (slot `chat:<hex>`) beside the
workspace's own host, which still runs one flow at a time: a chat never
ends or blocks a flow or another chat. A chat's host that nobody reads
between turns for 60 s (`hosting.FOLD`) folds the chat away and goes;
saying something in it starts `chat` with `resume=<epic>` on a new slot,
which reopens the same epic and carries the assistant's session on.

- daemon: hosts keyed by (workspace, slot) through hello, attach,
  running, host, list, remote; `where.slot`/`where.chat`
- runtime: per-slot `Hmz.host(slot)`; `Host` folds, says its slot and
  epic; a chat picked up reopens its epic (`reopened` event); chat flow
  resumable, keeping its conversation as it ends
- notices keyed per host, carrying `slot`
- web: `/api/held/*` take `slot`; a chat started without one gets its
  own host; rows carry `slot` by the epic each host reports; new.js no
  longer stops a chat; folded chats continue from their composer
- tui: `/clear` starts another chat without ending the last; flows run
  beside chats; `/epics` offers `continue` on a chat's epic
- specs and docs updated

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 881711d)
…x, exec user, running container (#426)

* feat(runtime): let a docker or swarm runtime name its containers and services (name_prefix)

A docker or swarm runtime can now set name_prefix. Its containers or
services are then named <name_prefix>-<role>-<8 hex> instead of
humanize-<runtime>-<role>-<8 hex>, for a daemon or swarm shared with
others whose rules ask for names of one's own. A runtime that sets
none names them as before.

- store: DockerRuntime and SwarmRuntime take name_prefix, refused
  where it is made unless it is one word of letters, digits, dashes
  and underscores from a letter or digit to a letter or digit, of at
  most 54 characters, so that a swarm service's name with its 8 hex
  digits stays within the 63 docker takes.
- environing: one helper builds every container, service and job name
  in place of four copies; Apple container and Slurm names are
  unchanged.
- tui: correcting a docker or swarm runtime on its form keeps its
  name_prefix, which the form has no row for.
- specs/coganchor/machines.md and specs/runtime/flowing.md say so, as
  do the reference docs and the containers guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(coganchor): docker:// targets that name the exec user, environment and a wrapper

A docker target may now say, after a `?` ahead of its endpoint, the user
every command on its road runs as (`user=UID[:GID]`), the variables each
runs with (`env=NAME=VALUE`) and the words of a wrapper each runs under
(`exec=WORD`). They reach `docker exec` as `-u`, `-e` and words after the
container name, in the order written, and survive `Target.describe()`, so
an anchor started from the target's spelling reaches the container the
same way.

A target naming a user keeps the archive in `/tmp/humanize-<UID>` and the
mirrors in `/tmp/humanize-mirrors-<UID>`. The far side's shell makes each
one and refuses it unless it is a directory (not a link) owned by that
user, then makes it 0700. Forgetting a container forgets every spelling
of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(runtime): work in a running container a docker runtime names, without starting or stopping it

A docker runtime may now name a container already running on its daemon
(`container`). Every environment put on that runtime then works in that
container: nothing is started, locked, counted against what the runtime
may hand out, labelled, reaped or stopped. Bringing an environment up only
checks that the container is running (`docker inspect`), and refuses one
that is not with EnvUnavailable. What the environment was given is what
the container's probe says it has, and closing it removes only its own
mirrors, which carry 8 random hex digits so that environments sharing the
container never remove one another's.

The runtime may also say who every command in an environment's container
runs as, with what variables and under what wrapper (`exec_user`,
`exec_env`, `exec_wrapper`), for a container of its own too. They reach
the container through the docker target's `user`, `env` and `exec`
options. Beside `container`, a runtime may not say an image, an OCI
runtime, run arguments, a name prefix (the container is named already),
amounts to hand out or a container limit. The container's name is the
one the runtime gives; a container of its own is still named by the
shared helper, under the runtime's name prefix where it sets one.

Checking such a runtime asks only whether its container is running. The
terminal's docker host form keeps the new fields when a runtime is
corrected there, and its row says which container a runtime works in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 555c95a)
…r's work (#423)

* fix(coganchor): write a mirror's record whole, for a turn starting beside another

A mirror's guard was written by truncating its record and filling it again, and every turn
writes it as it starts. A turn starting while another was writing it read no record, found the
directory full of files, and was refused as somebody else's: `already contains files and is not
an humanize mirror`, exit 1. A placement does not pass `force`, and on a real swarm one turn
was refused so in 8 of 40 rounds of three turns starting in one mirror. The record is now
written beside itself and moved into place, through `atomic.writes`, so a reader finds the old
one or the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(coganchor): share a mirror's bookkeeping between the turns working in it

Several turns of one run on one environment work in one mirror at once, each its own
`hmz internal anchor` with its own `ShadowTree`, and a tree that knew only what its own turn
did took the others' work for something to put right. Its first look at a directory swept away
a file another turn had written and not pushed yet, as one the target lacks; and made a
placeholder over one another turn had pushed or fetched, which that turn then read as zeroes or
pushed again at its next flush as its own edit, zeroes and all. Three turns at once against a
`local:` target, each writing two files with a command between, lost or zeroed at least one of
the six in every one of 30 rounds; on a Docker swarm a file one of three turns wrote came back
zero-filled.

What a tree knows -- each file's record and which were written here -- is now kept for every
turn in the mirror: each appends its changes to a journal beside the mirror's guard and reads
the others' from it, under a lock it holds while it changes the mirror. Every turn in a mirror
is then one tree, as every process of one agent always was, and a flush before a listing pushes
whatever any of them wrote. A turn starting alone begins the journal again, as a tree always
began knowing nothing; where the journal cannot be had, the turn works alone and says so.

Two windows the lock does not close are closed where they are: a file written after the flush
that precedes a listing, which another turn's write now lands in, is left for the next flush
rather than deleted, made a placeholder or fetched over; and a push records the file as it was
before it was read, so a write landing while it is sent is pushed too rather than taken for
what went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9b24c7b)
…e in a mirror (#424)

* fix(coganchor): carry a Claude conversation into the mirror a supervised fork runs in

A Claude Code agent whose turns land on another machine, but whose Claude
runs here supervised in this machine's mirror of that machine's work, keeps
its conversations here, in the run's sessions directory. Yet `_carry`
raised `NotImplementedError` for any agent with a machine, so neither a
fork into another directory nor a recalled conversation could be carried
on, although Claude's forks are documented to go into another directory.
And past that refusal it would have copied the transcript under the
project directory for the target's own path, where no Claude here looks.

- `SessionBase._mirrored(anchor, where)` is the mirror arithmetic
  `_workspace()` already did, factored out.
- `ClaudeCodeSession._carry` copies the conversation under the project
  directory of the directory Claude runs in for the fork's cwd: the cwd
  itself without a machine (unchanged), its mirror for a supervised agent.
  Both spellings are written atomically, and a copy already identical is
  left alone. A live conversation is looked up first where Claude ran it.
- An agent whose Claude runs on the other machine, natively or under a
  harness put elsewhere, is refused with `NotImplementedError` as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(flows): read a turn's NotImplementedError as UnsupportedOperation

What a CLI has no way of doing is refused with `UnsupportedOperation` when
a session opens -- a fork of a harness that cannot fork, a fork into
another directory where the CLI forks only in place. When coganchor
refuses the same thing during a turn, as a fork's first turn brings in the
conversation it carries on, `harness_error` read its `NotImplementedError`
as the `RuntimeError` it subclasses and raised `SessionError`. A flow could
not tell "this harness cannot do that here" from a session gone wrong.

`harness_error` now maps `NotImplementedError` to `UnsupportedOperation`,
ahead of the `RuntimeError` branch. Every other `RuntimeError` is still
`SessionError` (a copy of the conversation already held, among them), and
`OSError` and `ValueError` keep their leaves. Both classes are
`HarnessError`, so a flow catching that is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(flows): keep and carry on sessions whose CLI runs here in a mirror

A session whose turns land on another machine but whose CLI runs here,
supervised in this machine's mirror of that machine's work, keeps its
conversation in the run's sessions directory here, like one working here.
Yet it could not be kept in a flow's state, and a kept conversation could
not be carried on through it: every placement with a machine was refused
as if its CLI ran on that machine.

- `HarnessSession.keep` and `HarnessDriver.build` refuse a kept
  conversation only where the CLI runs on the other machine -- driven
  natively, or under a harness put elsewhere (`_afar`, the same test
  coganchor keeps its sessions by). The carry-on is checked against the
  machine as `_harnessed` settles it, before anything is built or copied.
- A harness that forks only in place (Qwen Code, Grok, pi, omp, the ACP
  CLIs) finds a conversation by the directory it runs in. Run here for a
  container's or a job's work, that is a mirror named for the environment,
  which no earlier run's conversation was had in. Such a carry-on is
  refused when the session opens (`_apart`), with `UnsupportedOperation`
  and before the CLI starts, rather than by the CLI at its first turn. A
  mirror at the work's own path (ssh) is left to the CLI, as a session
  working here is.
- specs/runtime/flowing.md: the read-back refusal is for a turn whose CLI
  would run on another machine, or a harness that forks only in place in
  a mirror apart from the work, rather than for any turn working on
  another machine.
- docs: sessions in state, both refusal tables, the `UnsupportedOperation`
  leaf, and remote-execution's limitations.
- tests: unit tests for keep and carry-on per placement, a Codex fork in a
  mirror, and an integration test that keeps a session on a docker
  environment's work in one `hmz exec` run and carries it on in another.

Builds on the two commits before it: Claude carrying a conversation into
a mirror, and a turn's `NotImplementedError` read as
`UnsupportedOperation`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 28c2af2)
…o, only where started (#439)

- One credential scrubber, telemetry.scrubbed, for error reports and exported bundles: ghs_,
  glpat-, hf_, JWTs, Bearer tokens and named secrets are now struck from reports as well.
- Transactions get their own before_send_transaction: spans' code paths named as frames are,
  subprocess spans called "A command" without their cwd, every string scrubbed. Profiles go
  through a transport that renames their frames and scrubs them before sending.
- crash() and snag() no longer start reporting: only a process that started it (hmz, hmz exec,
  a daemon host, Hmz().reports()) reports, as the docs say.
- hmz update prints an install URL without its userinfo, query or fragment.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5dff35f)
…r at once (#442)

The test of three turns working in one mirror at once failed now and then on Linux CI with a
mirror file left zero-filled while the target had its content. Those turns did not always
overlap. Each turn starts in about 150 ms and is done about 15 ms later, so a turn that started
a little ahead could finish before the others began. The next turn then started alone in the
mirror. As designed in #423 (and covered by the unit test
`test_a_turn_alone_in_a_mirror_knows_nothing_of_turns_gone`), it began the journal again and
made placeholders of the files the earlier turn had left. Those placeholders are fetched on
read, so nothing was lost, but the test reads the mirror's raw bytes.

Starting turn 0 a second before the others gives the CI failure every time
(`'\x00\x00\x00\x00\x00' == 'one 0'`). Each turn now waits until all three have written their
first file before it goes on, so all three are always in the mirror together, which is what the
test claims. With the same one-second head start it passes, and it passed 200 runs of the file
in a Linux container, eight at a time.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 34f3714)
… a virtual config home (#425)

* fix(agents): read Codex's dropped response stream as dropped, not unrecoverable

When the provider's response stream breaks partway through a turn, Codex
says `stream disconnected before completion: ...` around whatever the
transport said, and while it retries by itself it reports
`Reconnecting... n/5` under `codexErrorInfo.responseStreamDisconnected`.
None of the shared signatures matched either, so the turn's fault stayed
"": the session gave it no more tries than its place asked for (none by
default), and the flow saw HarnessUnrecoverable.

Add both to the end of the shared `dropped` signatures. They go last
rather than on Codex's profile because Codex files every failure it
retries under `responseStreamDisconnected`, with that failure's own words
beside it, and wraps some provider errors in the same sentence: a rate
limit or a 503 inside them must still be read as what it is. A stream
that breaks with nothing more specific is now `dropped`: tried again in
the session, and HarnessDropped if that fails too.

A line that names `content_filter` is left out: Codex reports the
provider's content-filter stop in the same sentence, and a response the
provider refused to finish is not a broken wire, so it stays
unrecognised as on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agents): start claude and codex in a virtual config home

Claude Code rewrites `.claude.json` with every project it is opened in, and
Codex's app server writes `projects."<cwd>".trust_level` into `config.toml`
for every workspace it opens a writing thread in. Run against the user's own
home, each was a change to their configuration that outlived the run.

Each process of a CLI whose Profile opts in is now started with its home
variable naming a directory built for that process, on the machine the CLI
runs on, and removed when it stops (`hmz.coganchor.homes`):

- what the CLI rewrites (`Profile.rewrites`) is copied from where it would
  have read it (`Profile.outside` while the variable is unset; a provider's
  copy where its account answers the path); a directory starts empty;
- everything else of the user's home is linked, so credentials, caches and
  sessions are the real ones; session paths not there yet are made in the
  user's home, and glob-named ones the CLI made are moved there on drop;
- the user-level skills dir (`Profile.lodges`) holds links to the user's
  skills with the flow's beside them, replacing the per-session copy into
  the workspace (`Profile.mounts`) for these backends;
- `Profile.secures` keeps Claude's keychain item and `.credentials.json`
  where the user's own home has them (`CLAUDE_SECURESTORAGE_CONFIG_DIR`).

A supervisor's credential and session swaps are said under the virtual home
too. A fenced agent may write where its homes are built. Under a native
anchor the anchor builds the home on the target (`AnchorConfig.home`,
`--home JSON`, a POSIX shell script), around a projected account without
any of the target user's credentials. Hooks and the flow's MCP server still
reach Claude through `--settings`/`--mcp-config` and Codex through `-c`.

claude: rewrites `.claude.json`, `backups`, `settings.json`; skills under
`$CLAUDE_CONFIG_DIR/skills`. codex: rewrites `config.toml`; skills under
`$CODEX_HOME/skills`, the server restarting when the agent's skills change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agents): give qwen, grok and pi a virtual config home instead of writing the user's

Qwen Code was pointed at a settings file of humanize's through
QWEN_CODE_SYSTEM_SETTINGS_PATH, replacing the machine's system layer. Grok
Build had a hidden [model.hmz-*] entry merged into the user's own
$GROK_HOME/config.toml for a gateway account, and pi had a humanize-*
provider added to the user's ~/.pi/agent/models.json, both under a lock and
taken back out on stop.

Each now starts a process at a time in a virtual home (hmz.coganchor.homes):

- qwen: rewrites settings.json; the effort, the PRE_TOOL_USE hook table
  (after the user's own, with disableAllHooks false) and the humanize MCP
  server are merged into that copy at the user level, and the headless
  defaults only where the user's file says nothing of them. Neither system
  variable is set any more; a user's is only watched. A settings.json that
  is not plain JSON fails the turn.
- grok: rewrites config.toml; the gateway entry goes into the copy. The
  grok-config.lock, _withdrawn and GrokBuildAgent.stop are gone. A gateway
  account under a harness on another machine is refused up front, as under
  a native anchor.
- pi: rewrites models.json and settings.json; the gateway provider goes
  into the copy. .pi.models.lock, _withdraw and PiAgent.stop are gone.
- qwen, grok, pi and omp lodge the flow's skills in the home's user-level
  skills directory instead of the workspace's .agents/skills (pi had none;
  PiAgentConfig.skill_paths stays as --skill).

A harness on another machine (afar) still gets no virtual home and no flow
skills; the gap is documented in docs/reference/agents.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agents): lodge flow skills at user level for agy, mcode, cursor, kimi and opencode

agy, cursor-agent, kimi and mcode are started in a virtual config home
that carries the flow's skills beside links to the user's own; agy's is
named with --app_data_dir (a new Profile.home_flag), and humanize's agy
agents are written into that home's agents/ rather than the user's.
mcode adds a LiteLLM gateway's provider to the virtual home's copy of
config.yaml, so the account's own is never written. opencode and mimo
are told the flow's skill paths under skills.paths in their config
variable. None of them gets anything in the workspace any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: format the merged skills test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(agents): drop the workspace skill mounts no backend uses

Every backend now takes a flow's skills at the user level (a virtual
home's skills/ via Profile.lodges, or opencode/mimo skills.paths), and
none sets Profile.mounts, so the path that copied them into the
workspace was dead. Remove Profile.mounts, skills.mount/unmount/carried
and Mounted, SessionBase._mounts, AnchorConfig.carries and --carry with
the anchor's claim-file carrying, the skill-name-taken snag, and the
tests that kept it alive with a fake mounting backend. Docs now say
nothing is ever put in the workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agents): write back credentials a CLI replaces inside its virtual home (#443)

A CLI that saves a file by writing another and renaming it over the name
replaced the link in its virtual home with a file of the home's own, so a
refreshed token (codex/grok/pi auth.json, claude .credentials.json, kimi
credentials, ...) or a session file it replaced went with the home.

Home.drop now writes each link so replaced back to what it pointed at, or
to the account's file a supervisor answers that path with: atomically, with
the mode the real file had, a directory a new or changed file at a time,
and never over a real file as new or newer. Rewrites copies still go. On a
native anchor's target, SCRIPT lists its links beside the home and BACK
does the same before the session is swept. agy's own agents/ directory,
which it puts in place of the link deliberately, is not written back.

Skills sent to a native target are written a file at a time beside
themselves and renamed into place with their owner's mode bits, so a
script stays executable; one that cannot be written whole is removed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zijian Zhang <35801754+futrime@users.noreply.github.com>
(cherry picked from commit 2efd879)
…c, refuse cross-machine workspace hooks (#444)

* docs(specs): add features/btw.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): draft features/epics.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/placements.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/chat.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add specs README and api/cli.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): number epics clauses in three levels

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/agents.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add api/sdk.md (WIP)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add api/flows.md, the flow API in normative clauses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/web.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/environments.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): tighten api/flows.md wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): tighten placements and environments wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): draft features/monitoring.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): resolve review findings in environments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/accounts.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): draft features/backups.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add api/settings.md, tighten cli clauses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add api/daemon.md (WIP)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/daemons.md and features/desktop.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/tui.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): draft features/telemetry.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add features/updates.md and tighten wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(web,tui): pick the btw agent in the panel and keep it with the epic

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): add api/http.md and tidy api/daemon.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): resolve review findings in cli and settings specs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): align S7 feature specs with code after review

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agents): refuse workspace config that would run code on another machine than the env's

A CLI starts the programs its project names (hooks, stdio MCP servers, helper
commands, plugins) where the CLI runs. Supervised and third-machine (afar)
placements run the CLI apart from the env's work, so such a program would run
on the harness machine rather than the env's.

Each CLI's Profile now declares, as data, the project files and keys that can
start a program (`Profile.launchers`). `hmz.coganchor.crossing` reads them off
the env's machine, in the session's directory and each one above it up to the
workspace. When a session opens, moves or carries on, and before each turn, a
supervised or afar session whose project sets any of them is refused with
UnsupportedOperation, naming each file and key. Local, native and
harness-on-the-target placements are not asked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): specify every file hmz creates, under specs/api/files/

Seven documents in the shared clause format: the home, a workspace, epic
directories and the .hmzepic archive, the machine and cache directories, harness
homes (virtual config homes, accounts, sessions kept in epics), the macOS app,
and targets (envs, bundles, mirrors, containers, per-turn homes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): replace specs with api/ and features/, align them with what was built, and say epic in the UI

Remove the old specs, which mirrored src/hmz and constrained internals. Align the new btw,
cross-machine and TUI clauses with the behaviour #433 and #437 implement, list each CLI's
program-running workspace configuration, point the docs and the stack diagram at the new
files, and rename the /epics rows and messages that still said run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): resolve review findings: contradictions, duplicates, leakage, wording

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(specs): say which harnesses #425 gives a virtual home, and that replaced links are written back

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 11816bb)
…er afar (#445)

A harness put on a machine of its own (`AnchorConfig.harness`,
`elsewhere.afar`) got no virtual config home and none of the flow's
skills, so the CLI there ran in that user's real home and could rewrite
its configuration (`~/.claude.json`, Codex's trust in `config.toml`),
against specs/features/agents.md §5.2 and api/files/harness-homes.md §6.

- homes: a home's managed items are data (`Virtual.merges`), applied by
  `build` wherever it runs through functions drivers register with
  `homes.merger`; a merge that replaces a link is never written back.
- qwen (effort, hooks, MCP server), grok and pi (gateway entries) and agy
  (its agents) say theirs from a session's `_merges` instead of writing
  into a built home; `AgentBase.virtual` says what a process's home is
  made of, `homed` builds it here.
- Under afar the anchor carries that spec (`AnchorConfig.home`, the
  session's skills and merges); `afar` puts the skills on the harness's
  machine under its cache by digest, and the `hmz internal anchor` there
  builds the home around the CLI it supervises, keeps it there (paths and
  programs), and removes it when the CLI exits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 6333ed5)
…lacements (#446)

A session's placement is now two plain settings: its env location (`local`, or
`standalone` on any other machine) and its harness location (`local` here,
`env` natively on the env machine, or `standalone` on a harness runtime). The
four named placements -- local, supervised, native, afar -- are gone from the
specs, the code and the docs.

A runtime's ordered `affinity` is replaced by one `harness` field: `local`,
`env` or `<backend>:<name>`, defaulting to `env`. Nothing falls back any more:
where the setting cannot hold the harness -- the CLI is missing, the fence
cannot be held, the runtime cannot be reached, or a PreToolUse or
PermissionRequest hook would gate a CLI on another machine -- the session, and
before the flow the whole epic, is refused with
`<where>: <cli>'s harness cannot run where its harness setting (<setting>) says: ...`.
Work on no saved runtime is `env`. Each session's epic line records `env`
where it recorded `self`.

BREAKING CHANGE: runtimes take `harness` instead of `affinity`, and work on
another machine with no setting no longer drops to a harness here when the CLI
is missing or a gating hook is hung: set the runtime's harness to `local`. A
saved `affinity` is still read, as its first entry with `self` as `env`.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit be7e586)
…message (#447)

* fix(tui,web): call an execution of a flow an epic in every label and message

Rename the /epics rows and Export sheet, the /epics and /resume descriptions, the
/resume refusals, the budget rows and sheet, the afar prompt, the monitor's environment
about, the General page summary and the web server's refusals that still said run.
Update the tests and docs that quote them and the tapes that wait on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(demo): re-record the casts that show a label renamed to epic

Step past the graph row on the way to export epic in epics.tape, which stopped on graph
since that row was added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): keep the refusals specs/api/http.md quotes as it words them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): say epic in the refusals specs/api/http.md quotes, and in the spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: keep main's harness and btw docs that the merge resolution dropped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3de80e8)
… release

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 14, 2026 09:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@futrime
futrime merged commit 33ed4dd into release/0.2 Oct 14, 2026
9 checks passed
@futrime
futrime deleted the chore/backport-main-to-release-0.2 branch October 14, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants