Repository navigation
v0.14.0
Added
-
--remote-controlon the sessions chela opens for a human. Windows started by
spawn_window(Telegram/new, the dashboard launcher, resumed sessions) and the
orchestrator persona now launch with Claude Code's--remote-control <project>, so they
are reachable from claude.ai too. Dispatched agents, reworks and judges never get it.
Default on; setCHELA_REMOTE_CONTROL=falseto disable. (CMX-375, #526; guards hardened
in CMX-376, #527) -
A
warmdashboard theme (Settings > Appearance): warm greys, an off-white accent,
Okabe-Ito sky/orange status colours, and a matching warm terminal palette whose 16 ANSI
colours each keep their own hue family. (CMX-381) -
Settings → General → Remote Control: an on/off switch for
--remote-controlon new
sessions.CHELA_REMOTE_CONTROLwas an import-time constant, so turning it off meant
editing the env and restarting the dashboard, Telegram bridge and daemon. It is now
config.remote_control_enabled(), read per call throughdashboard_setting()— env beats
~/.chela/config.json(remote_control) beats the unchanged ON default — so a toggle in
the dashboard reaches every process without a restart. It applies to NEW windows only
(dashboard launcher, Telegram/new, the orchestrator persona); a running session keeps
the flag it launched with, and dispatcher agents/judges still never get it. When
CHELA_REMOTE_CONTROLis set, the switch shows the effective value, is disabled, and says
so.examples/chela.envno longer exports it by default, since an exported value locks the
switch. (CMX-382) -
A real-browser suite for the dashboard's layout, status shapes and fonts
(tests/browser/, Playwright + headless Chromium). jsdom has no layout engine, so the
CMX-377 restyle's cascade-emulating guards kept missing one more CSS way to break the look
(grid-row: 2,.appnot a grid,opacity: 0,scale(0), a transparent ring border,
an inset shadow filling the idle ring). The new suite boots the realindex.htmlshell
against stubbed API fixtures (no daemon, no tmux, no network) and asserts what a user
sees: bounding boxes at 1440×900 and 390×844, each status mark's greyscale painted
silhouette (same in the sidebar row and the pane header, four states pairwise different,
never empty), and that Geist actually loads.npm run test:browserlocally; CI caches
Chromium and runs it insidepytest, whereCHELA_REQUIRE_JS_TESTS=1makes a browser
that cannot launch a failure. (CMX-383) -
Ctrl+, (⌘, on macOS) opens and closes Settings, like VS Code — from the dashboard
and from inside a focused terminal pane (the same pane shim that carries Ctrl+K), and
Esc now closes Settings too. Listed in the keyboard-shortcuts cheatsheet. macOS Chrome
may keep ⌘, for its own preferences. Needspm2 restart chela-dashboardto pick up the
pane shim. (CMX-385) -
The merge rule is enforced inside chela: a direct
gh pr mergeis denied in every Claude session. chela's plugin (0.2.5) carries a secondPreToolUsehook onBash(chela/mergegate.py, a stdlib-onlycommandhook that decides locally from$CHELA_DIR/mergegate.json— no dashboard needed) that DENIESgh pr merge,gh api …/pulls/N/mergeand agit pushto the workflow's base branch ormainin any repo with a chela workflow, pointing the session atchela merge cmx-N. The command is parsed (chains,bash -c,cd x &&,-R, worktrees), and the hook fails open — logged to$CHELA_DIR/mergegate.log— when it cannot decide.⚠️ Restart every agent and orchestrator session (andclaude plugin update chela@chela) to load it; the orchestrator's owngit push origin devis now denied too. (CMX-389) -
chela merge cmx-N --override --reason "…"— the one way past the judge, operator-approved and audited. It waitsCHELA_OVERRIDE_WAIT_S(default 300s) for the operator to approve on the dashboard (/override/<id>) or withchela merge-approve <id>in a plain terminal; a timeout is a deny. The approval (who, head sha, overridden judge state, reason) goes to the event log and the run's review history before the merge, the merge is pinned to the approved head, and the reason rides in the squash body. CI red or not-mergeable still refuse. (CMX-389) -
Out-of-gate merges are flagged. When the reconcile finds a chela PR merged without a clean judge on its merged head and without an approved override, it sets
runs.merged_outside_gateand the inbox pushes⚠️ cmx-N was merged outside chela's gate— once. (CMX-389) -
The judge holds some experiments out, and runs a sample twice. The judge agent now
tags about 30% of its mutation experiments"held_out": true(at least one once it
proposes three or more; set per workflow withjudge.held_out_fraction). They run like
any other, and a held-out survivor still sends the PR back. The PR comment, the review
history and the rework prompt only ever say how many held-out guards survived, never
which, so a rework has to fix the guard itself instead of patching the listed cases.
Separately, up to two experiments (survivors first;judge.consistency_sample) are run a
second time. One whose KILLED/SURVIVED outcome flips is markedflakyin the comment and
does not count toward blocking. If nothing else blocks, the verdict iscannot_verifyand
the operator decides. Each round is recorded privately under
$CHELA_DIR/judge-heldout/. The newchela judge show <run>prints rounds-to-clean,
visible vs held-out survival rates and the flip rate; add--held-outto see each
held-out experiment in full. Needs achela-daemonrestart to pick up. (CMX-395) -
Search in Settings. A search box at the top of the Settings drawer filters every
setting across all tabs, VS Code style. It matches a setting's label, its help text and
hidden keywords (for example, "phone" or "mobile" finds Remote Control). While you
search, the matching rows are grouped under their tab name and no tab is selected.
Clearing the search returns you to the tab you were on. Ctrl/⌘+, opens the drawer with
the search box focused. Esc clears the query first, and a second Esc closes the drawer.
A query with no results says "No settings match '…'", and screen readers hear the live
count ("3 settings"). No setting's behaviour, storage or API changed. Needs a
chela-dashboardrestart to pick up. (CMX-396) -
chela watch --if-unclaimed— a SessionStart hook can re-claim the orchestrator slot
after a reboot without ever stealing it. It registers the calling session exactly like
chela watchonly when the pin is empty, undeliverable (dead window or stale tmux epoch —
the inbox's ownaddress_state), or already the caller's; otherwise it changes nothing,
exits 0 and printsorchestrator is @N (live) — not claiming. Plainchela watchis
unchanged. The recommended hook line is indocs/HOOKS.md. (CMX-399) -
"Settings" is now a row in the Ctrl/⌘+K palette — and so in the phone sidebar's
"Jump to session" box, which drives the same list. On a phone there is no Ctrl+,, so
this was the missing way into Settings. Type "sett" (or "settings") and pick it; it
opens the Settings drawer. Restartchela-dashboardto pick it up. (CMX-401) -
scripts/demo/record.shre-records both demos. It runsscripts/demo/fleet.py, a throwaway fleet on its own tmux server (tmux -L chela-demo) with its ownHOMEandCHELA_DIRunder a fixed demo root (/tmp/demo) and a from-scratch environment. The fleet's panes are scripted, and itsclaudeonly reports their statuses. It looks like a healthy install on camera: chela runs from a git checkout with an upstream (Update reads "up to date"), the demo's own daemon is running, and the "+" menu's Favorites and Recent list the demo projects. Playwright then records the fleet, and ffmpeg writes the MP4s and GIFs over the published files.tests/test_public_media.pyfails if a README or landing page names a removed view, if any file underdocs/img/orlanding/contains the operator's user name, a private window name or a/home/path, or if the recorder's environment stops being isolated. (CMX-402) -
Shared terminals are view only by default; guests can type only into a verified
sandboxed session. A new Guest typing setting (Settings → Collaboration,
CHELA_SHARE_TYPING, off by default) gates all guest input on the host. With it on, a
share can allow typing only into a window that verifies LIVE as a sandboxed session,
checked from the process tree anddocker inspectat least every 2 s. Refused input
is dropped, and the guest gets a rate-limited "view only" notice. Start a sandboxed
session from New session → Sandboxed session… (desktop and phone) or with
chela share-session <project>. The new share dialog defaults to View only and
shows why Allow typing is disabled. The share pill shows 👁 or ⌨. See
docs/SHARE_SANDBOX.md. (CMX-403) -
Trusted-peer "Full access — UNSANDBOXED" override, one share at a time: you type
the window name to confirm it, it expires afterCHELA_SHARE_UNSANDBOXED_MINUTES
(default 30), it's bound to one joiner, it shows a red banner on the pane and the
share pill, the share kill switch revokes it, and the event log records the grant,
expiry and revocation. (CMX-403) -
Per-task risk levels (
high/normal/low) that size the judge's search and the
rework budget. Mark a TODO bullet<!-- risk: low -->(or give a GitHub issue a
risk:lowlabel); unmarked tasks arenormal, orhighwhen the brief's BOUNDARIES touch
the dispatcher, judge, merge gate, sandbox, inbox or secrets. The judge runs at most 12 / 8
/ 4 experiments and a run gets 5 / 4 / 3 rework rounds by level — all Settings → Dispatch
knobs. The judge still always runs, and a surviving mutation still blocks at every level.
The level shows on the Work card, the task modal and the judge's verdict. See
docs/RISK_LEVELS.md. Restartchela-daemonandchela-dashboardto pick it up. (CMX-405)
Changed
-
Dashboard restyle: calm desktop-app look — no top bar, shape-coded status, sans chrome.
The full-width<header class="topbar">is gone; its contents (wordmark, inbox, CPU/RAM/Disk,
settings) moved into the sidebar's own fixed head/foot, and the two safety-critical pills
(active-shares kill-switch, native-status-feed warning) now float above the canvas on every
breakpoint so they stay reachable even while the sidebar is off-canvas on a phone. Chrome type
is now sans (Geist, vendored, with a Hebrew-capable Heebo fallback —--ui-font), while
terminals and code keep the monospace--font. Status is shape-coded, not colour-only
(Liav is red-weak): Working = filled dot, Needs you = filled triangle, Done = check, Idle =
hollow ring — the SAME shapes in the sidebar rows, the pane headers, and the taskbar/mobile
dock. The sidebar gained a prominent "New session" button and a jump-to-session search (reusing
the existing ⌘K fuzzy jump), and its session list now groups Pinned (the orchestrator session,
if any) → per-project → Finished (moved last), with each row readingstatus mark · titlethen
state · ctx% ctx, relative time right-aligned. Pane headers are a fixed 40px; the focused pane
gets a thin inset accent ring instead of the old outer glow.--accentis now reserved for
focus, selection and the inbox unread dot — the sidebar wordmark is no longer accent-tinted.
Session rows carry only their status shape, title, "state · ctx" and time — no per-row
harness-letter badge, orchestrator/dispatched icon or favourite star, and no coloured state
word; group headers (Pinned/per-project/Finished/Navigate/Sessions) are plain, sentence-case,
uncoloured labels with no bordered/tinted box. The Wall still fills the full space at every
density and the pane footer keeps every field (model · branch · context · tokens · cost); no
theme palette or terminal colour changed.
(CMX-377, #529) -
chela's peer messages now arrive NAMED (
Message from @chela) instead of as an
anonymous "Another Claude session sent a message:" block. Measured against Claude Code
2.1.283 with a throwaway receiver: top-levelfrom_name/fromName/name/
from_modefields are ignored. A nativeSendMessagenames itself by wrapping its text
in the receiver's own<cross-session-message from=… from-name=… from-mode=…>envelope
insidemessage.content, sosend_peerandsend_peer_to_pidnow do the same. Only the
rendering changes. The arrival keepsorigin.kind: "peer", the socket-verified sender
pid,isMeta, and the full anti-laundering paragraph in the receiver's context.role
stays"user",fromstays the reply socket, andmsg_idstays a uuid4. The name is
alwayschela, including for relays of another agent's message.from-nameis
sender-asserted and the receiver never checks it against the verified pid, so the only
honest value is the process that really sent the message. The originating agent is still
attributed in the message text. Nofrom-modeis sent, although native senders send
one. The receiver's inbound gate reads that field, its effect on a bypass-mode receiver
was never measured, and the name alone is enough to render the message named. After
merging, restart the PM2 services that import
chela.messenger:chela-daemon,chela-dashboardandchela-telegram. (CMX-380) -
The repo's JS dev tooling is pnpm, not npm.
pnpm-lock.yamlreplaces
package-lock.json(same 41 resolved versions; playwright still exactly1.63.0), and
package.json'spackageManagerpins pnpm10.34.5— a 10.x, because pnpm 11 crashes on
the pinned Node 20. pnpm's content-addressed store hardlinks one copy of each package into
every worktree, which replaces the hand-built shared, symlinkednode_modulesof
scripts/npm-shared-install.sh(CMX-151, #508). CI installs with
pnpm install --frozen-lockfileand keys its Chromium cache onpnpm-lock.yaml;
hooks.before_runruns the same install. The judge provisions a pnpm tree with pnpm and,
for branches forked before the move, still falls back tonpm cion apackage-lock.json
tree. Runpnpm install --frozen-lockfilein your checkout after pulling. (CMX-388) -
Dashboard restyle fine-tunes. The sidebar foot is one row again: the CPU/RAM/Disk
readouts on the left and a single settings gear on the right. The gear opens the same
menu the ⋮ did, so every action is still one click away. The Decisions inbox moved up
to the sidebar head. Each Wall pane's title bar now draws its state once, in the
mockup's order: status shape · title · dim subtitle · a soft pill carrying only the
word (working,idle,done,needs you) · icon buttons. Shape plus word remain the
non-colour cue. The real-browser suite now guards all of this, plus the chrome font
on pane headers, pane footers and modals and the phone "+" /.safety-floatat 390px.
Needs achela-dashboardrestart to pick up. (CMX-393) -
The Wall's Minimized dock is slimmer and sits lower: one row of chips is now ~30px
tall (was ~38px) and ~8px off the bottom of the window (was 18px), and the panes still
fill exactly down to it. Chips keep their 12px text and a 24px click target. (CMX-397) -
The Wall's "Grid:" layout toolbar collapses to one button that shows the current
layout (or Focus / Auto-arrange) plus a chevron. Click it to show the full row. Picking
a preset or mode, clicking outside, or pressing Esc folds it back. It starts collapsed
while Focus is on and expanded otherwise, and remembers your own expand/collapse choice
in this browser. Needspm2 restart chela-dashboard. (CMX-397) -
The README and landing-page demos are re-recorded for the new design, from a synthetic fleet. The desktop clip shows the Wall with one agent in each state (working, needs you, done, idle), then Work's board, runs and schedules, then Settings opened with Ctrl+,. The phone clip shows the pill switcher, the "+" New session menu and the keybar. The README,
landing/index.htmlandlanding/docs.htmlno longer mention the removed Feed view, and each gains one line per recent feature: the judge-enforced merge gate, Remote Control per session, themes that reach the terminals (pluswarm), the phone "+", named@chelapeer messages and Ctrl+, for Settings. (CMX-402) -
A paired share guest can no longer type by default. Earlier shares gave full access to
anyone holding the code. (CMX-403) -
CHELA_MAX_REWORKS(default raised 2 → 5) is now the ceiling over the per-level rework
caps rather than the cap itself. (CMX-405)
Fixed
-
render_promptfails loudly on an unknown{{var}}instead of shipping it verbatim.
A misspelled variable in aWORKFLOW.mdprompt template (or the built-in first-dispatch and
rework prompts) now raisesTemplateRenderErrornaming every unresolved reference, instead
of silently reaching the dispatched agent as literal{{taks_title}}-style text. A bad
template fails only the one run attempt (dispatch, watchdog re-nudge, or judge spawn) —
never the whole tick — and a rework whose re-nudge hits this returns tochanges_requested
via the existing rework-failure path rather than being leftrunningwith no agent behind
it. (CMX-373, #525) -
Multi-line Telegram messages no longer strand unsent on the prompt (or split in two).
messenger.send_tmuxpasted multi-line text with a plaintmux paste-buffer, which turns
each newline into a bare CR: Claude Code read a short message as two separate prompts and
left a longer one sitting unsubmitted on the❯line. Measured on 9 days of bridge traffic
(single-line 225/226 delivered, multi-line 7/12 stranded) and reproduced on an isolated
tmux server (12/12 stranded at 0.5s, 2s and 5s Enter gaps). It now uses bracketed paste
(paste-buffer -p): 4/4 delivered. (CMX-375, #526) -
A dispatched agent's first prompt only ever carried the TODO bullet's title — every
OBJECTIVE/BOUNDARIES/GUARDS/VERIFY continuation line was silently dropped._prompt_vars
(spawn),dispatcher.dry_run's own prompt preview, and_rework_vars(rework) rendered
only{{task_title}};Task.body— the markdown source's title plus its dedented
continuation block, or agh_issuesissue body — reachedruns.brieffor the task-detail
modal but no template var ever carried it into a prompt an agent actually reads. Measured
on CMX-377 (#529): the agent's first user turn held the title only, whileruns.briefheld
the full 5.9KB body. Atask_bodyvar (task.body or "", never the literalNone) now
reaches WORKFLOW.md (both the repo's own copy andexamples/WORKFLOW.md), the--dry-run
preview, and the rework prompt — a bare one-line task still renders cleanly with an empty
body. (CMX-378) -
A judge stopped by a Claude Code auto-mode classifier outage no longer holds the judge
slot for an hour or spends a retry. When every tool call comes back "the server-side
auto mode classifier gave no verdict", the judge stops and sits idle with no verdict.
The judge watchdog now spots that case: the judge's native session status is idle and the
most recent tool results in its transcript carry the outage error. It reaps the judge
early as an uncountedcannot_verify, like CMX-282's expired login. The run is not
re-judged until a backoff passes, so it does not walk straight back into the same outage.
The backoff is the new Dispatch knobjudge_outage_backoff_seconds
(CHELA_JUDGE_OUTAGE_BACKOFF_S, default 600s). The inbox reports it once as
⚖️🌩️ judge for <branch> hit a classifier outage — re-judging after <backoff>. Restart
chela-daemonafter upgrading. (CMX-379) -
Themes stopped at the chrome — the terminals kept GitHub-dark colours whatever theme
you picked. Each theme now carries its own terminal palette (chela/dashboard/term_themes.py:
full xterm theme + scrollbar colours) and its own--term-bg, applied live inside every
open terminal on theme change (no ttyd restart, no reload); a newly opened pane comes up
in the current theme.CHELA_TERM_THEMEstays the operator override and wins over every
theme. Needspm2 restart chela-dashboardto pick up. (CMX-381) -
depends:chains longer than one hop blocked forever, silently. An open task's id
hashed its raw bullet,<!-- depends: … -->marker included, while adepends:
reference hashed the bare title, so no task could depend on a task that itself carried a
marker. Every task id (open, struck and parked) now hashes the bare title. An unmarked
task's id is unchanged. A task that carries a marker gets a new id, and on its first
tick the dispatcher re-keys any in-flight run still under the old id (one transaction,
logged). Also, adepends:marker quoted inside inline code (`) no longer counts
as a real marker. Needspm2 restart chela-daemonto pick up. (CMX-384) -
On a phone, New session was two taps deep (open the ☰ drawer, then its "New session"
button) once the topbar went away in CMX-377. A round "+" now sits beside the ☰ fab at
phone widths and opens the same New-session menu (Favorites / Recent projects, Init a
repo, Scheduled task, Shell window) with the drawer left closed. Desktop is unchanged.
Needspm2 restart chela-dashboardto pick up. (CMX-386) -
A
donerun whose PR was still open had no way back.chela reopenrefused anything
butneeds_humanandchela mergerefusesdone, so the only exit was a hand edit of
the runs DB.chela reopennow also accepts adonerun — but only when its PR is
live-read from GitHub as OPEN, its head has moved past the judged commit, and its task is
not struck- [x]in the tracker; anything it cannot read is a refusal. It re-enters
awaiting_reviewexactly like aneeds_humanreopen, on the fresh head. Needs a
chela-daemonrestart to pick up. (CMX-387) -
package.json'slicensesaidMIT; the project is, and always was,AGPL-3.0-or-later.
(CMX-388) -
A Node program in a workflow hook aborted with exit 134 under pm2. pm2 forks
chela-daemonthrough Node'schild_process.fork, so the daemon carries
NODE_CHANNEL_FD/NODE_CHANNEL_SERIALIZATION_MODE, and every hook inherited them. Any
Node program the hook ran (pnpm --versionwas the measured case) treated fd 3 as its IPC
channel and dumped core, which failedbefore_runand with it every agent and judge
launch. A newchela.envutil.child_env()drops Node's IPC markers (NODE_CHANNEL_*,
NODE_UNIQUE_ID) and pm2's bookkeeping vars (pm_*,PM2_*exceptPM2_HOME,
NODE_APP_INSTANCE). It is now the env for theafter_create/before_run/
after_donehooks, the agent/judge/humantmux new-window, thetmux new-sessionthat can
start the server, and the judge's suite,uv sync,npm ciandnode --check. Needs
pm2 restart chela-daemon chela-dashboardto pick up. (CMX-390) -
A PR URL seen in an agent's transcript can no longer replace the run's real PR. Claude Code writes a
pr-linkrecord whenever a PR URL appears in a session, including a test fixture's fake one echoed by pytest. The daemon's completion hop used to prefer the transcript's latestpr-linkover thepr_urlalready on the row. A run then recordedhttps://github.com/o/r/pull/5, saw it "merged", and was closed, struck and had its worktree deleted. The row's ownpr_url(recorded daemon-side byrequest-push) now wins, and a transcript URL is used only when it is a PR of the run repo's own GitHub remotes. The daemon also now reads a PR's state from the repo the URL names (gh pr view N --repo owner/repo). By number alone,o/r/pull/5resolved to this repo's own long-merged PR #5. (CMX-391) -
A task whose bullet carries a
depends:marker showed that raw HTML comment as its
name in run rows, the Work view and every inbox notice.Task.titleis now the bare
visible title (every<!-- ... -->marker stripped);Task.rawkeeps the whole line,
and the readers that need the marker (depends parsing, the CMX-384 legacy-id re-key,
chela doctor's unresolved-depends check) read it from there. Task ids are unchanged,
and the merge strike still finds the line. Needspm2 restart chela-daemonto pick up.
(CMX-392) -
The orchestrator pin no longer changes hands silently. Two Claude sessions sharing one
directory could swap the decisions-inbox pin, and merge verdicts went to the wrong
session. The trigger was achela watchthat a SessionStart hook ran in the other
session. Now any pin change that is not the window's ownchela watchis announced once
as anorchestrator.movedevent: a self-heal,chela restore, a dashboard or
auto-launch registration. So is achela watchthat takes the pin from an orchestrator
whose window is still live. The notice goes to the Feed and is delivered once to
whichever session now holds the pin. -
A plain
claudewindow that shares its directory now has an identity. chela reads
Claude Code's own session registry (<config dir>/sessions/<pid>.json) and checks its
procStartagainst the live pid. A window started without--resumenext to another
agent in the same directory now resolves to its session. Its hook events carry its
window instead ofwid: null,chela watchrecords its identity so self-heal works,
and the self-heal can find it again in a new window. The self-heal still rebinds only
to the recorded session identity, never to "a session in that directory". With no
identity it does nothing and the undeliverable alarm fires. Needs achela-daemon
restart, pluschela-dashboardfor hook attribution. (CMX-394) -
The sidebar-foot Settings/menu popover opens on screen. Since the menu button
moved from the old top bar to the sidebar foot, the menu still opened below it,
mostly off the bottom of the window. Popovers anchored to a sidebar or footer control
(the Settings menu, the New-session menu and the tap tooltips) now share one placement:
below the control when there is room, otherwise above it. They stay 8px inside the
viewport on every side, scroll when taller than the window, and follow a window resize
while open. The phone "+" menu is unchanged. Needs achela-dashboardrestart to pick
up. (CMX-398) -
The phone sidebar drawer is whole again on iOS Safari. Its footer (with the Settings
button) no longer hides behind Safari's bottom toolbar, because the drawer is now100dvh
tall with a100vhfallback. The New session + Jump row also shows even when this browser
once collapsed the desktop sidebar to its icon rail: every collapsed-rail rule is now
desktop-only. Restartchela-dashboardto pick it up. (CMX-404)
Security
- A suite chela runs can no longer reach live state or act as the live window. The judge,
chela judge self-checkandchela task-finished --self-check-experimentsall run the suite withCHELA_DIRset to a fresh temp dir, and with everyCHELA_*variable,TMUXandTMUX_PANEremoved. chelamux's own test suite now also fencessqlite3.connecton the real~/.chela, and it refuses to run ifCHELA_DIRresolves to the operator's real one. (CMX-391)