diff --git a/crates/ai/src/rebuild/check.rs b/crates/ai/src/rebuild/check.rs index 4179a485..8f35d584 100644 --- a/crates/ai/src/rebuild/check.rs +++ b/crates/ai/src/rebuild/check.rs @@ -1,4 +1,5 @@ //! Building a project with its own tools (cargo, npm, or the game engine's), for [`BuildCheck`]. +//! A plain static site has no build, and passes as it is. use super::*; @@ -83,6 +84,7 @@ pub(super) fn build_check(app: &Path, place: &str, stack: &str, editor: Option<& } _ if app.join("Cargo.toml").is_file() => run(Path::new("cargo"), &["build".into(), "--quiet".into()], 2 * 60), _ if app.join("package.json").is_file() => run(Path::new(if cfg!(windows) { "npm.cmd" } else { "npm" }), &["run".into(), "build".into()], 2 * 60), + (None, _) if static_site(app) => return (true, "Nothing to build: a static site.".into()), _ => return (false, format!("No project to build found in {place} yet.")), }; match result { @@ -96,10 +98,22 @@ pub(super) fn build_check(app: &Path, place: &str, stack: &str, editor: Option<& /// rather than the engine's. What was built decides, since the person may have asked for /// something other than the engine picked at the start. fn engine_for(stack: &str, app: &Path) -> Option { + if static_site(app) { + return None; + } let own_project = app.join("Cargo.toml").is_file() || app.join("package.json").is_file(); game::Builder::from_stack(stack).filter(|b| !own_project || engine_project(*b, app)) } +/// Whether `app` is a static site with nothing to build: a page with something in it, as the +/// preview finds it, and no game engine's project beside it. A site's `assets/` folder is no sign +/// of Unity (file names ignore case on Windows and macOS), so Unity needs its settings folder too. +fn static_site(app: &Path) -> bool { + let engine = app.join("project.godot").is_file() || uproject(app).is_some() || (app.join("Assets").is_dir() && app.join("ProjectSettings").is_dir()); + let page = |site: PathBuf| std::fs::read(site.join("index.html")).is_ok_and(|html| html.iter().any(|b| !b.is_ascii_whitespace())); + !engine && site_dir(app).is_some_and(page) +} + /// The Unreal project file in `app`, if there is one. fn uproject(app: &Path) -> Option { std::fs::read_dir(app).into_iter().flatten().flatten().map(|e| e.path()).find(|p| p.extension().is_some_and(|x| x == "uproject")) @@ -149,4 +163,50 @@ mod tests { std::fs::write(app.join("Cargo.toml"), "").unwrap(); assert_eq!(engine_for("Rust + macroquad", app), None); } + + #[test] + fn a_static_site_has_nothing_to_build() { + let d = tempfile::tempdir().unwrap(); + let app = d.path(); + let stop = CancelToken::default(); + let passed = (true, "Nothing to build: a static site.".to_string()); + let check = |stack, editor| build_check(app, "app/", stack, editor, &stop); + assert!(!check("Plain HTML and CSS", None).0, "nothing there yet"); + + // #496: the page itself, with no manifest. + std::fs::write(app.join("index.html"), "

Hello

").unwrap(); + assert_eq!(check("Plain HTML and CSS", None), passed); + // What was built decides: a page made under a game engine's stack isn't a missing project. + let unity = Path::new("C:/Engines/Unity/Editor/Unity.exe"); + assert_eq!(check("Unity", Some(unity)), passed); + + // A site's own assets folder doesn't make it a Unity project. + std::fs::create_dir(app.join("Assets")).unwrap(); + assert_eq!(check("Plain HTML and CSS", None), passed); + assert_eq!(check("Unity", Some(unity)), passed); + + // A real engine project doesn't pass for the page beside it, with or without the editor, + // and whichever engine the stack named. + std::fs::create_dir(app.join("ProjectSettings")).unwrap(); + assert!(!check("Unity", None).0); + assert!(!check("Plain HTML and CSS", None).0); + std::fs::remove_dir(app.join("ProjectSettings")).unwrap(); + for project in ["project.godot", "Game.uproject"] { + std::fs::write(app.join(project), "").unwrap(); + assert!(!check("Unity", None).0, "{project}"); + std::fs::remove_file(app.join(project)).unwrap(); + } + + // A blank page isn't a site, and doesn't pass for one under an engine's stack either. + std::fs::write(app.join("index.html"), " \n").unwrap(); + assert!(!check("Plain HTML and CSS", None).0); + std::fs::remove_dir(app.join("Assets")).unwrap(); + assert_eq!(check("Unity", Some(unity)), (false, "No Unity project found in app/ yet.".into())); + + // Already-built pages count too, as they do for the preview. + let built = tempfile::tempdir().unwrap(); + std::fs::create_dir(built.path().join("dist")).unwrap(); + std::fs::write(built.path().join("dist").join("index.html"), "

Hello

").unwrap(); + assert_eq!(build_check(built.path(), "app/", "Builder's choice", None, &stop), passed); + } } diff --git a/crates/ai/src/rebuild/kinds/mod.rs b/crates/ai/src/rebuild/kinds/mod.rs index 661afeb4..24fbd858 100644 --- a/crates/ai/src/rebuild/kinds/mod.rs +++ b/crates/ai/src/rebuild/kinds/mod.rs @@ -226,7 +226,7 @@ impl Check for DeepOnly { } } -/// The project's own build must succeed (cargo, npm, or the game engine's). +/// The project's own build must succeed (cargo, npm, or the game engine's); a static site has none. pub struct BuildCheck; impl Check for BuildCheck { diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1b7156fc..cb357be0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -718,10 +718,10 @@ What the person asks for comes first, in every kind; the stack table is only the - [ ] 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 -- [ ] Build check: a page asked for in plain HTML and CSS has nothing to build, but the check - only knows Cargo.toml, package.json and engine projects ("No project to build found in - app/"), so a builder added a placeholder package.json (`"build": "node -e \"0\""`) to pass; - count a static site (an `index.html` in app/) as built +- [x] Build check: a page asked for in plain HTML and CSS has nothing to build, and the check + failed it for having no manifest, so a builder added a placeholder package.json to pass. A + static site, as the preview finds it (`site_dir`), now passes as it is unless its page is + blank or a game engine's project sits beside it, and says nothing was built (#496) - [x] CLI: after the tool name, `--project` is the tool's own option when it has one (`rebuild-check --project `, `project-show --project `), as `--format` is; otherwise, and always before the tool name, it's the global investigation folder @@ -898,6 +898,10 @@ Gaps found while using the app for real projects. - [ ] 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 +- [ ] The keep-awake helper once outlived a CLI `rebuild` on Windows by more than 9 minutes and held + the caller's output pipe; not reproduced in 8 more runs, where it ended with the CLI. Dropping + its guard only raises a flag (`RunningProcess::drop` waits only for `hands_off` programs), so + ending on time rests on the Job Object closing, and its time limit is 7 days - [ ] 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 diff --git a/docs/STATUS.md b/docs/STATUS.md index 693d06bd..a881b8ca 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -22,7 +22,7 @@ workspace clippy, layers, fmt and the secret scan of all history were clean. Chi | Stand-in backends | `web_standin_build` turns a HAR into `standin.json` (secrets dropped, varying path parts templated); `web_standin_start` answers from it on loopback; `web_standin_show` lists what it had no answer for. Writes replay what was recorded and don't change later reads. App maps list GraphQL and tRPC operations; `web_walkthrough_check` checks a scripted session without a browser | Unit tests, real loopback requests, and a run with curl on a hand-written HAR. No browser-made recording or real front end yet | | Websites as targets | `web_site_open` keeps a private browser for the session and every browser tool inspects that page; `web_site_capture` records screenshots at each width (desktop, tablet and phone), rendered HTML, design tokens (colours in hex, spacing, content widths), the CSS breakpoints, the layout at each width (page width, heading sizes, each section's padding, gap, content column and items per row), page health (script and console errors, failed requests) and an accessibility outline, layout outline, assets with font files and requests, links and form outlines. In Clean room the capture keeps only what a visitor sees (no HTML, CSS rules, scripts, requests or errors), and opening a site reports no load errors | Live run on example.com; unit tests | | Electron window comparisons | `app_graphs_compare` retains every unassigned window and every construction with a reused local binding. Windows are paired by module, binding and occurrence, so inserting comments does not hide preference changes | Synthetic application graphs exercise changes in either anonymous window, unique and reused bindings, shifted source lines and unequal counts. Pairing remains positional within each binding group; reordered constructions and real application releases were not checked for this correction | -| Rebuild workflow (Study → Build → Check → Refine) | `crates/ai/src/rebuild/` (kinds, checks and stacks are pluggable: docs/extending.md). Three kinds: website, app, game (a project whose stack is a game engine is always a game). The request decides how to build: a language, framework, engine or platform it names wins over the kind's default stack (Rust + egui for apps, Unity for games, the original's for websites), and the build check, the recorded stack and Play follow what was built (an Electron app opens in its own Electron). Websites: Study also captures up to 4 other pages of the site; menus are opened on a computer and on a phone; the check serves the build on loopback and compares every page at 1440, 768 and 390 px (likeness = mean of layout and perceptual detail, SSIM, averaged over pages, leaving out the parts of the original that change by themselves, found by photographing it three times; the note adds text, page structure, the colours, font, text sizes and spacing the build misses, and per width a page that scrolls sideways, heading sizes and sections whose padding, items per row or content column differ, the errors, failed requests and accessibility gaps the build adds, and the opened menus and pop-ups). A stack or library named in the request (Next.js, shadcn/ui, Tailwind CSS…, `stacks.toml`) is what the builder uses; with none named, a website's stack is the builder's choice (told to match what the original is built with) and the stack it used is recorded from app/package.json. The builder leads: it studies, asks the tools follow-up questions, records what it can't find out as unknowns (`core_unknown_add`; the run reports the open ones), and tests its own build mid-turn with `rebuild_check` (the same checks, for every kind); a check with anything it couldn't compare, or any claim that fails (a website page that scrolls sideways, logs new errors, fails to load a file, lacks a landmark, or a menu that doesn't open; for games each self-test step, for Deep-mode sheets each original number in the code, for console apps the same exit and output as the original's runs), isn't good enough; apps and games by whether they build. Up to 6 refinement rounds, or until a website reaches 90% likeness. A turn cut off by a time limit, crash or dropped connection resumes (up to 3 times); builder turns may run 24 h. A project copied from another computer (or moved to another folder) keeps building with Claude Code: the project keeps a copy of the builder's session (`agent-session.jsonl`) after every turn and puts it back where Claude Code finds it on a computer without it, so the same conversation carries on (#451); when the session still isn't there, a new one starts from the brief, what's in app/ and the kept requests and replies (or the last summary), and questions about the build put the kept session back too, else are answered from the files and forget the missing session (#452). Live runs with Claude Code 2.1.316 on one Mac (2026-11-02), a second computer stood in for by a separate signed-in `CLAUDE_CONFIG_DIR`: a copied project's session was put back and resumed (the builder recalled its first reply from memory); a kept copy of another session was refused and the new session used the handoff; a real project synced from Windows whose session was missing started afresh and reached Ready. Not yet run between two real computers | Unit tests for prompts, stack choice, site detection, feed folding; live runs reached Build and decompiling (below) | +| Rebuild workflow (Study → Build → Check → Refine) | `crates/ai/src/rebuild/` (kinds, checks and stacks are pluggable: docs/extending.md). Three kinds: website, app, game (a project whose stack is a game engine is always a game). The request decides how to build: a language, framework, engine or platform it names wins over the kind's default stack (Rust + egui for apps, Unity for games, the original's for websites), and the build check, the recorded stack and Play follow what was built (an Electron app opens in its own Electron; a plain static site has nothing to build and passes the build check as it is). Websites: Study also captures up to 4 other pages of the site; menus are opened on a computer and on a phone; the check serves the build on loopback and compares every page at 1440, 768 and 390 px (likeness = mean of layout and perceptual detail, SSIM, averaged over pages, leaving out the parts of the original that change by themselves, found by photographing it three times; the note adds text, page structure, the colours, font, text sizes and spacing the build misses, and per width a page that scrolls sideways, heading sizes and sections whose padding, items per row or content column differ, the errors, failed requests and accessibility gaps the build adds, and the opened menus and pop-ups). A stack or library named in the request (Next.js, shadcn/ui, Tailwind CSS…, `stacks.toml`) is what the builder uses; with none named, a website's stack is the builder's choice (told to match what the original is built with) and the stack it used is recorded from app/package.json. The builder leads: it studies, asks the tools follow-up questions, records what it can't find out as unknowns (`core_unknown_add`; the run reports the open ones), and tests its own build mid-turn with `rebuild_check` (the same checks, for every kind); a check with anything it couldn't compare, or any claim that fails (a website page that scrolls sideways, logs new errors, fails to load a file, lacks a landmark, or a menu that doesn't open; for games each self-test step, for Deep-mode sheets each original number in the code, for console apps the same exit and output as the original's runs), isn't good enough; apps and games by whether they build. Up to 6 refinement rounds, or until a website reaches 90% likeness. A turn cut off by a time limit, crash or dropped connection resumes (up to 3 times); builder turns may run 24 h. A project copied from another computer (or moved to another folder) keeps building with Claude Code: the project keeps a copy of the builder's session (`agent-session.jsonl`) after every turn and puts it back where Claude Code finds it on a computer without it, so the same conversation carries on (#451); when the session still isn't there, a new one starts from the brief, what's in app/ and the kept requests and replies (or the last summary), and questions about the build put the kept session back too, else are answered from the files and forget the missing session (#452). Live runs with Claude Code 2.1.316 on one Mac (2026-11-02), a second computer stood in for by a separate signed-in `CLAUDE_CONFIG_DIR`: a copied project's session was put back and resumed (the builder recalled its first reply from memory); a kept copy of another session was refused and the new session used the handoff; a real project synced from Windows whose session was missing started afresh and reached Ready. Not yet run between two real computers | Unit tests for prompts, stack choice, site detection, feed folding; live runs reached Build and decompiling (below) | | Context-summary usage | Successful API context summaries contribute to the message totals consumed by build usage and receive a `model_turn` journal entry with `purpose: context_summary`. Cached-token pricing is retained; an unknown summary price makes the total unknown, and a failed call adds no invented usage | Scripted-model tests exercise the real `ApiConversation`, transcript resumption, exact input/output and cache-price totals, journal entries, empty summaries, local zero cost, unknown prices and failed summaries. No live provider or end-to-end rebuild was used for this correction | | Games | Engine detection for 12 engines (Unreal, Unity, Godot, Source, GameMaker, RPG Maker, Ren'Py, MonoGame, LÖVE, CryEngine, Cocos, Cube) with versions. Unreal and Godot games are rebuilt in the same engine, others in Unity; installed engines are found, Godot installs itself, others get guided installs. Picking an inner folder (Engine, Binaries, Paks…) finds the game's folder. Just look inside a game shows what it's made of (`core_target_triage` on its folder: engine and version, how its code ships, the libraries its Unity assembly names give away, its own code, scenes and size) and opens it in the chat to ask about it. The builder keeps `app/MECHANICS.md` and its structured twin `app/mechanics.json` (checked after each build), in Deep mode only, may reuse the original's readable files for private use (never decrypting; generated pictures and 3D models replace its art when the person turns them on; Clean room copies none of its files), lists them in `CREDITS.md` (checked, with the private-use line in `README.md`); a game is ready only when its `--self-test` shows the player moving, acting and the HUD drawn, and the result can be started from the project page | Unit tests; live runs on Windows | | Clean room and Deep mode | Clean room (the default) runs only the tools in `engine/src/clean_room.rs`: ones that touch no target or watch from outside (the website as a visitor sees it, pictures of a window, its controls, recorded runs, a phone's screen). Every other tool, a plugin's included, is refused in the app, CLI, MCP and agents, with a way to turn on Deep mode. A Clean room build studies an app or a game only from the person's pictures and words (and a picture of its window when runtime study is on); its builder's reads and commands go through an approval rule that keeps it out of the original's folder and site (with Claude Code, and with ACP agents for what they ask about; other agents aren't held to it, see roadmap R2.2). Deep mode (`deep_mode` in Settings, `--deep` for one CLI run) uses every tool: the builder sees them all, and a website's study also reads its JavaScript and watches its start page for ten seconds (capture/how-it-works.md). Ghidra adds its decompiler in Deep mode when it's installed: it is found, or installed after asking. Study starts one analysis of the main program, saved and shared with the builder's tool server through a lock file (up to 8 h, Java gets half the machine's memory or the amount set in Settings → Engines, 4 to 31 GB, four slow passes skipped). The builder must decompile before the build is checked; addresses the built-in analysis can't place go to Ghidra, which makes a function on demand (for this run only) where its analysis has none, e.g. code reached only through tables of pointers. Without Ghidra, Deep mode uses the built-in engines | Live runs with Ghidra 12.1.4 on Windows (a shared analysis, and a decompile in seconds on a large program). Live runs with Ghidra 12.2 on Linux x86-64 (decompiles by name in position-independent programs and shared libraries, whose addresses Ghidra moves). The guided install of Ghidra and its Java is offered on Linux and macOS too, but has only been tried on Windows. Clean room and Deep mode: engine, AI, MCP, CLI and app tests; no live Clean room build run yet | diff --git a/docs/pitfalls.md b/docs/pitfalls.md index 42bb1dd1..141165b3 100644 --- a/docs/pitfalls.md +++ b/docs/pitfalls.md @@ -412,6 +412,11 @@ mangled by the shell several times. Write multi-line edit scripts to a file and line endings, so a scripted size change rewrote 19 LF files with CRLF (git only warned). Write with `write_bytes(s.encode())` and check `git ls-files --eol -m` after scripted edits. +**A process search that found itself.** Looking for a helper by a word in its command line, with +that word in the search's own command, listed the search: "a helper is left behind" after every +run, with a new pid each time. The new pid was the clue. Build the pattern from parts +(`'Set' + 'Thread'`), and don't stop processes by pattern on a machine where real builds may run. + **A command that waits for a terminal.** `git stash show -p` opened a pager and an empty `python - <<'EOF'` heredoc opened the interactive interpreter; both sat waiting for input until the tool timed out, with work left in a stash. Never run anything that can page or prompt: pass