Repository navigation
Releases: Devail1/chelamux
Release list
v0.19.1
Fixed
- Moving the
v0major tag no longer leaves a red Release run. The Release workflow now
triggers only on a fullvX.Y.Ztag (v[0-9]+.[0-9]+.[0-9]+) instead ofv*, so the
major-tag force-move that follows every release no longer starts it and fails looking for a
## [0]CHANGELOG section.tests/test_release_workflow.pyevaluates the filter with
GitHub's documented pattern syntax. (CMX-366)
Changed
- The install instructions now point at PyPI. The landing hero is
uvx chelamux dashboard,
the judge-sweep one-liner uses--from chelamux, and the docs quickstart leads with
uv tool install "chelamux[dashboard,telegram]", keepinggit clone+uv sync --all-extras
as the from-source path. PyPI gets tagged releases only; agit+https://install tracks the
default branch. The README and Getting Started install block stays ongit+https://,
because the fresh-install check swaps that URL for the commit under review. (CMX-367)
Added
chela --version(andchelamux --version) printschela <version>from the installed
package's metadata, orchela 0.0.0+unknownfrom a source tree with none. It used to fail
withunrecognized arguments. A test now also reads the landing hero's install command out
of the page and fails if README.md stops showing it verbatim. (CMX-368)
v0.19.0
Added
-
Every judge round records where its time went. Each round in the private judge
record ($CHELA_DIR/judge-heldout/<task>.jsonl) now has atimingsobject: wall-clock
for setup (worktree checkout and the judge agent, up tochela judge run), worktree
rebuild, provisioning, the baseline suite, each visible and held-out experiment (with
its selected test count and whether it was confirmed on the full suite), the
consistency re-runs and the whole round. Every phase also reports how much of it was
spent waiting for a suite slot, and the round records how many agents and judges were
running when it started.chela judge show <run>prints the breakdown under each
round; rounds recorded before this change print "no timings recorded".
chela judge timings [--since 7d] [--json]gives the median, p90 and max per phase
across every task, biggest phase first. Nothing about what the judge runs or decides
has changed. (CMX-196) -
Keeper mode runs on its own, and a workflow can be keeper-only. A WORKFLOW.md with
keeper: {enabled: true, every: 45m}gets onechela keeper runeveryeveryfrom the
daemon. A run is skipped while the queue is held, while the previous one is still going,
and when no session in the repo typed a prompt or edited a file since the last run.
dispatch: {enabled: false}(ordispatch: false) makes the workflow keeper-only: the
dispatcher claims, reconciles and judges nothing for it, auto-merge and the nightly sweep
skip it, and it is removed from the merge gate's registry, so your own sessions can push
to its base. A bad value for either key fails the parse, so the last good config stays
in force.chela keeper stats [--since 7d] [--json]reports proposals made, accepted,
rejected and dropped, the acceptance rate, which is flagged under the 70% kill line, and
a by-action breakdown. (CMX-348) -
chelamux is on PyPI:
uvx chelamux dashboardruns the wall with nothing to clone. Each
release tag now builds the sdist and wheel, smoke-tests the wheel and publishes it with
Trusted Publishing (no API token). The package gains achelamuxcommand next tochela,
souvx chelamux …needs no--from. CI builds and runs the wheel on every PR, and a test
checks the wheel carries the dashboard bundle, the ttyd shims, the templates, the plugin's
merge-gate script and the statusLine hook. (CMX-363)
Changed
- The dashboard's packages are core dependencies. Flask, flask-sock and cryptography
install with chelamux itself, since a bareuvx chelamuxcannot name an extra. The
dashboardextra still exists (empty), sochelamux[dashboard]install lines keep
working.chela install-statuslinenow finds the hook inside an installed wheel too, not
only in a checkout. (CMX-363)
Fixed
-
chela ops scanno longer reports a short-lived test child that was re-parented to init
for a moment (four false "orphaned (ppid 1)" notices between 2026-10-24 and 10-27, each
process gone a minute later). An orphan is now a finding only when the same process (pid
and start time) is still alive with ppid 1 on the next scan. An orphan with no named owner
is also never one while it shares achela-suite-*scope with a live process of that
suite and is within the suite timeout. A real leak, like a test fixture left running for
two days, is still reported once. (CMX-332) -
A merged or closed run no longer leaves its judge behind. When a PR merged (an
operator-approvedchela merge --overrideincluded) or closed while its judge was still
running, the daemon left that judge alone as long as its tmux window existed. A window
holding only a bare shell counted too, so it stayed open long after the run was done and
was mirrored into every webterm session. Now the tick that reconciles the run todone,
closedorfailedstops its judge the same way a send-back does: the round is
cancelled, the detached run is sent SIGTERM, the judge window is killed and its worktree
removed. A judge on a merged PR therefore no longer gets to record a blocked-race verdict
after the merge. (CMX-336) -
The "CI flake, re-run green" notice names the test that flaked. It used to name the
first log line that said "failed" ("re-register failed nope", "Test Files 1 failed 1"),
because pytest'sFAILED <nodeid>line can carry a long assertion message that pushes the
line out of the log tail chela read. The test id is now read from the whole failing log:
pytest'sFAILED/ERRORlines, or Vitest'sFAIL <spec> > <suite> > <test>header (which
wins when a JS suite ran under pytest). When no line names a test, the notice says "failing
test not identified" next to the failing job. Flakes are recorded under that id, and
chela ops scannow counts repeat Vitest flakers as well as pytest ones. (CMX-351) -
The demo-env leak guard no longer fails on a machine without
$LANG. Its probe
os.environreturned a sentinel for an allowed name that was unset, sodemo_env's
os.environ.get("LANG", "C.UTF-8")saw the sentinel instead of its default and the
nightly sweep (which runs with noLANG) went red. The probe now readsPATH/LANG
exactly asos.environdoes, and the test runs withLANGboth set and unset. (CMX-364) -
The "self-check still working" notice no longer fires on every normal hand-in. Its
ceiling was a fixed 1h, but a normal self-check with a 2400 s budget routinely runs past
that. UnlessCHELA_SELFCHECK_ALERT_Sis set, the ceiling now comes from the run's own
WORKFLOW.md: the larger ofjudge.self_check_budget_secondsand
judge.sweep_budget_seconds, plusjudge.suite_timeout_seconds, times 1.5. That is 2h with
the current budgets. When the workflow cannot be read, the ceiling is 90 minutes. The
re-notify interval is unchanged. (CMX-365)
v0.18.2
Added
-
chela task show: a dispatched agent reads its own Linear issue. It prints the issue's
current title, description and comments (oldest first, author and timestamp), so a comment
on the issue now steers the next attempt. Only comments by the API key's own user and the
workflow'slinear.comment_authorsare shown; bot and integration comments are dropped and
counted. A full page says TRUNCATED, and a failed read says "could not fetch" and exits
non-zero. It is read-only, and the key stays in chela's process, never in the agent's
environment. A Linear workflow's dispatch and rework prompts tell the agent to run it at the
start of every attempt. (CMX-287) -
A notification digest with interrupt rules. Set
digest.enabledin
~/.chela/config.jsonand only needs-you events (a run that needs a human, an override or
release waiting for approval, a failed run/merge/release, CI still red on known flakes
after its re-runs, a low-memory kill, a refused permission) are pushed to the orchestrator
at once. Everything else is held for a digest sent atdigest.times(default 09:00 and
16:00) and on demand withchela digest: needs you (at most 3), waiting on others, done
since last, changed — at most 150 words, a link on every line. Held events are still in
chela eventsand the dashboard. Off by default;digest.interruptreplaces the default
list of kinds that interrupt. (CMX-320) -
chela keeper run <WORKFLOW.md>proposes board changes from the digest, and a person
approves them. It builds the keeper digest, asks one headlessclaude -pcall (no
tools) for proposals, and checks each one in code: a proposal must cite evidence that is
in the digest and quote it, and each item it cites must be about that issue (it names
the id or quotes a word of the title), or it is dropped asevidence_unrelated. It must
also use an allowed action:in_progressonly from Todo or Backlog,comment,
new_backlog_issue,flag_staleonly for an In Progress issue that nothing in the
window names, anddoneonly when it cites a merged PR that names the issue. The survivors go to the inbox as ONE item.
chela keeper approve <batch> [--only 1,3]applies them to the tracker, each with a
"chela keeper" attribution comment naming the approver and the evidence;
chela keeper dismiss <batch>writes nothing. Every proposal and its outcome is a row in
$CHELA_DIR/keeper/log.jsonl. The Linear source can now add comments. (CMX-347) -
A daily worktree GC. A worktree whose run closed without removing it, or whose run row
was pruned or requeued, used to stay on disk for good (26 worktrees, 1.9 GB, 21 of them
orphaned). The daemon now runschela ops worktrees --gconce a day. It removes each
worktree under a workflow'sworkspace.rootthat no run owns, or whose run has beendone
orclosedfor at least 3 days, and keeps the branch. A worktree with uncommitted changes is
first saved to$CHELA_DIR/worktree-salvage/<name>-<date>.tar.gz(the diff plus its untracked
files, without venvs,node_modulesor caches). A worktree with a live run or a process
running in it is never touched, and neither is one the pass cannot read.chela ops worktreesreports the count and size of orphaned worktrees without removing anything.
(CMX-357)
Changed
-
A spent rework cap re-queues the task instead of waiting on a person. The first time a
run hits its rework cap with the PR still blocked, chela closes the PR (the branch is kept),
comments every round's survivors grouped by mutation shape, and re-queues the task as a
fresh attempt on a new branch. The new attempt's prompt gets an addendum: what the earlier
rounds tried, every surviving group, and an instruction to start fresh from the base and
read the old branch for reference. Only a second exhaustion of the same task goes to
needs_human, and the notice says it is the second. (CMX-290) -
The public landing page, docs page and README now name the project chelamux, at
chelamux.com. Page titles, Open Graph and Twitter meta, canonical links, the nav
wordmark and the hero copy say chelamux; the command-line tool is stillchela, and
absolutechela.pages.devlinks now point tohttps://chelamux.com. The package name,
CLI entry point,~/.chelaandCHELA_*settings are unchanged. (CMX-361) -
The landing site serves a
robots.txtand asitemap.xmlfor chelamux.com, so
/robots.txtno longer falls back to the home page. (CMX-361) -
The GitHub Action's display name is now "chelamux judge" (was "chela judge sweep"). The
uses: Devail1/chelamux@v0reference, inputs and outputs are unchanged. (CMX-361) -
The landing page leads with the dashboard wall. One hero (every Claude Code session live
on one screen, on desktop or phone, with recaps and a flag on the ones that need you), the
wall demo right under the headline, and one primary call to action, Install. (CMX-362) -
Install on the landing is one line:
uvx --from 'chelamux[dashboard] @ git+https://github.com/Devail1/chelamux' chela dashboard, with the prerequisites
on one small line under it. (CMX-362) -
The merge gate and judge move to a section lower down ("And when agents open PRs…"),
with a short mention of the GitHub Action. The early / single-user status note moves into
the FAQ, and encrypted terminal sharing is cut to one line under the wall. (CMX-362) -
Hotfix: a parked run's idle-clock write is committed at once, so the dispatch tick no longer locks itself out (
database is lockedon every tick, which stalled hand-ins and claims).
Fixed
-
A parked run is no longer re-nudged and then failed by the idle watchdog.
chela park
kept a run out of the inbox's STUCK notice, but the dispatcher's idle-at-empty-prompt
watchdog never checked it: an agent parked and told to wait was re-sent its task prompt,
then failed "idle at empty prompt after re-nudge" about an hour later. The watchdog now
stands down for a parked run (a manual park, or the automatic park of a suite-slot wait)
and restarts its idle clock on every parked tick, so afterchela unpark, a reached
--untilor the park max, the agent gets the full idle budget again. (CMX-339) -
A red nightly-sweep baseline now names its failing tests and keeps its evidence. The
notice andnightly.logused to say only "1 failed, 11024 passed", and the JUnit report
went with the checkout. A red baseline now lists each failing test id (the first five, plus
a count) with the first line of its failure, says what re-running them alone showed, and
keeps the JUnit report and the output tail in
$CHELA_DIR/sweep-nightly/<date>/<project>/(the newest 14 dates are kept). A failing test
that passes when re-run alone is reported asbaseline flake: <ids>and is deselected
from every mutation run, so the battery still runs that night. The isolated re-run now
also finds failing test ids in the JUnit report when the output tail no longer holds
pytest'sFAILEDlines. (CMX-341) -
A changelog fragment named after a reused issue number no longer turns the suite red
late. Issue numbering restarted, so a new task often shares its number with an old one
that already shipped, and its barechangelog.d/CMX-<id>.mdread as an already-released
fragment. Fragments are now namedCMX-<id>-<slug>.md(slug from the branch), and
chela task-finishedrefuses a bare name that collides before any suite runs, with a
rename hint. A fragment whose text already shipped (bare or slugged) is told to go
instead: renaming it would only hide it until it republishes. (CMX-356) -
A watchdog write no longer locks out a later run's hand-in in the same tick. The
dispatcher's reconcile loop left several watchdog writes uncommitted (a parked run's idle
clock, a window-gone or idle failure, a nudge stamp), so the tick held the database write
lock while a later run'schela task-finishedrequest was applied on its own connection,
which then failed with "database is locked" and stalled the queue. Each of those writes
now commits before the loop moves on, with a two-run regression test per write. (CMX-359)
v0.18.1
Added
-
chela keeper digest <WORKFLOW.md>prints what happened in a workflow's repo since the
last digest. For each live session launched in the repo or one of its worktrees it lists
the window, its live session id, busy/idle, the latest recap, the prompts typed and the
files edited. It also lists the commits on any branch, the PRs opened or merged, the issue
ids mentioned anywhere, and every open issue on the workflow's tracker. It uses no LLM and
never writes to the tracker. Each section is capped and says how many rows it dropped. A
git,ghor tracker read that fails is shown as unreadable, never as empty. A cursor in
$CHELA_DIR/keeper/makes the next run start where this one ended;--since 90m
overrides it, and--jsonis the input for the next keeper slice. (CMX-346) -
An agent's own pytest run is memory-capped per test process. A suite started by
chela task-finishedor the judge runs in a memory-cappedchela-suite-*scope; an
agent's ownpytestdid not, and one runaway xdist worker reached 11 GB and got other
agents' suites reaped.tests/conftest.pynow sets a softRLIMIT_ASof 3 GiB on each
process that runs tests, unless achela-suite-*ormemcapcgroup already caps it, so
a runaway test fails withMemoryErrorin its own worker. Subprocesses (node, vitest,
Chromium) are exempt.CHELA_TEST_MEMORY_CAP=6Graises it;0turns it off. The
standing rules now say never to run a full suite yourself. (CMX-354) -
Tests no longer inherit the live
CHELA_*environment (e.g.CHELA_AUTO_MERGE=truefrom chela.env), which turned every nightly-sweep and agent-sweep baseline red (CMX-355).
Changed
- A blocking verdict lists every same-shape site, not only the one the judge named. Each
surviving mutation is grouped by its shape (a dropped or swapped call, a flipped comparison,
or the exact line) and its target symbol, and the verdict lists every line of the PR's diff
with that shape, found from the diff rather than recalled by the model. The rework is asked
to fix the whole group and to corrupt each site by hand before hand-in. Held-out survivors
are never grouped or named. (CMX-335)
Fixed
-
A Telegram photo is no longer reported "not submitted" when the agent got it. The
bridge now confirms the submit from the agent's ownUserPromptSubmithook, which fires
when Claude Code takes the turn; the pane read is only the fallback. When neither confirms
it, the reply says the submit is unconfirmed and asks you to check/screenshotbefore
pressing Enter, since an Enter on an already-submitted prompt sends an empty or duplicate
turn. (CMX-344) -
The frontend gate test no longer runs the whole Vitest suite a second time.
test_each_ci_gate_passes_the_committed_tree[test]wasvitest runagain, beside
test_frontend_vitest_suite, with a 180 s budget; it timed out twice in CI while xdist
shared the runner. That case is gone, and the remaining full Vitest run over the fixture
copy gets the 300 s budget the main suite already had (the suite's measured p95 is 112 s
on an idle runner). (CMX-352) -
chela ops scanno longer reports a live suite's test PM2 daemon as leaked. It
decided liveness by matchingPM2_HOMEin other processes'/procenviron, which an
xdist worker never shows (conftest sets it after start-up), so a running suite's daemon
was flagged, killed, and the suite hung to its timeout. A/tmp/chela-tests-pm2-*God
Daemon is now spared while any suite of its run is live: a process in its
chela-suite-*scope, one of the run's worktree in such a scope, or a test run of that
worktree. Once no suite is live it is reported only on its second scan in a row, and the
finding says "do not kill while a suite runs". (CMX-353)
v0.18.0
Added
-
Agents review their own diff before handing in.
chela task-finishedrefuses a code
PR whose body has no## Reviewsection (prose-only PRs are exempt). An agent that cannot
review passes--no-review-reason "…", which is recorded on the run and shown to the judge
and on the PR. (CMX-209) -
Every judge and self-check suite run now has its own memory ceiling. chela wraps each
run in its ownsystemd-run --user --scope(CHELA_SUITE_MEMORY_MAX, default 8.3G, which
is 3x the measured 2.75 GiB anon peak of a full suite run;0turns it off). A run that
blows through its ceiling is killed inside its scope and reported as CANNOT VERIFY ("suite
exceeded 8.3G"), never as a pass. chela claims the ceiling only when the suite's own
/proc/self/cgroupnames the scope. Whatever a suite leaks into its scope, such as a
daemon that ignores SIGTERM, is SIGKILLed when the run ends. (CMX-119) -
At most K suite runs at once, judges and agents together. Each suite run takes one of
K slots first (CHELA_SUITE_SLOTS, default half the RAM over the measured peak: 2 on a
16 GB box). A queued run says⏳ waiting for a suite slot, is served first come first
served, and is never reported as stuck, and the wait does not count against its budgets or
the judge's wall. (CMX-119) -
Spike: chela's merge gate and event posting as a Claude Code Mod.
mods/chela-hooks/
runs the same gate script and posts the same event bodies as the shell plugin. It needs
Claude Code 2.1.301+, chela never installs it, and it replaces nothing.
docs/MODS_SPIKE.mdholds the side-by-side measurements: it posts faster than the shell
plugin, missed no events, and denied the same commands. Its recommendation is to build
the in-session UI items as an opt-in Mod and to keep the shell plugin as the baseline.
(CMX-157) -
The judge keeps a failing suite's output. Every non-green infrastructure suite run in a
judge round (the baseline, its coverage run, and the base-branch diagnostic) is written to
$CHELA_DIR/judge-logs/<task>/<round>-suite-<n>.logwith the command, cwd, env diff vs the
operator, start/end time, exit code and up to 200,000 chars of output. Before this, only a
one-line summary survived, so acannot_verifycaused by a flaky baseline could not be
diagnosed later.chela judge showlists each round's logs, and acannot_verifyverdict
line and PR comment start with a~-relative pointer to them. A green baseline writes
nothing. Logs older than 30 days, or past the newest 20 per task, are pruned whenever a
new one is written. (CMX-163) -
Settings > Dispatch has "Agents at once" and "Judges at once" rows. One stepper per
workflow sets an operator-local override of WORKFLOW.md'sconcurrency.max(Reset goes
back to the file value), and one stepper sets the judge count, which now wins over
CHELA_JUDGE_MAX_CONCURRENT. Both are bounded 1 to 8, validated by
/api/config/concurrency, read on the dispatcher's next tick (no file edit, no restart),
logged to the event log assettings.concurrency(who, old, new), and shown with what the
count means for memory (N x the measured judge peak, next to available memory). Lowering
a count never stops a running agent or judge.judge_max_concurrentleft the generic
Dispatch knob list for this row. (CMX-164) -
chela judge sweepmutates TypeScript and TSX. A changed.ts/.tsxfile is no
longer UNKNOWN: it gets the JS mutations (if, comparison flips,return 0/1) plus
&&↔||,return true↔false, a ternary's condition forced each way, and JSX
conditional rendering ({c && <X/>}→{false && <X/>}). Every mutant is parse-checked
with the repo's owntypescriptpackage; one that does not parse is skipped, never counted
as killed or survived. With notypescriptto parse with, the sweep reports it cannot
verify..d.tsfiles and*.spec.ts/__tests__/files are not mutated. (CMX-173) -
A TS mutation runs only its related Vitest tests. When
judge.test_cmdis one Vitest
invocation (pnpm exec vitest run), the baseline also writes Vitest's JSON report, and each
mutated.ts/.tsxfile runs only the test files whose import graph reaches it, directly or
through other modules (./a,./a.jsand./dirresolve toa.ts/a.tsx/dir/index.ts).
A TS file no test reaches is UNTESTED: it still runs the full suite, and a survivor there
is reported as[UNTESTED], a survivor-class finding. (CMX-174) -
The judge and the self-check grade TypeScript.
chela judge run,chela judge self-checkandtask-finished --self-check-experimentsapply experiments to.ts/.tsx
files with the sweep's parse proof and Vitest test selection, within the same budgets. A
TS-only PR getsblockedorcleanlike any other. If the repo has notypescript(or no
node) to prove a TS file parses, the run is cannot-verify, never clean. Vitest's
Test Filessummary line no longer counts as tests run. (CMX-175) -
A React + TypeScript frontend scaffold for the dashboard in
chela/dashboard/frontend/
(Vite, strict TypeScript, Vitest, React Testing Library, ESLint), installed by the root
pnpm installas a workspace package. dependency-cruiser rules keep eachsrc/features/*
andsrc/shared/*module reachable only through its root entry files and forbid cycles. CI
gains afrontendjob (typecheck, lint, module boundaries, test), and the Vitest suite runs
inside pytest too, as do tests that feed each boundary, type and lint rule a deliberate
violation. Nothing is wired into the dashboard page yet; users still need no Node.
(CMX-176) -
The dashboard frontend now builds into Flask's static dir, and the build is committed.
pnpm -C chela/dashboard/frontend buildwrites hashed files plusmanifest.jsonto
chela/dashboard/static/app/, served understatic/v/<version>/app/with the same
cache headers as every other asset. Because the output is tracked, a wheel, an sdist and
a plaingit clone+uv syncall serve it with no Node installed. CI and pytest fail
when it differs from a fresh build ofsrc/, andchela releaserebuilds it before it
commits.chela release --dry-runruns every release step (frontend build included) and
leaves no commit, tag, request or push. (CMX-177) -
The dashboard loads its first React island. The page now loads the built frontend
(named by the Vite manifest) before the legacy modules, and the sidebar foot shows the
dashboard build version, rendered by React from/api/version. Legacy code publishes the
agents list and the sidebar layout to a small shared store React reads, so components can
be ported one at a time.docs/FRONTEND.mddocuments the stack, islands, the store, the
dev loop (pnpm dev) and how the judge grades TypeScript. (CMX-178) -
chela's status line as an opt-in Mod. On Claude Code 2.1.301 or later,
mods/chela-hooks/draws a status line with the context bar, the window id and an
orchestrator or dispatched-run marker, through$.ui.status. After every turn it also
writes the context cache the dashboard reads, through the same
scripts/cache-statusline.sh, so loading the Mod needs nosettings.jsonedit.
chela install-statuslineis unchanged and stays the default. The Mod's event posting
now runs only whenCHELA_MOD_HOOKS_URLis set, so a chela pane that loads the Mod does
not file each event twice. (CMX-181) -
A "needs you" band in the opt-in Mod. With
mods/chela-hooks/loaded (Claude Code
2.1.301 or later), the orchestrator session shows one line above the prompt with the
pending override approvals, access requests and runs inneeds_human: counts plus the
oldest, each linking to the dashboard. It polls the new read-onlyGET /api/needs-you
every 10 s, shows nothing when the daemon is down, and hides a reading older than 30 s.
Approving stays in Telegram, the dashboard or a plain terminal. (CMX-183) -
Classifier denials reach the inbox straight from the opt-in Mod. With
mods/chela-hooks/loaded, aclassic.PermissionDeniedis POSTed to the new
POST /api/inbox/denial, which files CMX-155's orchestrator-inbox notice (tool, one-line
reason, session, window) without tailing the event log. Withplugin/loaded too, one
denial is still one notice: both copies are keyed on itstool_use_id. A dead daemon
never affects the tool call. Under--permission-mode dontAskno denial fires in either
layer.plugin/drops nothing yet (docs/MODS_SPIKE.md§8.0). (CMX-184) -
Settings > Telegram topics. "Telegram: delete a topic when its window closes" (off by
default) makes the bridge delete a dead window's topic instead of archiving it, falling
back to archive if the delete is refused. "Delete archived topics (N)" deletes every
remembered archived topic after a confirm, never a bound one or General. Both apply on
the bridge's next tick, no restart. (CMX-204) -
Risk tiers for the merge gate. A PR whose every changed file is prose (text under
docs/, achangelog.d/fragment, a top-level*.mdother thanWORKFLOW.md/CLAUDE.md/
AGENTS.md, or an image or video) gets no judge round. The dispatcher records the verdict
prose_onlyon its head, andchela mergeaccepts it once CI is green and GitHub says
MERGEABLE. A PR that touches an always-human path (WORKFLOW.md,.github/workflows/**,
plugin/**, the merge gate and release code, plus any workflow'smerge.always_human)
needs an operator-approved--overrideeven with a clean judge, and both the refusal and
the override question name the path. If the file list cannot be read, the PR is never
treated as prose. (CMX-208) -
chela dispatch --pause --allquiets the whole fleet. It holds the queue, then
presses Escape in each dispatched agent's window and tells it to start nothing until
"resu...
v0.17.0
Added
chela judge sweep --githubpublishes a sweep to GitHub Actions with only
GITHUB_TOKEN: it writes the job step summary and theverdict/survivorsstep
outputs, and keeps ONE sticky PR comment — found by an HTML marker and edited in place, a
new one posted only when the result is not clean.--no-commentskips the comment. A
comment list that cannot be read is an error, never a duplicate post, and publishing never
changes the sweep's exit code: every failure is a::warning::line. (CMX-97, CMX-132)
Changed
- The README shows how to try the judge on your own repo with no setup: the
uvx … chela judge sweep --base origin/mainone-liner, a real sample run, the GitHub Action
snippet and the exit codes. Without--baseor a workflow,chela judge sweepnow diffs
against the remote's default branch (origin/HEAD), elseorigin/dev. A standalone run no
longer suggestschela task-finished --sweep-equivalent, which only exists in a chela-managed
run (one with aWORKFLOW.md). (CMX-134)
Fixed
-
chela watch @Nfrom a side session no longer steals a live orchestrator's pin. It now
claims the role only when the pin is empty, undeliverable (dead window, new tmux epoch) or
already the caller's; otherwise it adds the watch routed to the current orchestrator and
printswatching @N — events go to the orchestrator @M (live). A barechela watchfrom a
non-orchestrator gets the--if-unclaimedrefusal plus a hint. Taking the role from a live
holder is explicit:chela watch [@N] --take-over. (CMX-138) -
The landing deploy's "production serves this commit" check no longer fails on every
deploy since Cloudflare Web Analytics was switched on. Cloudflare injects its beacon
<script>into every pagechela.pages.devserves, so the served HTML never matched
landing/index.htmlagain and each deploy went red with a misleading "most likely a
PREVIEW deployment" error. The comparison now lives inscripts/landing_verify.py, which
strips only that beacon element before the whitespace-normalised token diff. Any other
difference still fails, down to one character of body text. The workflow's verify step is
tested by running its ownrun:block withcurl/sleepstubbed. (CMX-149, #692) -
A PR queued behind another judge no longer gets the "NO judge round has started" stall
notice. Withjudge_max_concurrentsaturated by its workflow's running judges, a run in
awaiting_reviewis a queue, not a stall: it stays silent for three
stall bounds (CHELA_JUDGE_NOT_STARTED_ALERT_S× 3, 1h by default), then says once that it is queued behind which
run, judging since when, and how many slots there are. The stall clock restarts from the
last tick that saw it queued, so a freed slot gets the full 20m before the stall notice
fires. The stall notice no longer tells the operator to runchela judge run CMX-N, which
needs the judge agent's--experimentsand is not how a round starts. It now says the
daemon starts the round on its next dispatch tick and points atchela dispatch --hold-status. A test runs that argv through the real CLI parser. (CMX-150)
v0.16.0
Added
-
Hold Alt to number the sidebar sessions; Alt+1..9 opens that row — like the Claude
desktop app's Ctrl-held numbers (Ctrl+1..9 is the browser's tab switch). The numbers go on
the first nine visible session rows, top to bottom; collapsed groups and the Archived
section are skipped. Alt+N now works in every mode, not only the wall, and from inside a
focused terminal pane. The wall panes' corner numbers are gone — the sidebar's is the one
numbering. (CMX-85, #657) -
Pending approvals on the dashboard — no Telegram needed. A ⚖️ pill on every view counts
the override (and, once CMX-87 lands, release) requests waiting on you; its sheet shows
what, who asked, why, age and time to expiry. Approve (a second tap confirms) and Deny go
through the same decide path as/override/<id>and re-check head / CI / mergeable /
expiry; a request that can't be approved says why instead of offering the button. Live over
SSE, with an opt-in browser notification (Settings → Notifications, off by default).
chela merge --overridenow prints the dashboard's absolute URL (CHELA_DASHBOARD_URL,
else the bind address) andchela merge-approve, and names Telegram only when a bridge is
configured. A dashboard decision is recorded asdecided_by: dashboard(or
dashboard:<tailnet login>). (CMX-103, #667) -
The orchestrator hears every override and release decision. Approving or denying an
override or a release now sends one inbox notice: merged (the commit, who approved and
where, and the deploy step: which chela services to restart, or the changed paths in
another repo), released (version, tag, what was pushed), or denied (by whom, where, the
reason). (CMX-123, #676) -
chela task-finishedrefuses an oversized diff. It measures the branch's changed lines
against its base (code + tests; docs, media and changelog excluded via
hand_in.size_exclude) and refuses overhand_in.max_pr_lines(default 300) BEFORE the
sweep runs, printing the measured count. A diff that truly cannot be split may pass
--oversize-reason '<why>', which is recorded on the run, shown to the judge and appended
to the PR body. An unmeasurable branch refuses rather than passing. (CMX-126) -
chela task-finished --split-plan <file>hands in slice 1 of an oversized task. With a
plan ({"slices": [{title, files, behaviour, tests}, …]}, slice 1 = this PR, the rest
parked on the branch<task>-rest), the hand-in passes the size gate only once this branch
is back underhand_in.max_pr_lines. chela then files one Linear sub-issue per remaining
slice under the run's issue, chained so they run in order, and posts asplit_filedinbox
notice. A failed tracker write refuses the hand-in and names what was already filed.
(CMX-140, #683) -
chela task-finished --handoff "<note>"leaves a note for the reviewer. The agent's
note (what changed, what to look at first, what it assumed) is stored on the run
(runs.handoff) and shown, sanitised, in the orchestrator's inbox notice and on the
Dispatch board card. It is informational only and never gates anything. (CMX-52, #653) -
A
claudeyou type by hand into a chela window gets Remote Control too. While the
Remote Control setting is on, every window chela opens for a human ("+ New shell", the
dashboard launcher, Telegram/new, a resume) defines aclaudeshell function, so a
hand-typedclaude …starts with--remote-control <window name>and shows up in the
Claude desktop app under the window's current name.-p/--print, help/version, a
prompt or subcommand, or an explicit--remote-controlare passed through unchanged.
Dispatched agents and judges never get it. Turning the setting off affects windows opened
afterwards; an already-open shell keeps its function until it exits. (CMX-84, #646) -
The orchestrator now hears what it used to find by hand. One inbox notice each, never
repeated on a later tick: a run waiting in review with no judge round started past
CHELA_JUDGE_NOT_STARTED_ALERT_S(default 20m); a dispatch hold that lapses by its TTL or
is released by someone else; CI red on the PR's head, naming the failing job, the first
failing line and thegh run rerun … --failedcommand (the rework the CI loop sends is no
longer a second notice); a PR that turns CONFLICTING with its base (once per head); a
running agent with no commit and no transcript activity pastCHELA_STUCK_AGENT_ALERT_S
(default 20m); and a task-finished / push-request marker left unapplied past
CHELA_MARKER_UNREAD_ALERT_S(default 5m), with the daemon's uptime. (CMX-86) -
chela release X.Y.Z --theme "…"— the orchestrator prepares a release, the operator
approves it with one tap, chela pushes. In its own worktree offorigin/dev(refused
unlessorigin/mainis an ancestor) it runs the release steps — the notes, the
pyproject.tomlbump,uv lock(which must re-record the new version), the release tests —
then commits and tags LOCALLY and files an approval request. It pushes nothing; a failed or
dirty step refuses with the reason and leaves nothing behind. The request is a Telegram card
(version, theme, the dev commit, the task ids in the new CHANGELOG section, Approve / Deny),
the dashboard's/override/<id>, orchela merge-approve <id>. Approving re-checks that
origin/devis still the commit the release was cut from andorigin/mainan ancestor,
then pushes main, the tag and dev — fast-forward only, once — and the card ends on the
released commit and, once the Release workflow built it, the GitHub Release's URL. A push to
dev expires the request; deny, expiry or a failed check push nothing and remove the local
tag. The approval stays human-only: the merge-gate hook (plugin 0.2.9) also denies a
session'spythoncall intochela.release.complete. A staged file outside the release
paths (CHANGELOG.md, pyproject.toml, uv.lock, changelog.d) refuses the release too. See
"Releasing" in CONTRIBUTING.md. (CMX-87) -
chela judge sweep: a mechanical mutation pass over every conditional the branch
changed. For each changed line in.py/.js/.mjs(not tests) vs--base(default
origin/<workspace.base_branch>), it rewritesif Xtoif False and (X)(plus the
True or (X)twin when there is anelse), swaps a return/CompletedProcesscode
0↔1, swapsreturn None↔return <expr>in a function that has both, and flips
==↔!=,<↔>=,>↔<=. Each mutation goes through the judge's own
apply/read-back/parse/per-mutation-selection mechanics and is restored in afinally. A
recovery marker (.chela-sweep-recovery.json) means a killed sweep is undone on the next
start. It exits 1 on any survivor. A mutation that does not parse is reported as skipped,
never as killed. -
chela task-finishedruns the sweep and refuses while a survivor remains. To accept a
survivor, the agent names it with a reason:
--sweep-equivalent '<file:line>: <why no outcome can change>'. The note is stored in
runs.sweep_equivalents, shown to the judge and appended to the PR body (one line per note — a newline in the reason cannot forge an
extra bullet). A note that names
a line which did not survive is refused. The judge itself is unchanged. (CMX-88) -
chela judge sweepruns on any git repo, with no chela setup. NoWORKFLOW.md, run
record, daemon, tmux or tracker: the suite is--test-cmd, else the workflow's
judge.test_cmd, else detected (pytest /npm test/go test/cargo test);--json
prints the result as JSON. A changed file in a language the sweep cannot mutate (anything
but Python and.js/.mjs) is reported as UNKNOWN (exit3), never as a pass. Run
standalone, the sweep does not provision the repo's environment. (CMX-97)
Changed
-
Docs: the README / landing demos are re-recorded for the new sidebar. The desktop clip
now shows the sidebar as it is today — folder groups with "Needs you" on top, the
orchestrator badge, hold-Alt row numbers, and a finished session archived (its window
closes) and unarchived (resumed in a new window) — before the Work view and Settings; the
phone clip is re-shot on the current pill switcher. Same file names. The demo recorder
gained what that needed: the synthetic fleet registers an orchestrator, its fakeclaude
answersclaude --resume <id>by re-running that scripted session, andrecord.mjscan cut
over a transient instead of freezing on it. (CMX-100) -
chela judge sweephas a budget, sotask-finishedcan no longer block for as long as
the diff is big. The sweep's cost was mutations × suite time; one hand-in ran 24+
minutes. It now stops at a wall-clock budget (judge.sweep_budget_seconds, default 900)
or a mutation cap (judge.sweep_max_mutations, default 60), whichever comes first;0or
unset means the default, andchela judge sweep --budget-seconds/--max-mutationsoverride
both. Mutations run conditionals first, then return codes and values, then comparison
flips, round-robin across files. Each mutation the budget did not reach is listed as
[UNRUN]: CANNOT VERIFY, never counted as killed.chela judge sweepexits 2 on a partial
sweep with no survivor.task-finishedstill refuses a measured survivor, but proceeds past
the unrun remainder and reports it as "PARTIAL", never as a pass. (CMX-111) -
The agent brief now has a guard-category checklist to walk before hand-in. Step 3 of
WORKFLOW.md's agent prompt lists seven categories the agent checks its own diff against
(exceptclauses, subprocess calls, literal values and regexes, kind branches, fail-closed
reads, flag precedence, the authoritative record), writing one test per item that applies
and naming the rest in the PR body. CMX-87 took 13 judge rounds because each round found
the next gap in a category the tests co...
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 ... - The proxy resolves each name itself and refuses any private, loopback, link-local,
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, th...
v0.13.0
Added
- The sandbox boundary for coding and rework agents ships, off by default (issue #502).
docs/SANDBOX_BOUNDARY.md§6.3's frozen config —sandbox.enabled, a narrow
filesystem.allowWrite,network.strictAllowlistwith a measuredallowedDomainslist,
anddenyon the GitHub token file — is now written to a chela-owned
<claude config dir>/chela-sandbox.settings.jsonand passed as--settings <path>to a
dispatched coding or rework agent whose workflow opts in withagent.sandbox: truein its
WORKFLOW.md. The judge is excluded unconditionally (role="judge"short-circuits before
the opt-in check), since it runs a fixed command in a throwaway tree and never pushes,
while a coding agent takes an arbitrary task description and pushes to a public repo.
CMX-366 (the completion hop) and CMX-368 (the push/PR-open) already moved the two
privileged effects that would have made this cosmetic onto the daemon; this lands the
config itself. A workflow that has not opted in launches byte-identically to today.
(CMX-369, #518)
Changed
-
The completion hop is now the daemon's write, not the dispatched agent's (issue #502).
chela task-finishedused to runUPDATEs against~/.chela/scheduler.dband kill the
agent's tmux window inside the agent's own process — the single thing blocking OS-level
sandboxing of dispatched agents, since an agent that can write the runs DB can rewrite every
other run's row, and one that can reach the tmux control socket cansend-keysinto any
window, including the operator's, with nofromattribution.chela task-finishednow
writes a request marker into the run's own worktree (a path the agent can already write,
sandboxed or not);tick(), unsandboxed and already polling every run, picks the marker up
and performs the transition toawaiting_reviewand the window kill on the daemon's side. A
request the daemon cannot apply for an unexplained reason (not: someone else already settled
the row) now escalates the run toneeds_humanand keeps the marker as evidence, instead of
logging a warning nobody reads and leaving the rowrunningforever.
(CMX-366) -
The push and the PR open are now the daemon's, not the dispatched agent's (issue
#502). Done Criteria step 5 used to rungit pushthengh pr createinside the
agent's own process — the second blocker (after CMX-366's completion hop) standing
between OS-level sandboxing and being cosmetic. Measured with a matched unsandboxed
control (docs/SANDBOX_BOUNDARY.md§5 B2): masking the GitHub token keepsghworking
(it sendsAuthorization: token <token>verbatim, so the sandbox's substring
substitution catches it) but notgit push(it sendsAuthorization: Basic base64(user:token), so the token is never present verbatim and nothing is substituted —
GitHub rejects the push).chela request-push <task_id> [--pr-title T --pr-body-file P]
now writes a request marker into the run's own worktree instead (a path the agent can
already write, sandboxed or not);tick(), unsandboxed and already polling every run,
picks the marker up, pushes the branch, and — only when the run has nopr_urlyet —
opens the PR, so the token never has to enter the agent's process at all. A request that
keeps failing gets 5 ticks to succeed before it escalates toneeds_human, preserving the
marker as evidence, instead of retrying forever and pinning the run's concurrency slot.
(CMX-368)
Fixed
-
npm-shared-install.shnow checks whether the declared npm packages actually resolve,
not whether the sharednode_modulesis empty. The previous freshness check
(-z "$(ls -A ...)") still read a partially-unpacked shared install — e.g. one left by an
npm ciinterrupted mid-unpack — as "already installed" if it had any content at all, so a
worktree could symlink into a sharednode_modulesstill missing jsdom. The check now
reuseschela.judge's owndeclared_npm_packages/_unresolvablehelpers. (CMX-367, #510) -
ensure_schemanow names the missing column and the underlying sqlite reason when a
migration genuinely fails (a read-only connection whose schema is missing a column,
e.g. a sandboxed agent's denied~/.chelawrite). Anyone running chela against a database
they cannot write now gets a loud, specificSchemaMigrationErrorinstead of an inscrutable
no such columnsurfacing later, elsewhere. (CMX-370, #519) -
context.ensure_schemanow names the missing column and the underlying sqlite reason when
a migration genuinely fails (a read-only connection whosecontext_snapshotsschema is
missing a column, e.g. a sandboxed agent's denied~/.chelawrite). Anyone running chela
against a database it cannot write now gets a loud, specificSchemaMigrationErrorinstead
of an inscrutableno such columnsurfacing later, elsewhere. (CMX-371, #520)