Accessibility (review6-c):
- A panel is named "Panel 2 of 7" and its caption is read once, as its
content (it was the name and the content both).
- Balloons say who speaks, visually hidden ("Claude says: ", "You say: ",
"Claude asks: ", "Notice: "); Save image leaves those words out.
- The overlay is named "Story Mode: <session>", the drawer by its kind and
title ("Diff 2 files"), the final panel's chips by what they open, and
the wire's panel labels are headings.
- While a file preview opened from a panel is up, the overlay is not
aria-modal (the preview sits beside it), and is again once focus returns.
- The focus ring on a click region gets a light band under its dashes; on
the loop's magenta halftone it vanished.
From review6-b:
- "Mission complete!" no longer shows while a helper or a background
command is still out ("To be continued…").
- A background command's drawer says "Started in the background…", then
"Finished in the background, exit code 0…", instead of the launch note
meant for Claude ("do not poll that file").
- `sed -n '5,25p' README.md` is a printed file, offered in the drawer.
- A report turn no longer counts the helper it answers as one it started.
- A helper's `cd /work/x && ls` keeps its folder as ".".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 6: commit, then push as a second command (the most
common way to ship) drew as "Claude saves the work in 2 commits and
uploads it." for one commit, and the commit's message was dropped. A
command's `commits` was only recorded above one, and the save panel then
counted every save command as one. It is now recorded whenever a command
commits and is the only thing counted: "Claude saves the work and uploads
it: "feat: add mode()"." A push of earlier commits reads "Claude uploads
the work."
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The bar on a phone took four rows (216 px of a 664 px screen) with the
close button alone on the last; it now takes three (170 px): title and
close, the turn, the view and Save image. The drawer starts exactly
under it and follows it when the phone turns (the bar is observed).
- A panel below the fold had no buttons until it was scrolled to (only
drawn panels get click regions), so Tab skipped it. A quiet frame now
draws one such panel, a band at a time; its entrance still plays when
it comes into view.
- "Fix and test: the first run fails, the second passes." was chosen by
rounds, so a loop with three runs (a failing test, then ruff, then a
pass) said it under ✗ ✗ ✓. It now needs exactly two runs; that loop
reads "Fix and test, 2 rounds: 2 runs fail, then it passes.", and the
corpus pins every loop's marks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 5 (a phone with touch):
- Touch screens: every control at least 44 px (prev/next, close, the
Story/Director's cut toggle, Save image, the final panel's chips, drawer
buttons and links), and the small type at 13 px.
- The drawer on a phone starts below the bar (it cut through it), closes by
dragging its head down, and keeps its head small in landscape.
- The turn picker says where you are ("Turn 2 of 5"), learns about new
turns while you look at an older one, and a turn you picked yourself is
not announced as news.
- A check whose command also wrote files offers "See the 2 files it wrote".
- Test counts are read from the whole output: a later command's lines had
pushed pytest's "5 passed" out of the kept tail.
- A save names its commit: "Claude saves the work: "feat: add wrap() for
modular wrapping into a range"." (from -m, -am, --message or the
here-document of git commit -F -; a trailer is never the subject).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review round 5 had an agent read only the comic of the nine real sessions
(20 turns), write down what it believed happened, and only then compare
with the transcripts. Three panels told it something false, one hid the
failure the user asked to see:
- A push that went through, followed by a check that failed in the same
command, drew as a failed push; the output (`[new branch]`, `->`) now
decides: "Claude saves the work in 2 commits and uploads it, but a step
after that fails."
- A formatter's "would be reformatted" preview, printed before it fixed
the file, made a clean run a failure: the last verdict wins.
- A blocked command ended in confetti and "Mission complete!": a run panel
where everything failed says "Claude tries a command, and it is
blocked.", and a turn where nothing worked ends "Turn over" / "Nothing
Claude tried worked, so the job is not done." with a neutral robot.
- The loop stamped only the final verdict (POW ×2), so a first run that
failed on purpose never showed. It now marks every test run red or green,
and says it: "Fix and test: the first run fails, the second passes." /
"Build and test, 4 rounds. A check fails once, then everything passes."
Also:
- Files written inside a check or a save reach the plain caption ("Claude
writes 4 files; the tests pass.").
- Balloons keep the next sentence when the first reports a problem ("…left a
stray file. I'll remove it…") or is very short ("Not done. The harness
blocked sleep 65…"); Claude's last words keep a caveat that follows them
("Nothing is committed because this folder isn't a git repository.").
- The advisor never shares a panel with helpers that do work; ToolSearch
reads "Claude gets the tools it needs ready."; Codeman's own case file is
not drawn as a file Claude read; looking around shows what Claude meant
(the call's description), not raw commands; a montage at the start of a
turn reads "Claude gets started".
- Words: boards count tests ("19 tests"), "uploads it" replaces "sends it
up", and the prompt balloon keeps 400 characters.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stepping through 40 turns made 46 long tasks of 51 to 286 ms, enough for
the app's guard to drop the terminal's WebGL renderer for the rest of the
page's life. Drawing switched off made none: the cost was canvas raster at
paint time, which the per-frame JS budget never saw. Now:
- Panel backgrounds are shared between panels and turns (kind × size ×
palette), and a new one is built in four bands, one per frame (a wide
halftone at 2x was ~35 ms in one go; a band is ~8 ms).
- At most two panels are drawn per frame, the first one served rotating so
none starves; clocks still run for all.
- Measured on 20 turns stepped 40 times at 2x: 1 long task (52 ms), against
39; the settled overlay is still ~2% of a core.
Also from round 4:
- The turn picker says when older turns were not loaded (`older` in the
response: the transcript is longer than the part that was read).
- A compaction after the last turn closes that turn; a local command's
output is the final panel's balloon ("Compacted"), without its markdown
ticks, and a turn under a second reads "Done.".
- Exact durations keep their seconds past an hour, so the two captions never
disagree on the minute ("1 h 10 min." / "1 h 9 min 39 s").
- A turn that arrives already finished is announced as new too.
- Whole files written by here-documents read "Claude writes 3 files."; the
advisor's caption reads "a second opinion" instead of repeating its name.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nine transcripts from the review rounds, each a real Claude Code worker
building a small program (a Python CLI with a fix loop, a web page with
node:test, a bash script with a background helper, a long turn with a
prompt added mid-turn, teammates then an interrupt, a question then tests
first then a push, a text plan with a scratch dry run, /compact then plan
mode, a test failed on purpose). Each is cut down to the lines and fields
the reducer reads, with every path, user name and email rewritten and the
file contents of Reads left out; replaying them before and after gives
the same pages.
The expected file beside each pins every page (panel kinds, captions,
balloons, counts), so a change that alters a story fails with a diff of
exactly what changed; STORY_CORPUS_UPDATE=1 regenerates them. The beats
the reviews were about are also stated outright.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
feat(codex): raise codex approval, question and trust dialogs as BLOCKED
Conflict resolved against #634 in src/session.ts _probePaneWorking():
#624's assignment (a dialog on screen is never work) is kept, and #634's
trust-dialog verdict block (log the hold once, remember the reading)
follows it, before _readWatching().
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On codex-cli 0.162.0, with 0.163.0 released, every launch first shows the
"Update available" prompt, and choosing Skip leads to a variant of the
workspace trust prompt: "2. Back to Agent Command Center" over the key-hint
row "enter continue · esc back". The pattern only accepted cancel,
interrupt, quit, answer or all as the closing word, so a fresh git repo
launched from Codeman sat on that trust prompt reading idle, with no red
alert and no blocked signal (measured live on a throwaway instance).
Adding "back" is safe because the hint row must still START with one of
the anchored hints ("enter continue" here): the popups the user opens at
the keyboard (/model, /permissions, /review, Implement this plan?) end in
"esc back" too, but their row starts with "enter select", so they stay
unmatched, as every existing negative fixture confirms. The new fixture is
the live capture with the scratch path replaced.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex is the one CLI Codeman launches with its approvals on by default, and
it declares `hooks: 'none'`, so nothing tells Codeman it stopped to ask. A
codex session parked on "Would you like to run the following command?" read
as plainly idle, and one parked on a question read as WORKING, because the
question's own key hints end in `esc to interrupt`, codex's working line.
This adds a generic, optional registry field,
`capabilities.workDetect.blockedLine` (plus `blockedLines`, the window), and
the session state that reads it:
- `isBlockedScreen()` (session-activity.ts, pure) joins the last
`blockedLines` non-blank rows of a capture and tests the pattern once, so a
pattern can demand several markers and anchor the dialog's footer on the
end of the window.
- `Session` reads it off the same capture the idle confirmation already
takes, emits `blockedChanged`, and exposes `screenBlocked`. It raises on
one reading, and while a dialog is up re-reads the screen every 2 s,
because answering resumes the turn and a busy pane would not reach the
settle path again until the turn ended. It clears only after two readable
captures in a row without the dialog, so a capture taken mid-repaint cannot
clear and re-raise it; an unreadable capture changes nothing. A blocked
screen wins over the working line in the same capture.
- The pattern goes through `compileVersionRegex()` in both the schema refine
and at use (lazily cached), like every other config regex.
Codex's pattern was measured on a live codex-cli 0.162.0 pane on 2026-10-14
(`-a on-request -s read-only`, widths 200 down to 36): it needs a `› N.`
option row and then the dialog's key-hint row as the last row, allowing for
that row to wrap below about 60 columns. Command, edit and network-escalation
approvals, single and multi-question dialogs and the startup trust prompt
match; the idle and working views, menus the user opened (`/model`,
`/permissions`, `/review`), the composer holding the dialogs' words and a
transcript quoting them verbatim do not. `Implement this plan?` is left out
on purpose: its footer is the same as `/model`'s and the turn has ended
there. The captures are kept as fixtures (trailing whitespace trimmed, the
scratch path replaced).
Nothing consumes `blockedChanged` yet; the next commit raises the alert.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The XLSX preview hands an admitted copy of the workbook to ExcelJS 4.4.0,
which links each worksheet to its comments, comment notes (VML), tables and
drawing by looking the relationship Target up verbatim in maps keyed by its
own spelling, then dereferences the result unchecked. openpyxl, the library
most agents write .xlsx files with, uses absolute Targets
(/xl/tables/table1.xml), its own comment part name (xl/comments/comment1.xml,
which ExcelJS never loads) and a drawing XML ExcelJS parses to nothing. One
comment, table or chart was enough for "Spreadsheet preview failed".
repairExcelJsModel() (spreadsheet-xlsx-core.js) runs on ExcelJS's parsed
model right before its reconcile: it re-points a link to the key ExcelJS
stored for the same part (so an openpyxl table is kept) and drops a link,
tablePart, drawing or hyperlink reference that would dereference nothing.
It adds nothing and resolves keys as own properties only. The worker wraps
workbook.xlsx.reconcile per instance to call it, and throws if a future
ExcelJS renames that method. SPREADSHEET_ASSET_VERSION is refreshed.
Tests: unit tests on model objects, and three real openpyxl 3.1.5
workbooks in test/fixtures/xlsx that fail the worker tests without the hook.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Custom Model Endpoint Profiles (#393) let a session point its CLI at a
custom OpenAI-compatible endpoint by injecting env vars or a config file
and restarting the CLI in place. Review of the apply path found four
things, two of them destructive. This lands all four plus the smaller
items from the same review.
1. Clearing a selection did not clear it. The injected vars reach the CLI
via `tmux setenv`, which persists at the tmux-session level and is
inherited by `respawn-pane` (measured: `setenv FOO bar` survived two
successive `respawn-pane -k`), so deleting the keys from the session's
envOverrides relaunched the CLI still pointed at the old endpoint, and
for the configDir kinds at a HOME/CODEX_HOME/GROK_HOME that had just
been deleted. `Session.setCustomModel()` now reports the removed keys,
queues them (`_pendingEnvUnsets`), and `RespawnPaneOptions.unsetEnvKeys`
carries them into `applyEnvOverrides()`, which `setenv -u`s them before
re-applying the live overrides, on the same path that already unsets
the legacy CLAUDE_CODE_EFFORT_LEVEL. Verified on a private tmux socket
that `setenv -u HOME` hands the next respawn the global HOME back.
2. Applying a model to a local claude session killed the pane. The
relaunch was `claude --session-id <id>` and Claude refuses an id that
already has a transcript, and unlike the dead-pane respawn this one
kills a working pane first. `restartCli()` now pins the live
conversation id as the resume id for that respawn when the CLI's launch
declares a `fallback` chain, which renders the same
`--resume <id> || --session-id <id>` shape the docker and remote pane
commands use. Gated on the registry shape, not the CLI id: an entry
whose resume id is minted by the CLI itself never declares that chain.
3. pi, omp and grok wrote their config file and then launched without the
`--model` that selects it, so the file was ignored. The registry entry
now declares `customModelInjection.launchModel` (`custom/{modelId}` for
pi and omp, grok's `[model.codeman-custom]` block name), the builder
renders it, and `_withCustomModelLaunchModel()` applies it onto the
respawn options through `legacyConfigField`, leaving the stored
<Mode>Config untouched so a clear falls back to the user's own model.
A model id the CLI's `model` token pattern cannot carry is refused
with a 400 rather than silently dropped by the argv engine.
4. Remote (SSH) and Docker sessions reported `restarted: true` and changed
nothing: their `restartCli()` reattaches the durable tmux rather than
relaunching the agent, and the env lands on the local pane. Both are
refused with a 400 until those paths are plumbed.
Smaller items from the same review:
- The selection survives a Codeman restart as the disk-only `__customModel`
bookkeeping (endpoint, model, injected key NAMES, config dir, launch
model; never the values, which carry the API key). Recovery re-derives
the values from the endpoint store through the same apply path the route
uses and keeps the bookkeeping even when the endpoint is gone, so a
later clear still has keys to unset.
- Discovery goes through `webviewFetch()`, so the RESOLVED address is
judged by the same egress guard the web-tab proxy uses, and `baseUrl`
reuses `webviewUrlSchema` (http(s) only, no embedded credentials,
link-local and cloud-metadata addresses refused). undici's `fetch failed`
wrapper is unwrapped so the user sees the ECONNREFUSED underneath.
- `custom-model-hosts.json` is written 0600 via tmp+rename, the per-session
config dir 0700/0600 (pi and omp embed the key literally), and that dir
is removed with the session.
- `PR.md` is gone from the repo root and the design doc moved to
`docs/custom-model-endpoints-plan.md` with the LAN address and the
personal name scrubbed; every reference follows. The guide's `authStyle`
text matches the shipped schema (`bearer | api-key`, default `bearer`)
and says that `customModelEndpointsEnabled` is read by nothing until
the picker lands.
- `config/tsconfig.scripts.json` typechecks `scripts/test-local-llm-harnesses.ts`
(four real type errors fixed). It is not yet wired into `npm run typecheck`
because that line differs on master; adding `&& tsc -p config/tsconfig.scripts.json`
there is the one-line follow-up.
Tests: `test/session-custom-model-restart.test.ts` drives a real Session and
fails on the unfixed code for items 1 to 3; the route suite covers item 4
and the pattern refusal; `test/tmux-manager.test.ts` pins that the unsets
run before the overrides and that a shell-metachar key never reaches tmux.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Point any Codeman-supported harness (Claude, opencode, Codex, Gemini, Pi,
Grok, DeepSeek, OMP) at a custom OpenAI-compatible endpoint instead of its
native cloud backend, for a given session. Covers local hardware (llama.cpp,
Ollama, vLLM, DGX Spark, Strix Halo) and cloud (Azure AI Foundry, OpenRouter).
Off by default (customModelEndpointsEnabled, synced, default OFF).
- Registry: capabilities.customModelInjection per CLI entry (env /
configContentEnv / configDir / unsupported kinds)
- Pure injection builder (custom-model-injection.ts) turning an endpoint +
model id into the real env vars / config content per CLI
- Endpoint store + CRUD routes (custom-model-hosts.ts,
custom-model-routes.ts), discovery via GET /v1/models, SSRF-guarded
- Session integration: Session.setCustomModel()/restartCli()
(POST /api/sessions/:id/custom-model), reusing the existing
respawn-pane -k primitive to restart the CLI process with new env
- Multi-user hardening: every new redirect-capable env var added to its
CLI's privilegedEnvKeys, closing a pre-existing gap where several were
already reachable via the generic envOverrides field's prefix allowlist
- Standalone scripts/test-local-llm-harnesses.mjs: spawns real CLI binaries
against a real endpoint outside the web UI, independent of tmux/sessions
- Mock-server contract tests (test/fixtures/mock-openai-server.ts) replaying
every CLI's injected values through a real HTTP shape
Real end-to-end validation against a live llama-swap server (inside a
codeman/agent:llm-test Docker image with all 9 CLI binaries) found and
fixed three real bugs before they shipped:
- Codex's config.toml schema was wrong ([model].default table instead of
a top-level model string + [model_providers.custom]); fixing it then
surfaced a genuine, documented protocol incompatibility (Codex only
speaks the Responses API since Feb 2026, which llama.cpp/llama-swap
don't implement)
- Claude Code's async session-title-generation call validates
ANTHROPIC_DEFAULT_HAIKU_MODEL against its own internal model list and
hangs the whole -p invocation on an unrecognized name; documented for
chunk 6, worked around in the standalone script only (--bare is NOT
safe for a real interactive session, which needs hooks)
- The discovery route's authStyle: 'both' option (send both Authorization
and api-key headers) reliably hung a real server; removed the option
entirely rather than just changing the default
Status: draft. Chunk 6 (frontend toolbar/settings UI) not yet built — see
PR.md and deployment_plan.md for the full chunk breakdown and confidence
table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3