mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
4419c674cf
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
1155 lines
87 KiB
Markdown
1155 lines
87 KiB
Markdown
# Roadmap
|
||
|
||
The shared plan for reaching the product described in the README, for people and agents alike.
|
||
`docs/STATUS.md` says what works today; this file says what comes next, who owns it and what an
|
||
agent may pick up. Drafted 2026-10-18 from a survey of `main` at `1d47a3e` (624 tests passing);
|
||
brought up to date on 2026-10-22.
|
||
|
||
## How to use this file
|
||
|
||
- Each item has an **id** (`R<milestone>.<n>`), an **owner**, a **status** and a list of
|
||
**pending changes**. Pending changes are small, independently mergeable units.
|
||
- **Claiming:** before you start, put `— claimed: <name>, branch <branch>` on the pending change
|
||
and push your branch early (or open a draft PR) so others see it. Don't take a change someone
|
||
else has claimed. The full working rules are in `AGENTS.md` ("Working rules", mandatory). Items owned by **maintainers** are steered by the
|
||
project maintainers: agents may take their *unclaimed pending changes*, but design decisions
|
||
marked **decision** wait for a maintainer.
|
||
- **Finishing:** tick the change (`[x]`), add the commit or PR, update `docs/STATUS.md` if what
|
||
works changed, and add a line to `log/devlog.md`.
|
||
- **Status:** `todo` · `in progress` · `blocked (<why>)` · `done`.
|
||
- Follow `AGENTS.md` (layering, never crash, everything is a tool, `platform::process` for every
|
||
external program) and the architecture rules below.
|
||
|
||
## Architecture rules for new work
|
||
|
||
The project should be easy to fork and bend. New features land behind a seam, not inside a
|
||
`match`:
|
||
|
||
1. **Traits plus a registry, not enums.** Target kinds, checks, builders, asset providers and
|
||
external engines are traits with a registry that holds built-in implementations. Adding one
|
||
means adding a file and one registration line (as tools already work).
|
||
2. **Data over code for catalogues.** Stacks, game-engine build recipes, model ids and prices,
|
||
and engine install pins live in typed tables (or TOML files embedded with `include_str!`), not
|
||
in string matches or prompt text.
|
||
3. **Headless first; flows are tools.** Everything works without the GUI. Anything a person
|
||
can start from the UI (rebuild, follow-ups, answering questions, engine installs, launching a
|
||
result) is reachable through `Engine::call` and the CLI, so MCP, agents, scripts and other
|
||
frontends get it for free. Orchestration, persistence and process control never live in
|
||
`ui-egui` or `apps/*`.
|
||
4. **Versioned files.** Every file we write and read back (`craft.json`, `config.toml`,
|
||
`project.toml`) carries a version, has a JSON schema, and has a migration path.
|
||
5. **Structured results.** Decisions such as "did the build pass" read typed fields, never
|
||
English text prefixes.
|
||
|
||
## Milestones at a glance
|
||
|
||
| Milestone | Goal | Owner | Where it stands (2026-10-22) |
|
||
|---|---|---|---|
|
||
| M0 Foundations | Docs match the code, licences checked, rule violations fixed, one live end-to-end run | maintainers | Done; protecting `main` is a maintainer setting still to switch on |
|
||
| M1 Extensibility | Seams for target kinds, checks, builders, asset providers, engines, plugins | maintainers | Done except runtime and behaviour checks (with R2.4, R2.5) and engine transports (R1.5) |
|
||
| M2 Product completeness | Every target kind gets study → build → check → refine with a real check; building works without an agent app | maintainers | Websites complete; apps and games build and run; API-key builds and other agents are built but not yet run live |
|
||
| M3 Engine depth | More tools, and the known partial ones | open | 284 tools; device-backed tools wait for an Android device; game asset export continues |
|
||
| M4 Release | Packaging, CI, signing, managed tier | maintainers (later) | CI passes; release workflows for every platform are on `main`; v0.1.0 is released, signed, on every platform |
|
||
|
||
## Open work
|
||
|
||
Every unticked `- [ ]` line in this file is open work, under the item it belongs to.
|
||
`cargo xtask pending` lists them all, grouped by item (`--count` for the totals). When you find
|
||
work that's missing, add it as a `- [ ]` line under the right item in the same PR, rather than
|
||
leaving it in a PR description or a chat.
|
||
|
||
## Order and dependencies
|
||
|
||
Work in phases. Items in the same phase can run in parallel. An item starts only when everything
|
||
in its **Depends on** column is done; a *soft* dependency should come first but doesn't block.
|
||
M3 is an open lane that can run at any time.
|
||
|
||
| Phase | Item | Depends on | Why here |
|
||
|---|---|---|---|
|
||
| 0 Baseline | R0.1 Docs match `main` | — | Every agent reads them |
|
||
| 0 | R0.7 Live end-to-end run | — (needs a signed-in `claude`) | A working reference before refactors |
|
||
| 0 | R0.3 Licence hygiene | — | Cheap; parallel |
|
||
| 0 | R0.4 Rule-10 fixes | — | Clears direct process calls before R1.6 moves them |
|
||
| 0 | R0.5 Small correctness items | — | Parallel |
|
||
| 0 | R0.2 Salvage PR #1 | **decision** | Unblocks M4 later |
|
||
| 0 | R0.8 Tests never touch the real machine | — | Found when a test started a real download |
|
||
| 0 | R0.6 Tool naming | **decision** | Fixes ids before new tools (R1.6) and plugins (R1.8) |
|
||
| 1 Seams | R1.1 `TargetKind`, split `rebuild.rs` | R0.7 *(soft)* | Most scattered `match` code; everything in M2 builds on it |
|
||
| 1 | R1.2 `Check` trait | R1.1 | Checks hang off the target kind |
|
||
| 1 | R1.3 Builder adapter registry | — | Independent of R1.1; parallel |
|
||
| 1 | R1.4 Asset provider trait | — | Parallel |
|
||
| 1 | R1.5 External engine trait in L3 | — | Engine installs in R1.6 sit on it |
|
||
| 1 | R1.9 Versioned files | — | The project service in R1.6 writes new files |
|
||
| 2 Headless | R1.6 GUI stops being the glue | R1.1, R1.5, R1.9, R0.4, R0.6 | Everything after it is reachable without the GUI |
|
||
| 3 Product | R2.1 Building with API keys | R1.3, R1.6 | Removes the agent-app requirement |
|
||
| 3 | R2.2 More coding agents | R1.3 | |
|
||
| 3 | R2.3 Website coverage, better metric | R1.2 | |
|
||
| 3 | R2.6 Several rebuilds at once | R1.6 | |
|
||
| 3 | R2.7 The request decides how to build | — | A request that names a stack was ignored |
|
||
| 3 | R2.8 Clean room means clean room | — | Clean room still read the original's code and files |
|
||
| 3 | R2.9 Everyday use: typing, projects, settings | — | Found by live use of the app |
|
||
| 3 | R2.10 Pinned builds | — | Keep a build to open while the project changes |
|
||
| 4 Depth | R2.4 Runtime study for apps and games | R1.2, R1.6 | |
|
||
| 4 | R2.5 Behaviour checks for non-web builds | R1.2 | |
|
||
| 4 | R1.7 Dynamic tool registry | R1.6 | |
|
||
| 4 | R1.8 Plugins | R1.7, R0.6 | Freezes the extension API, so it comes last |
|
||
| Any | R3.1–R3.10 Engine depth, game archives and assets, app facts | — | Open lane |
|
||
| Last | M4 Release | R0.2, R0.3 and phase 3 | |
|
||
|
||
---
|
||
|
||
## M0 Foundations
|
||
|
||
### R0.1 Bring the docs up to date with `main`
|
||
Owner: maintainers · Status: done (branch `docs/r0.1-status-refresh`)
|
||
STATUS.md is dated 2026-10-16 (branch `feat/simple-mode-mvp`). It doesn't cover games, engine
|
||
detection and builds, fal.ai assets, Deep/Auto mode or the long-run limits.
|
||
- [x] Refresh STATUS.md: games, Deep mode with Ghidra, Auto mode, asset budget, current limits
|
||
- [x] ATTRIBUTION.md lists the third-party material the repository ships
|
||
- [x] AGENTS.md: `log/` is gitignored; say the devlog is local, or track it
|
||
- [x] Regenerate the tool list (asset tools); README tool count (228); claude-code.md rounds
|
||
|
||
### R0.2 Salvage PR #1 (`ci-release`)
|
||
Owner: maintainers · Status: done (branch `chore/r0.2-release-pipeline`, supersedes #1)
|
||
The PR branches from the initial commit, about 35 commits behind `main`. CI isn't a priority, but
|
||
its packaging, `cargo xtask version` and web viewer are worth keeping.
|
||
- [x] **decision**: rebase completely onto `main` (all six commits kept, `ci-release` untouched)
|
||
- [x] Port `apps/craft-anything-web` (wasm viewer) and `cargo xtask version`
|
||
- [x] Port the platform fixes: capture is Linux and macOS only (FreeBSD loses output); the spawn
|
||
lock was reverted inside the PR itself
|
||
- [x] Keep workflows parked until M4: manual-only (`workflow_dispatch`); `release.yml` unchanged
|
||
|
||
### R0.3 Licence hygiene
|
||
Owner: open · Status: done (branch `chore/r0.3-licences`)
|
||
No `deny.toml` exists. The survey found no GPL, AGPL or SSPL crates, and only MPL-2.0
|
||
`option-ext` (via `dirs`) in the shipped app.
|
||
- [x] Add `deny.toml` (allow MIT, Apache-2.0, BSD-2/3, ISC, Zlib, 0BSD, Unicode-3.0, BSL-1.0,
|
||
CDLA-Permissive-2.0, OFL-1.1, MPL-2.0 as an explicit exception), and `cargo xtask licenses`
|
||
- [x] Generate a third-party licence bundle (`cargo about`) for releases (`about.toml`; first run at M4)
|
||
- [x] NOTICE/ATTRIBUTION: egui's embedded default fonts (OFL-1.1, Ubuntu Font Licence)
|
||
- [x] Policy in this file: external programs we *run* (Ghidra, rizin, tshark, Frida, JADX) may
|
||
have any licence. We never link or bundle copyleft code. (Lives in `docs/licences.md`.)
|
||
- [x] Follow-ups from the first `cargo deny check`: rustls 0.23.46; ruzstd 0.9.1 (a tiny `.zst`
|
||
could exhaust memory); `publish = false` in every workspace crate; `cargo deny check` passes
|
||
(advisories, bans, licences, sources)
|
||
- [ ] Revisit the two advisory exceptions in `deny.toml`: `rsa` (no fix; we only verify with public
|
||
keys) when rsa 0.10 lands, and unmaintained `zbus_names`/`zvariant` (Linux-only via keyring
|
||
and accesskit) when those move on
|
||
- [x] `crates/engines/src/game.rs` (installer, PowerShell signature check: no deadline)
|
||
- [x] `crates/ui-egui/src/project.rs`, `crates/ui-egui/src/engine_ui.rs`
|
||
- [x] Add a check to `cargo xtask layers` that fails on `std::process::Command` outside `platform`
|
||
|
||
### R0.5 Small correctness items from the survey
|
||
Owner: open · Status: done (branch chore/r0.5-cleanups)
|
||
- [x] Checked arithmetic on file offsets in `elf.rs`: `va_to_off` adds a segment's file offset to an
|
||
address delta unchecked, so a `PT_LOAD` at offset `u64::MAX` overflows, found by a lint scan
|
||
of `crates/formats` (#43)
|
||
- [x] Capped allocation in `unreal_iostore.rs`: a `.utoc` header naming 2^31 compression methods of
|
||
length 0 collected empty names until memory ran out (6 GiB from a 559-byte file); only the
|
||
255 a block can point at are read, the rest skipped. Found by mutation fuzzing of `crates/formats`
|
||
- [x] No endless loop on code at the top of the address space in `disasm/src/program.rs`: the
|
||
prologue and PLT scans stepped past `u64::MAX`, which in release wrapped to 0 and walked the
|
||
whole 64-bit space (`native_functions_list` on a 4 KB ELF whose `.text` is at
|
||
`0xffffffffffffffc0` never returned, and Stop couldn't end it). Found by mutation fuzzing of `crates/disasm`
|
||
- [x] The Agent console keeps its size: its history and prompt box came out 17 points taller than
|
||
the bottom panel, which grew by that every frame until its maximum and then cut off the top
|
||
of the Agent / Jobs / Log tabs (`chat::conversation`)
|
||
- [x] `yaxpeax-arm` 0.5.0 panicked ("register number out of range") on an A32 `STRD`/`LDRD` with
|
||
Rt = 15 (e.g. the word `0xc0c7fef3`), so one such word failed the whole analysis of a 32-bit
|
||
ARM program; the word now decodes as invalid
|
||
- [x] Unchecked `o + 4`/`o + 8` on section and program header offsets in `formats/src/dart.rs`
|
||
(`Elf::parse`): a header table near `usize::MAX` panicked in debug builds (in release the
|
||
wrapped reads were thrown away, since the entry's first field was already out of range); an
|
||
entry that starts past the end of the file is now skipped
|
||
- [x] `cargo xtask layers` also scans `apps/`
|
||
- [x] Engines in `KNOWN` without a `ToolPaths` override (tshark, objdump, rizin, java)
|
||
- [x] Missing `database` icon for SQLite in the UI (`kind_icon`)
|
||
- [x] Deduplicate the five private `fn spec(` helpers in the tool modules
|
||
- [x] rustfmt drift on main (11 files)
|
||
- [x] Checked arithmetic on file offsets in `fsread/extfs.rs`: an inode offset near `usize::MAX`
|
||
overflows `o + 128` (two sites), found by a lint scan of `crates/formats` (#42)
|
||
- [x] `macho.rs` `method_list`: `list + 8 + i * entsize` adds to a pointer read from the file, so a
|
||
segment that ends at `u64::MAX` overflowed it (found by a lint scan; now reproduced by a
|
||
regression test, and the entry addresses are checked)
|
||
|
||
### R0.8 Tests never touch the real machine
|
||
Owner: open · Status: done, except protecting `main` (a maintainer setting)
|
||
`Session::default()` points at the real config folder and engine store, so a test can read, write
|
||
or download into them.
|
||
- [x] `Session::isolated(dir)` and `engine::testing::session()`: config, plugin caches, engine store
|
||
and projects in a temp folder; `settings_set` and plugins follow `Session::config_dir()`
|
||
- [x] Every test uses it (41 files); `cargo xtask layers` fails on `Session::default()` anywhere
|
||
- [ ] Main maintainers: protect `main` (PRs required, squash only)
|
||
|
||
### R0.6 Tool naming
|
||
Owner: maintainers · Status: done
|
||
Craft Anything's own ids, grouped by what a person does (`native_functions_list`,
|
||
`web_page_capture`…). Each tool answers to its id (and CLI spelling) only.
|
||
- [x] Id scheme in `docs/tool-ids.md`: `<area>_<object>_<action>` (no dots: MCP and model APIs
|
||
allow only letters, digits, `_` and `-` in tool names)
|
||
- [x] A test keeps every id in the scheme
|
||
- [x] **decision** (2026-10-20): rename properly, before launch
|
||
- [x] Code decides on a tool's own id and declared effects, never its name as text
|
||
(`Registry::canonical`, `Effects::CARRIES_TARGET_CONTENT`, id constants next to their tools)
|
||
- [x] Renamed at the source: 227 tools get their own ids and a duplicate install tool is retired;
|
||
text naming tools uses the ids; each tool's Rust function has its id as its name
|
||
- [x] Earlier names retired before launch: tools, MCP prompts and their arguments answer only to
|
||
their own names (2026-10-22)
|
||
- [x] New tools (R1.6 onwards) use the new scheme from the start (`project_*`, `engines_*`,
|
||
`settings_*`, `rebuild_*`, `build_*`, `plugins_*`)
|
||
- [x] A second naming pass before launch (2026-10-22): 81 tool ids and two MCP prompts get plainer
|
||
names (`adb_apps_list`, `native_function_study`, `web_walkthrough_record`…), tool inputs say
|
||
`function` instead of `procedure`, the error codes are regrouped (`bad_input`, `not_found`,
|
||
`engine_missing`, `over_limit`…; categories say what to do: `input`, `engine`, `missing`…),
|
||
and the rebuild ledger uses its own words (strengths `unit-test` … `outside-check`, cases
|
||
`works`, `refuses`, `bad-input`, `cancel`, `shutdown`)
|
||
|
||
### R0.7 One live end-to-end rebuild, kept as an opt-in test
|
||
Owner: maintainers · Status: done (branch `test/r0.7-live-e2e`; runs on 2026-10-18, Windows 10)
|
||
- [x] `craft-anything-cli rebuild https://example.com "Clone this website" --rounds 1` completes:
|
||
Ready in 1.5 min, 96% alike (98% at 1440 px, 94% at 390 px)
|
||
- [x] App target: a small desktop app → Rust + egui, Ready in 9 min (checked by "does it build" only)
|
||
- [x] Game target: Muck → Rust reached Build (game written and compiled, the builder screenshotting
|
||
its own result) and was stopped on purpose before Check; a polished game isn't the goal.
|
||
It first failed at once because "in Rust" was ignored (fixed in #4)
|
||
- [x] Opt-in test (`CRAFT_E2E=1`) that runs the website case
|
||
(`apps/craft-anything-cli/tests/live_rebuild.rs`)
|
||
|
||
---
|
||
|
||
## M1 Extensibility
|
||
|
||
### R1.1 `TargetKind` trait; split `rebuild.rs`
|
||
Owner: maintainers · Status: done
|
||
Today `enum Kind {Website, Game, App}` is matched in about ten places across `ai/src/rebuild.rs`
|
||
(1.5k lines) and the UI.
|
||
- [x] **decision** (approved 2026-10-18): `TargetKind` (`id`, `words`, `detect` → score,
|
||
`default_stack`, `named_stack`, `goal`, `brief_sections`, `checks`, `refine`…) and `Check`
|
||
(`id`, `run(CheckCx)` → `CheckResult`) as trait objects in a `KindRegistry`
|
||
- [x] Split `rebuild.rs` into `rebuild/{mod,kind,study,check,brief,locate,output,tests}.rs` with
|
||
no change in behaviour (branch `refactor/r1.1-split-rebuild`)
|
||
- [x] Website, App and Game become implementations (`rebuild/kinds/`); `RunOptions::kinds` holds
|
||
the registry; briefs proven byte-identical to before across 162 combinations
|
||
- [x] The UI reads labels from the trait (`CraftApp::kinds`); the `Kind` enum, `choose` and
|
||
`kind_of` are gone
|
||
- [x] Data table for stacks: `rebuild/stacks.toml` (steps, phrases that ask for a stack, engine),
|
||
read by `rebuild::stacks`; game engines' own names stay in `engines::game::Builder`
|
||
|
||
### R1.2 `Check` (comparator) trait with structured results
|
||
Owner: maintainers · Status: in progress (runtime-visual and behaviour checks: R2.4, R2.5)
|
||
- [x] `CheckResult { check, passed, similarity, note, artifacts }` replaces
|
||
`note.starts_with("Build succeeded")`; older `craft.json` checks are read compatibly
|
||
- [x] Built-in checks: `BuildCheck` (per engine recipe) and `WebVisualCheck` (per width)
|
||
- [x] Web routes (R2.3: every captured page is compared)
|
||
- [ ] Later checks: runtime-visual and behaviour (R2.4, R2.5)
|
||
- [x] Widths, thresholds and rounds come from settings (`[build]`: `good_enough`, `max_rounds`,
|
||
`web_widths`, `max_pages`, `picture_good_enough`); a check may carry its own bar
|
||
(`CheckResult::good_enough`)
|
||
- [x] The builder tests its own work during its turn, for every kind: `rebuild_check` asks the
|
||
running build (through `checks/asks/`) to run the same checks as round 0, not counted as a
|
||
round; refusals for an in-process API builder, whose tools hold the engine
|
||
- [x] What a check couldn't compare is kept (`CheckResult::unknowns`) and keeps it from being good
|
||
enough: missing evidence never counts as alike
|
||
- [x] Claims: a check lists what the build must do, each judged on its own (`CheckResult::claims`);
|
||
one that fails keeps it from being good enough. Websites claim per page: no sideways scroll
|
||
where the original has none, no new errors, every file loads, the original's landmarks; and
|
||
that each of the original's menus and pop-ups opens (`page_claims`)
|
||
- [x] A windowed app pictured by runtime study claims each of the original's interactive controls
|
||
(buttons, menus, tabs, check boxes, fields, links; `screenshot::INTERACTIVE`) by label; text
|
||
labels stay advisory, since they may hold content the builder replaces
|
||
- [x] Apps and games make claims too: each self-test step of a game (the core loop's, an untested
|
||
one failing, then the game's own) and each number from the original in the mechanics sheet
|
||
being used in the code
|
||
- [x] Console apps: with runtime study on, a console original (PE console subsystem) is run with each
|
||
input in `runs::CASES` (no arguments, --help, --version) through `runtime_walkthrough_record` and
|
||
kept in capture/runs/; `RunsCheck` runs the rebuild the same way and claims, per input, the same
|
||
exit and the same output (80% of the words, the programs' own names set aside); the brief asks
|
||
for a console program instead of a picture
|
||
- [x] Console originals on Linux and macOS: an ELF or Mach-O executable that loads no windowing library
|
||
(`runs::WINDOW_LIBRARIES`) is taken for one; on every platform, a program that sat silent and
|
||
open through every input is treated as windowed after all and pictured instead (`ran_like_console`)
|
||
- [x] The brief is the builder's own loop for every kind: study overview first, focused follow-ups
|
||
with the tools, `app/UNKNOWNS.md`, build, `rebuild_check` until it would pass, then review the
|
||
checklist and unknowns before replying
|
||
- [x] The builder's open questions are unknown records (`core_unknown_add` / `core_unknown_edit` /
|
||
`core_unknowns_show` in the project's investigation folder) instead of a markdown file; its own
|
||
check lists the open ones and the finished run says how many are left
|
||
- [x] The brief says which toolsets to turn on (`enable_toolset`: browser, javascript, native,
|
||
managed, formats) and, for websites, how to study behaviour with the web debugging tools
|
||
(event listeners, execution, scripted walkthroughs, exported scripts), never pasting the code
|
||
- [x] A console original is rebuilt with the "Rust (console)" stack by default (`AppKind::default_stack`
|
||
reads the file); live: a small console tool rebuilt end to end, the builder used `rebuild_check`
|
||
and unknown records, and all 6 run claims held
|
||
- [x] Live run where a coding agent uses `rebuild_check` mid-turn on a game: a request-only snake game
|
||
in Rust passed build, picture, mechanics and all 5 self-test claims (2 steps of its own)
|
||
- [x] A round's verdict is one line over all its checks and claims (`self_check::summary`), in the
|
||
builder's own check and the finished run's status, not the last check's note
|
||
|
||
### R1.3 Builder adapter registry
|
||
Owner: maintainers · Status: in progress (Codex resume needs a real install)
|
||
- [x] `AgentAdapter` trait (`ai::cli_agents::adapters`): ids, args, required flags, stream
|
||
parsing, resume, process setup, sign-in preflight; `AgentAdapters` registry in
|
||
`CliSpec`/`RunOptions`; unknown agents get the plain adapter. The rebuild's Claude-only
|
||
sign-in check now asks whichever adapter runs
|
||
- [x] ACP evaluated: it's a long-lived, session-based JSON-RPC protocol, so it fits as another
|
||
`Conversation` implementation (an `AcpConversation` beside `CliConversation`), not as a
|
||
per-turn `AgentAdapter`. Claude Code stays native (unmodified binary, documented flags).
|
||
Follow-up: R2.2
|
||
- [ ] Codex: resume support (`codex exec resume <id>`), verified on a real install
|
||
|
||
### R1.4 Asset provider trait
|
||
Owner: open · Status: done (branch `feature/r1.4-asset-providers`)
|
||
- [x] `AssetProvider` (image, 3D, later audio) with a cost estimate; fal becomes one impl
|
||
- [x] Model ids and prices in a table; prompts read prices from it (today `assets_note` repeats
|
||
"$0.53")
|
||
- [x] Default budget as a setting (each project stores its own, but the default is the constant
|
||
`ASSET_BUDGET_USD`)
|
||
- [x] Follow-ups from R1.4: Settings → Builds sets the default budget and the picture model; the
|
||
app's "raise budget" adds the setting through `project_assets_budget`; the tally no longer
|
||
shows "$-0.00"; the app's price text comes from the asset table
|
||
- [x] Each asset provider its own key: `assets:<id>` in the key store, `AssetProvider::key_env`,
|
||
`AssetKeys` in the builder's tool server, `provider` on the `assets_key_*` tools
|
||
- [x] Settings → AI shows a key field for each asset provider (`AssetProvider::keys_page` gives
|
||
its "Get a key" link; the app keeps which providers have a key, by id)
|
||
- [x] Check the FLUX schnell price in `assets.toml` against fal's current list: FLUX schnell
|
||
$0.003/MP, FLUX dev $0.025/MP, Tripo P1 $0.40 / $0.50 textured all match (2026-10-19)
|
||
- [x] Game assets are the person's choice: a per-project opt-in (`generate_assets` in craft.json,
|
||
the switch on Make and on the project, `project_assets_allow`, `rebuild --generate-assets`
|
||
and `rebuild_start`'s `generate_assets` for the CLI and MCP) that the brief and the asset
|
||
tools need along with a key; turning it on without a fal.ai key asks for one; Nano Banana Pro (`fal-ai/nano-banana-pro`, $0.15 a picture, checked 2026-10-20) is the
|
||
default picture model, and the table prices per picture or per megapixel
|
||
- [x] A run no longer overwrites what the person changed while it builds: the fields they own
|
||
(asset budget, generated assets, always-allowed actions) change only through
|
||
`Workspace::set_person`, and every other save keeps them as they are on disk. The app's
|
||
asset switch works during a build again
|
||
- [x] A resumed builder hears the asset choice as it is now: `TargetKind::follow_up_sections`; a
|
||
game's follow-up carries the asset note (budget, spent) when assets are on with a key, or
|
||
says they're now off when some were made before
|
||
- [ ] Run Nano Banana Pro once with a real key (a picture, then a 3D model from it) and check the
|
||
picture's size and the invoice ($0.15)
|
||
- [ ] fal bills pictures "by rounding up to the nearest megapixel"; estimates use the exact size.
|
||
Confirm on a real invoice whether a 1024×1024 picture bills as 1 or 2 megapixels, then round
|
||
in the table (needs a key)
|
||
|
||
### R1.5 External engine trait in L3
|
||
Owner: open · Status: in progress (branch `feature/r1.5-engine-table`: table and pins done; transports pending)
|
||
- [x] One engine table drives `KNOWN`, `ToolPaths`, install pins and the Engines settings page
|
||
(`engines::ENGINES` in `engines/src/table.rs`; a test keeps `tool_paths!` identical to it)
|
||
- [x] Install pins: SHA-256 for every download; tar.gz/tar.xz (TODOs in `install.rs`)
|
||
(adb r37.0.1 on all three desktops, JADX 1.5.6, Ghidra and its Windows Java; tars unpack
|
||
in memory up to 512 MB)
|
||
|
||
Pending changes:
|
||
- [x] Move the CDP client from `engine/src/tools` (L4) to `engines` (L3) (`engines::cdp`; `CancelToken`
|
||
now lives in `platform::cancel`, re-exported from the engine)
|
||
- [x] Move the GDB and JADX transports from `engine/src/tools` (L4) to `engines` (L3) (`engines::gdb`:
|
||
the MI session; `engines::jadx`: the single-class run)
|
||
- [x] Single-file pins (`ArchiveFormat::File { name }`, found by `locate` under that name in the
|
||
engine store); Vineflower 1.12.0 is pinned with it (Maven Central's SHA-256, the same as the
|
||
GitHub asset digest), and Apktool 3.0.3 (GitHub asset digest); a located `.jar` runs with the
|
||
configured Java (`external_disasm_tools::program_for`)
|
||
- [x] Temurin Java 21 pinned for Linux and macOS (x64 and arm64; Adoptium's `.sha256.txt`, the same
|
||
as the GitHub digests); macOS archives are stripped to `Contents/Home`, so Ghidra's setup
|
||
(`ghidra::get::can_install`) now works there too
|
||
- [ ] Ghidra setup tried for real on Linux and macOS (the Java pins were checked by tests only)
|
||
- [x] Rizin 0.9.1 pinned for Windows x64 (the static zip; GitHub asset digest), installed for real
|
||
by the opt-in `a_pinned_engine_installs_for_real` (`CRAFT_ANYTHING_TEST_INSTALL`)
|
||
- [ ] More pins once their hashes are checked against the publisher: Chromium (no published
|
||
checksums found yet), Rizin for Linux (the tar.xz unpacks to ~800 MB, so it needs streaming
|
||
extraction) and macOS (a .pkg)
|
||
|
||
### R1.6 Headless first: the GUI stops being the glue
|
||
Owner: maintainers · Status: done
|
||
The rebuild run itself lives in `ai::rebuild` and the CLI uses it, but the GUI still owns parts of
|
||
the product that a headless run doesn't get:
|
||
- **Project feed:** `feed.json` is written and rebuilt only by `ui-egui/src/project.rs`
|
||
(`load_feed`, `save_feed`, `history`), so a CLI run leaves no feed and the app can't show it.
|
||
- **Game engine and Ghidra setup:** inventory, downloads and guided installs run from
|
||
`ui-egui/src/engine_ui.rs` (calling `engines::game` and `engines::ghidra` directly).
|
||
- **Launching and previewing results:** `project.rs` and `engine_ui.rs` start programs with
|
||
`std::process::Command` and manage the preview server.
|
||
- **Decisions in the view:** target kind and stack (`rebuild::choose`, `kind_of`) and asset
|
||
readiness are worked out in `make.rs` and `project.rs`.
|
||
- **CLI is missing:** list/show projects, answer the inbox, follow-ups on a finished project,
|
||
engine installs, play/preview, settings get/set.
|
||
|
||
Pending changes:
|
||
- [x] The project feed moves to `ai::rebuild::feed` (versioned `feed.json`); `RebuildRun` writes
|
||
it (events, the person's messages and notes, answers), so CLI runs keep it too; the app only
|
||
shows it
|
||
- [x] Projects as tools (`Projects` toolset): `project_list`, `project_show` (status, checks,
|
||
waiting questions, paged conversation), `project_inbox_answer`; the feed's data and file
|
||
live in `engine::feed`, the run's folding in `ai::rebuild::feed`
|
||
- [x] Follow-ups and rebuild control as tools (`rebuild_start` with pictures, `rebuild_status`,
|
||
`rebuild_note`, `rebuild_note_withdraw`, `rebuild_stop`), registered from the AI layer (R1.7);
|
||
the CLI's `rebuild` and the tool share `rebuild::project::new_project`
|
||
- [x] A note sent while Claude Code builds reaches it at its next step, not after the turn: the run
|
||
leaves it in `inbox/notes/`, a PostToolUse hook (`--settings`, `craft-anything-cli
|
||
builder-notes`) claims it and hands it to the model as context, and the run shows it as read;
|
||
unclaimed notes still go in after the turn (`rebuild::notes`)
|
||
- [ ] The same for other coding agents with tool hooks, and for the API-backend loop (between tool calls)
|
||
- [x] Engines and settings as tools (`Setup` toolset): `engines_list` (external engines, Ghidra,
|
||
game engines and readiness), `engines_install` (Ghidra, Godot, pinned engines, with
|
||
progress; Unity/Unreal get their download page), `settings_show`, `settings_set`
|
||
- [x] Find registered Linux Unreal installations and their bundled Clang toolchain; support an
|
||
explicit `UE_ROOT` ([PR #191](https://github.com/storytold/craft-anything/pull/191))
|
||
— claimed: one-bit, branch `bugfix/r1.6-linux-unreal-detection`
|
||
- [ ] Give Linux-specific Clang guidance in Unreal's guided installer when the bundled compiler
|
||
is missing (the requirements summary is already platform-specific).
|
||
- [x] The app's Godot, Unity/Unreal installer and Ghidra flows, and its play, preview and
|
||
open-folder buttons, call the tools (`CraftApp::act`); inventory reads stay in the UI
|
||
- [x] Launch, preview and open-folder as tools: `project_play` (needs consent: it runs the
|
||
builder's code), `project_preview` (loopback; `wait` serves until cancelled),
|
||
`project_open_folder`; what a build produced lives in `engine::outputs`
|
||
- [x] Setup and doctor as tools (`setup_plan`, `setup_apply`, `setup_doctor`, `setup_uninstall`,
|
||
registered from the setup crate; changing other apps' configs asks first)
|
||
- [x] Tools for the fal.ai key (`assets_key_status`, `assets_key_set`, `assets_key_remove`, from the
|
||
AI layer; the journal redacts secret-named fields) and a project's asset budget
|
||
(`project_assets_budget`)
|
||
- [x] CLI `rebuild --build-with <engine|stack>` (the app's "Build with"; `stacks::resolve`)
|
||
- [x] Headless runs use the game's own engine: `GameKind::default_stack` detects it (Unreal,
|
||
Godot) from the target, as the app's Make screen does
|
||
- [x] CLI: `project-list`, `project-show`, `project-inbox-answer` (from the tools)
|
||
- [x] CLI: `engines-list`, `engines-install`, `settings-show`, `settings-set` (from the tools)
|
||
- [x] CLI: `project-play`, `project-preview --wait`, `project-open-folder` (from the tools)
|
||
- [x] Headless test: every UI action maps to a tool id or CLI command
|
||
(`ui-egui/tests/headless_parity.rs`: covered, pending with the roadmap reason, or UI-only;
|
||
every `CraftApp` method must have a row; checked in CI later)
|
||
- [x] `cargo xtask layers`: `ui-egui` may not use `std::fs` writes or `std::process` directly
|
||
(`ui_has_no_side_effects`, with a baseline allowlist of today's offenders that can only shrink)
|
||
- [x] Empty that allowlist: report export (`core_report_save`) and opening Unity Hub or the Epic
|
||
Games Launcher (`engines_app_open`) go through tools; the UI now writes no files and starts no
|
||
programs itself
|
||
|
||
### R1.7 Dynamic tool registry
|
||
Owner: maintainers · Status: done
|
||
`ToolSpec` is `&'static` with fn pointers and the registry is fixed at build time.
|
||
- [x] Higher layers register their own (static) tools: `Engine::shared(session, extra)`, with an
|
||
`EngineRef` in the session so those tools can reach the engine; the apps pass
|
||
`craft_anything_ai::tools()`
|
||
- [x] Tools defined at run time: `registry::RuntimeTool` (owned names, a JSON schema, a function
|
||
that tells its tools apart by `Ctx::tool`), via `Engine::shared_with`;
|
||
`Registry::input_schema` serves every client the right schema
|
||
- [x] Toolsets are open: `Toolset::Custom(id)` (`registry::custom_toolset`), and
|
||
`Registry::toolsets()`/`toolset(id)` replace `Toolset::ALL` in MCP, the CLI and the agent
|
||
|
||
### R1.8 Plugins
|
||
Owner: maintainers · Status: done
|
||
- [x] Phase 1: external MCP servers as toolsets: `[[plugins.mcp]]` in config.toml,
|
||
`engines::mcp_client` (stdio JSON-RPC through `platform::process`), cached tool lists
|
||
(`plugins_refresh`, `plugins_list`), servers started on first call, effects from the
|
||
server's annotations, consent by default, results as outside-service evidence
|
||
- [x] Settings → Plugins lists `[[plugins.mcp]]`, turns each on or off and reads its tools
|
||
(`plugins_refresh`); adding one is still done in config.toml
|
||
- [x] MCP servers over Streamable HTTP (`url`, `headers`; JSON or event-stream replies, session
|
||
id); `mcp_client::Transport` for other transports
|
||
- [x] Adding a plugin from Settings: `plugins_add` and `plugins_remove` (CLI and MCP too); a plugin
|
||
added this way always asks before each call; the app re-reads the list after the tool runs
|
||
- [x] **decision**: wasmtime with a WIT interface, behind the off-by-default `wasm-plugins` build
|
||
feature (it's a large build)
|
||
- [x] Phase 2, tools: `[[plugins.wasm]]` components exporting `craft-anything:plugin/tools`
|
||
(`crates/engines/wit/plugin.wit`: `describe`, `call`, JSON strings). They import nothing (no
|
||
files, network, environment); each call is a fresh instance with a fuel and 256 MiB memory
|
||
limit, so their tools run without asking
|
||
- [x] **decision**: plugin checks get the build as JSON (every file's path and size, plus the text
|
||
of the extensions they ask for); asset providers stay out of plugins (they'd need the network)
|
||
- [x] Phase 2, checks: `craft-anything:plugin/checks` (`describe` with kinds and extensions, `run`
|
||
on the build JSON → passed, similarity, summary), run after a kind's own checks; a plugin that
|
||
fails to run passes with a note instead of failing the build
|
||
- [x] WASM plugins: Settings → Plugins lists, adds (a `.wasm` path), turns on or off and removes
|
||
them, `plugins_add`/`plugins_remove`/`plugins_list` take both kinds (`wasm`), and
|
||
`cargo xtask ci` lints and tests the engine with `--features wasm-plugins`
|
||
- [x] WASM plugins: `examples/wasm-plugin-wordcount` (Rust, wit-bindgen 0.63,
|
||
`wasm32-unknown-unknown`, `wasm-tools component new`), built and loaded through the host
|
||
(opt-in test `CRAFT_ANYTHING_TEST_WASM_PLUGIN`)
|
||
- [x] Plugin API versions: semver for `package craft-anything:plugin@…`; the host finds a plugin's
|
||
interfaces by name and accepts any compatible version (same major, and same minor while 0.x),
|
||
refusing others by name; patch versions only add optional JSON fields (docs/extending.md)
|
||
- [x] A check plugin in Rust: `examples/wasm-plugin-readme-check` (does the build's README say how
|
||
to run it), built and loaded for real (opt-in test `CRAFT_ANYTHING_TEST_WASM_CHECK`)
|
||
- [x] `docs/extending.md`: how to add a tool, a target kind, a check, a builder or a plugin
|
||
|
||
### R1.9 Versioned files
|
||
Owner: open · Status: done (branch `feature/r1.9-versioned-files`)
|
||
- [x] `craft.json`: `version` field, JSON schema (schemars) published in `docs/schemas/`
|
||
- [x] `config.toml`: version field and migration hook
|
||
- [x] `.cany` `FORMAT_VERSION`: migration function stub and test
|
||
|
||
---
|
||
|
||
## M2 Product completeness
|
||
|
||
### R2.1 Building with API-key backends
|
||
Owner: maintainers · Status: in progress (built; live runs need keys)
|
||
- [x] File and command tools for our own agent (`Build` toolset): `build_files_list`,
|
||
`build_file_read`, `build_file_write`, `build_file_edit`, `build_command_run`, in the session's
|
||
workspace, through `platform::process` and the same approval policy as Claude Code builds
|
||
(`interaction_tools::approve`)
|
||
- [x] `RebuildRun` accepts Anthropic, OpenAI, OpenRouter and local backends: our agent loop with
|
||
the Build toolset (`AiHub::start_builder`, up to 400 steps a message), Build tools' consent
|
||
answered by the run, follow-ups continue the kept conversation (`transcript.json`); the builder is
|
||
the named backend, else the Builder role, else an agent app, else any enabled AI backend
|
||
- [ ] A live API-key build (needs a key on the test machine)
|
||
- [x] Kimi K3 through OpenRouter (`moonshotai/kimi-k3`, offered by Detect with a pasted key, priced
|
||
at OpenRouter's list price), and choosing who builds: "Build with this" in Settings → AI and
|
||
`ai builder <name>` set the Builder role, which the app now honours before an agent app
|
||
- [x] An extraction step for Deep-mode game rebuilds: a backend chosen for the Extractor role ("Extract
|
||
with this", `ai extractor <name>`; an API backend such as Kimi K3) takes the game's own files
|
||
out into `app/Original/` after the study, with the game archive tools, and lists them in
|
||
`app/Original/MANIFEST.md`; the builder is told to use them. A failed extraction says why and
|
||
the build goes on
|
||
- [ ] A live extraction run with Kimi K3 (needs the person's OpenRouter key)
|
||
- [x] A checklist tool for API builders: `build_checklist` (TodoWrite's shape), shown pinned like
|
||
Claude Code's checklist
|
||
- [x] Context management for long builds (`ai::context`): past the budget (150k input tokens,
|
||
24k for local models, `LoopConfig::context_tokens`) old tool results are trimmed, then the
|
||
oldest part is replaced by a summary the model writes; the conversation is kept in
|
||
`transcript.json` after every step and a follow-up resumes it
|
||
|
||
### R2.2 More coding agents
|
||
Owner: open · Status: in progress
|
||
- [x] `AcpConversation`: chat with agents that speak ACP (`cli_agents::acp`; agents and their ACP
|
||
arguments in `ACP_AGENTS`: Gemini CLI, Qwen Code, opencode, goose). Opt-in per backend
|
||
(`acp = true` in its options); one agent process per chat, permission requests become
|
||
consent questions, sessions continue with `session/load`
|
||
- [x] Builder runs over ACP: with `acp = true`, rebuild runs use `AcpConversation`; the agent's
|
||
permission requests go through the approval policy (`interaction_tools::approve_with`:
|
||
allowed, refused, or asked through the project's inbox; Auto mode honoured)
|
||
- [ ] Gemini CLI, opencode, goose via ACP or native adapters; each verified live, then turn ACP on
|
||
by default for the ones that pass
|
||
|
||
### R2.3 Website coverage and a better visual metric
|
||
Owner: open · Status: done
|
||
- [x] Study: up to 4 same-site pages from the start page's links, each captured into
|
||
`capture/routes/<page>/` (`capture/routes.json`); the brief asks for each at the same path
|
||
- [x] Study: open menus, dialogs and tabs; record states (`web_page_states_capture`: up to 4 on the start
|
||
page, pictured in capture/states/ and listed in the brief; never form controls)
|
||
- [x] Study: links more than one level deep (pages linked from the captured pages fill the rest of
|
||
`build.max_pages`; `pick_deeper_routes`)
|
||
- [x] Perceptual metric next to tile similarity: `perceptual_similarity` (mean SSIM of
|
||
brightness, own code) in `web_pixels_compare`; a website check's likeness is the mean
|
||
of layout and detail, both shown in its note
|
||
- [x] Check: every captured page compared on the build at the same path; the likeness is the
|
||
mean over pages, each page's score in the note (live: info.cern.ch, 95% + 83% → 89%)
|
||
- [x] Check: interaction states (the build's menus, pop-ups and tabs opened like the original's, paired
|
||
by kind and label, pictures compared) and structure (`structure_likeness`: landmarks, headings,
|
||
lists, media and controls in order, missing parts named); both in the note, the score stays visual
|
||
- [x] Check: text/token diff with the original's missing headings and words (`text_likeness`;
|
||
in the check's note and the builder's refine message; the score stays visual)
|
||
- [x] Report: per-route likeness on the project page (`CheckResult::pages`; a page is picked to see its pictures)
|
||
- [x] Stacks and libraries named in a request reach the builder: a Next.js stack (static export to
|
||
app/out) and a `[[library]]` table in `stacks.toml` (shadcn/ui, Tailwind CSS) whose steps join
|
||
the stack's; phrases match the request's words, so "Next.js", "nextjs" and "next js" all count
|
||
- [x] A website whose request names no stack leaves it to the builder ("Builder's choice", told to
|
||
match the original's); the stack it used is recorded from app/package.json (`stacks::used_in`)
|
||
- [x] Fidelity at every width: Study and check use the same widths (default 1440, 768, 390);
|
||
`web_site_capture` records CSS breakpoints, spacing and content widths, hex colours, font
|
||
files and the layout at each width; menus are also opened at the phone width; the check's
|
||
note names missing colours, fonts, text sizes and spacing, sideways scrolling, heading sizes
|
||
and per-section padding, items per row and content column (`design_notes`)
|
||
- [x] Page health: `web_site_open` reads script, console and log errors and failed requests (400+,
|
||
failed loads) from the browser's events (`page_health`, no query strings); `web_site_capture`
|
||
records them with an accessibility outline (language, landmarks, unnamed controls, pictures
|
||
without alt); the check names what the build adds (`health_notes`, failing paths compared)
|
||
- [x] Parts that change by themselves aren't compared: Study photographs each width three times at
|
||
uneven gaps (`web_site_capture` `noise_pass`) and keeps the moving squares as noise-<width>.png;
|
||
`web_pixels_compare` `ignore` leaves them out of layout and detail (`NoiseMask`,
|
||
`block_similarity_masked`, `perceptual_similarity_masked`), and the website check passes it
|
||
- [x] Live clone (2026-10-21) of a fixture page of our own with known values at three breakpoints
|
||
(type 48/36/28, padding 64/48/32, a 1100 px column, 3/2/1 cards per row), in Next.js +
|
||
shadcn/ui, 2 rounds: Ready, 96% alike (97/96/95% at 1440/768/390 px), page heights equal.
|
||
The build matched every measured value exactly, so the thresholds (4 px padding, 2 px type,
|
||
8 px column) stay: no note fired from noise. It found two real faults, fixed: sections were
|
||
paired by heading only, so a section the builder renamed (as it must) was never compared,
|
||
and now pairs by tag in page order; and a nav's link count read as "items per row" (a link
|
||
under 40 px was dropped) and gave a false note, so per-row isn't compared for nav or header
|
||
- [ ] Thresholds under pressure: a clone that's off by a few pixels (a page with fluid type or
|
||
`clamp()` spacing) to see whether 4 / 2 / 8 px is too strict
|
||
|
||
### R2.4 Runtime study for apps and games (with consent)
|
||
Owner: maintainers · Status: in progress
|
||
- [x] Window screenshots: `platform::window` (xcap) and the `native_window_capture` tool (start,
|
||
wait, picture the largest window of the process or its app, close; consent required); Study
|
||
saves `capture/window.png` when the `runtime_study` setting is on (off by default); the
|
||
screenshot check compares the rebuild with it too; the CLI is DPI-aware on Windows
|
||
- [x] UI tree on Windows and macOS: `runtime_ui_read` reads UI Automation on Windows (a PowerShell
|
||
script we ship; role, title, id, value, class, position; never password values) as well as
|
||
the macOS accessibility tree; `Requires::Os` names the hosts a tool runs on
|
||
- [ ] UI tree on Linux (AT-SPI)
|
||
- [x] Scripted UI steps on Windows: `runtime_ui_walkthrough_record` presses, expands, collapses,
|
||
toggles, selects or focuses controls by title or id through UI Automation (no mouse or
|
||
keyboard) and records the tree before and after; each step reports its outcome
|
||
- [x] Study saves the original's UI tree next to `capture/window.png` when runtime study is on
|
||
(`native_window_capture` `ui_tree`, read just before the program closes; `capture/ui-tree.json`,
|
||
pointed to in the capture README)
|
||
- [x] A Settings control for `runtime_study` (Safety → "Study apps by running them")
|
||
- [ ] Window capture tried on macOS and Linux (only Windows so far); games behind a launcher
|
||
- [ ] Short scripted input sessions (enigo) to record screens of the original
|
||
- [x] Same capture of the rebuild: with runtime study on, the screenshot check also starts the build
|
||
normally and pictures it like the original (`checks/round-<n>/window.png`, `ui-tree.json`);
|
||
the note says how alike the windows look and names the original's labelled controls the
|
||
build lacks (`controls_likeness`); advisory, the score is unchanged
|
||
|
||
### R2.5 Behaviour checks for non-web builds
|
||
Owner: open · Status: in progress
|
||
- [x] Standard `--screenshot <path>` contract in every app and game brief, enforced by
|
||
`ScreenshotCheck` after the build check (no picture, an unreadable or blank picture fails);
|
||
checks run in order and a failed one stops the rest
|
||
- [x] Compare against the person's reference pictures: the closest PNG's likeness is in the note
|
||
- [x] Let picture likeness decide: with `build.picture_good_enough` set, the screenshot check's
|
||
likeness has that bar and refining continues until it's met (unset: reported only)
|
||
- [x] Compare JPEG, WebP and GIF reference pictures too (`web_common::decode_picture`)
|
||
- [x] `MECHANICS.md` gains a structured twin (`app/mechanics.json`, `engine::mechanics`, schema in
|
||
`docs/schemas/`): `MechanicsCheck` fails a game without a readable sheet and notes values
|
||
without a source and numbers not in the code as written
|
||
- [x] Apps in Deep mode keep the sheet too: the brief asks for it when a program target is
|
||
decompiled, and `DeepOnly(MechanicsCheck)` checks it only then (`Check::applies`)
|
||
- [x] Honest game checks (a Deep-mode Unreal rebuild reached Ready as a first slice: a cave, no
|
||
player, enemies, mission or HUD): every game brief asks for `--self-test <file.json>` (play
|
||
the core loop with scripted input, measure it from the game's own state) and
|
||
`SelfTestCheck` runs it after the screenshot, failing until `move`, `act` and `hud` all pass;
|
||
`MechanicsCheck` fails when a sourced number isn't in the code as written (estimates may
|
||
stay out; Unreal's generated folders aren't read as code); in Deep mode on a program,
|
||
`DeepOnly(CreditsCheck)` requires `app/CREDITS.md` and `app/README.md` opening with the
|
||
brief's private-use line
|
||
- [ ] Behaviour checks that use the sheet: run the build with scripted input and measure values
|
||
(speed, jump height, fire rate) against it (needs R2.4's scripted input)
|
||
- [ ] Hold the self-test to what the check can see too: picture the game after its scripted
|
||
input and require it to differ from the `--screenshot` picture (a report alone is the
|
||
build's own word)
|
||
- [x] Publish the self-test report's schema in `docs/schemas/`: `engine::self_test` owns the
|
||
report's types, `cargo xtask schemas` writes `self-test.schema.json`
|
||
|
||
### R2.6 Several rebuilds at once
|
||
Owner: open · Status: done
|
||
- [x] More than one `RebuildRun` per window: `rebuild::RunSet` runs up to `build.parallel` (Settings →
|
||
Builds) and queues the rest, for the app and the rebuild tools alike; events come by project
|
||
folder and a building project's feed is taken from its run (`feed_now`)
|
||
- [x] Start queued builds from MCP without waiting for the next rebuild tool call:
|
||
`rebuild::keep_queue_moving` polls the tools' `RunSet` every 2 s while builds wait
|
||
- [x] A UI test with stand-in runs: `rebuild::StandIns` makes runs without a builder; the smoke
|
||
test queues a second project and follows it on screen until it starts
|
||
|
||
### R2.7 The request decides how to build
|
||
Owner: open · Status: in progress
|
||
"Build it in Electron" was built in Rust: the brief gave the kind's default stack as a firm
|
||
instruction after the request, and the app, the build check and Play followed the recorded stack.
|
||
What the person asks for comes first, in every kind; the stack table is only the default.
|
||
- [x] The brief puts the request first: a rule says what it names (a language, framework, engine,
|
||
library or platform) wins, and the kind's stack is given as "How to build it, unless their
|
||
request says otherwise"; a website's default no longer says to copy the original's stack
|
||
over the request, and its refinements no longer say `npm run build`
|
||
- [x] The build check, the recorded stack and Play follow what was built in app/: an engine's own
|
||
build runs only for its project, a non-engine stack is re-recorded from package.json after
|
||
each turn, and an Electron app opens in the Electron it installed (and is never a website)
|
||
- [x] Make: a game engine is required only when the person picks one or the request names none
|
||
("Built with Electron, as you asked, so no game engine is needed")
|
||
- [x] Electron in the stack table (asked by name, recorded from package.json, with steps that fit
|
||
the build check, Play and --screenshot). Live: "A small notes app in Electron" was built in
|
||
Electron and reached Ready with both checks passing
|
||
- [ ] Screenshot check: in that live run the final check once got no picture from the Electron app
|
||
within 90 s while the same check passed in the builder's turn and on a re-run; not
|
||
reproduced. The app waited on animation frames, which a window behind others may not get;
|
||
the Electron steps now say to capture after loading instead
|
||
- [ ] Make: a game whose request names a technology the stack table doesn't know (Phaser, Bevy…)
|
||
still needs the default engine installed to start, though the builder is told to use what
|
||
was named; ask the person, or let such a game start without an engine
|
||
- [x] CLI: after the tool name, `--project` is the tool's own option when it has one
|
||
(`rebuild-check --project <folder>`, `project-show --project <folder>`), as `--format` is;
|
||
otherwise, and always before the tool name, it's the global investigation folder
|
||
|
||
---
|
||
|
||
### R2.8 Clean room means clean room
|
||
Owner: maintainers · Status: done (branch `feature/r2.8-clean-room`)
|
||
**decision** (2026-10-23, maintainers): Clean room never looks inside the original. Only Deep mode
|
||
reads its code, files or page source, with our own engines and with Ghidra when it's installed.
|
||
Deep mode is its own switch, not tied to Ghidra.
|
||
- [x] Deep mode as its own setting (`deep_mode`, config.toml version 2 moves `engines.ghidra.enabled`
|
||
there); Ghidra is used in Deep mode when it's installed
|
||
- [x] A table of the tools Clean room allows (no target, or watching from outside: the visible
|
||
website, screenshots, pictures of a window, recorded runs, the screen's controls); every other
|
||
tool is refused in Clean room, everywhere (app, CLI, MCP, agents), pointing at Deep mode
|
||
- [x] Clean room website capture keeps what a visitor sees: screenshots, visible text, measured
|
||
layout, colours, fonts and spacing, menus opened; no HTML, CSS rules, scripts, requests or
|
||
script errors
|
||
- [x] Clean room builds: the study reads nothing inside an app or game (pictures, the person's
|
||
description, and watching it run when Settings allows); the builder can't read the original's
|
||
files, fetch its pages or run commands on it (its Read, Glob and Grep go through the approval
|
||
policy); briefs say so
|
||
- [x] App: Just look inside, Game files and the Advanced workbench ask for Deep mode in Clean room;
|
||
the mode switch explains both modes
|
||
- [x] CLI `--deep` for one run; MCP lists only Clean room's tools in Clean room
|
||
- [x] Deep mode uses every tool: the builder sees them all (`--all-tools`, every toolset for API
|
||
builders), a website's study also reads its JavaScript and watches its start page
|
||
(capture/how-it-works.md), and briefs name the tools for each kind of target
|
||
- [ ] A live Clean room build of a website and of an app, checked end to end
|
||
- [ ] A remote (SSH) builder needs a craft-anything-cli that knows `--deep` and `--clean-room`:
|
||
check its version first and say so when it's older
|
||
- [ ] A remote (SSH) builder doesn't get the time limits or effort set through `Process::env`
|
||
(`MCP_TOOL_TIMEOUT`, `CLAUDE_CODE_EFFORT_LEVEL`…): they're set on the local `ssh`, which doesn't
|
||
pass them on. Set them in the remote command
|
||
- [ ] Failure logs for the other ways an AI stops (API backends, MCP server start-up), like the
|
||
ones agent CLIs write to `logs/` in the config folder
|
||
|
||
### R2.9 Everyday use: typing, projects, settings
|
||
Owner: maintainers · Status: in progress
|
||
Gaps found while using the app for real projects.
|
||
- [x] Up at the start of the prompt box takes back the last note still waiting for the builder,
|
||
to edit it (as the x beside it does)
|
||
- [x] With nothing waiting, Up and Down in the prompt box cycle through previous prompts, as in a
|
||
terminal: a project's are read from its feed, so they survive a restart, questions come
|
||
back starting with `/btw` so they stay questions, Down past the newest returns what was being
|
||
typed, and the assistant's box does the same
|
||
- [x] Up and Down only cycle prompts from the very start or end of the text, and move the cursor
|
||
otherwise, so a prompt of several lines can be walked through line by line
|
||
- [x] Slash commands in a project's prompt box: `/btw` asks about the build in place of the old
|
||
`?` prefix; a new one is a `slash::Command` and a row in `COMMANDS`. A mistyped command stays
|
||
in the box rather than starting a build; a path like `/usr/bin` is a plain message
|
||
- [ ] Typing `/` lists the commands, and Tab completes one
|
||
- [x] Questions about a build show in the conversation where they were asked, so a long answer
|
||
scrolls with it and leaves the prompt box alone; each folds to one line, and they're kept in
|
||
the project (`questions.json`, the newest 200, `rebuild_questions_list`), in their place
|
||
among its prompts for Up and Down
|
||
- [x] A folded sidebar stays lined up with the open one: its button is centred over the row
|
||
icons, its sections keep their height, so nothing moves when it folds, and a rule takes the
|
||
place of each heading, with Refresh and the Pinned arrow still there
|
||
- [ ] A prompt brought back with Up keeps its pictures (only the text comes back now)
|
||
- [x] The smoke test `a_game_project_shows_its_asset_spending_and_can_raise_the_budget` failed
|
||
once in a full run and passed in the next 11: wait for `a.facts.assets.0 == 1` instead of a fixed frame step
|
||
- [x] The prompt box no longer jumps while typing: a frame laid out with its old height is
|
||
thrown away and laid out again before it's shown
|
||
- [x] A message sent to a project waiting its turn stays in the box instead of being lost
|
||
- [x] Several project folders: list projects from any number of folders, and start a project in
|
||
a folder the person picks, which may already have files; ask whether those files are the
|
||
app to edit in place or material to start from, with "don't ask again" in Settings
|
||
(`Workspace::create_at`, `project_folders`, `existing_files`, `rebuild --project-folder
|
||
--existing`, `rebuild_start`'s `project_folder`; craft.json version 2, config.toml version 3)
|
||
- [ ] A live build in a picked folder each way (edit in place, start from), checked end to end
|
||
- [x] Start sits below the fold on the Make screen at common window heights (900 px)
|
||
- [x] The project list notices projects added, moved or removed on disk (on focus, and a Refresh)
|
||
- [x] Rename, delete and duplicate a project (`project_rename`, `project_duplicate`, `project_delete`:
|
||
a delete keeps the person's own files, and a copy leaves out dependencies and build output)
|
||
- [x] Remove or turn off an AI from Settings (`ai on`/`ai off` in the CLI); a pasted key isn't
|
||
kept in the app after it's added
|
||
- [x] Simple mode keeps the Project screen in Advanced mode too, so builds and approvals stay in sight
|
||
(the workbench is a screen beside the projects: a Workbench row in the sidebar, a Projects
|
||
button on the workbench)
|
||
- [x] One answer to "can it build?" (`craft_anything_ai::default_builder`: Make's warning and
|
||
the builder chosen agree), and a setup dialog with what was found, Look again and API keys
|
||
instead of "Got it"
|
||
- [x] "Built by" on Make: every AI that can build, by name, even when one is set up and builds
|
||
by default; a pick stays with the project (`craft.json` `builder`, also from `rebuild
|
||
--backend` and `rebuild_start`), and its later changes are built by it while it's on
|
||
- [ ] Change a project's builder from its page (a tool, so the CLI and MCP can too)
|
||
- [x] Settings are saved after typing stops (one save at a time), and path fields keep spaces
|
||
- [x] No file reads while drawing: the project's outcome, reference pictures, screenshots and
|
||
side panel come from a worker when something changes (at most once a second)
|
||
- [x] Making a project and copying pictures (of the original, or attached to a message) run on
|
||
a worker, not on the click
|
||
- [x] A build repaints the window when it has news instead of a 10-a-second timer; animations
|
||
run at 15 frames a second, and once a second while the window is in the background
|
||
- [x] The feed isn't copied every frame and a paragraph is one label, not one per word;
|
||
screenshot thumbnails are scaled down and dropped when another project opens
|
||
- [x] The "Working" row's clock counts from the build's start instead of restarting whenever
|
||
it reappears
|
||
- [x] Stop on the Ask screen stops only its own answer, not downloads and builds, and "Working on
|
||
your answer" shows only for it
|
||
- [x] Opening another target starts a new conversation, so answers about two things don't mix
|
||
- [x] Closing the Ghidra question without downloading means the project doesn't start later; one
|
||
that waits for the download starts as it was when Start was pressed
|
||
- [x] Make's Clean room / Deep choice says it's the app's setting, not the project's
|
||
- [x] A project that needs an answer says so in words in the sidebar, and the taskbar flashes when
|
||
a build asks something while the window is behind others
|
||
- [x] One Pause (in the prompt box); the plan card carries the clock instead of a second Working
|
||
card; Progress shows where a paused build stopped, and its steps have names for screen readers
|
||
- [x] App or game is the tile picked (and a project's engine), not whether the path is a folder
|
||
- [x] The website dialog's field is ready to type into
|
||
- [x] Typed drafts survive a restart, and each project keeps its own unsent text
|
||
- [x] Narrow windows: the sidebar folds to icons (by itself below 900 px, or by its button) and the
|
||
project's side panel moves into "Project details" above the feed below 860 px
|
||
- [x] Messages float over the bottom of the window instead of pushing the screen up; errors stay
|
||
until dismissed, so a later message never hides one
|
||
- [x] Simple-mode text is 12 pt or more (captions, buttons, the title bar, Progress)
|
||
- [x] The Language menu shows once there's a second language
|
||
- [x] French UI (`fr.tsv`, Settings → General → Language); a test checks every translation keeps its
|
||
`{placeholders}`; `--lang fr` on the snapshot example
|
||
- [x] Simplified and Traditional Chinese UI catalogs, system-locale selection and translated
|
||
tool-title search; catalog integrity, coverage checks and isolated language-switch tests
|
||
([localisation](localization.md))
|
||
- [ ] A Simplified Chinese README (`README.zh.md`) translated from `README.md`, linked from the
|
||
language switcher in `README.md` and `README.ja.md` — claimed: maple323, branch docs/readme-zh
|
||
- [ ] `README.ja.md` is behind `README.md`: the whole Craft App Suite section is missing, so the
|
||
Japanese translation needs a catch-up pass
|
||
- [x] First run sets up who builds, not only who answers, and no longer says the purpose is saved
|
||
with projects (it stays on the computer); its Back has a name for screen readers
|
||
- [x] An error whose fix quotes a command (`claude auth login`, `codex login`) offers to copy it,
|
||
instead of the app matching Claude Code's English wording
|
||
- [x] A consent card's id is a short number, with the call it stands for kept in memory, not the
|
||
call serialised into the id
|
||
- [x] The workbench's list isn't copied every frame (it can hold thousands of rows)
|
||
- [ ] Simple-mode text sizes checked by a test (no literal sizes under 12 pt outside the workbench)
|
||
- [x] Typed text no longer blinks in the prompt box; Open folder opens the project's folder
|
||
- [x] Scroll bars show at rest (6 px, the text colour at 3:1 or more on every surface), not
|
||
only on hover
|
||
- [x] Play starts a Rust workspace's window app (from its manifests), not whichever program was
|
||
built last (a CLI that quit at once)
|
||
- [x] Play says when what it started quit straight away (with its last output), instead of
|
||
"Starting…" and nothing, and that it's already open instead of starting it twice
|
||
- [x] Questions from the builder reach the person: "Builder's questions: Ask me / Decide
|
||
yourself" under each project's prompt box (`project_questions_set`, craft.json `questions`)
|
||
and a default in Settings (`questions`, which follows Auto mode until picked), apart from
|
||
Auto mode's approvals; a builder that decides starts its next message with "I decided:"
|
||
- [ ] When the builder decides a question, show the question and its choice as an answered card
|
||
in the feed (the run sees only the tool call today; the builder's words say what it chose)
|
||
- [x] Ask questions about a build without starting one: "Just ask" under the prompt box (or a
|
||
message starting with `/btw`), answered as a card, never a round (`rebuild_question_ask`).
|
||
Claude Code answers from a copy of the builder's session (`--fork-session`, not kept) with
|
||
Read/Glob/Grep only (none in Clean room) and no tool server; an API AI with the tools that
|
||
change nothing (`agent::ReadOnlyHost`); other agents are refused
|
||
- [ ] A question's own cost: a forked Claude Code session reports the whole session's running
|
||
total, so a forked answer shows none; count questions in the project's usage
|
||
- [ ] "Make this change" on an answer that suggests one (sends it as a normal message)
|
||
- [ ] A program started from the CLI (`project_play`) keeps the shell's output pipe open, so the
|
||
command doesn't return until it closes: start it without inheriting the CLI's handles (#207)
|
||
- [x] "It's done" beside Continue and Try again: the person keeps a build as it is and it stops
|
||
offering to carry on (`project_build_finish`, #210)
|
||
- [x] The desktop app keeps `desktop.log` beside its settings: startup steps, warnings, a window
|
||
that couldn't start, panics
|
||
- [x] The cost says when it's an estimate: Claude Code's figure is its own guess at API prices,
|
||
so the project page says "AI cost (estimate)" with a note that it isn't the bill, and the
|
||
title bar says "(estimate)"; what an API key was charged is kept apart (`api_billed_usd`
|
||
in usage.json; older files count as estimates)
|
||
- [x] A running build says what it's on ("Now: Running cargo test (42s)", the last action between
|
||
them), and "More detail" opens its activity as it arrives: stages, every command and file
|
||
with how it ended, what the builder said and what programs printed outside the
|
||
conversation, with secrets hidden. Kept in `build-activity.log` ("Open log",
|
||
`rebuild_activity_show`, and every tool the CLI's `rebuild` prints)
|
||
- [ ] Show a command's output while it runs: Claude Code reports a tool's result only when it
|
||
ends, so a long `cargo build` is one quiet line until then. Watching the agent's child
|
||
processes from outside could say what's running and for how long
|
||
- [ ] Sometimes starting the app shows nothing: find why from `desktop.log` (a GOG Galaxy Vulkan
|
||
overlay layer loads into the app; try DX12 first on Windows if that's it)
|
||
- [x] Try again picks up where a run stopped instead of starting over: a project whose builder
|
||
has worked isn't studied again or given the first brief; when the builder had finished and
|
||
only the check didn't (`awaiting_check` in craft.json), only the check runs. A check that
|
||
returns an error gets a second go, and then the run says the build is finished but couldn't
|
||
be checked
|
||
- [ ] Find why a site capture lost its browser ("the WebSocket is closed", 419 ms into
|
||
`web_site_capture` for a second page, seen once; the same capture passed 41 s earlier)
|
||
- [ ] Two engine tests fail on a Mac, on `main` too: `real_binary_end_to_end_if_present` expects
|
||
`/bin/ls` to be Linux, and `auto_mode_keeps_going_but_never_does_the_dangerous_things` is
|
||
refused `rm -rf` in a temp folder under `/var/folders`
|
||
|
||
### R2.10 Pinned builds
|
||
Owner: maintainers · Status: in progress
|
||
Keep a build that works at hand: pin it, and open it from the sidebar whatever the builder does
|
||
to the project next. Pins are the engine's (tools), kept in the project's `pinned/` folder.
|
||
- [x] Pin a build as a frozen copy (the default: the app's files and the program, without what
|
||
a build makes again, in `pinned/<n>/`, with its size shown first) or as a live pin that runs
|
||
the project's app as it is; open, stop, rename and remove pins; one copy runs per folder
|
||
(pins and Play alike) unless another is asked for; a start that quits at once says so with
|
||
its last output (`project_pin_create`, `project_pins_list`, `project_pin_open`,
|
||
`project_pin_stop`, `project_pin_rename`, `project_pin_delete`; `pin.json` version 1;
|
||
`platform::Launched`)
|
||
- [x] A light pin in place of the live one, which only opened what Play opens: the program, the
|
||
code and anything a program may write to are copied, dependencies and media are hard-linked
|
||
to the project's (`SHARED_KINDS`), and a file that can't be linked is copied. Replacing or
|
||
deleting a shared file in the project leaves the pin's as it was; rewriting one in place
|
||
changes both. Old live pins still open; new ones are refused
|
||
- [x] Pins leave out what a compiler keeps beside a program that nothing needs to run it (`.pdb`
|
||
debug symbols, `.lib`, `.d`, lock files): measured on real Rust projects, that took a
|
||
291 MB pin to 49 MB, far more than sharing could
|
||
- [x] Leave out a project's old program copies (`*.running-*.exe`, 22 MB each in one project), which
|
||
a pin can't tell from the program's helpers by their name alone
|
||
- [ ] Say when a light pin ended up copying what it meant to share (a drive without hard links)
|
||
- [ ] Pins of one project sharing with each other, so a third frozen copy of the same assets
|
||
takes no more space
|
||
- [ ] Pin a website (a frozen copy of the built site, each served on its own port)
|
||
- [ ] A pin started before the app restarted shows as running (keep its process id and start
|
||
time in the pin, and check both), and can be stopped
|
||
- [ ] Cap a run's log (`last-run.log`, `play.log`): it grows as long as the program prints
|
||
- [x] A collapsible Pinned section in the sidebar (icons with a running dot when folded; open,
|
||
open another copy, close, go to the project, rename, remove), Pin this build on the project
|
||
page with the frozen/light choice and each copy's size; the pin tools are in the command
|
||
palette with every other tool
|
||
- [ ] Opening a pin that's already running brings its window to the front, instead of a message
|
||
- [ ] Show a pin's folder (the right-click menu), and how much space pins take in Settings
|
||
- [ ] Live check on Windows: rebuild a project while its frozen pin runs, and the build isn't blocked
|
||
|
||
## M3 Engine depth
|
||
|
||
### R3.1 More tools (all 22 built)
|
||
Owner: open · Status: in progress (a live Cutter test is left)
|
||
- [x] Radio signal decoding: `radio_recording_decode`, `radio_decoders_list`, with rtl_433 run as a
|
||
program (an optional engine, path in Settings → Engines)
|
||
- [x] Cutter sessions: `native_cutter_sessions_list`, `native_cutter_command_run`, for sessions
|
||
the person shares with `=h& 9090` in Cutter's console (read-only commands, 127.0.0.1 only)
|
||
- [ ] Cutter sessions: try them against a real Cutter, including whether `=h&` keeps Cutter
|
||
responsive (only a stand-in server so far)
|
||
- [x] rizin debug sessions: `native_rizin_session_begin` (a program, stopped at its start, or a
|
||
pid), `_command_run`, `_show`, `_end`, driving `rizin -d` with a marker after each command
|
||
- [x] Rizin sessions: live test passed again with Rizin 0.9.1 installed from its pin (program
|
||
arguments reach the program; a `dc` that outlasts its wait keeps the session busy and the next
|
||
command waits for it; nothing left running)
|
||
- [x] Every tool is listed in `docs/tools.md` (`cargo xtask tools`)
|
||
|
||
### R3.2 Known partial tools (see tool-status.md)
|
||
Owner: open · Status: done
|
||
- [x] Pseudo-C arguments for imports without prototypes: a table of common libc and Win32 argument
|
||
counts (`disasm::arity`, plus stdcall `@bytes` names) shows every argument, unset ones as the
|
||
register
|
||
- [x] `calls_out` counts tail calls (and `tail_calls` reports them on their own)
|
||
- [x] Imports as search hits and call-path goals: `native_functions_search` lists matching imports
|
||
(`kind: "import"`); `native_call_path_find` takes an import as a forward goal (`native::Goal`)
|
||
- [x] `native_strings_browse` section filter (`section`, any case; an unknown one names those with strings)
|
||
- [x] Edge vs Electron labelling: `js_electron_pages_list` names the runtime from `/json/version`
|
||
(`web_common::runtime_of`) and says when a plain Edge or Chrome answered
|
||
- [x] `core_evidence_exports_compare` tampered record: every record is checked against its own id; edited records
|
||
are reported and never count as unchanged; duplicate keys are listed
|
||
- [x] AnyCPU .NET bitness: triage says "any CPU" from the CLI flags (`PeImage::dotnet_arch`,
|
||
shared with the .NET inventory) and reports `dotnet_arch`
|
||
- [x] `apple_app_graph_build` on bare Mach-O: only a bundle counts as an app; the result says
|
||
`shape` (app-bundle, bare-executable, none)
|
||
- [x] Nested field names in CLI help: fields inside object options (and arrays of objects) are listed
|
||
as `option.field`, two levels deep
|
||
- [x] `native_function_pseudocode` on an address no analysis made a function: the hint says so and points
|
||
to `native_function_find` and xrefs, instead of claiming the built-in decompiler answered
|
||
- [x] Ghidra: decompile code its quick analysis never made a function (reached only through pointer
|
||
tables, as seen in a live run): disassemble and create the function on demand in
|
||
the bridge, in memory only (the analysis stays read-only)
|
||
- [x] Ghidra: when the bridge stops before it starts, its logs are copied beside the analysis
|
||
(`<analysis>.bridge.log`, as the analysis log is) and the error points there, since the
|
||
runtime folder is removed with the error
|
||
|
||
### R3.3 Frida sessions for real
|
||
Owner: open · Status: todo
|
||
- [ ] Replace the in-memory session bookkeeping with real attach, resume and detach
|
||
- [ ] `frida_devices_find` with `remote`: `frida-ls-devices` has no `-H`, so listing a remote's devices fails
|
||
|
||
### R3.4 Decompilers for managed code (external engines)
|
||
Owner: open · Status: in progress
|
||
- [x] C#: ILSpy's command-line tool (MIT): `managed_ilspy_decompile` (C# of the whole assembly or one
|
||
type, the IL, or its types; update check off; native files refused before it runs)
|
||
- [x] Java: Vineflower (Apache-2.0): `java_vineflower_decompile` (a .class or JAR; its .jar run with
|
||
the configured Java, or a `vineflower` script; sources read back from a temp folder)
|
||
- [x] Vineflower: a checked install pin (Settings → Engines or `engines_install`)
|
||
- [ ] Vineflower: run it once on a real JAR (no Java on the machine it was written on)
|
||
|
||
### R3.5 Device-backed tools
|
||
Owner: open · Status: blocked (needs an Android device or emulator)
|
||
- [ ] Verify the 23 unverified adb tools; record results in tool-status.md
|
||
|
||
### R3.6 Browser walkthroughs in depth
|
||
Owner: open · Status: done
|
||
Scripted web sessions (`web_walkthrough_record`) should be repeatable and see what a person
|
||
needs to understand a site.
|
||
- [x] Fixed environment: viewport, scale, locale, time zone, colour scheme, reduced motion,
|
||
service workers (defaults 1280x720, en-US, UTC, light); the result says what was applied
|
||
- [x] Start with given cookies and local/session storage (`storage`; every value is treated as a
|
||
secret and redacted; results name what was set, never values)
|
||
- [x] Optional request/response bodies and header values per run (`keep_bodies`, off by
|
||
default; text capped at 64 KiB, digested after redaction; credential headers always redacted)
|
||
- [x] Locators by role and accessible name (accessibility tree), and by test id, next to CSS
|
||
selectors; an optional `step_id` per step, echoed in the result
|
||
- [x] Artifacts at the end or after each step (`capture`): screenshot, DOM (redacted, capped),
|
||
accessibility tree, history, storage key and cookie names
|
||
- [x] Event families (`capture.events`): console, page errors, WebSockets, frames, workers, popups,
|
||
downloads (blocked), each with its step and a sequence number; a goto no longer drops events
|
||
- [x] Compare two walkthrough runs step by step (`web_snapshots_compare`: steps aligned by `step_id`,
|
||
network and event-count differences), with plain text `normalization` rules
|
||
|
||
### R3.7 Program runs you can compare
|
||
Owner: open · Status: done
|
||
Running the same app or game twice (`runtime_walkthrough_record`) should only differ where its
|
||
behaviour differs, so `runtime_captures_compare` can say what changed.
|
||
- [x] Normalise captures (`normalization`): process ids, loopback ports, working/temp paths, literal
|
||
patterns and time buckets, with what was replaced recorded
|
||
- [x] `runtime_captures_compare` reports which terminal lines changed (first changed line, lines only
|
||
on each side with line numbers), not only their counts
|
||
- [x] Windows: `runtime_walkthrough_record` runs the program through `platform::process` (its Job
|
||
Object ends every child) and reads its output through pipes, in the same result shape, saying
|
||
so (`pty_backend: "pipes"`, a `no_terminal` limitation); typed input, resizes, signals and
|
||
watched files still need the terminal capture (Linux, macOS) and are refused
|
||
|
||
---
|
||
|
||
### R3.8 Game archives
|
||
Owner: open · Status: done
|
||
Games keep their files in archives; someone studying a game first wants to see what's inside.
|
||
- [x] Unreal `.pak`: version, mount point, compression, file list (v1–v11 indexes); an encrypted
|
||
index or encrypted files are named, never guessed (`game_pak_list`; `formats::unreal_pak`;
|
||
checked live on large archives)
|
||
- [x] Unreal `.pak`: extract a file, or a whole folder a page at a time (stored or zlib;
|
||
`game_pak_extract`, streamed, SHA-256, output folders made, files already there skipped; encrypted
|
||
and Oodle files refused with the reason; checked live)
|
||
- [x] Unreal IoStore (`.utoc`/`.ucas`): list the files (`game_iostore_list`, `formats::unreal_iostore`,
|
||
TOC versions 1–8, signed and partitioned containers; encrypted names counted, never guessed;
|
||
checked live)
|
||
- [x] Unity: `*.assets` and level files (serialized files): list the objects by class, with their
|
||
names (`game_unity_list`, `formats::unity_serialized`, format 14+ (Unity 5 on), either byte
|
||
order; checked live on files from Unity 2017 to Unity 6)
|
||
- [x] Unity: UnityFS bundles (`*.bundle`, `data.unity3d`): list the files inside, and the objects
|
||
by class in each serialized file (`game_unity_bundle_list`, `formats::unity_bundle`, LZ4 and
|
||
LZMA; checked live)
|
||
|
||
### R3.9 Game assets out
|
||
Owner: open · Status: in progress
|
||
After seeing what a game packs (R3.8), the next step is taking its pictures and sounds out in
|
||
formats anyone can open.
|
||
- [x] Unity: textures to PNG (uncompressed formats and DXT1/DXT5, pixels inline or in a `.resS`
|
||
file, from `*.assets` files and bundles; `game_unity_asset_export`, `formats::unity_texture`;
|
||
the Texture2D layout of Unity 2017.3–6000 checked live on thousands of textures)
|
||
- [ ] Unity: sounds out (the stored stream, usually FSB5; PCM ones as WAV)
|
||
- [x] Unity: BC7 and BC6H textures (every BC7 mode; BC6H unsigned and signed, tone-mapped to 8-bit
|
||
sRGB, brighter than white clips; `formats::bptc`, written from the Khronos Data Format
|
||
Specification; Unreal's PF_BC7 and PF_BC6H too; checked on two Unity games: 11 of 11 BC7
|
||
textures and 2 BC6H lightmaps saved and looked at)
|
||
- [ ] Unity: crunched textures (DXT1Crunched, DXT5Crunched; two of the games checked use them) and
|
||
the float formats (RHalf, RGHalf, RGBAHalf, RFloat, RGFloat, RGBAFloat)
|
||
- [x] Unreal: textures from `.uasset`/`.uexp` files taken out of a pak, only when the person
|
||
confirms they may unpack the game (`game_unreal_texture_export`, `permission: true`;
|
||
`formats::unreal_texture` for Unreal 4.26–4.27 cooked packages, `formats::pixels` shared with
|
||
Unity, which also gains BC4/BC5; checked live: every texture tried was saved)
|
||
- [ ] Unreal 5 and Oodle: IoStore packages, unversioned properties and Oodle-compressed paks
|
||
- [x] App: a Game files screen: pick a game, see its archives, and take its pictures out after the
|
||
person confirms they have permission (sidebar → Game files; `game_archives_list` finds a
|
||
folder's paks, IoStore files, Unity files and bundles; the permission question is asked once
|
||
per game, then a save dialog)
|
||
- [x] Game files: save pictures from Unity bundles (`game_unity_list` lists a bundle's objects by
|
||
path id, from every serialized file inside it, each naming its `file`; bundles built without
|
||
their Unity version use the one in the game's globalgamemanagers; checked on a Unity game's
|
||
bundles: 27 of 27 sprite atlases saved, and a scene bundle with two serialized files listed)
|
||
- [ ] Game files: save pictures from IoStore containers (needs the Unreal 5 support above)
|
||
- [x] The rebuild's Deep-mode brief points the builder at these tools (`enable_toolset` formats,
|
||
then the `game_*` tools) before any outside converter; a live run had set
|
||
up a third-party unpacker instead
|
||
- [ ] Unreal: sounds and meshes out (so a Deep-mode rebuild needs no outside converter for them)
|
||
|
||
### R3.10 Narrow reads of saved results, and more of an app's set-up
|
||
Owner: open · Status: in progress
|
||
A saved analysis should answer a narrow question without running again, and an app's own code
|
||
should give away more of how it is set up.
|
||
- [x] `core_evidence_browse` pages keep only the rows that match (`matching`: field → value or list
|
||
of values, `a.b` inside a row, null for a missing field), return only chosen fields (`fields`)
|
||
and list the field names of the matching rows (`field_names`); offsets count matching rows.
|
||
Checked on a saved analysis of a 1,706-file Electron package: 4,472 effects → 12 process starts
|
||
- [x] `android_resources_read` takes several locales in one call (`only_locales`, added to
|
||
`locale`), each with its own string table; ones the app lacks are named (`locales_not_found`),
|
||
and a table cut at 5,000 strings says so
|
||
- [x] Electron windows whose options sit in a local object (`const opts = {…}; new
|
||
BrowserWindow(opts)`, or `webPreferences: prefs`) have their webPreferences and preload read,
|
||
with the object's name and line (`options_from`); an object that is written to, exported or
|
||
used any other way, anywhere in the module, keeps the window `dynamic`
|
||
- [x] A spread or computed key in window options or webPreferences makes what it may replace
|
||
unknown (`dynamic`), instead of "not set": found on an installed app whose preferences
|
||
spread in another object. Nine installed Electron apps (19 windows) read the same otherwise
|
||
- [x] An app's file inventory tells a Java class from a universal Mach-O by its contents, not its
|
||
file name
|
||
- [ ] Android locales: try `only_locales` on a real APK (only the test fixture so far)
|
||
- [x] `app_graphs_compare`: chunks whose names differ only by a build hash
|
||
(`Doctor-DIpf_I7N.js` → `Doctor-DrkWsav4.js`, `main.3f2a9c1b.js`, `chunk-UGNM3QZK.mjs`) are one
|
||
changed module with the old name (`was`), not removed + added; only when the name without
|
||
its hash is unique on each side (Vite's several `index-<hash>.js` aren't guessed between)
|
||
- [ ] `app_graphs_compare` on the two real Electron releases again, to count how many of the
|
||
162 "removed" now pair (and pair the `index-<hash>` chunks by structure if many stay)
|
||
- [x] Results over Claude Code's tool-output limit (about 25k tokens) come back summary-first: a
|
||
connection whose client is Claude Code (`clientInfo.name` = `claude-code`, checked against
|
||
Claude Code 2.1.305) gets an 80 KB response budget instead of 8 MiB, so a bigger evidence
|
||
record collapses to its summary with `core_evidence_browse` to page it;
|
||
`CRAFT_ANYTHING_MCP_MAX_RESPONSE_BYTES` still sets it explicitly
|
||
- [ ] A live chat run with a big comparison result, to see Claude Code page it with
|
||
`core_evidence_browse` (and whether 80 KB is the right size)
|
||
|
||
### R3.11 Single-file apps, game files and stand-in backends
|
||
Owner: open · Status: in progress
|
||
Read what real apps and games ship as (one file carrying the rest), and let a rebuilt website run
|
||
against what its original was recorded answering.
|
||
- [x] One seam for files packed inside a file (`formats::packed`), with `core_packed_files_list`
|
||
and `core_packed_files_unpack`; a new kind is one reader and one table line
|
||
- [x] Readers: Bun and Node.js single-file programs, .NET single-file apps, PyInstaller, Godot PCK.
|
||
A real 279 MB Bun program read and unpacked (2,889 modules); real Node blobs read
|
||
- [x] `formats_bytecode_show` (Python bytecode), `game_unity_code_names_show` (Unity IL2CPP metadata),
|
||
`formats_ai_weights_show` (GGUF, safetensors; a real file of each read), `native_publisher_show`, `native_library_calls_list`
|
||
(delay-load too), `native_offered_symbols_list`, Zstandard ZIP members
|
||
- [x] `app_folder_explain`: what an install folder is made with, and what to run next, from a table
|
||
of runtimes; checked against about 25 installed Mac apps
|
||
- [x] A stand-in backend from a network recording (`web_standin_build`, `_start`, `_stop`, `_show`),
|
||
GraphQL and tRPC operations in app maps, and `web_walkthrough_check`
|
||
- [x] `app_builds_line_up` (two real builds of a 276 MB program in 42 s),
|
||
`native_source_folder_write` (resumable), `android_components_list`
|
||
- [x] CLI: `--json -` takes parameters from a pipe; `--format csv` and `tsv` write a result's rows,
|
||
with spreadsheet formulas kept as text. Kimi Code, Factory Droid and WorkBuddy in setup
|
||
- [x] Real samples built with the real tools and read back (none are kept in the repo): PyInstaller
|
||
with Python 3.9 and 3.14 (every archive member the same bytes as PyInstaller's own reader,
|
||
on the 3.14 program), .NET 8 for macOS, Linux and Windows, plain and compressed self-contained,
|
||
Godot 3.6, 4.3 and 4.5 (formats 1, 2, 3; MD5s match on the files checked), a real APK. 214 real
|
||
Python modules read the same as CPython's `marshal`
|
||
- [ ] A real IL2CPP `global-metadata.dat` (needs a Unity build or a shipped game); synthetic so far.
|
||
Compare a real APK's components against another tool's reading
|
||
- [ ] Stand-in backend: a browser-made HAR and a real front end talking to it (only a hand-written
|
||
HAR so far); GraphQL and tRPC on real bundles (only hand-written minified shapes)
|
||
- [ ] Stand-in backend: writes that change later reads (recorded answers are replayed as they are)
|
||
- [ ] Tell the builder about the stand-in backend in a website's brief, and start one from a recording
|
||
- [ ] Stand-in backend: a 304 recorded with no body is replayed as a bare 304, which a fresh browser can't use; say so or leave it out
|
||
- [ ] Check the Kimi Code, Factory Droid and WorkBuddy registrations in the real apps
|
||
- [ ] Godot format 4 and IL2CPP metadata 38 and 39 are header-only; read their tables once a real
|
||
file can be checked
|
||
- [ ] Bun: take source maps and bytecode out as files; a standalone `.pyz`; PyInstaller scripts carry
|
||
no Python version of their own, so `formats_bytecode_show` needs `python_version`
|
||
- [ ] `native_source_folder_write` uses the built-in decompiler only: use Ghidra in Deep mode when it's there
|
||
- [ ] `native_publisher_show` hashes the file minus the signature fields, not section by section;
|
||
a file with unusual gaps between sections can read as different
|
||
- [ ] Two engine tests fail on a Mac, on `main` too: `real_binary_end_to_end_if_present` expects
|
||
`/bin/ls` to be Linux, and `auto_mode_keeps_going_but_never_does_the_dangerous_things`
|
||
refuses `rm -rf` of a temp path under `/var/folders`
|
||
- [ ] Looked at and left for later, as niche for now: firmware image kinds (ESP32, U-Boot, device
|
||
trees, Intel HEX), Erlang BEAM, GPU code objects, kernel modules, GameCube DOL, Mac crash
|
||
reports, SAZ and qlog captures, protobuf bodies, R8 mappings, JNI links, MCP over HTTP
|
||
|
||
## M4 Release (later)
|
||
|
||
Owner: maintainers · Status: in progress (v0.1.0 is released; managed tier and follow-ups remain)
|
||
- [x] Secret scan: `cargo xtask secrets` (in `cargo xtask ci` and `.githooks/pre-commit`); files and full
|
||
history scanned clean on 2026-10-19
|
||
- [x] **decision**: commit author emails stay as they are (they're public on other repos too)
|
||
- [x] Before going public: `cargo xtask secrets --history` on every branch: clean on 2026-10-20
|
||
(all 16 remote branches); run it again right before the switch if more lands
|
||
- [x] `CONTRIBUTING.md` and `SECURITY.md` (private reports through GitHub's "Report a vulnerability")
|
||
|
||
Launch checklist (source-only first; signed installers can follow):
|
||
- [ ] Maintainers: protect `main` (PRs required, squash only) and turn on private vulnerability
|
||
reporting (Settings → Code security), which SECURITY.md points to
|
||
- [x] Live website build on current `main` (2026-10-20, Claude Code, `example.com`, 1 round):
|
||
Ready, 91% alike (95% / 91% / 87% at 1440 / 768 / 390 px); the builder used `rebuild_check`
|
||
mid-turn (89% → 91%); no processes left. App and game left for after launch (maintainers'
|
||
choice)
|
||
- [x] R0.6: the tool-id mapping table and both ids working, before the ids are public
|
||
- [x] STATUS: a last pass so everything not run live says so (2026-10-20: ACP agents, WASM checks
|
||
from Rust, Cutter, Vineflower/Apktool, and Ghidra's guided install off Windows are marked)
|
||
- [x] Pull request and issue templates (`.github/`): the roadmap item, what was and wasn't
|
||
verified, the checks; security reports go to private reporting
|
||
- [x] README quick start for someone new: Rust version, Linux packages (as CI installs them),
|
||
clone, run the app, `cargo install` the CLI, `doctor`, then a first rebuild
|
||
- [ ] Follow the README quick start on a clean machine (Windows, and Linux or macOS)
|
||
- [x] CI runs on every pull request and push to `main` once the repository is public (skipped while
|
||
it's private, so it costs nothing now): fmt, clippy, all tests with `--no-fail-fast`,
|
||
layers, the tool list, schemas and the secret scan, plus the wasm32 check
|
||
- [x] CI and release workflows on `main`: Linux builds no longer need screen-capture libraries,
|
||
so CI passes there; the README lists each download (2026-10-22)
|
||
- [x] Packaging like the other Crafting Apps (2026-10-22): `packaging/icons.sh` rebuilds every app
|
||
icon from the SVG (`cargo xtask ico` packs the Windows .ico); the DMG opens on a laid-out
|
||
window (`packaging/macos/dmg`: background, icon positions) and is built with
|
||
`hdiutil create`, with the app inside the mounted image checked by `codesign --strict`; the
|
||
FreeBSD workflow runs on main and packaging PRs (when public) with a package-contents check
|
||
- [ ] Build the new DMG on a Mac (release dry run) and look at the window once
|
||
- [x] The first release: v0.1.0 built from `release`, signed (Apple Developer ID and notarization,
|
||
Azure Trusted Signing) and published on 2026-10-31
|
||
- [ ] Move the release workflows off actions that target Node.js 20 (`actions/checkout@v4`,
|
||
`upload-artifact@v4`, `download-artifact@v4`); GitHub warns on every job and forces them onto Node.js 24
|
||
- [x] **decision**: the free, open-source release doesn't wait on Anthropic. Each person runs their
|
||
own Claude Code under their own account and Anthropic's terms (claude-code.md)
|
||
- [ ] Managed tier: account backend, usage metering (Claude API under our own key only)
|
||
- [ ] Before the managed tier or any paid offering: confirm the Claude Code orchestration question
|
||
with Anthropic (claude-code.md)
|