web_coverage_measure took limit 0 as 1 when paging but recorded 0 examined (and counted skipped scripts when offset was set); web_modules_trace did the same with its coverage. Both now settle the page size once and record what they show. Fixes#331.
A .text section that runs past the end of the file is decoded as far as the bytes go, with a text_truncated limitation and partial coverage. Fixes#306.
The HTML scanner skipped only `<script>` bodies whose type mentioned "json" or "template". Import maps (`importmap`), speculation rules (`speculationrules`) and data blocks such as `text/html` became JavaScript modules in app maps: an import map was reported as a broken script, and an inert HTML template could add made-up fetch endpoints.
A `<script>` now counts as a script only when the page runs it as code: no type, `module`, or a JavaScript MIME type (parameters such as `; charset=…` are ignored), plus `text/babel` and `text/jsx`, which in-browser Babel runs. Other types are left out, both inline and with `src`.
**Verified:** a new scanner test. Five non-JavaScript types (inline and with `src`) give no scripts; six JavaScript spellings, including a mixed-case type and one with a charset, give one each. `cargo test -p craft-anything-js` (41) and `-p craft-anything-analysis` (85), clippy and fmt pass.
**Not run:** `cargo xtask wasm`. The change is pure string logic in an existing function.
Fixes#285
`app_changes_summarize` decided whether each comparison found changes. For records with `counts` it added added + removed + changed, which left out moves. So an archive or folder comparison whose only difference was a moved file (reported `identical: false`, `moved: 1`) counted as unchanged ("0 of 1 comparisons found changes").
A record's own `identical` verdict now decides when it has one, and moves count otherwise. Archive `counts.changed` keeps its meaning (content-edited files).
**Verified:** a new test of the verdict: a move-only comparison is changed (with and without `identical`), and an identical one is not. The engine's compare tests, clippy and fmt pass.
Fixes#286
`core_archives_compare` matched moves by content through a map from hash to one new path. Two identical old files missing on the new side both "moved" to that one path, so a removal went unreported. With two renamed copies, one copy was reused and the other was reported as added.
Each new-only file is now a move destination at most once. Old files are paired with distinct new copies of the same content, and the rest are removed or added. Every file on each side is accounted for once.
**Verified:** a new test with the reporter's fixtures. Two old copies and one new copy give 1 move and 1 removal; two and two give 2 moves to different paths, with no additions or removals. The existing comparison test still passes. `cargo test -p craft-anything-analysis` (86), the engine's artifact tests, clippy and fmt pass.
Fixes#283
`runtime_captures_compare` compared the filesystem dimension by the set of changed paths only. A run that deleted a watched file and a run that created the same file both "changed" that path, so they matched, and the comparison said the two runs were the same.
Each path is now compared together with how it changed (created, deleted or modified). Two deletions of the same file still match, and different paths still differ.
**Verified:** a new test of the filesystem comparison covering deleted vs created (changed), deleted vs deleted (unchanged), different paths (changed), and an `unchanged` effect against none (unchanged). The engine's process-tool tests, clippy and fmt pass.
Fixes#300
OpenAI-style APIs report `prompt_tokens` including the cached part (`prompt_tokens_details.cached_tokens`). We stored the whole `prompt_tokens` as `input_tokens` and the cached part again as `cache_read_tokens`. The context budget and the cost meter add the two, so cached tokens counted twice: a 100,000-token prompt with 65,536 cached read as 165,536, crossing the 150k budget and summarising early, and it was charged both rates.
`input_tokens` is now `prompt_tokens` minus the cached tokens, as Anthropic reports it, so every consumer counts each token once.
**Verified:** a new stream test with the reporter's numbers (100,000 prompt, 65,536 cached, 10 output): input 34,464, cache reads 65,536. `cargo test -p craft-anything-ai` (186), clippy and fmt pass.
Fixes#252
`enable_toolset` reports `visible_tools`, the number of tools now on the connection's list. It counted every tool in the enabled toolsets, but in Clean room `tools/list` leaves out the tools Clean room refuses. So enabling all toolsets said 299 while 86 were listed.
The count and the list now use one check (`listed`): a tool's toolset is shown and, in Clean room, Clean room runs it. Deep mode counts are unchanged, and the two meta-tools are still left out of the count.
**Verified:** a new MCP test in a Clean room session enables all toolsets and checks the count against the tools Clean room runs. It fails without the fix (299 against 86). `cargo test -p craft-anything-mcp` (16), clippy and fmt pass.
Fixes#263
Selecting a macOS app folder is recognised as an app bundle (its root-relative Contents/Info.plist), and nested XPC services appear as services and xpc-service graph nodes.
Fixes#282
The builder's approval check refused dev servers before looking at the safe build list. `npx vite build` is on that list, but it also starts with `npx vite` (a dev server), so it was refused as one, whatever the mode or Auto setting.
A listed build is no longer read as a dev server: `npx vite build` and `cd app && npx vite build` are allowed, while `npx vite` and `npx vite --host …` are still refused. A command in watch mode (`--watch`) is now refused with the same hint, since it never exits either. That includes a listed build such as `npx vite build --watch`. The safe-list match moved into one helper used by both checks.
**Verified:** new cases in the approval tests (three spellings of the build allowed; two server forms and a watch build refused). The engine's approval tests, clippy and fmt pass.
Fixes#291
When an ACP agent answered `initialize` with an error (for example, not signed in) and kept running, the error was shown and the turn ended. But the agent's session was never started, and nothing started it later. The next message was queued for a session that never came, so the chat stayed busy. Stop cleared the queue without ending the turn, so every later message was refused as "still working".
- A message sent when the start has failed and nothing is under way now starts the handshake again (`initialize`, then the session). Another failure is reported and ends that message's turn like the first.
- Stop now ends the turn of a message still waiting for the session.
**Verified:** a new test of the protocol core: after a failed `initialize`, the next message sends `initialize` again, a second failure ends its turn, and Stop ends a waiting message. A late answer then sends no prompt. `cargo test -p craft-anything-ai` (184), clippy and fmt pass.
**Not verified:** a live run against an agent that refuses `initialize`.
Fixes#271
`core_action_approve` answers a coding agent's permission prompt with `{"behavior":"allow","updatedInput":…}`, echoing the action's input. For a large action, such as a ~96 KiB `Write` under Claude Code's 80 KB response budget, the MCP server replaced that answer with an `over_limit` error, so the decision was lost.
Approval answers are now exempt from the response budget. They are protocol messages, not result data, and their size comes from the input the client itself sent. Every other result is limited as before.
**Verified:** a new MCP test with a 300-byte budget and an allowed `Read` carrying a 4 KiB field: the answer is a single text block with `isError: false` and the full allow decision. `cargo test -p craft-anything-mcp` (16), clippy and fmt pass.
**Not verified:** whether Claude Code applies its own output-token limit to permission-prompt answers. If it does, a very large action still can't be approved through it, but the decision is no longer replaced on our side.
Fixes#253
The engine had its own `write_atomic` (used for `craft.json`, the inbox, reports and session files). Its temp file was named by process only, so two threads saving the same file at once wrote into one temp file. One rename then found it gone ("craft.json: No such file or directory"), and the file could end up with two versions mixed ("trailing characters").
The engine now uses the platform crate's `write_atomic`, which already gave each write its own temp file and kept the file's permissions. It also gains the engine's short retry when Windows briefly can't replace a file another program holds open. There is one atomic writer now; docs/pitfalls.md says so.
**Verified:**
- A new engine test: four threads each save one file 50 times and read it back. Every save succeeds, every read is one whole version, and no temp files are left. It fails against the old engine writer (checked) and passes now.
- The existing "file held open for a moment" test still passes.
- `cargo test -p craft-anything-engine` (397), the platform fs tests, clippy on both crates and fmt.
**Not covered:** two saves that overlap can still each start from what was on disk, so the later one wins for fields that aren't the person's (the person's fields are kept, as before). That's separate from the write itself.
Fixes#273
`AiHub` read the `[ai]` settings with a fallback: when they couldn't be parsed (for example, a price override missing `output_per_mtok`), they counted as empty. Adding a backend, choosing a role, renaming a model, turning a backend on or off or removing one then saved that empty configuration plus the one change over the person's backends, roles, fallbacks and prices.
Every change now reads the `[ai]` section strictly (`config_to_change`). If it can't be read, the change stops with the existing BadSettings error ("The [ai] settings couldn't be read: … Fix or remove the [ai] section in config.toml") and nothing is saved. Adding a backend reads the settings before storing its key, so a refused add stores no key either. Displaying the configuration still falls back as before.
**Verified:** a new test with two backends, a chosen builder and a broken price override. Adding a backend, clearing a role, turning one off and removing one all fail with BadSettings; the settings file stays byte for byte the same, and the refused add stored no key. `cargo test -p craft-anything-ai` (184), clippy and fmt pass.
Fixes#261
The fallback key file (`ai-keys.toml`, used when there's no keychain) was read leniently: a file that couldn't be read or parsed counted as empty. The next save then wrote back only the new key, so a stray quote or a bad byte cost every other key in it, while the save reported success.
Saving and deleting a key now read the file strictly. A missing file is still empty. A file that can't be read, isn't UTF-8 or isn't valid TOML stops the save with "Couldn't read the key file …: <why>" and "Fix or move that file (it was left untouched)". Reading a key (`get`) is lenient as before. After a keychain save, the stale-copy cleanup skips a file it can't read instead of replacing it.
**Verified:** a new test with the reporter's two cases (an unclosed quote, and a trailing invalid byte): the set and the delete both fail and the file stays byte for byte the same; a valid file keeps its other key after a save. `cargo test -p craft-anything-ai keys`, clippy and fmt pass.
Fixes#262
`android_package_show`'s summary, shown in CLI help and the MCP tool list, said "signatures are not verified". The tool does verify them: v2/v3 signatures over the signed data with a recomputed content digest, and v1 PKCS#7 with the per-file digests. Its own `verification_scope` limitation says so. The summary now says signatures and content digests are verified, but not whether the certificate is one you trust.
**Verified:** text only. `cargo xtask tools` leaves docs/tools.md unchanged, and fmt passes.
Fixes#247
An inline `<script>` becomes a module of its own (`index.html#inline-0`). Its summary took its hash from the input file with the same path, and there is no such file, so the hash was always empty. Comparing two maps then read "both empty" as "the same", and an edited inline script counted as unchanged.
- **Inline scripts now get the hash of their own code.**
- **Maps saved before this have no hash for them.** Where either side lacks a hash, the comparison now checks the module's structure fingerprint, size and strings instead.
**Verified:** a new test with a page whose inline script changes from `console.log("A")` to `("B")`:
- the inline module and the page are reported changed, and a map compared with itself has two unchanged modules;
- the same holds for maps with the inline hashes blanked, as saved before this fix.
`cargo test -p craft-anything-analysis` (80), the engine's app tests (31), clippy and fmt pass.
Fixes#236
Pi rejects an empty `mcp.json`, so setup marks Pi as "empty is invalid". The check also ran when the file didn't exist yet: its absent text read as empty, the plan was blocked ("the file is empty, and this app requires valid JSON"), and `setup --all-detected --yes` stopped with it.
A missing file now starts from `{}` and is created with our `mcpServers` entry. An existing empty file is still refused. The preview of a new file shows only the added lines.
**Verified:** a new setup test where Pi's folder exists without `mcp.json`: the plan isn't blocked, apply writes a valid file with our entry, and an empty file is still blocked. `cargo test -p craft-anything-setup` (24), clippy, fmt.
Fixes#238
`adb_screenshot_take`, `adb_bugreport_save`, `adb_file_download` and `adb_app_download` create their fresh `output_directory` first, then check adb and the device. A device that was offline (or a missing adb, or a failed pull) left that folder behind, empty. The retry the error recommends then failed with "already exists", without ever reaching adb.
These tools now hold the folder in a small guard (`UnlessWritten`, next to `create_absent_dir`). When the tool stops, the guard removes the folder only if nothing was written into it, so partial pulls stay. On success the tool keeps the folder.
**Verified:**
- A new test: the guard removes an empty folder, keeps one with a file in it and keeps one marked kept.
- The same test calls all four tools through the engine with adb missing: no folder is left, and the identical retry reaches the same adb error rather than "already exists". That half runs only where adb isn't installed, like the other adb tests; it was skipped here because adb is on this machine.
- `cargo test -p craft-anything-engine adb`, clippy, fmt.
**Not changed:** the website, web, firmware and packed tools create output folders the same way. They can take the guard if needed.
Fixes#234
`adb_app_show` treated `dumpsys package <name>` output as describing the package if it merely contained the name. Android's answer for a missing package, `Unable to find package: <name>`, contains the name, so the tool returned successful evidence with no fields and complete coverage.
It now requires that package's own `Package [<name>]` block and otherwise returns the existing `NotFound` error with the "List installed packages with adb_apps_list." remediation.
**Verified:** a unit test with Android's missing-package reply, another package's block and an installed package; `cargo test -p craft-anything-engine adb`, clippy, fmt.
**Not verified:** a run against a real device.
Fixes#235
On a timeout with a grace period, a capture sent SIGTERM to the group, then waited only for the leader. When the leader exited, it skipped the SIGKILL, so a helper in the group that ignores SIGTERM kept running after the capture returned (reported as `still_running` / `incomplete`).
Now the grace period waits for the whole group to be gone, reaping the leader as it goes so a zombie doesn't count. Whatever is left afterwards gets SIGKILL. `settle` uses the same group check.
**Verified:**
- A new Linux/macOS test with the reporter's helper (ignores TERM and HUP, outlives the leader): the capture must settle, verify cleanup and report SIGKILL.
- `cargo clippy -p craft-anything-platform --all-targets --target x86_64-unknown-linux-gnu -D warnings`, fmt.
**Not verified locally:** this machine is Windows without WSL, so the new test first runs in CI (Ubuntu). macOS isn't run; it uses the same code.
Fixes#230
At the smallest window the app allows (760×520), the first-run "What brings you here?" step was taller than the window and had nothing to scroll, so its Continue button sat below the window, out of reach.
The first-run steps now sit in a scroll area that stops above the footer (Back and the step counter). When a step fits, nothing changes; when it doesn't, it scrolls.
**Verified:**
- A new smoke test at 760×520 scrolls Continue into view above the footer and clicks it through to the last step. Without the fix, Continue is drawn at y=646 in a 520-high window and the test fails.
- `cargo test -p craft-anything-ui-egui` (all suites), clippy, fmt.
- Rendered the first-run screens at the default size (unchanged, no scroll bar) and at 760×520 (scrolls, ends above the footer).
Fixes#231
ELF files whose section headers were removed (common in NAS and router firmware) now show their functions, imports and exports. The dynamic symbol table is found through PT_DYNAMIC (DT_SYMTAB/DT_STRTAB, counted from DT_HASH, then DT_GNU_HASH, then an estimate with a warning), shared with the Flutter reader. The prologue scan walks executable segments when there are no sections.
Fixes#223
The pseudo-Java for a method with one try/catch rendered the protected region, the handler and what follows, but never the statements before the `try`. So `v0 = 7; try { v1 = 2; } catch (RuntimeException e) {} return v0;` came out without `v0 = 7;`.
- Those statements are now rendered first. A branch from them into the try region falls back to a labelled `goto`, as elsewhere, so the method is no longer marked fully structured.
- The single-handler wrap is now refused when anything sits between the try's closing `goto` and the handler. That code would have been dropped the same way; such methods now get the plain rendering with the "exception handler(s) in the table" note.
**Verified:** a test with the reporter's 152-byte class (it fails without the fix); `cargo test -p craft-anything-analysis` (79), the engine's Java tool tests (9), clippy, fmt.
**Not checked:** `cargo xtask wasm`. The change is pure logic inside an existing function.
Fixes#232
A runtime capture compared a file's size, type and hash. When the file was over the hashing budget (or couldn't be read), neither snapshot had a hash, so a rewrite at the same size was reported as `unchanged`. Now:
- such a file is `unknown`;
- a file is `modified` only when its size or type changed, or both hashes exist and differ;
- a symlink pointed somewhere else with a target of the same length is `modified` (only the target's length was compared before).
**Verified:** a new unit test of `effects` (unhashed on either side, equal and different hashes, a size change, symlink retargets); `cargo test -p craft-anything-platform` (47 passed), clippy, fmt. The test runs on every OS, unlike the shell-based capture tests.
**Not verified:** a live run of the issue's capture probe on macOS.
Fixes#229
Hermes Agent gets its own adapter: it runs as `hermes [-m <model>] [args] -z <prompt>`, never falls back to a plain `hermes <prompt>` (which opens its interactive session), and can run over ACP (`hermes acp`) when ACP is turned on.
A request for a plain HTML and CSS site ("no frameworks", "no build step", "static html") now picks a Plain HTML and CSS stack: index.html and a stylesheet in app/, with nothing to build. Before, the request named no stack and the builder chose Rust + egui.
Fixes#161
A local build by Claude Code, Codex or an ACP agent now checks that craft-anything-cli starts before studying or starting the agent. When it is missing or won't start, the build stops with a plain message and what to do, instead of Claude Code's "MCP tool not found" error. A check that times out lets the build go ahead, and stopping the build ends the check.
Refs #91, #64, #113.
A paused, stopped or failed build only offered Continue or Try again; there was no way to say "enough now". project_build_finish marks the project Ready and adds a Done step to its conversation, keeping everything; the project screen shows It's done beside Continue and Try again when nothing is running.
Fixes#210
web_site_open keeps its browser for one session, and each CLI command is a session of its own that closes its browser when it ends. From the CLI, a capture with no open site now says so, and to run the steps together in an MCP client, the app or rebuild. Documented in tool-status.
Fixes#153
project_play's help says that on Windows a started program inherits the caller's output pipe (#207), and the reproducing test is kept in platform as an ignored test.
Every computed colour was painted on a 1x1 canvas and read back; Brave adds noise to canvas reads, so colours came back one unit off per channel (#F5F2EC as #f4f2ed). Computed rgb()/rgba() values are now read as numbers, and only other notations go through the canvas.
Fixes#154
checked_shl only rejects shift amounts of 64 or more, so payload bits shifted out of the u64 on the tenth byte were dropped; uleb128 now returns None when bits would be lost, and u64::MAX still decodes.
Each method entry's address and its fields' addresses are computed with checked arithmetic, so a method list near the end of the address space ends the list instead of panicking (debug) or reading a wrapped address (release).
An A32 LDRD/STRD with Rt=15 (e.g. 0xc0c7fef3) made the decoder library panic on the nonexistent r16; such a word now decodes as no instruction, before the library sees it.
When the check of codex exec --help didn't find --json, the run fell back to the plain adapter, codex <prompt>, which is Codex's interactive TUI and fails with "stdin is not a terminal". An adapter can now say its plain form doesn't work; Codex keeps its own adapter and the run says the check couldn't confirm the flag.
Fixes#199
Reads Linux registrations from ~/.config/Epic/UnrealEngine/Install.ini, honours a valid UE_ROOT, takes the version from Build.version, and counts the selected engine's own bundled Clang as its C++ tools.
Chrome reports Tailwind v4's rounded-full as its largest length, 3.35544e+07px; the capture now records any corner above 9999px as 9999px and keeps the other corners.
Fixes#155
A visitor's view of a site no longer keeps the page's class names (outline items and unnamed-control examples) or the query strings and fragments on image and link addresses, which can carry deployment ids and tokens.
Fixes#156
Adds an Antigravity adapter (stream-json output, resume by conversation, builder permissions) and lets setup and the doctor connect and check it under `antigravity` or `agy`.
web_site_capture checks where the page is first and refuses, without making the output folder, when the browser is on its own error page or a blank one after a failed open. Fixes#158.
Claude Code's "does not support this model" error says Claude Code is too old, with the fix (claude update for the command-line tool, or another model in Settings → AI), and isn't retried. Fixes#165.