Repository navigation
v0.15.0
Added
-
chela close <run> --requeuemakes a closed task claimable again. The next tick claims
a fresh attempt (attempt 1) on its own-r<N>branch and worktree, never the closed run's.
The old PR is closed, the requeue is recorded in the review history, and a run whose PR
merged is refused. Without--requeue,closestays terminal, and it now says so: it
prints that the task will not be re-dispatched and the command that requeues it. Moving
the issue back to Todo by hand does not requeue it. (CMX-65, #627) -
The Cost tab has a Usage view: what is eating the plan limits, in tokens. A "Cost |
Usage" toggle shows the 5h and 7d limit bars (from the freshest statusLinerate_limits,
with a %/h burn rate and whether it reaches 100% before the reset; missing, stale or reset
data shows as unknown, never 0%) and the top token consumers over the last 30 min and today
(UTC) across EVERY Claude Code transcript — judges, subagents, background sessions and, on
WSL, Windows-side ones (/mnt/c/Users/*/.claude/projectsby default; setusage_extra_roots
in~/.chela/config.jsonorCHELA_USAGE_EXTRA_ROOTS,[]for none — the view shows the
roots read-only). Each row has its requests, token kinds, a weighted total and cache-hit
rate; a session under 50% hit over ≥10 requests and >1M tokens is flagged ▲ CACHE BROKEN. (CMX-38, #618) -
A held decisions inbox, or a clean PR nobody merges, now pushes one ntfy alert. When the
inbox queue's oldest event has waited pastCHELA_INBOX_HELD_ALERT_S(default 30m), the
announcer pushes once per held episode, naming the orchestrator's status and the last failed
delivery (an open prompt, an unreachable socket, an adverse receipt). A run that stays
judge-clean on its current head and unmerged pastCHELA_CLEAN_UNMERGED_ALERT_S(default
30m) pushes once.chela doctorgains aninbox.heldfact that goes red while the queue is
held. (CMX-26, #610) -
chela spawn <cwd> [--command CMD]opens an agent window from the terminal. It runs
through the samespawn_windowpath as the dashboard launcher and Telegram/new. With no
--commandit launches$CHELA_AGENT_CMD(defaultclaude), not the bare shell that
spawn_windowopens on its own. It prints the new@Nand cwd, and adds the cwd to the
launcher's Recent list on a best-effort basis. (CMX-13) -
chela close @Nkills one chela window, so nobody has to reach for raw
tmux kill-window. It refuses a window that is not live in the chela session, the
orchestrator's own window unless--orchestratoris passed, and a window that an
in-flight dispatched run still claims unless--forceis passed. For that last case,
chela close <task> --reason …is the right tool. Closing a run by id works as before.
(CMX-13) -
A profile of chela's services under normal load (
docs/perf/2026-10-profile.md).
The dashboard spends most of its CPU starting processes. About 90% of its tmux and pgrep
spawns come from/api/agentsand/api/agents/context, which probe each window
separately every 4 s per open Wall tab. The cost grows linearly: 7% of a core at 5
windows, 73% at 40. The report ranks three fixes: batch the probes through the existing
sessions.panes(), remove a 36-second CPU burst inchela doctor, and have Telegram
capture only panes that changed. It also lists what is not worth doing, including
swapping Werkzeug for waitress. The isolated harness that produced the numbers is in
docs/perf/2026-10-harness/. No production code changed. (CMX-15) -
chela doctorfacttmux.leaked_env. It goes red when tmux's global env holds a
proxy aimed at a dead localhost port, a Claude session marker, or aTMUX_TMPDIRwhose
dir is missing. The daemon's doctor sweep pushes it once through the notify path.
(CMX-21) -
The perf harness (
docs/perf/2026-10-harness/) now passestmux -L chela-perfon every
call. It refuseskill-serveron the default socket, starts the ttyd supervisor under
env -iwith an allowlist inside its own process group, andstop.shreaps that whole
group. (CMX-21) -
A push when an agent is blocked by its permission classifier. An auto-mode denial
never shows a prompt, so the session never reacheswaitingand the needs-input push
stayed silent.notify.DeniedWatchnow tailshook.permission_deniedevents and sends
one ntfy push per window per 10 minutes, from whichever process holds the announcer
lease (daemon or dashboard). (CMX-25, #609) -
A judge window shows its detached mutation battery's live progress instead of
"idle". Whilechela judge run --detachruns, the judge pane's state pill reads
⚖️ testing · 3/6 · 15m(its status dot reads working), andchela peekon the window adds a
judge:line with the KILLED/SURVIVED/INVALID tally and the experiment running now (a held-out one
is never named). A run whose process died before a verdict reads⚖️ judge run died — no verdict, never idle. The badge disappears when the run finishes. (CMX-40) -
chela close <run> --reason "…"— walk away from a run on purpose, with a visible
reason. A run that was superseded or abandoned used to stay on the Work view as a
misleading FAILED card (for example "tmux window disappeared" after its idle window was
closed). Now it is markedclosed: it is never claimed or retried again, its slot is
freed, the reason goes in its review history and arun_closedevent, and the card reads
"Closed — ". It acceptsfailed,needs_human,changes_requested,
awaiting_review,runningandclaimedruns. A running or claimed run is refused
unless its window is gone or its agent is idle;--forceoverrides that. An open PR gets
the reason as a comment,--close-pralso closes it, and on a terminal with neither
--close-prnor--keep-pryou are asked. The branch is never deleted, and the worktree
is kept unless you pass--remove-worktree. Restartchela-daemonand
chela-dashboardto pick it up. (CMX-406) -
chela judge eval— an offline eval of the judge's experiment DESIGN. It runs only
the design step (the live judge prompt at each risk level, and the model returning its
experiments JSON) against a read-only copy of a PR's tree. It never runs a test suite.
Per risk level it reports recall on seeded and historical real regressions, the share of
proposed experiments that are contrived, validity (each experiment is checked with the
judge's ownapply_mutation/parse_check), grader flips, and a decoy negative control.
Every score has a 95% interval. The dataset is 85 historical cases mined from this repo's
judge verdicts plus 4 seeded regressions. It is split train/test by PR, and a test-split
run prints aggregates only. The cost estimate prints before any model call, and nothing is
spent without--yes.--limit,--max-costand a per-call budget cap the rest.
Subcommands:mine,label,controls,spot-check,agreement. It makes no change to
how the judge behaves. Seedocs/JUDGE_EVAL.md. (CMX-408) -
Drop or paste a file into a Wall terminal. Dropping a file on a terminal pane, or
pasting one (a screenshot, "Copy image"), now saves it to that session's
uploads/folder and types@uploads/<name>into the prompt, without sending it, so
Claude can read a file that lives on your phone or laptop and not on the host. A small
toast names the saved file, and an error toast explains a refusal. Files are never
overwritten (-1,-2… suffixes). A name with a path separator,.., or a leading dot
is refused, and so is a symlinkeduploads/. Uploads are capped at 25 MB each
(CHELA_UPLOAD_MAX_MB) and 20 per minute (CHELA_UPLOAD_PER_MINUTE). Every upload
records anupload.savedorupload.refusedevent. It is for the operator only: a
share guest's request is refused whatever Guest typing says. Turn it off in
Settings → General → File drop into terminals (on by default, orCHELA_FILE_DROP).
Restartchela-dashboardto pick it up. (CMX-412) -
Every pane shows its tmux window id (
@N). The orchestrator,chela peek @N, inbox
notices and peer messages all address agents by@N, and the dashboard now shows it too:
as a small, dim@Nat the start of each pane's bottom bar (click it to copy, with a
"Copied" toast), on each sidebar session row's second line and in its tooltip, and in the
⌘K / "Jump to session" palette, where typing@32jumps straight to that window. The id
stays on one line at desktop and phone widths, however long the session or branch name.
It is the same idchela statusprints and renumbers after a tmux restart, as tmux does.
Restartchela-dashboardto pick it up. (CMX-417) -
Web access for a sandboxed share session (opt-in, per session). Start one with
New session → Sandboxed session · allow web access… or
chela share-session <project> --web. The default is still no network. The guest
container stays alone on its--internalnetwork with no host address. One more
sidecar joins it:chela/share_web_proxy.py, a filtering HTTP(S) forward proxy that
carries all the guest's web traffic.- The proxy resolves each name itself and refuses any private, loopback, link-local,
CGNAT, host or embedded-IPv4 address after resolution. It then connects to the
address it checked, so there is no DNS-rebinding window. - It allows ports 80 and 443 only.
- It rate-limits each site (1 request/s by default) and all sites together.
- It logs every request to
~/.chela/share-web/<session>.log. - Optional
CHELA_SHARE_WEB_DENY/CHELA_SHARE_WEB_ALLOWlists. Unset means any
public host.
The guest runs a locally built
chela-share-webimage with headless Chromium and a
browsecommand. Its WebFetch, curl and pip go through the proxy. The pane shows
🌐 web. The live share check now also verifies the mode the session was launched
with: anonesession that gains a proxy variable, a web sidecar or any other network
member fails. Seedocs/SHARE_SANDBOX.md→ Web mode. (CMX-418) - The proxy resolves each name itself and refuses any private, loopback, link-local,
-
Sandboxed sessions now relay to their Telegram topic. Their transcript stays inside
the container, so the share proxy writes each completed turn (the assistant's text, with
each tool call as one line) to a per-session outbox that only the proxy sidecar mounts.
chela telegramreads that outbox for a window that verifies as sandboxed, with the
usual formatting, chunking and dedup, andchela doctorstops reporting such a window as
dead outbound while its outbox is current. Everything the session says now also reaches
the operator's topic (seedocs/SHARE_SANDBOX.md). Turn it off for one session with
chela telegram --sandbox-relay @N=off. (CMX-420) -
Change a live share's mode without re-sharing. Active shares now shows each share's
mode as a control: 👁 View only, ⌨ Allow typing, or ⚠ Full access — UNSANDBOXED. Switching
keeps the same link and pairing code, and a guest who already joined keeps the
connection. Dropping to View only applies at once. Going up passes the same checks as
creating a share with that mode: Guest typing on and a verified sandboxed window for
typing, or the typed window name for UNSANDBOXED, which then starts its own expiry and
binds the first guest to type. The guest sees "You can type now" or "View only now".
Every change records ashare.mode_changedevent (from, to, by). The pane's share row
shows the mode (👁 / ⌨ / ⚠), and each row in the sheet says what that share allows,
replacing the one sentence used for every share. Clicking Share current session on a
window that's already shared opens that share's row instead of looking like a new
share. Restartchela-dashboardto pick it up. (CMX-421) -
A Linear tracker:
tracker: kind: linear. A workflow can now take its queue from a
Linear team. The issue description is the brief. Blocked by relations hold a task the
same fail-closed waydepends:does, and the claim order is priority, then the board's
manual order. Only theready_states(defaultTodo) are claimed, but every state that
is not completed or canceled counts as open, so an issue Linear's GitHub integration
moved to In Progress is not mistaken for finished. A failed read (network, auth, rate
limit, malformed reply) never closes a run.The run's id is the identifier (
CMX-12), its number is Linear's, and its branch and
window are Linear's suggested branch name. That branch is never reused if the remote
already has it. A barecmx-12that names both an old TODO.md-era run and a Linear run
resolves to the Linear one, never to an old finished run.When a PR merges, chela marks the issue Done if it isn't already and archives it. A sweep
every 15 minutes archives any closed issue that is still unarchived.chela doctor
(tracker.linear_issue_cap) warns above 200 non-archived issues, since the free plan
stops at 250.The API key is read only from
chela.envasLINEAR_API_KEYand never from
WORKFLOW.md. It is never logged and never passed to agents, judges or hooks. Setup is
indocs/LINEAR_TRACKER.md. Existing workflows are unchanged. (CMX-432) -
Dismiss a session from the sidebar's "Recent sessions". Each resumable row now has
a × (or swipe left on touch). The row leaves the list at once and an Undo toast offers
it back for a few seconds. With two or more rows, "Clear all" at the section header
dismisses them all after a confirmation. Dismissals are stored server-side
($CHELA_DIR/dismissed-sessions.json, keyed by session id), so they hold on every
device. Dismissing only hides the row: the transcript and the restore records are kept,
and a dismissed session that comes back to life shows under Sessions as usual. (CMX-437) -
Work board: a Linear-backed card links to its Linear issue. Every card for a Linear
task — queued, running, in review or done — shows its identifier asCMX-12 ↗, opening
the issue in a new tab without triggering the card's own click. The href is exactly the
urlthe Linear adapter fetched (recorded on the run at claim, in a new
runs.tracker_urlcolumn), never built from a workspace slug. Cards from markdown and
GitHub-issues workflows are unchanged. (CMX-5) -
"New task" in the dashboard creates a Linear issue. On a workflow whose tracker is
kind: linear, the Work view's toolbar (and a ⌘K palette row) opens a form: title,
markdown description, priority and optional "blocked by" (open issues). Submit creates
the issue in the workflow's team, in its ready state, plus ablocksrelation per
blocker. It is never created as a sub-issue. The browser posts to
/api/dispatcher/linear/issue, and only the dashboard calls Linear, soLINEAR_API_KEY
never reaches the page. Share guests are refused. A Linear error is shown and the typed
form is kept. The created issue shows in the queue on the refresh, linked to its Linear
page by the board's existingCMX-N ↗link (CMX-5). (CMX-6) -
A View menu for the dashboard sidebar, like the Claude desktop app's. The sliders
button on the Sessions header picks what shows and how it is laid out:
Status (Active / All / Archived), Environment (Interactive, Dispatched agents,
Judges, Background sessions; all but Judges by default), Last activity (1d / 7d /
30d / All; default 7d), Group by (Date, Folder, State, Custom groups, None),
Sort by (Last activity, Name, Created), plus Show empty groups and Show PR
status (a#NPR badge on dispatched-run rows). Custom groups are created, renamed
and deleted in the menu and assigned from any row's menu ("Move to group…"); they
follow the window, not its name. Filters never hide a session that is working or
waiting on you, or the orchestrator. Choices are kept per browser. Submenus open by
tap, so the menu works on a phone./api/agentsnow carries each window's
last_activityandcreated. (CMX-66) -
A closed run that silently blocks a READY task is now flagged, with a one-click way
out.chela doctorreportsdispatch.closed_run_stalls: a task in its tracker's ready
state whose run isclosed(no requeue pending) is never claimed again, so doctor names it,
says "closed run blocks this task: requeue or refile", and gives the--requeuecommand. An
unreadable tracker reads CANNOT VERIFY, never "no stalls". On the Work board, the task's
card carries the same flag, computed over all of the workflow's runs and not only the 10
most recent. Its Requeue button (or Close & requeue on a run still in flight) runs
chela close --requeuethrough the newPOST /api/dispatcher/runs/<id>/close. (CMX-68) -
A sandboxed guest can request more access, and the operator approves or denies it.
Inside a sandboxed session, the guest or its Claude runs
python3 ~/bin/chela-request mount|domain|operation <target> --why "…". The request goes
to a write-only route on the session's token proxy and lands in a host-only file. It does
nothing by itself, and the guest can't read it back or see the decision.- The operator sees each request in the dashboard (the 🙋 N access requests pill),
as an event in the log, and as a push whenCHELA_NOTIFY_URLis set (ntfy or
Telegram). They can also runchela share-requestsin a terminal. Approvals last
60 min by default. - Mounts are read-only unless the guest asked for write and the operator ticks
allow write. - An approved mount is applied by re-launching the guest container with it at
/extra/<name>. A running container is never widened. Expiry or revocation relaunches
the container without the mount. - The secrets directories (
~/.ssh,~/.claude,~/.chela,~/.config,~/.secrets,
…),$HOMEand its ancestors, the tmux socket directory, system directories and
/mnt/*are refused even when approved, and so is a symlink into any of them. The
refusal is checked at approval, at launch and by the live sandbox check, and the UI
shows why. - Domain approvals extend a web session's
CHELA_SHARE_WEB_ALLOWlist. Operations are
only recorded. - Every request, approval, denial, refusal, expiry and revocation is a
share.request_*event. Stopping the share or turning Guest typing off revokes
every approval. - Nothing approves automatically, and chela's merge-gate hook denies
chela share-requests approveand the approve route to Claude sessions. (CMX-7)
- The operator sees each request in the dashboard (the 🙋 N access requests pill),
-
Telegram Approve / Deny for override requests. Each request is posted once to the
orchestrator's topic (task, PR link, judge state on the head, reason) with inline buttons,
and the message is edited to the outcome: merged<sha>/ denied / expired / failed.
chela override list [--all]shows requests. (CMX-79)
Changed
-
With
tracker: kind: linear, the Work board's columns are the team's Linear workflow
states, in Linear's order. Canceled and Duplicate are hidden, as Linear hides them. chela's
run state (failed, judging, rework N, blocked race) now shows as a badge on the card, never as
a column. Other tracker kinds keep chela's own lanes. chela also drives the issue's state:
claim or rework round → In Progress, PR out of the agent's hands → In Review, merge → Done,
chela close→ Canceled, or Duplicate plus the relation when the reason names the issue
that supersedes it. Each edge is written once, and a failed write never blocks anything. A run whose window
died is re-claimed from In Progress or In Review while it has retries left. (CMX-23, #607) -
The Telegram pane watch re-captures only panes that changed. Each tick it reads every
window's#{window_activity}in ONEtmux list-windowscall and runscapture-pane
only for windows whose stamp moved since their last capture (or whose stamp is in the
same second that capture began, since the stamp is whole seconds). Unchanged windows are
handed their last captured text, so every detector, hook-log lookup and re-post backoff
still runs every tick. A full sweep re-captures every window at least every 30 s as a
backstop for changes that write no output. The transcript relay and the pane loop now
ask tmux for the server epoch once per tick (epoch.per_tick()), not once per window.
Measured at 40 idle windows:capture-panespawns 1,160 → 92 per run and the
service's CPU 10.1–10.3% → 5.9–6.6% of a core. (CMX-18) -
The dashboard's polled endpoints no longer probe tmux and
pgreponce per window.
/api/agents,/api/agents/contextand/api/orchestrator/statusnow read one shared
pane snapshot (sessions.panes(): onetmux list-windowsplus/proc, cached ~1 s
across requests and tabs) and a shared tmux epoch, instead of 2–4 spawns per window per
poll. A window that is missing from the snapshot forces one fresh read, so new windows
still show on the next poll. The responses are unchanged: a golden test renders 5 and 20
real windows both ways and requires byte-identical output. On a host without/proc
(macOS), oneps -axo(plus onelsoffor the claude cwds) now replaces the per-process
pgrep/ps/lsofcalls insessions.panes(). Measured with the CMX-15 harness (dashboard
alone, one phone tab on the Wall) at 5 / 20 / 40 windows: dashboard CPU 7.0 / 23.2 / 59.9% →
2.7 / 4.1 / 7.6% of a core,/api/agentsp50 98 / 388 / 1,044 ms → 20 / 38 / 117 ms. (CMX-20) -
Sessions chela opens for a human now launch with a bare
--remote-control. chela
passed a name, frozen at launch: theshell-Nplaceholder for a home-dir window, the folder
name for a project. The claude.ai / desktop sidebar showed several sessions as "shell-3",
and later tmux renames never reached it. With no name, the sidebar shows Claude's own
generated session title; it already groups sessions by project folder. Applies to the
dashboard launcher, Telegram/new,chela spawnand the orchestrator persona. Dispatched
agents and judges still get no Remote Control. Running sessions keep their name until
relaunched. (CMX-34) -
The dashboard sidebar groups sessions by project folder, like the Claude desktop app.
Each folder gets a quiet header with a+(a new session in that folder), a collapse
chevron and a group menu (right-click, or the ⋯ on touch): New session, Move up / Move
down, Collapse all / Expand all, and Archive all. Home-dir sessions sit in "Other" at the
bottom, and two folders with the same name are told apart by their parent path. Archive
all only hides finished rows from your sidebar (reversible via "Show archived"). It
never kills a window, and never hides one that is working or waiting on you.
Dispatched work is one row per run in a "Dispatched" group,CMX-N · <title>: the
agent and judge windows fold together, and the row shows the run's state. The
"Finished" cluster is gone: a done session stays in its folder. (CMX-35) -
The Remote Control name in claude.ai / the Claude desktop now equals the chela window
name, at launch and after every rename. Every launcher (dashboard, Telegram/new,
chela spawn, resume) names a new window after its folder — the cwd basename, or the
login for the home dir, collision-safe-N— instead of ashell-Nplaceholder, and
starts Claude Code with--remote-control <window name>(the auto-launched orchestrator
passesorchestrator). A dashboard rename or a duplicate's-Nsuffix queues
/rename <new name>, typed into the session whenever its prompt is empty, busy or idle (a
ghost suggestion counts as empty; a typed draft, a permission dialog or an unreadable
status holds it); the daemon retries a held rename every tick. A window launched before
this, whose live claude runs--remote-controlor has Remote Control on, gets renamed
too. A rename made in the desktop app does not sync back. (CMX-39, CMX-70) -
A judge takes minutes, not most of an hour. Each mutation now runs only the tests that
can observe the mutated file: the test files that import it (Python and, transitively, JS),
tests that name it, and a per-test coverage map whenpytest-covis installed. Selection
stays inside what the baseline actually ran. The baseline is still one full-suite run, a
red subset is still final, and every subset survivor is re-run on the full suite before it
can block, so verdicts mean what they did. An unknown file type, aconftest.pyor a
test_cmdthat is not one pytest invocation runs the full suite, and
judge.select_tests: falseturns selection off. The verdict header now shows how long the
judge took, and each experiment shows how many tests decided it. Held-out experiments
(CMX-395) are confirmed on the full suite exactly like visible ones, and the consistency
re-run replays each experiment's own first-run selection; a subset survivor the full suite
kills is never sampled for it, so it can never be counted flaky. Restartchela-daemonto
pick it up. (CMX-407) -
A dispatch pause now says that it holds the judges too, and lists what is still
running.chela dispatch --pause,--hold-status,chela doctorand the Settings
"Work dispatcher" row now read "Held: new claims, judges and rework re-spawns. Still
finishing: N agent(s), M judge(s)", followed by each in-flight run's CMX id and how long
it has been running. The behaviour is unchanged: a hold starts no new agent of any kind
and stops nothing already running. The first tick after--resumejudges any PR that
went green while the queue was held. Tests now pin both halves. (CMX-413) -
The Wall's top toolbar row is tighter and lines up with the sidebar. The row that
holds the layout toggle and "+ New shell" is now exactly as tall as the sidebar's header,
so the first pane starts on the header's bottom rule instead of below it. Both controls
share one height, the gap between the row and the first pane is the same gap the Wall
leaves between panes, and the height saved goes to the panes rather than showing up as
empty space under the Wall. The expanded layout toolbar and the phone layout are
unchanged. Restartchela-dashboardto pick it up. (CMX-414) -
The shipped skills describe chela as it works now.
orchestrateteaches the merge
rule (chela mergeonly; the merge-gate hook deniesgh pr mergeand pushes to the base
branch;--overrideis approved by the operator), waiting on the judge's verdict rather
than on the PR opening,review --request-changes,retry/reopenafterneeds_human,
risk levels, held-out experiments, the tracker's first-line and marker rules, deploying by
import graph, and releases.telegram-setupandtelegram-sendnow describe the
chela telegrambridge (a private forum group, auto-topics, the bindings file,
secrets.env) instead of three bare env vars.chela-setupmatches the 0.14.0 install
(--all-extras,chela.env, the hooks plugin, the judge).handoffgains aname, an
argument-hint, the "this session, distilled" rule and its three live-state probes. Copy
the updated directories into~/.claude/skills/to pick them up. (CMX-415) -
A test now fails when a skill names a
chelasubcommand or flag the CLI no longer
has (tests/test_skills.py), everySKILL.mdmust carry anameanddescription,
andskills/joins the private-strings sweep intests/test_public_media.py. (CMX-415) -
The trusted-peer UNSANDBOXED share override can now last up to 14 days, and you pick
how long in the share dialog. The dialog offers 30 min · 4 h · 1 day · 7 days ·
14 days, starting onCHELA_SHARE_UNSANDBOXED_MINUTES(still 30 when unset). That
setting's range is now 1–20160 minutes instead of 1–240. A pick longer than 4 h needs
the window name typed a second time, and the server refuses it without that. The chosen
expiry is recorded asexpires_atinshare.unsandboxed_granted. The red
"UNSANDBOXED — guest can type" banner and Active shares show the time left
(e.g. "12d 4h left"); a row's end time now names the day when it isn't today.
Upgrading a live share to UNSANDBOXED from Active shares offers the same picker and
needs the same second confirmation past 4 h. Expiry, Stop, the kill switch, the one-joiner binding and the
share reaper are unchanged. Seedocs/SHARE_SANDBOX.md. (CMX-419) -
chelamux's own dispatch queue moved from
TODO.mdto Linear. The repo's
WORKFLOW.mdnow readstracker: kind: linear(teamCMX, ready stateTodo, done
stateDone). The markdown adapter andTODO.mdhandling are unchanged for every other
workflow. A test pins the repo's own tracker so a revert fails CI. (CMX-438) -
Window names change only to resolve a duplicate, and a manual rename always wins. The
reconcile loop no longer rewrites a unique window name (shell-N, a spawn name, anything)
to its cwd basename — a freshly spawnedshell-1used to becometradeplan-2within one
tick. When two windows share a name, the newer one (higher window index) gets a
collision-safe-N; the older keeps it. A dashboard rename now flags the window
@chela_manual_name, and a flagged name is never renamed: it keeps the name over a
non-manual duplicate, and two colliding manual names are both left alone with a warning.
The dashboard Start button no longer renames the window either; it only locks it. A raw
tmux rename-windowis not detected as manual (rename through the dashboard for that),
though a unique name is never rewritten regardless. -
The sidebar row and pane header lead with Claude's session title. Label order is
manual name > Claude's session title > window name; the short window name rides
secondary (the pane subtitle, the row's hover). No title yet ⇒ the window name — the old
cwd-basename fallback is gone. (CMX-62) -
Docs: the README,
docs/and the landing site describe 0.15. The README opens with
what chela is, one install command (uv sync --all-extras) and a first run, then a
"What's new in 0.15" list. Two new references:docs/RUNS.md(a dispatched run from claim
to merge — the judge, risk caps,retry/reopen/close --requeue, and the one-tap
override whose approval completes the merge) anddocs/DASHBOARD.md(the sidebar's folder
groups, VIEW menu, orchestrator badge and Archive-closes / Unarchive-resumes, window naming
and Remote Control, the Linear Work board, the Usage view).docs/GETTING_STARTED.mdhas
one install path and no longer says the agent strikes its ownTODO.mdline or opens its
own PR.docs/LINEAR_TRACKER.mdcovers the state transitions and the Work board, and
docs/RISK_LEVELS.mdtherisk:label on a Linear issue. The landing pages drop stale
claims (a "full access" share — shares are view-only unless a sandboxed session allows
typing; an override that waits; a rework cap of 2). (CMX-71) -
Docs:
docs/TRUST_AND_APPROVAL_POSTURE.md§2 no longer says per-agent sandboxing "does
not exist yet". The sandbox boundary (chela/sandbox.py) is implemented and measured,
opt-in per workflow (agent.sandbox: true) for the coding and rework roles only, and off
for chelamux's own dispatch, so its agents still run unsandboxed. (CMX-71, CMX-47) -
Docs: spikes, upstream requests and the dated Symphony conformance review moved to
docs/archive/, with every reference in code comments, tests and docs updated. (CMX-71) -
Archiving a session in the sidebar now closes it, like the Claude desktop app, and
Unarchive resumes it. Before, Archive only hid the row in one browser: the window
kept running, and the phone and the laptop disagreed about what was archived. The
archive is now chela state (~/.chela/sidebar-archive.json), so every device shows the
same set. Archiving a finished session records its session id, folder, window name and
Remote Control name, then closes the window; its Telegram topic is closed. Unarchive
opens a new window in the same folder under the same name, runsclaude --resume, and
reopens the same Telegram topic. Archive is refused when the session id cannot be
determined, and for a busy or waiting session or the orchestrator. A settled dispatched
run is only hidden, because its windows belong to the dispatcher. Archived rows now sit
in one collapsed Archived (N) section at the bottom of the sidebar (View ▸ Status
All or Archived), not inside their folders. A browser's old archive list is imported
once, as hidden rows, without closing anything. (CMX-75) -
CI now tests Python 3.11 and 3.13, and no longer 3.12. 3.11 is the supported floor
(requires-python = ">=3.11"). 3.13 is what a live install runs today, and CI had never
tested it. The job count stays at two.requires-pythonand the documented ≥ 3.11 minimum
are unchanged. (CMX-77) -
chela merge --overrideno longer waits — it requests, and the approval merges. The
command (or its request-only spelling,chela override request cmx-N --reason "…") runs the
gate minus the judge, records one request bound to the PR's head SHA, prints the request id
and exits 0. When the operator approves — a one-tap Approve button in Telegram, the
dashboard's/override/<id>, orchela merge-approve <id>in a plain terminal — the merge
completes by itself through the normal merge path, after re-checking that the head is
unchanged, CI is green and the PR is mergeable. Any miss ⇒ no merge, the request is marked
failed/expired with the reason, and the operator is told. A new push expires a pending
request;CHELA_OVERRIDE_WAIT_Sis now its safety TTL (default 24 h, was a 300 s wait).
(CMX-79) -
The Linear tracker keeps the 100 most recently finished issues visible and archives
only older ones. Before, chela archived an issue as soon as it was Done or Canceled,
which emptied the board of recent history. A newtracker:option,keep_done
(default 100), sets how many finished issues stay unarchived, ranked by when they
finished.keep_done: 0archives every finished issue, as before. Open issues are never
archived and never count toward it. Closing a merged task now only marks it Done; the
sweep does the archiving, every 15 minutes and once right after a close. Once the sweep
has caught up, the free plan's headroom is 250 − open −keep_done.chela doctor
still WARNs above 200 non-archived issues, and now names loweringkeep_doneas the
fix. (CMX-8) -
Sidebar: an idle session can be archived, and every row has its own Archive. A live
Claude session sitting idle (waiting for your next message) now counts in a group's
"Archive all (N)", and each row's menu (right-click, or its ⋯) has an Archive item,
enabled by the same rule and sent the same way. A busy session, one waiting on you, the
orchestrator, a judge mid-battery, an unsettled run, and a session whose status cannot be
read are still never archived. Dispatched runs are still only hidden, never closed. (CMX-81)
Fixed
-
A Telegram photo or file that times out downloading no longer claims it exceeded the 20 MB
limit. The bridge retries a timeout or network error (3 attempts, with backoff) and gives
both the getFile call and the download a 60s read timeout. If it still fails, the reply names
the real cause: "⏳ Download timed out (network). Please resend." for a timeout, the size
message only when Telegram says "file is too big" or the update'sfile_sizeis over 20 MB
(then no download is attempted), and "Could not download the file ()" otherwise. The warning
log line includes the exception class. (CMX-63, #628) -
Sandboxed share sessions keep their conversations across relaunches, so
/resume
works. The guest's HOME is a tmpfs, so every relaunch (a crash, an approved
chela-request mount,chela share-session <name>again) threw its transcripts away. Each
workspace now gets one owner-only host directory under$CHELA_DIR/share-transcripts/,
bind-mounted read-write at~/.claude/projectsin that workspace's guest only — nothing
else of~/.claudeis mounted. The launcher refuses to start if the directory can't be
made or any part of its path is a symlink,python -m chela.share_sandbox checkaccepts
exactly that mount and still fails on any other, and a guest'schela-request mountfor it
(or forCHELA_DIR) is still refused. Relaunch the share windows to pick it up — only after
any live conversation in them is done, since it exists only in that window's tmpfs. (CMX-12) -
A docs-only PR is parked once for a human instead of being judged four times. When
every file a PR changes is documentation (underdocs/orchangelog.d/, or a
top-level*.md/*.rst/LICENSE-style file other thanWORKFLOW.md,CLAUDE.mdor
AGENTS.md), the dispatcher no longer spawns a judge. It moves the run toneeds_human
in one write ("docs-only — nothing to verify; needs a human OK /chela merge --override") and the inbox sends one notice. A.pyor other non-prose file under
docs/still counts as code, as does any PR that changes code alongside docs. The fix
also covers judges that do run: acannot_verifythat re-running the same commit cannot
change (docs-only, nojudge.test_cmd, a deletion-heavy diff) now parks the run the same
way. Before, it used up thejudge_max_unknown_retriesretry budget, sending one notice
per attempt. Flaky unknowns are still retried. A parked run is still not mergeable without
a clean judge or a human override. (CMX-19) -
An agent can no longer kill or silently re-create the live tmux server. chela's
PreToolUsehook (plugin 0.2.7) now deniestmux kill-server, and anykill-session
aimed at chela's session, unless the command pins a private socket with-L/-S. A
TMUX_TMPDIR=prefix does not count, because$TMUXoverrides it inside a pane. The
self-heal inscripts/agent-terminals.shandchela.discovery.ensure_sessionnow starts
the server from a scrubbed env (no Claude session markers, proxies,PERF_*) and unsets
the same set from the new server's global env. It refuses to heal whenTMUX_TMPDIR
names a missing dir, since tmux would fall back to the default socket. A supervisor
whose cwd was deleted exits instead of healing. (CMX-21) -
Agent windows no longer inherit a parent Claude session's markers.
child_env()
dropsCLAUDECODE,CLAUDE_CODE_SESSION_ID,CLAUDE_CODE_CHILD_SESSIONand the other
markers. Every launch path also scrubs them, along with proxies andTMUX_TMPDIR, from
tmux's global env, so a pm2 service that carries them no longer turns agent transcripts
off. An operator behind a real proxy can name it inCHELA_CHILD_ENV_FORWARD. (CMX-21) -
Telegram: a screenshot an agent opens with
Readnow relays as a photo. With tool calls
hidden (the default) the image-bearing tool result was dropped whole, and the Read tool's
"[Image: original … Multiply coordinates …]" note relayed as a 👤 user message instead. The
photo now posts (tool text stays hidden) and the note is never relayed. (CMX-24, #608) -
A window whose agent moved to a Claude Code background session no longer reads
"unknown" / "Done". The pane's ownclaudehas noclaude agents --jsonentry after the
move; chela now follows the entry that descends from it (background first, then newest) for
status, cwd and transcript across the Wall, sidebar,chela peek, collab and needs-input
pushes.chela peekandchela status --sessionsshow the background session's name — the
one ListAgents/SendMessage need. (CMX-28, #612) -
A window's context bar and live cost show that window's own session. The statusLine
hook names the cache file after the window resolved from$TMUX_PANE, which every process
launched from a pane inherits. A Claude Code background session therefore overwrote the
window's cache, and the bar showed its numbers (69%, $43) instead of the interactive
session's (9%, $0.8). The hook now also writescontext/by-session/<session_id>.json.
/api/agents/contextand/api/cost?window=liveread the session the window's own claude
process is running. The transcript fallback reads that window's own transcript, not the
newest one in its working directory. Stale per-session files are pruned on the
snapshot-history schedule. (CMX-29) -
Dispatched agents and judges show their context bar and cost again. The statusLine
hook wrotecontext/<window name>.json, so a window named<org>/cmx-N-<slug>pointed
at a subdirectory that does not exist, and since 2026-10-02 every dispatched window cached
nothing. The file is now keyed bychela/cachekey.py, the one mapping both the hook and
chela.contextuse. A failed cache write now exits non-zero and appends to
$CHELA_DIR/statusline-errors.loginstead of failing silently. (CMX-30) -
The Cost tab's Today / 7d / 30d views list only agents that ran in the period. An
agent with no snapshot inside the window no longer gets a $0.00 row. A live session with
no reported cost shows—, and a real $0.00 is no longer turned into unknown. (CMX-30) -
iPhone Safari no longer zooms the dashboard in when you tap an input. On a touch
device every text control (Settings search, the Jump palette, the new-session form,
settings fields) and the terminal's hidden input now use a 16px font, the size below which
iOS auto-zooms on focus. Desktop sizes are unchanged, and pinch-zoom still works. The same
fix covers the shared-terminal joiner page. Restartchela-dashboardto pick it up.
(CMX-409) -
The dispatcher watchdog now re-nudges an agent stranded behind Claude Code's ghost
suggestion. Claude Code draws a faint suggestion (❯ Try "fix lint errors") into an
empty prompt, and the watchdog read it from a plain pane capture as typed text, so an
agent whose brief never landed sat idle for hours and was never re-sent it. The stuck
check now captures the pane with its escapes and treats a faint-only input line as empty.
A typed draft still counts as input, and an agent that is visibly working is still never
nudged. Restartchela-daemonto pick it up. (CMX-410) -
A long judge battery is no longer killed by the judge agent's own Bash tool. The judge
agent now runschela judge run … --detach, which starts the battery in its own session
and process group, logs to$CHELA_DIR/judge-logs/<task>.log, and returns at once. Before
this, the tool backgrounded the run at 10 minutes and killed it at 30, and the agent
restarted it from zero until the 60-minute judge wall fired. The wall now counts from the
run's own start rather than from when the agent was spawned. A second run on the same task
while one is live is refused.chela doctorlists a live judge run with its elapsed time
and experiment progress (k/N). Restartchela-daemonto pick it up. (CMX-411) -
docs/SHARE_SANDBOX.mdnow says shift+tab cycles manual → accept edits → plan → auto
(still no bypass). (CMX-418) -
The active-shares pill no longer covers "+ New shell" on the Wall. The floating
kill-switch pill sat in the same top-right corner as the Wall toolbar's "+ New shell", so
with a share live the two were drawn on top of each other. On desktop the toolbar row now
carries the pill itself, in line just left of "+ New shell" and at the same height; the
floating copy steps aside only while the Wall is showing, so a live share is still visible
on every other tab. With no share, "+ New shell" stays where it was. Phones are unchanged.
(CMX-422) -
A pasted or dropped image reaches Claude as an image again. Since CMX-412, pasting a
screenshot into a Wall terminal typed@uploads/paste-….png, a plain file reference.
Images (PNG, JPEG, WebP, GIF) now take the earlier image path: the image is saved under
/tmp/chela-paste-images/and its path is typed into the prompt, so Claude Code attaches
it and shows[Image #N]. Every other file still goes to the session'suploads/folder
as@uploads/<name>. A drop of several files sends each one to its own path, in order.
A share guest's image paste is now refused too, as its uploads already were. Restart
chela-dashboardto pick it up. (CMX-423) -
The phone keybar's Paste button works again, and says why when it can't. It now
reads the clipboard the same way Ctrl/Cmd+V does inside a pane. Text is pasted into the
active terminal. An image, such as a screenshot, takes the image path above. It used to
ask only for text, so a clipboard holding an image produced nothing and the tap did
nothing. An empty or blocked clipboard now shows Empty or Denied on the button
instead of failing silently. (CMX-423) -
A live judge run no longer reads as dead when the host's clock is stepped. A judge
lock recorded its owner's start as a wall-clock time (btime+ ticks since boot). The
kernel recomputesbtimeevery time the wall clock is stepped, and WSL2's time sync
steps it about every 34s. When that happened, the same untouched process failed the
identity check:chela doctorstopped listing in-flight judge runs, the per-task judge
lock could be taken over, and the watchdog could no longer stop a detached run. Locks
and run status now also record the start in ticks since boot, which no clock step
changes, and the identity check compares that first. Older locks still use the
wall-clock comparison. This also fixes thejudge.live_runsdoctor test, which passed
when run directly but failed in judge baselines. Restartchela-daemonto pick it up.
(CMX-424) -
An open dashboard no longer keeps running old JS and CSS after a deploy. The page
now loads its scripts and stylesheets from a versioned path (static/v/<version>/…,
a content hash computed when the dashboard starts), so a reload always gets the new
build and never mixes old modules with new ones. Versioned files are cached for good;
the page itself isno-cache. An open page checks/api/versionon its regular poll
and, when the dashboard was redeployed, shows a small "chela was updated — Reload"
banner. It never reloads by itself, since a share or a typed draft may be in progress.
Restartchela-dashboardto pick it up. (CMX-426) -
A re-created share shows its guests on every open dashboard page. Stopping a share
and sharing the same pane again keeps its relay room but mints a new pairing code. A
dashboard page that didn't do the stop and re-share itself (another tab, the phone)
kept its presence session on the old code, so it couldn't read the new guest's
presence and the pane showed only the host's avatar until a refresh. Each share now
carries ashare_epochon the agents poll. When it changes, the page re-reads the
share and restarts presence with the new code. When a share stops, its avatars go
away. A share that hasn't changed keeps one presence connection. Restart
chela-dashboardto pick it up. (CMX-427) -
The phone keybar's Paste button pastes on an iPhone instead of showing Empty.
iOS Safari allows one clipboard read per tap, and Paste read the clipboard twice
(read(), thenreadText()), so the second read came back empty. It now reads once.
Text that iOS provides only as a link (text/uri-list) or as rich text (text/html,
converted to plain text) is pasted too, not onlytext/plain. Empty now appears only
when the clipboard has nothing usable. A refused read shows Denied. Restart
chela-dashboardto pick it up. (CMX-428) -
A judge whose window is gone is no longer declared lost while its detached run is
still going. Sincechela judge run --detach, the judge agent stops right after
launching the run, so its window disappearing is normal. The watchdog read it as a dead
judge, marked the PR CANNOT VERIFY and spawned a second judge into the same worktree
while the first run was still mutating it. The watchdog, the judge spawn and
--detachnow first read the run's own record ($CHELA_DIR/judge-logs/<task>.jsonand
the slot lock, identified by pid and/procstart ticks). A live run is held: no
CANNOT VERIFY, no reap, no second judge. Its 60-minute wall counts from the run's own
start. Only a run whose process is gone (or was recycled) without a verdict becomes
CANNOT VERIFY. Both records are on disk, so a daemon restart mid-judge finds the run
too. Restartchela-daemonwhen no detached judge is running. (CMX-429) -
A failed or partial tracker read no longer marks live work
done. Both tracker
adapters (markdown,gh_issues) now havefetch_by_ids(ids), the ID-refresh
operation Symphony SPEC 11.1 requires. It returnsNonewhen the read failed (a missing
TODO.md, aghtimeout, an auth or rate-limit error, bad JSON, or a listing cut off
before every id was found). Reconcile re-reads by id every run it would close out
(claimed/running and every review state). OnNoneit changes no run from the tracker,
logs once, and retries next tick. A task the tracker positively reports closed (a struck
line, a closed issue) reconciles as before. An open issue that only lost its
require_labelis no longer treated as done. Restartchela-daemonto pick it up.
(CMX-430) -
A long judge run is no longer thrown away at 60 minutes while it is still working.
The judge wall now grows with the battery: 10 minutes, plus 6 per experiment, plus 1.5 ×
the run's own baseline for each full-suite confirmation, never below the old 60 minutes
and never above 3 hours. Past the wall, a run is only called "stuck, not thinking" when
its progress has stopped. A run still moving is left alone. A run that has finished
every experiment gets 15 more minutes to publish its verdict. Before this, a
12-experiment high-risk run that had finished its battery was killed at 62 minutes and
its real BLOCK was never published.chela doctoruses the same rule. The four limits
are Settings → Dispatch knobs (CHELA_JUDGE_WALL_BASE_S,
CHELA_JUDGE_WALL_PER_EXPERIMENT_S,CHELA_JUDGE_WALL_CEILING_S,
CHELA_JUDGE_WALL_GRACE_S). Restartchela-daemonwhen no judge is running. (CMX-431) -
A sandboxed share session keeps working after the host refreshes its Claude login.
The proxy sidecar used to mount~/.claude/.credentials.jsonas a single file. Claude
Code refreshes its login by writing a new file in place of the old one, which revokes
the old token, and a single-file mount keeps showing the old file. After a few hours
every request failed with401 OAuth access token has been revoked, and the guest was
offered/login. Now the launcher copies only the access token into a per-session
directory (~/.chela/share-token/<id>/), checks for a refresh every 2 seconds, and the
sidecar mounts that directory read-only. The refresh token no longer enters the
sidecar, and the guest still mounts no token. On a 401 the proxy reads the token again
and retries once if it changed. A 401 that persists is answered with a 502, "The
host's Claude login expired — ask the operator…", instead of sending the guest into
/login. For long-running shares,docs/SHARE_SANDBOX.mdnow recommends a
claude setup-tokentoken inCHELA_SHARE_SANDBOX_TOKEN_FILE. (CMX-433) -
A dashboard deploy no longer ends live shares. A share's bridge ran inside the
dashboard, so everychela-dashboardrestart cut the guest off with no notice. The
bridges now run in their own service,chela collab(PM2chela-collab, added to
examples/ecosystem.config.js), and the dashboard talks to it over an owner-only
socket.chela updaterestarts that service only when the share code changed — including code a
baregit pullbrought in, found by the HEAD the service started on (the HEAD reflog). Each
live share is also kept in$CHELA_DIR/shares.json(0600, never inside a git work
tree), so whichever process hosts the shares restores them after a restart with the
same link and pairing code. A share whose window is gone or changed isn't restored. Nor
is a typing share whose window no longer verifies as a sandbox, or an UNSANDBOXED
override that expired. Withoutchela-collabthe dashboard hosts the shares as before
and restores them itself. A restored host never reuses an AES-GCM nonce. It also
refuses replayed keystrokes until the guest answers a fresh challenge. The guest page
shows "host restarting…" instead of "This share has ended" and reconnects by itself;
it needs awrangler deployofchela/collab-relay.chela updateprints "N live
share(s) will be interrupted" before a restart that would interrupt them, and
chela sharesreports the same for a hand deploy. Start the new service with
pm2 start ~/.chela/ecosystem.config.js --only chela-collab && pm2 save. (CMX-434) -
A sandboxed session's Claude banner no longer says "API Usage Billing". The guest's
Claude was given a placeholderANTHROPIC_AUTH_TOKEN, so it labelled itself as API
billing, though the share proxy always sends your Claude subscription's OAuth token.
It now gets a placeholder login in its own temporary HOME that names your plan (read
from the non-secretsubscriptionTypeof your credentials file, or
CHELA_SHARE_SANDBOX_SUBSCRIPTION), so the banner reads "Claude Max" / "Claude Pro".
Every request still goes through the proxy, which still replaces the auth, and the real
token still never enters the container. When the plan can't be read, the old
placeholder env is kept. Applies to sandboxed sessions started after the deploy. (CMX-435) -
Sandboxed sessions show working/idle instead of "unknown". Claude in the container
is invisible to chela's usual status sources, so the share proxy now records its
in-flight model requests in the session's proxy-only directory.chela peekand the
Wall read that for a window that verifies as sandboxed: working while a request is in
flight (or finished within 4 s, to cover tool calls), idle after. The tooltip says
"from the sandbox proxy". A permission prompt can't be seen this way, and ordinary
windows are unchanged. (CMX-436) -
The dispatcher-sessions toggle in "Recent sessions" reads correctly. It said
"Hide 1 dispatcher session hidden". Collapsed, it now reads "Show 1 hidden dispatcher
session" ("sessions" for more than one); expanded, "Hide dispatcher sessions". Those
rows still have no Resume button. (CMX-437) -
Settings → Update flags a service as stale only when code it runs changed. Before,
any new commit marked every runningchela-*service stale: a dashboard-only change
flaggedchela-daemon,chela-telegramandchela-agent-terminals, none of which run
that code. Now each service's code set is worked out from its import graph, launcher
scripts and data files, and compared with what changed since the commit the service
started on. A service whose start commit can't be read is shown as "unknown". The same
change fixes therepo.services_currentdoctor fact and the restart-only path of
chela update. -
"Update now" no longer does nothing when there is nothing to pull. If services are
stale, the button now reads "Restart stale services (N)" and restarts exactly those. It
waits while a dispatched run is in flight, like the update path does. Every click now
shows a message: "already up to date", "restarting X, Y", or the error. The card's text
also wraps properly on a phone. (CMX-56)
Security
-
A typing share stops streaming the moment its pane stops verifying as a sandboxed
session. Before, swapping the pane's process stopped the guest's typing, but until the
share ended the guest's view could still show the new host shell (a prompt with an email
address and cloud region in it). Now a share created with Allow typing checks the pane
before every frame it sends: if the pane's process changed, the container check fails, or
the check can't tell, that frame is dropped and the share ends, telling the guest "the
session stopped being a verified sandbox". A view-only share of an ordinary window is
unchanged. Restartchela-dashboardto pick it up. (CMX-416) -
Secrets from
chela.envandsecrets.envno longer reach the processes chela launches.
chela loads both files into its own environment, so before this change every agent and
judge window, workflow hook and judge test suite the daemon started carried them, including
theLINEAR_API_KEYthe Linear tracker will use. Those children now get the environment
withoutLINEAR_API_KEYand without any name shaped like*_API_KEY,*_TOKEN,
*_SECRETor*PASSWORD*. Before opening a tmux window, chela also unsets those names
from the tmux server's global environment, which is what new windows inherit. Claude
Code's own credentials andGH_TOKEN/GITHUB_TOKENare still passed on. Name any other
variable a hook needs inCHELA_CHILD_ENV_FORWARD.PATH,HOMEand theCHELA_*
settings are unchanged. Restartchela-daemonandchela-dashboardto pick it up.
(CMX-425) -
An override approval stays human-only:
chela merge-approverefuses in a process carrying
$CHELA_ACTORor a Claude Code session marker, and the merge-gate hook (plugin 0.2.8) also
denies a session'spythoncall intochela.overrides.decide. (CMX-79)