From review round 7 (PR review):
- With no hook path (normal after a server restart), the transcript was
looked up by conversation id only; it now also tries the Codeman id and
a recovered session's `restored-<fragment>` transcript, as
last-response does, and keeps what it found briefly (a lookup reads
every project folder; a miss is kept only 4 s).
- A session Story Mode cannot draw is not polled, and a page with nothing
drawn yet is polled like a finished one (every 15 s), not every 4 s.
- Helper steps: one request per helper is drawn, the newest; a failed
refetch never writes over steps already shown.
- The Director's cut toggle, a skin change and the fonts arriving redraw
within the frame budget, not every panel in one task.
- A bare name under an `ls` header finds its folder in one pass over the
lines; a search per link over the text before it was quadratic (about
1.5 s for 700 links on agent output).
- A page of many questions still keeps to 12 panels: questions fold last.
- Opening Story Mode while it is open never starts a second runtime; the
status announcer carries data-i18n-skip; the turn key in the ETag is
clipped.
- The crafted-input timing test runs at sizes where the old code took 6
to 29 s and the new one under 10 ms, with a 1.5 s bound.
- Docs: `older`, `origin` (`scheduled`), which route answers a 500;
mobile.css's stale comment; an orphaned doc comment.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 7 (security):
- Eviction past the per-transcript caps dropped entries one shift() at
a time after the whole tail was parsed: quadratic and never yielding
(a 2 s event-loop stall on a 70 MB long turn, 51 s from 300k entries).
It is one splice, done at every 1 MB yield, so the peak stays near the
caps; and a window that saw a turn start which was then trimmed away no
longer widens to a bigger tail that would be trimmed the same.
- A FIFO or a device in place of a transcript or a helper's .meta.json
(stat said size 0, then open() waited for a writer) pinned a libuv
worker per request; four of them hang every fs, dns and crypto call on
the server. Files are now opened non-blocking, checked with the
handle's own stat (a regular file only) and read through that handle.
- The cache held 6 transcripts for everyone, so a seventh open view made
every poll a full re-read, and a 304 was decided only after reading. An
unchanged file is now a 304 from a stat alone, and the cache holds up
to 24 transcripts within 30 million characters.
- Story Mode is the first route to send a transcript's content to the
browser, so the hook-reported path is used only when it really lies
(symlinks resolved) in a Claude projects folder: the session's
relocated CLAUDE_CONFIG_DIR (now also used for the lookup by id) or the
server's. Anything else falls back to the lookup by conversation id.
Tests move their transcripts into the (temp) projects folder and cover a
path outside it, a symlink out of it, a FIFO transcript, a FIFO meta file
and a 100,000-entry tail.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
API reference: `roundsOk` carries `null` for a run still going, a
background command's `output`, script writes in `edits`, what `helpers`
counts, and the page text budget behind `truncated`.
src/web/CLAUDE.md: panels kept by identity, the balloon text span the
clamp must target, the drawer's unchanged-refresh skip and held turn, the
drawer under the bar and the inert page behind a full-width one, and text
tokens versus accents.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The final panel's chips were rebuilt on every redraw, so the chip that
opened a drawer was gone by the time it closed and focus fell to the
page body. Unchanged chips are now kept, and a trigger replaced anyway
hands focus to the control that opens the same thing.
- API reference: `older`, background Bash calls (`running`), `subject`,
append hunks (`atEnd`), `files` resolved against the command's folder,
test counts read before the output is cut, `roundsOk`, the `note`
speaker and `noneWorked`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eleven batches of findings had grown it by accretion. It now reads in
parts: the routes, where turns start and end, how background work joins
back, and what a page, an event and a panel carry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From review round 3 (compaction, then plan mode):
- A typed `/compact` and its <command-name> record share one promptId; they
were two turns ("Done in 0 seconds", then a turn left running). A prompt
carrying the open turn's promptId now joins it: one "You run /compact."
turn of 24 s, the real compaction time. A command typed as such
(`/model opus`) is a command too.
- Plan mode: writing the plan file under ~/.claude/plans/ is planning (a plan
panel with its numbered steps), not a code change ("3 files changed" for
a commit of 2). ExitPlanMode's input is empty in current builds; its
result carries the plan and your answer: "Claude shows you its plan, and
you approve it."
- A command's Director's caption names it ("Command · /compact").
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A local slash command (`/compact`, `/model`) gets no reply from Claude and no
turn_duration, so its turn stayed "Claude is working" until the next prompt.
It now ends at the command's own output (<local-command-stdout>, kept for
the mission drawer) and reads "You run /compact.". `/compact` writes its
compaction marker before the command line itself; a marker that lands
between turns now belongs to the turn after it (it was drawn at the end of
the previous, finished turn), and one mid-turn stays where it is.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Files a Bash call writes through here-documents (`cat > f <<'EOF'`, `tee`)
are whole-file edits on the event: in the diff drawer, on the code page,
in "N files changed". Before, a worker that wrote seven files that way
drew a page with zero files changed. Labels leave the bodies out (the wire
row was a wall of file text), and a relative target resolves against the
transcript line's cwd unless the command changed directory first.
- A caption names the part of a one-liner that decided it (`node --test`,
the commit), not its first line.
- The advisor (a server tool, answer encrypted in the transcript) is drawn:
"Claude asks the advisor for a second opinion." It is not counted as a
helper. Server web search maps to the web panel.
- Checks that all pass from the start read "Build and test, N rounds. Every
check passes.", not as fixing.
- pytest's collection errors are counted apart: "The tests don't even load."
- A result remark replaces a step announcement on its check.
- A turn still open when the next prompt starts has ended; an interrupted
turn is timed by its span.
- The mission drawer keeps prompts up to 4000 characters.
- Drawer: diff headers, titles and commands read relative to the session
folder (the folder itself reads "."); a bare name under an `ls dir:`
header links into that folder; the chosen answer is marked, not struck
through; boards count errors.
- While a question waits, the pencils say "Waiting for your answer…";
balloons start below the LIVE tag; the loop's balloon points at its
verdict, above the board.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found by replaying round-2 review transcripts while the reviews ran, then
surveyed across 150 local transcripts:
- A prompt typed while Claude works writes no user line in current builds,
only a queued_command attachment where Claude took it in (602 of them
locally, 485 of those background-task reports). It now joins the running
turn; a report that lands mid-turn is a notice on the wire, not a new
mission panel, and a background helper's answer joins back from it.
- Some sessions never write turn_duration (13 of 135 locally): the Stop
hook's stop_hook_summary (never preventedContinuation in 815 seen) now
ends the turn, timed by its own span. Before, such a session's turns
never ended.
- A helper started as a teammate (teammate_spawned) is a background helper;
its <teammate-message> reports (text or a JSON idle notification) join it
by name, and the turn they start reads "readme-writer: README done".
- A <cross-session-message> names its sender; pasted text loses Claude
Code's <pasted_content> wrapper (it showed in balloons and the picker).
- The mission panel, its balloon and its drawer follow who sent each
prompt (you, a background job, another session), not who started the turn.
- A caption names a script by its first line that says something, not
`set -e`, a bare `cd` or `S=/tmp/...`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three review agents watched real Claude Code workers build small programs
and compared every panel with the transcript. Replaying their transcripts
through the core found, and now fixes:
- A Bash call was classified from its 400-character display clip, so a
commit or a test run late in a long one-liner vanished (a failing pytest
run drew as "saves the work"). The intent, the printed files and whether
a save ships are now read from the whole command.
- A test run that reports failures failed, even when `| tail` hid the exit
code. With that, the real fix-test-fix-test sequence folds into one loop:
"Fix and test, 2 rounds. It passes in the end."
- node:test (`# pass 7`), all-failing pytest (`3 failed in 0.1s`) and
pytest errors are counted.
- A helper started in the background was drawn as finished, with Claude
Code's internal launch notes as its answer. It now stays running until its
task notification arrives; the answer, tool count and duration are joined
back onto it, and the final panel says when helpers are still out.
- The notification turn reported Claude Code's own duration, counted from
the first prompt (2 min 38 s for a 21 s turn); a turn is capped at its span.
- Logging into scratch (`| tee "$S/dry.txt"`, `> /tmp/x`) counted as an edit;
redirect targets are read quote-aware, and a here-document's body is data.
- A probe (`pytest --version`) reads as looking around, not running a tool.
- A plan Claude writes as a numbered list (no plan tool, as in current
Claude Code builds) is drawn as a plan panel; files it cats are file cards.
- A balloon: code spans stay whole, an ellipsis or e.g. does not end the
sentence, a remark about the check that just ran stays on that check, and
a loop shows the last word of its rounds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Save image: the page as one PNG to share (each panel canvas, then every
caption, balloon and chip redrawn from its computed style and box).
- The helper drawer lists what the helper did, read live from its own
transcript (GET .../story/helpers/:agentId), led by the task Claude gave it.
- Session Options has a Story button (same setting), the way in on phones.
- A turn a background task or another session started reads as a notice,
not as your speech balloon.
- Wide panels keep their scene in a centered stage instead of spreading the
art to both ends; the fix panel's agent stands next to the page.
- An unchanged page (same ETag) is neither parsed nor redrawn.
- Paths inside the session's folder are shown short; clicks keep full paths.
- Tab stays inside the overlay; a missing 2D context skips the art, not the page.
- A rebindable Story Mode shortcut (no default chord).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Story Mode is an opt-in header button (showStoryButton, per device, default
off, kept off phones) that opens the active Claude Code session as comic pages:
one page per prompt, panels for reading code, planning, helpers, edits, tests
(POW/KRAK), questions and saving, each with a plain story caption and a
Director's cut caption naming the tools. Files open in the existing file
preview, the code page opens the exact diff, a test board opens the output,
and a running turn redraws as Claude works.
Server: GET /api/sessions/:id/story (experimental) reads a bounded tail of the
session's own transcript. A pure core (src/story-mode.ts) reduces lines,
joins tool calls with their results, splits turns at prompts and
turn_duration, and groups events into at most 12 panels; Bash calls are
sorted by what the command does. The reader (src/story-reader.ts) caches
reduced entries and reads only appended bytes while the file grows.
Frontend: story-mode.js (canvas frame engine, live refresh from transcript and
hook SSE events plus a poll while a turn runs, details drawer, the wire) and
story-mode.css, with two self-hosted OFL comic fonts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follow-up to the previous commit, after a fact check against the code.
- Answer surfaces: the header bell answers Claude prompts, the phone
strip only Claude dialogs (and only with the inbox on), and push
Approve/Deny only a Claude permission prompt (PUSH_EVENT_MAP gives
actions to permission_prompt alone). README, its Chinese mirror, the
notifications page and the phone guide now say so.
- Antigravity and Grok Build: after the 3 s launch settle, idle is armed
only by Claude's prompt marker and working only by a braille spinner,
so "idle when output stops changing" was wrong. The wiki, the grok
guide and the API reference now call their status unreliable.
- DeepSeek Harness: its idle and working reports become stop and
agent_working, which drive respawn, waits and the inbox but not the
session's own working/idle display. Keeping Agents Running says that,
and that the respawn path needs the status bridge.
- The wait section of the API reference no longer calls exit the only
dependable signal for screen-checked modes; the inbox page names how
Codex and DeepSeek items clear; Core Concepts and Hooks And
Integrations no longer call DeepSeek "the one" exception; Agent CLIs
notes that Codex dialogs still raise the red alert; the inbox design
doc notes the alert-only items.
- Skill: stop and blocked list deepseek, and SKILL.md no longer says
codex has neither a transcript nor blocked. Mirrored with
scripts/sync-plugin.mjs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README (and its Chinese mirror) said the Approvals Inbox queues every
pending prompt and answers it from the bell or the phone. It holds prompts
from Claude Code, Codex (screen-read) and DeepSeek Harness only, and only
Claude's are answerable: Codex and DeepSeek items are alerts that say to
answer in the terminal, with no Approve/Deny on the push or the phone strip.
The wiki, the TUI guide and the phone guide now say the same.
Keeping Agents Running, Troubleshooting and the API reference still said
every CLI but Claude, Codex and DeepSeek falls back to output
stabilization. OpenCode, Gemini CLI, Pi and OMP declare
capabilities.workDetect too and get the same screen check; only
Antigravity and Grok Build declare none. Core Concepts now mentions
Codex's screen-read dialogs, and the respawn gotcha notes that DeepSeek's
own end-of-turn report reaches the respawn controller.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- docs/api-reference.md: a non-admin's GET /api/settings omits
voiceSettings.apiKey, caseOrder and lastUsedCase; a new "System
operations" table for the admin-only routes and the scoped
POST /api/auth/revoke; the error-code table now lists FORBIDDEN and the
other multi-user codes (PASSWORD_CHANGE_REQUIRED, USER_NOT_FOUND,
USER_EXISTS, LAST_ADMIN).
- docs/wiki/Multi-User-Mode.md: what a regular user reads, that their run
mode and respawn presets stay theirs on the device, the slow sign-in
case, and the admin-only system operations.
- docs/multi-user-plan.md: Settings and System ops rows marked implemented.
- CLAUDE.md: the Multi-user mode paragraph names the new pieces and tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
api-reference gains a Settings section (GET/PUT /api/settings, the model
config, the case order and PUT /api/config, with the 403 FORBIDDEN a non-admin
gets in multi-user mode). The Multi-User Mode wiki page explains what a regular
user's App Settings save does now, Settings-Reference links it, the plan's
Settings row is marked implemented, and CLAUDE.md notes the gate for the
Multi-user paragraph and for anyone adding a settings write.
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>
The Docker rule was applied to the CASE folder and its common git dir
only. A Docker case can be linked to any host folder, including a managed
worktree (`<repo>/.claude/worktrees/<name>`), which is a natural setup for
running one of the parallel agents in a container. Removing that worktree
from the local case's dialog, or through Close Session's "Kill & remove
worktree", ran `git worktree remove`, which runs a git status INSIDE the
tree (container-written files, checkout filters on this machine) and then
deletes the whole tree, ignored files included, while the container still
mounts it. Reproduced on an isolated instance: DELETE answered 200 and the
Docker case's workspace was gone.
That contradicts the invariant the PR itself documents in CLAUDE.md ("git
never runs ... at or inside a Docker workspace"). removeCaseWorktree now
refuses (400) a present worktree that is, sits inside, or holds a Docker
case workspace, before any git runs; the DELETE route passes the Docker
roots it already reads for the case check. A worktree whose folder is gone
is unaffected (git only drops its record there). Unit and route tests
cover both directions and an unrelated Docker workspace; docs updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The dialog's own prompt glyph armed idle confirmation and the quiet dialog passed it, so a fresh case reported idle about 4 s in. An agent that waited for idle and then prompted typed into the dialog, and its Enter confirmed No, exit and killed the pane.
With Codeman Agent Skill on (agentSkillEnabled), every session create
writes the skill into the folder the session runs in, so a session in a
worktree leaves an untracked .claude/skills/codeman/ (claude) or
.agents/skills/codeman/ (codex, gemini and the rest) behind. git then
refuses `git worktree remove` for that worktree forever, and an agent
running `git add -A` there would commit Codeman's skill into its branch
(reproduced: the worktree's status showed "?? .agents/" and
"?? .claude/").
A create now excludes <skills dir>/codeman/ next to the hooks file, for
every skills folder the registry names (capabilities.agentSkillDir,
read at call time by the route and passed in as data, so no CLI id is
branched on). Like the other lines it is only added when git does not
already ignore the path, and only to the repository's local
info/exclude.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a managed worktree's folder is deleted by hand (a user in a file
manager, an agent's rm -rf), git keeps its record and lists it as
prunable. Codeman then had no way out: the dialog showed the row as
"missing" with no action, DELETE answered "No worktree named ...", and
the name could never be used again (the branch still existed, and with
another branch git refused "a missing but already registered
worktree"). Reproduced on git 2.43; git's own `git worktree remove` on
the missing path drops the record with exit 0.
Removal (only removal, never a session start) now accepts a worktree
whose folder is gone when git lists it at exactly the managed path, as
prunable or locked, and no component on the way is a symlink or a file.
The live-session and lock refusals still apply. The dialog offers
Remove... (not Open) on such a row, with a note saying what it does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Document what the two previous commits changed for users and for whoever
adds the field to another CLI:
- docs/cli-registry.md: the `blockedLine`/`blockedLines` fields, how codex's
pattern was measured, and the three rules any declaring CLI keeps (end on
chrome only the CLI draws, alert only; raise on one reading, clear on two;
hooks win).
- docs/architecture-invariants.md: a "Screen-read blocked dialogs (codex)"
entry, plus the wait-signal, idle-detection, watching and Approvals Inbox
entries it changes.
- docs/api-reference.md, docs/extending-codeman.md and the wiki (Agent CLIs,
Notifications and Approvals, Hooks, HTTP API, Driving Codeman From An
Agent): `blocked` now fires for codex, `stop` still does not, codex
approvals are alert-only cards answered in the terminal.
- CLAUDE.md: `stop` and `blocked` are no longer one rule, the config-regex
list gains `blockedLine`, and a short pointer that a codex dialog match is
only ever an alert.
- The codeman skill's signal table and verbs note, mirrored into
plugins/codeman by scripts/sync-plugin.mjs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Documents the worktree endpoints and quick-start's worktree field in
docs/api-reference.md, adds a Git Worktrees page to the wiki (sidebar,
Home index, a pointer from Core Concepts where sessions sharing one
case folder is explained), and records the rules that keep the feature
safe in docs/architecture-invariants.md with a short CLAUDE.md pointer:
only the managed .claude/worktrees path is ever touched, git never runs
over a Docker workspace, removal is never forced and branch delete is
-d only, and sessions start through the existing create paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Attach button, the Compose and paste-dialog pickers, terminal drop and the
clipboard now take documents beside images and videos: the office formats
pdf/docx/pptx/xlsx/zip and the text family txt/md/csv/tsv/json/log/yaml/yml/xml.
A document is admitted by its name's extension (the MIME a phone attaches to a
.md is anyone's guess), buffered under the same 50MB image cap, and judged on
its bytes by the same fail-closed sniffer: a magic-number signature for the
binary formats, UTF-8 with no NUL byte for text. It is saved under a sanitized
stem of its name, so Q3 report.pdf is findable as paste-<ts>-<hex>-Q3-report.pdf.
sh/py/js/html are refused by name with a clear 400. That is a scope choice, not
a security property: file-raw already serves non-media text inert, so this keeps
the server from storing what it would serve or an agent could run, and html and
svg are the ones deliberately kept out. One client classifier (_promptAttachKind)
now returns image, video or document, and the server's dispatch mirrors it: an
image MIME wins whatever the name says, a document extension takes the document
branch, and a foreign extension with no image MIME is refused only after the
image sniff fails, so nothing the image path accepted by its bytes is lost.
#604 makes `~/` links clickable and passes them through unchanged, since only
the server knows the session host's home. The html-view mint read `~/x.html`
as workspace-relative and answered 404, so the preview fell back to the
source view. It now expands `~/` against this host's home, as the attachment
route does, and refuses it in a Docker case (that `~` is the container's)
with the same message as the attachment and folder routes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved against #604, #606 and #601:
- constants.js: the rooted link pattern takes html-md's `markdown` and
`html?` with #606's m4v/ogv; #604's relative pattern gets the same list
(link-provider-regex pins the two equal); #604's findFilePathLinks /
folder helpers and html-md's rendersAsPage() are both kept, and
window.CodemanFilePaths exports all of them.
- terminal-ui.js: #604's resolved `target` (relative, ~/ and folder links)
now also goes through rendersAsPage(), so every link kind routes .html to
the page preview; .md routes there through FILE_PREVIEW_EXTENSIONS.
- CLAUDE.md, api-reference, architecture-invariants: both sides' text kept.
- test/folder-grid-ui.test.ts (#604) stubs rendersAsPage() like the other
panels-ui harnesses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved against #605 (and #602's test mock):
- CLAUDE.md Prompt uploads: #606's rewrite is kept and #605's bounded
workspace probe sentence follows the remote (SSH) refusal.
- test/routes/session-routes.test.ts: the remote-pane-history and
bounded-path-probe mocks are kept beside #606's _resetPasteRateBuckets
import, and both PRs' paste-image tests are kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict resolved in docs/api-reference.md: #607's prompt cache section and
#528's tabGroupId section are both kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A remote session's local pane hosts the remote tmux client in the alternate
screen, so its history_size is always 0. #583 read that as "nothing to load"
and hid the partial-history notice, but with #602 a full=1 load reads the real
scrollback from the remote tmux. The route now leaves the field out for remote
sessions, so their notice keeps the byte-based wording and its Load full
history button.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved against #583 and #588:
- One history-depth field: #583's `historyLines` / `capturedHistoryLines`
(reported as `paneHistoryLines`) is kept, and #601's lines cut now reads it
instead of a second `capturedHistorySize`. #601's `alternateOn` /
`capturedAlternateOn` joins it, and the one display-message format string
ends `#{history_size} #{alternate_on}`.
- selectSession keeps #588's full=1 switch for panes with no scrollback, gated
off remote and Docker sessions, and #601's SHELL_LOAD_QUERY for shells.
- computeHistoryTruncationNotice names a line count when paneHistoryLines is
known (#583) and otherwise uses #601's remainder wording.
- docs: api-reference lists both historyLinesCut and paneHistoryLines; the
notice invariant keys on history_size while the shell's automatic load is
the one place alternate_on decides.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner decision 2026-10-13 (#547): a rendered .html page behaves like a
file opened from disk in a browser. It may load scripts, styles, fonts,
images and media from any host and make fetch, XHR, WebSocket, beacon
and form requests to any host.
The /html-view Content-Security-Policy is therefore the `sandbox`
directive alone (HTML_VIEW_CSP, now exported and documented): the same
sandbox flags as before, never `allow-same-origin`, and no fetch
directive. It replaces Codeman's own `default-src 'self'` policy on these
responses. Everything that confines a page stays: the opaque origin (no
Codeman cookies or document, API writes with `Origin: null` refused, API
reads not readable cross-origin), and the capability serving only the
page's own folder under every path, symlink, dot, extension and
confinement rule.
A new test builds the route behind registerSecurityHeaders, as in
production, and pins the exact header: one `sandbox` directive with the
current flags, no `allow-same-origin`, no `-src` directive. The security
architecture, architecture invariants, API reference, CLAUDE.md and the
Working With Files wiki page now say what a page can do (reach the
network, read its own folder) and cannot do (Codeman's origin, cookies,
API, other folders), and that this is the owner's decision.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds GET /html-view/:cap/* to the list of endpoints exempt from the JSON
envelope, and a "Rendered HTML files" section covering the mint
(POST /api/v1/sessions/:id/html-view: body, response, capability
lifetime, every refusal and its status) and the capability-gated serving
route (what it serves, the sandbox CSP and other headers, Range, the
preflight answer, and the plain-text 404 / auth 401 refusals).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agents pick tools from MCP clients and registries, and Codeman had no MCP
surface: an assistant in Claude Desktop, Cursor, Codex or Gemini CLI could
not start or drive Codeman sessions without hand-written HTTP calls.
`codeman mcp` is a stdio MCP server and a CLIENT of the existing HTTP API
(it reuses codeman agent's transport and guards; no new route). Seven
tools: list_sessions, start_session, send_prompt, wait_for_session,
read_output, interrupt_session, delete_session.
- Dual-era: answers the legacy initialize handshake (2024-11-05 to
2025-11-25) and the 2026-07-28 revision (per-request _meta,
server/discover, resultType, and the ttlMs/cacheScope hints a modern
tools/list must carry: Claude Code 2.1.296 drops every tool without
them).
- Never guesses a server: the URL comes from --url or CODEMAN_API_URL.
Unconfigured, it still lists its tools and every call says how to
configure it.
- Prompting, interrupting and deleting are limited to sessions the process
started (or, inside a Codeman pane, that session's own workers) unless
--allow-any-session; never its own session. Control bytes are refused
before any request.
- Waits default to 45 s (Codex's MCP tool timeout is 60 s), max 600 s.
- read_output returns text only: a client that prefers structuredContent
otherwise showed the model metadata instead of the output.
- config/mcp-registry/server.json and package.json mcpName prepare the
official MCP registry listing; sync-plugin.mjs keeps its version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent skill only reached Claude Code: injection wrote
.claude/skills/codeman, which no other CLI reads, and the per-session
preamble was seeded for claude alone. A Codex, Gemini, OpenCode,
Antigravity, Grok, DeepSeek or OMP session inside Codeman never learned it
could start and drive workers.
- Registry field capabilities.agentSkillDir (a closed enum, since
clis.json can set it and it decides where the server writes) names the
skills directory each CLI scans. Claude keeps .claude/skills; those seven
CLIs get the same skill in <case>/.agents/skills/codeman (each measured to
scan it; Claude Code does not, so it never sees two copies). pi gets none:
a project skill makes pi ask "Trust project folder?" at startup.
- The preamble is seeded for every local session whose CLI takes the
skill; nothing in it is claude-specific.
- The skill text is CLI-neutral and names the two CLI-side failures to
report instead of retrying: a sandbox that blocks loopback (Codex's
default), and commands that do not carry this session's environment
(Codex 0.162 runs them in a shared app-server daemon).
- POST /api/sessions injects after the launch pre-flight, so a CLI that
cannot start leaves nothing in the workspace.
- codeman skill install gains --mode; uninstall --case removes the copy
from every directory the registry can write.
- The settings toggle moves to Agents & CLIs > All agent CLIs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude Code's tracker nulls recache_tokens_if_cold at a compaction or a tool-result clearing and only counts the expected rebuild at the next request, which also closes the window, so no report carried both and the old rule never fired. Stamp on the first warm null report after one that carried a value, carry it through idle re-reports and drop it at the next request; the readout now says "Context compacted at HH:MM" since a clearing opens the same window.
Also persist a changed report so a crash restores the latest one, rename the session field to statusLineExporter (statusLineTelemetry is the registry capability) and document its values, word the stake as re-cached, move the readout block above the SSE staleness heading, and drop keep-warm mentions until it lands.
Review follow-ups for loading a shell from tmux's rendered history.
tmux adds no history while a pane is in the alternate screen, and a
nested tmux client (remote SSH and Docker cases, or tmux run by hand)
holds its pane there, so a full capture of it is one screen. The
display-message query that reads #{history_size} now also reads
#{alternate_on} (PaneCaptureOptions.capturedAlternateOn), and a request
carrying `alt=recording` (only SHELL_LOAD_QUERY sends it) is answered
the old way on such a pane: the visible frame over the tailed byte
recording. Agents, scroll pulls and Load full history are unchanged.
alternate_on cannot tell a nested tmux from vim or less, so a local
shell with one of those open takes the same fallback while it is open.
A full capture without `lines=` was reported as a 'tail' cut whenever
the pane held more than the configured ceiling (possible on tmux before
3.7), offering a Load full history that could never return more. The
cut is now reported only for a client bound below the ceiling.
The response carries `historyLinesCut` when that bound left lines in
tmux, and the banner then says earlier output may be retained instead
of naming the bytes a later tail cut, which undercount what tmux holds.
A bounded grid tile's shell load sends its own `lines=` (via
SHELL_LOAD_BASE_QUERY). CLAUDE.md, docs/architecture-invariants.md and
docs/api-reference.md describe the shell load, the cut report and the
fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in i18n.js SKIP_SELECTOR: master added '.tab-name' itself
(the same fix this branch made for zh-CN tab names), so master's line is
kept and this branch's duplicate comment dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shell's automatic loads (tab select, dropped-output recovery, clear
terminal, a split/grid tile's load) replayed `session.terminalBuffer`,
the raw PTY recording, ahead of the visible frame. That recording holds
full-screen repaints (an ssh'd tmux, less, any TUI: cursor home, then the
rows joined by newlines, prompt on the last row) drawn for the pane
height of the moment. Replayed into a terminal with fewer rows, each
repaint's last newline scrolls and pushes its top row into scrollback,
so one line tmux holds once shows up dozens of times (26 copies in the
case that found this, after the pane went from 24 to 23 rows). Remote
SSH shell sessions hit it constantly: their inner tmux repaints often.
Shells now load a bounded window of tmux's rendered history instead
(`SHELL_LOAD_QUERY`: full=1&tail=1MiB&lines=10000), the same source the
scroll-to-top pull already used. `lines=10000` bounds the synchronous
capture-pane, measured at ~8 ms per 1000 lines (~0.1 s here vs ~0.8 s
for a 100k-line history). Because that bound is shallower than xterm's
scrollback, the server now reports a cut: the capture's existing
display-message query also reads `#{history_size}`
(`PaneCaptureOptions.capturedHistorySize`), and the route marks a full
capture `truncated` / 'tail' when tmux holds more than it read, so the
banner keeps offering Load full history and scroll-to-top keeps pulling
older lines. The byte cap still reports 'capped'. The banner no longer
names an amount it cannot know ("less than 1 KB more") for such a cut.
Agent sessions are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A phone-only toolbar Attach button hands the agent a photo or a video with the
keyboard down. It owns its own file input, positioned over the button so iOS
anchors the picker popover to it; the picked paths land on the prompt as unsent
text, so Compose adopts them together with anything already typed and Use
terminal keyboard hands a single-line draft back the other way.
Videos take a second path on the same route: ?kind=video streams the file to
disk under MAX_PASTE_VIDEO_BYTES (1GB, env CODEMAN_MAX_PASTE_VIDEO_BYTES)
instead of buffering it, sniffs the container from the first bytes as they
arrive, drains a mislabeled body unwritten and unlinks on any failure. It draws
on its own 3/min per-(IP, session) rate bucket beside the image path's 30/min:
at 1GB each, 30/min would be 30GB of disk a minute. Without the flag the route
is byte-identical to the image-only version, same 50MB cap and same bucket.
One client classifier, _promptAttachKind (MIME first, then the name's
extension), drives every pick and paste surface: the Attach button, terminal
drop, the tile grid's drop, the Compose and paste-dialog pickers and the
clipboard handlers. A pick that lands while the tile grid owns the terminal
goes to the PTY rather than the parked main-terminal overlay.
Claude Code's statusline has carried a prompt_cache object since v2.1.251, and Codeman's statusline exporter already received it, but StatusTelemetrySchema stripped the whole object, so Codeman never knew whether a session's conversation was still cached. Keep it and show it.
StatusTelemetrySchema now lists every documented scalar field null-tolerant (several are documented as possibly null, and the object gains fields across CLI versions, so a strict shape would 400 the post and blank the footer), while parsePromptCache normalizes only the subset Codeman stores, so a hostile localhost payload cannot bloat state. Session.promptCache rides toState with a change-only session:promptCache event, and the route broadcasts the stored object.
The Session Options (Respawn tab) readout shows warm-until or cold with the re-read stake, the cache hit rate, the last miss's causes, and, for Claude Code's own idle compaction (recache_tokens_if_cold null while expected_rebuilds rose), a "compacted at HH:MM" note that is never raised as an alert. setPromptCache stamps that on a new object rather than mutating the parsed report, so the stamp reaches the broadcast. Sessions without a statusline of their own also get a cache:until HH:MM / cache:cold group in the terminal footer. The last report is restored on a Codeman restart (like displayModel) so the readout is not blank until the pane's next render; the readout is time-aware, so a value whose expiry has passed reads as cold, not stale-warm. A launch without the exporter records why (collection off, workspace statusline, remote) so the row says the cause instead of "not reported yet" forever.
This is the sensor a later opt-in keep-warm would read, and the TTL it reports is the source of truth rather than the plan tier.
The "Showing the most recent 1.0 MB of this session. 4.8 MB more may still
be retained." bar covered the top of the terminal on nearly every tab switch,
and for fullscreen claude panes it could never be acted on: `truncated`
measures the server's byte stream, while Load full history asks tmux, which
holds no scrollback for a pane that lives in the alternate screen.
- The capture reads `#{history_size}` in the cursor query it already makes
and the terminal route returns it as `paneHistoryLines`.
- A reported 0 hides the notice; a positive count replaces the byte gap in
the message.
- The notice is lazy: it appears once a wheel/touch gesture reaches the top
of the browser buffer (after the pull that gesture starts), leaves when
the user scrolls back to live output or switches tabs, and its dismissal
sticks per session until reload.
- formatHistoryBytes no longer prints "1024 KB" just under 1 MiB.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>