# Changelog
0.42.9 β 2026-08-12
- Windows: subprocess output is decoded as UTF-8, not the ANSI code page. Every
subprocess
encoding="utf-8", errors="replace" β 27 sites, no exemptions.
text=True alone uses locale.getpreferredencoding(False), which is UTF-8 on Linux and cp1252
or cp850 on Windows, so any non-ASCII byte in a captured pane produced mojibake, and any byte
undefined in the code page raised UnicodeDecodeError.
The failure was invisible by construction. The exception is raised in subprocess's *reader
thread*, so run() returns returncode 0 with stdout set to None, and the caller dies later
on the None instead. Nothing ever logged UnicodeDecodeError β two separate searches for that
string came back empty and were reported as negatives. The absence of a traceback was the
signature, not a refutation.
errors="replace" is load-bearing rather than merely cautious: bare UTF-8 fixes the mojibake and
still raises on invalid bytes, and capture-pane reads whatever a TUI painted β including a
partial escape sequence at a buffer boundary. session_bridge feeds probe_pane, so a raise
costs the caller its idle/busy/modal verdict and its fail-safe. (#226)
0.42.8 β 2026-08-12
- tmux wakes now submit on Codex.
_tmux_wakesends the payload literally (-l, since 0.42.7)
Enter key events. Codex's multiline composer requires the second
to submit; in single-submit composers the first already sent and the second reaches an empty
composer. Confirmed on Windows: codex CLI submit works. Without this, a literal-but-unsubmitted
wake sat in the pane looking delivered. (#223)
A wake failure now also says the pane may hold an unsubmitted wake, so a partial failure is diagnosable rather than silent.
KNOWN AND UNTESTED, recorded rather than implied away: the "second Enter is a no-op" rationale
holds when a composer has focus. On a numbered menu β the Claude Code rating modal is
1: Bad 2: Fine 3: Good 0: Dismiss β Enter is a SELECTION. _tmux_wake does not probe pane
state, unlike lab-orchestrator's wake_cell, which refuses to inject into a menu for exactly
that reason. Board card #421 carries the two measurements that would close it.
0.42.7 β 2026-08-11
Windows cells were running 0.42.6 and hitting defects already fixed in source. A fix that exists and cannot be installed is, from the affected cell's position, not a fix.
Two distinct defects, different scopes β stated separately because conflating them is how the wrong one gets believed fixed.
- Wake injection was not literal (cross-provider).
tmux send-keyswithout-lis a
_tmux_wake now sends the payload with -l and submits with
a separate Enter. Latent on Linux β real tmux passes an unrecognised key-name argument
through as literal characters; psmux does not. Hit Claude and codex cells alike. (#219)
- Hook commands could not execute on Windows (provider-specific).
swarph hooks addwrote
C:\Users\x\.swarph\hooks\activity-marker.sh collapsed to
C:Usersx.swarphhooksactivity-marker.sh. Every hook a Windows cell installed failed, silently,
at every fire. Now emitted with forward slashes, with a migration for settings.json files
already holding backslash bindings and a third match site so hooks list stops reporting
legacy bindings as absent. (#216)
Also:
ensure_monitor.shno longer starts a monitor under a GUESSED identity β an unset
SWARPH_SELF used to default to a specific peer name, so a cell with no identity started a
monitor as that peer and drained its DMs. Refuses the action, loudly, and still exits 0,
because a hook that can wedge a session is worse than the deafness it prevents. (#217, #360)
MeteredMistralBackendβ the fifth bench lane, wired with a declared dependency. (#191)
Notable changes to swarph-cli. Earlier history: git log.
0.42.6 β 2026-08-11
- feat(spawn): the launcher stamps the cell's identity into the spawned env (#360).
lab-ovh rather than closed. The root sat one layer above
the three known defaults: swarph spawn KNEW which cell it was creating and never
said so, so every cell had to INFER itself from a config file keyed on cwd β and
cwd is not a cell identifier (two Claude-family cells run with cwd=$HOME, where
the "project" settings file IS the user settings file). One _spawn_env_base(cell)
now performs the scrub, the spawn marker and SWARPH_SELF = cell.name, and all
five provider env builders route through it. cell is positional and required, so
a future provider's omission does not compile.
This is inert until env.SWARPH_SELF is removed from ~/.claude/settings.json:
measured, Claude Code's settings env OVERRIDES the inherited process env, so the
package half and the config half must land together.
- fix(spawn): the Windows Terminal fallback bypassed BOTH the billing scrub and the
_relaunch_in_windows_terminal built {os.environ} and
launched the provider binary DIRECTLY, so an ANTHROPIC_BASE_URL /
ANTHROPIC_AUTH_TOKEN in the operator env reached the relaunched cell untouched β
the adversarial-sweep CRIT, alive on the fallback path, reachable on Windows
whenever tmux/psmux is absent. The membrane now hands its env down through one
accessor, passed as a factory because grok/vibe builders create directories.
Found independently by Copilot in review and by path enumeration.
- ci(publish): the tag/version guard checked only
pyproject.toml. A bump that
src/swarph_cli/__init__.py would publish a wheel whose __version__
disagreed with its own metadata. Both declarations are now verified against the tag.
0.42.0 - 2026-08-08
- fix(waker): only addressed question DMs may create a Codex App Server turn (#199).
answer and fyi messages remain in the monitor ledger but cannot advance the
controller cursor, create an outbox authorization, or start an App Server turn.
- feat(waker): package the Windows Task Scheduler installer (#199). The
0.41.9 β 2026-08-07
- fix(tokens): a NAMED cell's credential outranks the ambient one (#190). On a
MESH_GATEWAY_TOKEN cannot be the credential for
more than one cell, so naming a cell selected an IDENTITY WITHOUT CARRYING ITS
CREDENTIAL. Deliberately narrow: the flip applies only when the identity is
EXPLICIT β a defaulted name keeps the old env-first order, so nothing changes for
an operator who names no cell. A named cell whose credential file exists but
yields no usable token now REFUSES rather than falling back to the ambient one:
naming a cell is a decision, not a hint.
- The refusal names the file it tells you to fix. It previously rendered the path
(None) β peer_src is assigned only on a successful read, and
that branch is reachable only when every read failed. The behaviour was correct
and the diagnostic was useless, which is the worse of the two to ship.
- Known gap, carded not fixed (#374): presence is tested with
exists(), so a
- feat(waker): portable Codex App Server controller + Linux supervisor (#193, #195).
Changelog gap, recorded rather than quietly closed
0.41.7 and 0.41.8 shipped to PyPI with no entry here. What they contained:- 0.41.7 β `fix: an explicit SCOPED --token-file must beat an ambient SHARED-ROOT
--token-file to peer identity.
- 0.41.8 β
fix(tokens): skip the POSIX mode warning on Windows(#192).
0.41.6 β 2026-08-05
- feat(board):
cards thread+cards sayβ the CLI half of cardβDM fusion.
GET /board/cards/{id}/thread and the card-gated attach
path since earlier the same day and nothing could reach them: no CLI verb
existed, so the fusion lived in the database and the OpenAPI schema β shipped,
deployed, migrated across ~300 cards, invisible to every human and cell. Found
by trying to write a finding onto a card and having to send a DM instead.
say refuses when a card has no assignee and no --to rather than inventing a
placeholder recipient: the gateway accepts an unregistered to_node with a 200,
so a placeholder is byte-identical to delivery while addressed to nobody.
- fix(spawn): the Windows Terminal rescue is provider-generic. It lived in
ClaudeMembrane.pre_launch β nothing in its body was claude-specific, only its
call site was β so codex and antigravity never inherited it. Those were
exactly the two cells the operator had been launching by hand for months.
ClaudeMembrane.pre_launch is deleted; it collapsed into the base.
- fix(spawn): codex + antigravity now
chdirlike claude/grok/vibe, and pass
-C . / --add-dir . instead of the absolute cwd. Measured failure:
swarph spawn with a cwd of C:/β¦/synced workspace/project folder
died with an unexpected-argument error β the path re-split on
spaces crossing the exec boundary. Codex itself parses that path correctly;
the mangling was ours. Fixing by removing the dependency on quoting rather than
out-guessing which Windows layer re-tokenises.
>>> VERIFICATION LIMIT, STATED: CI is green on windows-latest, but the
suite mocks _launch_via_tmux, _relaunch_in_windows_terminal and os.execve
β the exact boundary this fixes. Windows CI proves the code imports and the
logic holds; it does not prove argv survives execve. A broken fix returns
the same green. Metal verification is outstanding. <<<
- fix(cli): bound the shared pin (
swarph-shared>=0.4.0,!=0.6.0,<0.7) so a
--help shows the
real product surface (#308, #301).
- fix(monitor): land
TmuxNotifySink, which was uncommitted and
git checkout from
deletion.
- note: the three fixes above had been running in production on the lab-ovh
main or PyPI. No version string showed it: swarph --version reported 0.41.5
(a claim the source makes about itself) while the installed dist-info said
0.41.4 and the executing code was neither.
0.39.4 β 2026-07-27
- fix(packaging): ship
scripts/ensure_monitor.sh. 0.39.3 shipped a README
.py
files must be declared in [tool.setuptools.package-data] and it was not.
twine, CI and the build all reported success; a clean-room
pip install --no-cache-dir from PyPI is what caught it.
- test:
test_packaged_artifacts_exist.pyβ a file the docs tell operators
src/ is not enough; the wheel omits it silently.
Verified to FAIL when the declaration is removed.
0.39.3 β 2026-07-27
deploy/monitor/swarph-monitor.serviceβ a silent DM monitor unit. The
tmux send-keys -t "check mesh" Enter on every DM:
it typed into the cell's pane, cost a full turn per message, and worked only
while the pane existed. The new unit runs the monitor `--deliver pull
--foreground` under systemd and pokes nothing.
- SILENT MEANS YOU MUST PULL. Nothing will type at you any more. Wire
swarph monitor status into a SessionStart hook (or call
scripts/ensure_monitor.sh), or a cell that relied on the check mesh
prompt to be woken will simply go quiet.
monitor startis idempotent via pidfile, so a hook calling it is a no-op
- systemd owns the process: a crash restarts within
RestartSec. A
Type=oneshot + timer supervisor was tried first and failed β
KillMode=control-group reaped the detached monitor one second after the
oneshot exited, while logging "started" as a success.
deploy/sidecar/swarph-mesh-sidecar.serviceβ DEPRECATED IN PLACE, not
scripts/ensure_monitor.shβ check status, start if down, report pending.
0.39.2 β 2026-07-26
- fix(mesh):
--token-fileis now one parser. It previously did
read_text().strip() and returned the ENTIRE FILE, so pointing it at an
env-style file β the shape the shipped systemd unit, the README and peers'
docs all document β put comments and unrelated variables into the
Authorization header. An em-dash in a comment crashed a peer's monitor
with a latin-1 codec error. swarph daemon read the same flag through a
different, correct parser: one flag, two parsers, diverging silently
where both happened to work. Now accepts env-style
MESH_GATEWAY_TOKEN= (quotes stripped, comments and blanks skipped,
unrelated keys ignored) or a bare token line, and a test asserts the two
readers agree so the divergence cannot recur at the next call site.
- Not introduced in 0.39.1: that release added a foreground call site,
monitor.log while the parent
exited 0. It made a pre-existing silent failure loud.
- fix(mesh): a token that cannot go in an HTTP header now raises a named
http/client.py β which points the reader at the HTTP
layer for what is a malformed config file.
- test: shipped
*.serviceunits are now parsed in CI, and every `swarph
0.39.1 β 2026-07-26
- fix(monitor): refuse a derived identity that is not a registered peer.
self_name falls back to the state dir BASENAME, so
swarph monitor start --state-dir /var/lib/swarph/droplet-monitor produced a
monitor for the peer droplet-monitor β which does not exist. Nothing can be
addressed to an unregistered peer, so it polled a nonexistent inbox, saw zero
DMs forever, and reported itself running and healthy. Silent deafness
reintroduced through configuration rather than code. Found by peer cell
droplet within minutes of installing 0.39.0.
- Refuses only on a positive "not a peer" answer: unreachable gateway,
start must stay safe to call unconditionally from a hook, and an empty
list is what a half-initialised gateway returns β refusing on it would stop
every monitor in the fleet at once.
- Only the derived path refuses. An explicit
--as/$SWARPH_SELFis a
- The check runs after the already-running fast path, which is the hook path
0.39.0 β 2026-07-26
swarph monitorβ pluggable DM delivery, PULL-first (card #122).- The state split. An observation cursor (what the monitor has READ,
inbox.log rather than
finding its mail already consumed.
--deliver pull(default; ledger advances on ACK),tmux:,
stdout, none. Repeatable β several sinks, independent ledgers.
webhook: exits non-zero naming its gate; a held feature that silently
no-ops rots into a phantom capability someone believes is delivering.
startis idempotent and safe to call unconditionally from a hook (quiet
status exits 0 = nothing pending,
1 = DMs pending, 2 = not running, so a hook can self-heal and notify in one
line. --brief prints nothing when there is nothing.
- Under
--deliver nonestatusreports "unread: CANNOT REPORT", never
- Refuses to share a state dir with a live
swarph daemon. Both write
cursor.json + inbox.log; two writers on one cursor lose or repeat DMs
silently. Detected by a daemon pidfile and by tasks_snapshot in the
cursor, which is what catches daemons already running.
swarph mesh sidecarkeeps working as a deprecated alias (notice on
- fix(mesh): the sidecar read cursor no longer shares a failure mode with
if _tmux_wake(...), so a
dead tmux pane froze it forever and every poll re-selected and re-logged the
same messages, with no backoff and no alarm. Waking is delivery; the cursor
is bookkeeping. Failed wakes are now visible instead of silent.
- fix(cli): a mistyped verb errors instead of billing an LLM call.
swarph inbox (the real verb is swarph mesh inbox) fell through to the
one-shot path as prompt="inbox" and paid a provider to answer a question
nobody asked. Close typos get suggestions; swarph -- still sends a
single-word prompt.
0.38.1 β 2026-07-24
- docs: README
swarph benchsection + this changelog (0.38.0 shipped the
0.38.0 β 2026-07-24
swarph benchβ a deterministic LLM benchmark-pack runner (card #101).bench runβ N-way model showdown on a pack β confusion-matrix report
--strict aborts.
bench validateβ four trust gates: schema/integrity, answer-leak scan,
bench addβ validates + self-registers a pack atpacks/.json
bench pricesβ the full LiteLLM price list (~1500+ models), a shared cache.- Task types: numeric / categorical / ranking / text. Packs are data-only;
[bench] extra pulls google-genai.
- Spec + reference impl by droplet; ported into swarph-cli under review.