The two stubs from #776 cover the global ignore file and the global config. A core.excludesFile in the system gitconfig still reached the code under test and failed the same test (reproduced with GIT_CONFIG_SYSTEM pointing at one). GIT_CONFIG_NOSYSTEM closes that, as test/git-hooks.test.ts already does for its code under test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved as on test/desktop-macbook: config/test-suites.ts keeps both browser tests (desktop-connect-page and local-model-ui), and the CLAUDE.md state-files line keeps #778's cli-installed entry with the local model's local-model.json and local-model/ entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved as on test/desktop-macbook: install.sh usage() lists CODEMAN_NO_ADMIN=1 or 0 and #773's CODEMAN_AI_CLI and CODEMAN_PROGRESS=1; probe.rs keeps #781's agent() with TIMEOUT, MAX_BODY and probe_within all pub; the desktop README keeps both known-limits entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`GET /api/approvals` re-reads the pane of every pending dialog and resolves
one whose frame shows no dialog, either because its options vanished or
because the frame shows a turn running. `notePrompt` deliberately waits 3 s
before making that same judgement, since the Notification hook can land
before Ink paints the dialog and the working line is still on screen then.
The list did not wait. That rarely mattered while it ran once per page load,
but a client that polls it would resolve a live prompt as answered in the
terminal, clearing the red alert on every device and the push Approve button
with it. `sweepForList` skips a dialog younger than the delay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a Codeman server of this user ran at an address the app could not open
as its default one, the This Mac tab only named host and port and sent the
user to Another machine to type the address again. A second instance beside
the one that answers (a beta, a `codeman web --port 4000` in a terminal) was
not shown at all.
The look at this Mac now works out an address for every listening socket of
this user's Codeman servers (`found_addresses`, pure): 127.0.0.1 for a bind
to loopback or to every interface, `[::1]` when that is the only loopback
socket, the address itself for a bind to one; `https` and the base path come
from `--https` and `--base-url` on the command line. A server at the default
address with no `--base-url` of its own keeps the default URL, since
install.sh's launchd job has the base path in its environment, where `ps`
does not show it. The default address comes first, then the others by port.
Each address other than the default one is asked once, all at the same time,
with no login. The default probe's answer is reused for its own row. When
nothing answers at the default address, the first found server that answers
is still what Open is for, so each gets the probe's usual 5 s; next to a
Codeman that answers they are only listed and get 2 s. `LocalStatus.found`
replaces `elsewhere`.
The tab lists them: "Codeman is already running on this Mac." with a row
each when none answers at the default address, and "Also running on this
Mac" under Open otherwise. A row has the address, the version and Connect,
or the reason the app cannot open it. Connect fills the address in and runs
the page's one connect(), so the app checks it, uses its saved login and
remembers it like an address typed under Another machine. Nothing connects
by itself, and a refused connect from a row never arms the reopen that Open
has for a starting server.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The violet button was the one thing on the start screen outside the app's
blue, and it carried white text and a see-through disc where its neighbour
has dark ink and a solid disc. It is now white with dark ink and a solid
disc, the same on the Install button, and the chooser's selected row, tile
and model bar follow. Light skins turn it over to dark, since a white button
vanishes on a pale page. The context takes the app's blue.
The tokens are named for what they mean now (--lm-brand, --lm-context).
Brings test/local-model-ui.browser.test.ts up to the redesign it had fallen
behind: it is outside the gate, so the button's 46 px, the 12 px bar and the
memory panel's edges had gone unnoticed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner's verdict on the chooser: nice, but not nice enough, too much just
blue. He also wants "Run a local model" on top of the start screen, above
Run Claude Code.
The memory bar is now a map: the model, its context and the brief peaks side
by side on the track of what this Mac can give a model, each with its size in
a legend, and the verdict on the fit beside the figure. recommend() sends what
the need is made of as `Candidate.memory`.
Colour now carries meaning: green for good and tested, amber for tight, orange
for slow, teal for the context. Violet is the feature's own colour, on its
start screen button and its main action. Every row has a size tile and a speed
gauge scaled to the fastest row in the list. The light skins get deeper values
of the same colours, and the window is 40 px taller to hold it without
scrolling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window is one frame for the whole flow: a fixed height on desktop, App
Settings' boxed close button, the buttons in a foot of their own outside
the part that scrolls, so a click never moves the next button away and
nothing is cut off; the short screens are centred in it on desktop and sit
above the buttons on a phone.
The chooser lists the pick first and then the other models by download
size, largest first, a column the reader can see. A pick that only just
fits says "Best fit, tight". The memory figure is an estimate and says so
once, in whole GB; its legend names the peak, and the installed line uses
the same words beside what the model takes now, so the two figures are not
read as a contradiction. The legend, the column headers and the foot's
promise are set in --text-dim, the bar's track in --text-muted so its free
part can be seen, and only the download figure keeps tabular figures.
The consent screen is the download as a table: the chosen row, the runtime,
the coding agent with its installer's command as the row's second line, the
total in the strong weight, then where it goes in one line. The download bar
means the model download alone and is gone once the model is in; the screen
that ends an install shows "Start a session" alone; an error on an installed
model is the headline with a grey dot; an error's first sentence is the
headline; Remove is quiet everywhere but red in its own question. The
no-recommendation reasons are one sentence each, and the untested note is
in Codeman's voice. A refusal the server names is printed from its sentence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Only the two session routes asked launchRefusalMessage(). The attach that
finds no pane, the respawn of a dead pane (a click on the tab, a respawn
start, the boot recovery), restartCli() and the reboot-restore rebuild
started an agent on the endpoint unasked. Session gains setLaunchGuard(),
asked right before a NEW agent process is started and never for an attach to
an agent that runs; WebServer.setupSessionListeners() sets it for every
session; a refusal is a LaunchRefusedError that never falls back to a direct
PTY. The reboot-restore route asks before it builds and skips the entry with
its own reason, left on offer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The connect step records the connection in the state file BEFORE it writes
the endpoint record and the settings flag (the flag's earlier value read by
inspectLocalEndpoint()), so a state write that fails or a server killed
between the two never leaves a record of ours that the state file does not
know; a record under the reserved id that says exactly what ours would say
is ours. changePort() writes the state first and puts the old port back when
the endpoint move is refused.
A job whose end could not be written (a full disk) is kept in memory: the
view shows it, Resume and a new install start from it, the next verb writes
it, and Cancel never answers a 500. A first install on a full disk is a 507
with the sentence. "installing" in the file while this process runs no job
always reads as paused or that failure.
An installed model whose runtime does not start says so (runtime-start-failed
with Start as the next step, and the launch check says it too); the stored
runtime version is started while its folder is unpacked. Stop, Remove and
the boot of anything that is not a running installation end a runtime an
earlier server left behind before any file is deleted. Stop and "Use another
port" answer 409 sessions-open while a live session uses the model. A pull
that failed because the runtime is gone is runtime-stopped, not a network
problem. After "does not fit" only a model that needs less memory is offered.
A replaced job's partial blobs are deleted. Every 400 carries its sentence as
data.message, and the six inline sentences are labels with zh-CN entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The supervisor reports the child's pid the moment it is spawned, before it
answers, so a server killed during a start still leaves a pid that names the
runtime as ours. stop() takes that stored pid (previousPid) and ends a
runtime an earlier server left behind when its command line is a binary
under our runtime folder, so no caller deletes the folder under a live
process. start() takes a version: an installed model keeps the runtime
version it was installed with when a later build pins another one.
removeLocalModelFiles() refuses a local-model folder that is itself a
symbolic link (every check compared against the real path of the root, so
a link in its place passed whatever it pointed at). removePartialModelBlobs()
deletes what an interrupted download left in the model store, which the
runtime answers "not found" for and never deletes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The real routes, service, recommendation, runtime download and unpack,
supervisor, pull, checks, connection and state file, joined together. Only
the runtime (a fake HTTP server the injected spawn "starts"), the hardware,
the coding agent's installer and the waits between retries are stand-ins.
Walks: the whole install and its removal (the folder is gone, the settings
flag is restored); a runtime download that arrives damaged; a model download
that fails twice and then succeeds; one that keeps failing; a model that
calls no tool; Cancel during the model download; a server restart during the
model download, which comes back paused and waits for a click.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The read-only card is gone. The welcome screen has one button, "Run a local
model" ("Local model" once one is installed), and App Settings > Models has
one row; both open the same window.
The window follows `install.state`: choose a model (a radio group, with a
memory bar filled to what the selected model needs), confirm the sizes and
the folder, watch the download and the steps, and end on "Start a session".
Paused, failed, installed (running or stopped), Remove and "no model fits"
are screens of the same window. It polls the GET once a second while open.
Every dynamic string goes in through textContent, labels and sentences come
from the payload, no CLI id is compared, and every new string has a zh-CN
entry.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
POST /api/local-model/install, cancel, resume and runtime, and DELETE
/api/local-model. The GET gains an `install` object the window polls. All of
it is experimental and admin only in multi-user mode.
One job of five steps (runtime, model, verify, agent, connect) driven by a
pure reducer over explicit state, step and transition tables. One job at a
time (409). The consent is bound to the size shown: `acknowledgedBytes` must
be the server's own total (409 size-changed). A job a server restart
interrupted comes back paused and never continues without a click. Every
failure is a code with one next step.
The model is judged against the budget the runtime measured before it is
downloaded. A missing coding agent is installed with its stock command
through the shared runner, audited. Connect writes one endpoint record under
the reserved id through mutateCustomModelHosts() and switches
customModelEndpointsEnabled on through mutateSettingsFile(), remembering the
flag's value. The model's digest is stored and read again at boot and before
every session start on that endpoint; a changed model refuses the launch.
Remove answers 409 with the session names while a live session uses the
endpoint, undoes the two writes first, restores the flag only when this
feature switched it on and no other endpoint is saved, and deletes only
inside local-model/. The coding agent stays.
The server starts the runtime at boot when a model is installed and stops
it at shutdown beside stopDeepSeekWeb().
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The four endpoint routes and the 5-minute discovery sweep each read
custom-model-hosts.json, changed their copy and wrote it back through one
shared temp file name. Two of them at once could tear the file or drop each
other's record: ten creates sent together did not all land.
Every write now goes through mutateCustomModelHosts() (one promise chain, a
random temp name, mode 0600), with each check inside the same step as its
write. A file that does not parse is refused with 409 and never replaced.
A delete of an unknown id no longer rewrites the file. A guard test fails
any other writer in src/.
The id `local-ollama` is reserved for the model Codeman runs on this Mac: a
new endpoint may not take it, and while the record is the managed one it is
not edited, deleted or re-discovered from these routes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The runner that executes a CLI's stock install command (the environment
without CODEMAN_*, the npm prefix redirect, the bounded process group, the
one install per CLI at a time) moves from cli-registry-routes.ts to
src/cli-install-runner.ts, so the local model install can use the same one.
The route re-exports installEnv and npmGlobalPrefixWritable and behaves as
before; its tests are unchanged.
New in the shared module: an abort signal that kills the process group,
stockInstallCommand() (the compiled stock command, never the clis.json one)
and tryBeginCliInstall(), the one install slot per CLI for both callers.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
runtime.ts: the runtime is pinned in a compiled constant (version, URL, size,
sha256). The archive is streamed to a .partial file while it is hashed,
continued with a Range request, retried at most 3 times, and the hash is
compared before anything is unpacked; a mismatch deletes the file. It is
unpacked with /usr/bin/tar into a temporary folder and renamed.
The supervisor runs the runtime as a non-detached child with an environment
built from an allowlist by the pure runtimeEnv(), on a port chosen once by
binding. It restarts the child at most 3 times, stops it with SIGTERM, adopts
or replaces its own orphan on the port and answers port-in-use for any other
listener. parseInferenceComputeTotal() reads the device total the runtime
logs when it starts. Under vitest nothing spawns or fetches.
ollama-client.ts: the pull is parsed incrementally by a pure parser (a 23 GB
pull is about 1.4 million lines), with up to 5 retries on an error line or a
cut stream. verifyModel() loads the model with one tiny chat, checks what the
runtime reports (all on the GPU, the chosen context) and makes one tool
probe; each failure is a code.
developer-tools.ts: only /usr/bin/xcode-select -p, bounded, darwin only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The catalogue rows carry the figures measured on a Mac (the MLX build of
qwen3.6:35b-a3b is 23,105,798,956 bytes, about 37 KB held per context token,
about 3 GB of runner overhead, about 5 GB more for a moment while a long
prompt is processed). A model's need is now its peak. Rows that were not
measured use the same per-token and overhead figures and say `estimated`.
qwen3.6:35b-a3b gets the tested entry for an Apple M5 Pro with 48 GB and
OpenCode, with its measured 67.7 tokens per second, so that row may say
"Tested on a Mac like this".
The ranking changes with it: a pair tested on a Mac like this one comes
first, then speed (ok, unknown, slow), then model rank, harness rank and the
larger context. The fit grade decides whether a row is listed and what it
says, it no longer sorts, so a model is offered at the largest context it
fits at. recommend() takes the budget the runtime measured
(`budgetSource: measured`), and modelFitsBudget() judges one model against it.
Candidate rows now always carry `needSource`, `harnesses`, `installBytes`,
`installGb` and, where one applies, `tag`. `kvBytesPerToken` and the prompt
cache tiers are gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The symlinked .git folder test now also runs from a session in the main
checkout, as well as from a linked worktree. A new pane-watcher test
checks that an unparseable docker-cases.json follows nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the 1.49.0 API meaning: liveWorktree and root=live stay inside the
session folder, and an outside worktree is published as outsideWorktree
and walked only with root=live-any.
Refuse a symlinked .git folder as the main worktree, sensitive files
inside a followed worktree, and any outside worktree while attachment
confinement is on or around a blocked tree. The file routes re-check
the guard settings on every request, and a throwing rule check refuses.
The panel keeps its Back to worktree button while pinned to the session
folder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `LocalModelService.launchRefusal(endpointId)`: what a session launch asks
before it starts on the managed endpoint. It answers the error that
refuses the launch (the runtime child is not alive, another program holds
the port, the runtime keeps stopping, the model's digest changed) or
nothing. New error code `runtime-not-running` with its sentence, for a
runtime the person stopped: only a refused session start says it.
- The fake runtime's MacBook log line is rebuilt from the two values the
measurement notes quote, so it is `INFERENCE_COMPUTE_LINE_MACBOOK` now.
Only the Mac mini line is a verbatim capture and keeps `REAL_`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The types, the module skeletons and the test fake that let the runtime, the
install job and the window be built at the same time.
Types:
- src/types/local-model-install.ts (new): the runtime pin, the install
states, steps and error codes with one sentence and one next step each,
the progress, the `install` object of GET /api/local-model, the bodies and
409 answers of the install verbs, the state file. `LocalModelStatus` moves
here; `install` is optional until the route emits it.
- src/types/local-model.ts: the catalogue row's Mac memory figures, the
measured speed of a tested entry, the `measured` budget source,
`RecommendOptions`, and the row tag, the coding agents and the install
total of a candidate. All optional until the catalogue and `recommend()`
carry them, so the read-only stage keeps compiling and its tests stay
green.
Modules with final signatures and bodies that throw:
runtime.ts (the compiled pin is final), ollama-client.ts, developer-tools.ts,
state-store.ts, install-job.ts (the state, step and transition tables are
final), connect.ts, custom-model-hosts-writer.ts, cli-install-runner.ts.
Nothing imports them yet, so the server is unchanged.
Implemented: paths.ts (every folder of the feature, from dataPath()), and
`modelFitsBudget()` in recommend.ts, the judgement the job makes with the
measured budget before it downloads a model. `recommend()` takes the options
parameter and does not read it yet.
Test fake: test/fixtures/fake-ollama.ts, a runtime on a port the system
picks, in the shapes measured on a Mac: version, tags, ps, the pull stream
(MLX and GGUF lines, an error line, a cut stream, a resumed pull), chat with
and without a tool call, the model list, a runtime archive with Range
support, and a fake child process that logs the device total.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#772 made the Files panel follow an agent into a worktree inside the
session's folder. A worktree beside the repository (git worktree add
../feature, or an EnterWorktree into one that already exists there)
stayed unfollowed, since serving it needs a second confinement root.
findOutsideWorktree() answers that case without running git. The
nearest .git above the pane must name <common dir>/worktrees/<id> of the
session's own repository, and that registry entry must name the
candidate back (or the .git folder must be the common dir itself). It
refuses /, the home folder and above, any folder holding workingDir and
Docker case workspaces, and multi-user mode applies the owner's
folderAllowedForUser rule. The answer is liveWorktree with outside: true
and the folder's real path.
The file routes serve an absolute path inside that worktree, and only
while the folder still resolves to the recorded path. A relative path
still resolves against workingDir alone. The browser routes such paths
to the editable preview, and root-relative markdown refs resolve from
the top of the worktree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the read-only recommendation and its card.
Recommendation (pure):
- `limited` is now a statement about the Mac: no model with 12 billion
parameters or more fits. It is judged on everything that fits, before
the disk filter and whatever the pick is. A 32 or 36 GB Mac whose pick
is a 9B model no longer reads "This Mac can run a small model" above a
list of 20B, 27B and 35B models, and knowing the chip no longer lowers
the verdict. A small pick under `ok` carries a note that says so.
- A model that fits the memory but has no room on the disk is no longer
dropped in silence: the best-ranked one is named in a note with the GB
that are missing. A full disk cannot turn a 48 GB Mac into a
small-model Mac.
- "Models fit, but not at a context a harness needs" has its own
sentence with the budget and the need, worded as an estimate. The
16 GB Mac mini misses by 1.3 points (10.3 of 10.4 GB) and was told
flatly that its memory is too little, the sentence of an 8 GB Mac.
- The installed memory is named by the size the Mac is sold as (48, not
51.5): the hardware line and the memory sentence. Every computed size
stays decimal GB from the one formatter. An unknown memory size is no
longer reported as "0.0 GB".
- Slow is graded on the unrounded speed: 9.95 to 9.9996 tokens per
second printed as 10 and read ok.
- The disk sentence names the 10 GiB that must stay free, so its
numbers add up for the reader.
Card and Settings group:
- The Settings group "Model on this Mac" is hidden on a server that is
not a Mac, and until the first answer says it is one.
- Only `403 FORBIDDEN` is a refusal. `403 PASSWORD_CHANGE_REQUIRED`
(an admin who must change the password) is a failed request and is
asked again, so that admin no longer loses the card until a reload.
- Each fit and speed label carries what it measures as its tooltip and
accessible name, an alternative with a speed estimate prints it, and
the count beside "Other models that fit" is gone (it was the number
of rows, capped at four, not the number of models that fit).
- The line saying nothing can be set up yet sits right under the pick,
in the colour of every other line (it was 3.9:1), and says whose step
it is.
- The dismissal key is named for this stage, so hiding the read-only
card does not hide the install offer later.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On a Mac without the developer tools, /usr/bin/git is a placeholder: running
it, even as `git --version`, makes macOS open its "install the command line
developer tools" window. The server asked "is git here?" exactly that way on
the first page load (isGitAvailable() from renderIndexHtml), so a no-admin
install on such a Mac ended with that window one second after the server
started, and it would have come back at every start. Found on a new MacBook
on 2026-11-06: the system log names the Codeman server as the parent of that
git process.
One gate now stands in front of every git the server starts
(src/utils/git-availability.ts). On macOS it resolves git on the PATH itself;
when that is the placeholder, `xcode-select -p` must name a developer folder
that holds a git of its own. xcode-select only prints and never opens a
window. Without the tools, git counts as absent and is never started: every
caller gets the ENOENT it already answers with "git is not installed".
A refusal is asked again after a minute, so installing the tools later needs
no server restart for the git features, and isGitAvailable() no longer
memoizes that one refusal.
Gated: the clone and worktree runners, the Git panel's status and fetch, the
update check and repository status, and Read My Mind's workspace signals.
test/git-spawn-gate.test.ts covers the decision and scans src/ for a git spawn
in a file that does not call the gate, with the reviewed list of sites pinned.
Checked for real on macOS 27. With the developer tools present (Command
Line Tools on one Mac, Xcode on another) the gate resolves /usr/bin/git,
finds the tools and allows git, in 3 to 5 ms. On a MacBook with no
developer tools, a fresh no-admin install with this fix started the server
and loaded the page with git reported as absent, no window opened, and the
system log holds no request to install the tools; the same install without
the fix logged that request one second after the server started.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The label list was "frozen for the parallel build" and the card's test said
the route answers 501: both were true while the pieces were written side by
side and are not any more. Comments only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The read-only face of GET /api/local-model: what the Mac the server runs on
can run as a local coding model.
- local-model-ui.js: one builder paints both containers from the same
payload: the hardware line, the server's headline, the pick with its numbers
(context, download, memory need against the budget, speed), the server's
notes, the other models that fit with their Good, Tight, Slow or Speed
unknown labels, and one line saying that setting it up from Codeman is the
next step and is not built yet.
- The welcome card ships hidden and shows only once the server answered for a
Mac. It stays hidden for not-macos, for a refused request (a non-admin in
multi-user mode) and after "Hide", a per-device localStorage key. The App
Settings group "Model on this Mac" (Models) carries the loading and failure
sentences and is the way back after a Hide.
- Every dynamic string goes in through textContent, both containers are
data-i18n-skip and the module translates its own sentences, repainting on a
language change. Sizes are printed as sent. Labels and harness names come
from the payload: the module names no CLI and the word "Recommended" is not
in it.
- One request a minute at most, shared while in flight; a failed refresh keeps
the last answer on screen.
- i18n.js: zh-CN entries for the module's strings and for every server label.
- frontend-cli-no-id-branching.test.ts scans the new module.
Tests: test/local-model-ui.test.ts (JSDOM over the real markup),
test/local-model-i18n.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The read-only route behind the recommendation, and the one module that reads
the CLI registry for it.
- harness-facts.ts: one fact per catalogue harness row, resolved in the CLI
registry at call time: the registry's label, `installed` from the existing
availability probe (asked once per build), `installable` for a stock CLI
whose install command for this platform needs no administrator rights, and
`acceptsModelId`, the test a custom model launch applies to the model id.
A row whose CLI is unknown, disabled or has no custom model mechanism gets
no fact. No CLI id is compared with a literal.
- routes/local-model-routes.ts: answers { hardware, recommendation,
catalogUpdated } in the usual envelope and reformats nothing. Admin only in
multi-user mode (403), checked before anything is probed. A failed probe is
an answer (200, the hardware's `source` says what was read); a 500 is left
for a fault in the catalogue, the harness facts or recommend(). It writes no
file and reads no setting.
Tests: test/local-model-harness-facts.test.ts,
test/routes/local-model-routes.test.ts (app.inject(), no port; the temp HOME
is byte-identical before and after the requests).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The core of the read-only recommendation: what this Mac is, what the catalogue
knows, and which model fits. Nothing is downloaded, installed or written.
- hardware.ts: one bounded async spawn of /usr/sbin/sysctl with seven keys,
without -n, parsed as "key: value" lines by the pure parseSysctl(). Free disk
of the data dir through statfs behind the bounded path probe. Fallbacks from
node:os, a 60 s cache, never throws, never system_profiler. A wired limit of
0 means "not set".
- local-model-catalog.ts: seven models, three harness rows (pi gated) and the
chips table as a compiled constant, validated by a Zod schema on first use.
No override file. Model ids are name:tag in lowercase without a slash; every
model carries paramsB and an (empty) tested list. Chip names match on the
end of the chip string, so 'M4' is not the chip of 'Apple M4 Pro'.
- recommend.ts: device budget (the wired limit when above 0, else 2/3 of
memory below 36 GB and 3/4 from 36 GB, less the reserve), need = weights +
KV cache + 1 GiB + prompt cache, fit good up to 0.85 and tight up to 0.98,
speed ok, unknown or slow (a chip in no chips row is unknown), and the
ranking. Verdicts ok, limited (best model under 12 billion parameters) and
none with its reason. "Recommended" only for a pick with a tested entry for
this Mac, which no row has today.
- types/local-model.ts: comments brought in line with the code (chip matching,
budgetShare rounding, a budget of 0 on an Intel Mac).
- cli-registry-no-id-branching.test.ts: the guard also catches a comparison of
a `cli` field with a CLI id (plus one stray blank line Prettier refused).
Tests: test/local-model-format.test.ts, test/local-model-hardware.test.ts
(the captured output of two real Macs), test/local-model-catalog.test.ts,
test/local-model-recommend.test.ts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A running Codeman remembers a missing CLI for up to 5 minutes, so a status
poll never spawns a login shell, and its Run button reads that same memory.
So "Run fails, add the agent, Run again" kept saying the CLI was not
installed, right after the app had said it was ready.
install.sh now touches `cli-installed` in the data dir once a CLI has landed,
and resolve() leaves the backoff when that file's mtime changes. It is one
stat, inside the backoff branch, and buys one run of the chain, so a resolved
CLI never pays for it and a CLI that is still missing goes back to waiting.
Re-walking PATH during the backoff would have found hand installs too, but it
would put a synchronous stat of every PATH entry on the request path, where
one dead network mount freezes the server (#516), and execute candidates that
were already refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After "Skip for now" there is no other agent, so a failed add hit
install_offered_cli's gate first, and the app showed its first-install text
("re-run the installer") instead of naming the agent and its command. That
is the main path of adding an agent.
run_offered_cli now runs a trusted command and looks again; the gate stays in
install_offered_cli for the install, and `agent` judges its own run. The one
line that executes a trusted command is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent chooser only appeared during the install, and only on a Mac with
no agent. After "Skip for now", or with Codeman already installed, there was
no screen for it.
The same chooser now works after the install: "Add a coding agent" on the
This Mac tab, "Add a Coding Agent to This Mac..." in the app menu (it also
works from a Codeman page), and after a Skip the app says Codeman has no
agent yet before it opens Codeman.
`install.sh agent <id>` installs one agent and nothing else. It checks that
THAT agent landed: install_offered_cli is fatal only when no CLI is left, so
with another one present a failed install would have reported success.
The app runs it as a run of its own (`gui_install::Run::Agent`), in its own
temporary folder, so `ps` never reads it as "Codeman is being installed". It
may run next to a live server, which an install must refuse, and stops only
for another run. `--no-admin` goes to both `preflight` and `agent`: it decides
what counts as installable, so the two must agree.
CI: the bash 3.2 preflight check never tested anything. Its child run
inherited CODEMAN_INSTALL_SH_LIB and stopped at the sourcing guard, and a
false [[ ]] does not trip set -e on bash 3.2. Both are fixed for that check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
changesets/action copies a CHANGELOG.md section into the release as it stands,
so a whole release rendered as one bullet behind its commit hash, with
`### Thanks` as literal text and "Minor Changes" as the only real heading.
The Release workflow now rebuilds the body from that section: Thanks first,
then What's new with a heading per change, Fixes, Also in this release and a
compare link, with "Behaviour change:" and "Breaking:" in bold. The release is
titled `Codeman <x.y.z>`; the tag stays `codeman@<x.y.z>`, which is what the
updater and the update toast read.
This cannot be written into CHANGELOG.md instead: `### <Type> Changes` is
hardcoded upstream, and a `## ` line would end the section for changesets/action
and versionSection(). So the changelog keeps its format and the toast is
unaffected. npm has already published when the step runs, so a formatter failure
leaves the changesets text with a warning and the retag still happens.
SECTION_LABELS is now shared with whats-new.mjs so the toast and the page agree
on what counts as a headline. docs/releasing.md records the changeset shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quick-start wrote a selection's launchModel into the session's stored
<Mode>Config, which is what a clear falls back to, so a clear relaunched
the CLI with the endpoint's model id on its native backend. Only API
callers can reach that, since no page sends a clear.
The first launch now takes the forced model from the session's selection,
through the same _withCustomModelLaunchModel() the respawn path uses, so
the stored config keeps the model chosen at create. That also carries the
model onto a new tmux session for a selection applied to a running
session, which the create path used to lose.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four tests failed on a machine with a de_DE locale and a global git ignore
file, with no product change involved:
- custom-model-run-menu-ui (3): the warning formats counts with the runtime
locale (131.072 under de_DE) and the tests hard-coded 131,072. The
expectations now ask the same runtime through toLocaleString().
- git-worktrees (1): git also reads $XDG_CONFIG_HOME/git/ignore, which
GIT_CONFIG_GLOBAL does not cover. A global ignore of
.claude/settings.local.json kept the exclude line from being appended. The
test now points XDG_CONFIG_HOME and GIT_CONFIG_GLOBAL away for the code
under test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button sat empty in the page while its strip was hidden, which the
mobile accessibility suite counts as a button with no name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
classifyDotGit() checked with lstat and then opened by name, so a FIFO
swapped in between the two blocked a threadpool worker. The open itself
now refuses both, and the handle is judged.
Reported on #772.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes the contributor's wording for the picker title and the toast, the
preview refresh after the preflight and the clearer 409 text, and keeps
the fuller tests and the invariants paragraph from the release branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The poll had no hidden-tab gate, and the local scan's 5 s cache sits under the
8 s interval, so a single tab never hit it: every tick was a full socket sweep
(1.1 s and 772 forks on a box with 212 sockets) for a list nobody was reading.
`_foreignPollTick()` is now the one gate for the first load, the timer and the
return to a visible tab. The `visibilitychange` listener is stored and removed
in `stopForeignPolling()`, since this runs on every trip to the home screen.
Checked in real chromium with document.hidden stubbed: 2 polls in 9.5 s visible,
0 in 17.5 s hidden, 1 on return, and a tab opened in the background makes none
until it is shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A subfolder whose bounded probe answered `unknown` came back as `children: []`
with no `incomplete` and `truncated` false, so a dead mount, or a healthy one
slower than the 1.5 s probe on first touch, read as an empty folder and a
search missed it in silence. `unknown` is never `absent` (#516).
Such a folder now goes on the walk's `unread` list, which flags the node and
sets `truncated` in both walks. A folder the filesystem says is gone drops out
quietly as before. The row said "file limit reached", which would be false
here, so it now reads "Not fully scanned" for either reason.
Reproduced on a real disk with a parent at mode 0444, where the child's stat
answers EACCES: the API, the search and the panel all report it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
That description is the only warning that the switch weakens Codex's
sandbox, and it had no zh-CN entry. The test runs the real translator over
the real index.html.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>