app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
Merge PR #278: 0.2.12: tour fields stays on the container's host; Altium board reads survive the schematic in front
7 files changed
+84−20
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.11"+version = "0.2.12" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.11"+version = "0.2.12" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.11"+version = "0.2.12" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.11"+version = "0.2.12" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
crates/adom-aiflow/src/main.rs+31−6@@ -513,6 +513,13 @@ fn primary_machine() -> Option<String> { preboard::primary_from_targets(&serde_json::from_slice::<Value>(&o.stdout).ok()?) } +/// The desktop this container runs on, when that is knowable: under Hydrogen's local mode+/// (ADOM_DESKTOP_MODE=local, a WSL distro) the container shares the Windows machine's hostname.+fn container_host() -> Option<String> {+ if std::env::var("ADOM_DESKTOP_MODE").ok().as_deref() != Some("local") { return None; }+ std::fs::read_to_string("/proc/sys/kernel/hostname").ok().map(|h| h.trim().to_string()).filter(|h| !h.is_empty())+}+ /// The board's own copper as plan-shaped entries (one per net and track width, one per via), so the /// analyses see a board whose copper is already on it (an adopted, hand-reworked or natively routed /// board) and not only what a routing plan adds.@@ -4796,8 +4803,15 @@ fn main() { // hosting this container: film it there (the user's primary machine), not on the test box let local_proxy = what == "fields" && std::env::var("VSCODE_PROXY_URI").map(|u| preboard::proxy_is_local(&u)).unwrap_or(false); let mut on_note = String::new();- let br = match on {- Some(box_name) if what == "fields" => Bridge { ai_thread: t.clone(), target: box_name.clone() },+ let host = if local_proxy { container_host() } else { None };+ let br = match (on, &host) {+ // a box other than the host cannot open the localhost URL: it would film an error page+ // (2026-10-20: a run sent the walkthrough to the user's own machine this way)+ (Some(box_name), Some(h)) if what == "fields" && !box_name.eq_ignore_ascii_case(h) => err(+ &format!("`--on {box_name}` cannot film the Fields app: its URL ({}) is a localhost proxy, and only {h}, the machine running this container, can open it", std::env::var("VSCODE_PROXY_URI").unwrap_or_default().replace("{{port}}", &port.to_string())),+ &[format!("Run `tour fields --port {port}` with no --on: it films on {h}. Never move the walkthrough to another machine to get past a window that did not appear; retry it on {h}.")]),+ (Some(box_name), _) if what == "fields" => Bridge { ai_thread: t.clone(), target: box_name.clone() },+ (_, Some(h)) => { on_note = format!("filmed on {h}, the machine running this container: the Fields URL is a localhost proxy only it can open"); Bridge { ai_thread: t.clone(), target: h.clone() } } _ if local_proxy => match primary_machine() { Some(m) => { on_note = format!("filmed on {m}, the user's primary machine: the Fields URL is a localhost proxy only the container's host can reach (--on <box> overrides)"); Bridge { ai_thread: t.clone(), target: m } } None => { on_note = "the Fields URL is a localhost proxy only the container's host can reach, and `adom-bridge targets` named no primary machine: filmed on the test box; say --on <the machine hosting this container> if the window does not appear".into(); bridge_of(&r, &cli) }@@ -4956,11 +4970,16 @@ fn main() { std::thread::sleep(std::time::Duration::from_secs(3)); } // a fresh pup session every time: a session whose browser died answers browser_gone forever- let sid = format!("aiflow-fields-{}", now().chars().filter(|c| c.is_ascii_digit()).skip(8).collect::<String>());+ let mut sid = String::new();+ let mut open_err = None;+ let mut hwnd: Option<i64> = None;+ // one more try in a fresh session when the first window never shows (a transient pup miss, 2026-10-20)+ for attempt in 0..2 {+ if attempt > 0 { let _ = br.call("pup_close_window", &json!({"sessionId": sid})); }+ sid = format!("aiflow-fields-{}{}", now().chars().filter(|c| c.is_ascii_digit()).skip(8).collect::<String>(), if attempt > 0 { "-retry" } else { "" }); let op = br.call("pup_open_window", &json!({"sessionId": sid, "url": proxy, "width": 1600, "height": 1000, "reason": "aiflow fields walkthrough: the AI's current and heat analysis on camera"}));- let open_err = if Bridge::ok(&op) { None } else { Some(op["errorCode"].as_str().unwrap_or("?").to_string()) };+ open_err = if Bridge::ok(&op) { None } else { Some(op["errorCode"].as_str().or(op["error"].as_str()).unwrap_or("?").chars().take(200).collect::<String>()) }; // pup can take a minute to bring a browser up on a cold box: the window is polled for, not assumed- let mut hwnd: Option<i64> = None; for _ in 0..40 { std::thread::sleep(std::time::Duration::from_secs(2)); let lw = br.call("pup_list_windows", &json!({}));@@ -4973,7 +4992,13 @@ fn main() { .filter(|h| *h > 0); if hwnd.is_some() { break; } }- let Some(win) = hwnd else { let _ = br.call("pup_close_window", &json!({"sessionId": sid})); err(&format!("the app's window did not appear on {} (no window for this walkthrough session{})", br.target, open_err.map(|e| format!("; pup said {e}")).unwrap_or_default()), &["`adom-bridge --target <box> pup_readiness` says whether pup is ready there; a browser_gone means pup's browser died: `pup_close_window` the old sessions or restart pup on the box.".into(), if preboard::proxy_is_local(&proxy) { format!("The URL {proxy} is a localhost proxy: only the machine hosting this container can open it. `tour fields --on <that machine>`.") } else { String::new() }, on_note.clone()]); };+ if hwnd.is_some() { break; }+ }+ let Some(win) = hwnd else { let _ = br.call("pup_close_window", &json!({"sessionId": sid})); err(&format!("the app's window did not appear on {} (no window for this walkthrough session{})", br.target, open_err.map(|e| format!("; pup said {e}")).unwrap_or_default()), &["`adom-bridge --target <box> pup_readiness` says whether pup is ready there; a browser_gone means pup's browser died: `pup_close_window` the old sessions or restart pup on the box.".into(), match (preboard::proxy_is_local(&proxy), &host) {+ (true, Some(h)) if br.target.eq_ignore_ascii_case(h) => format!("This is the right machine: {h} runs this container and is the only one that can open {proxy}. Retry `tour fields --port {port}` here (pup can take longer than this wait on a cold box); never pass --on another machine, which cannot open a localhost URL."),+ (true, Some(h)) => format!("The URL {proxy} is a localhost proxy: only {h}, the machine running this container, can open it. `tour fields --port {port}` with no --on films there."),+ (true, None) => format!("The URL {proxy} is a localhost proxy: only the machine hosting this container can open it. `tour fields --on <that machine>`; ask the user which machine that is rather than guessing."),+ _ => String::new() }, on_note.clone()]); }; let save_to = std::fs::canonicalize(&dir).unwrap_or(dir.clone()).display().to_string(); let (step, visit) = (r.data["currentStep"].as_str().unwrap_or("fields").to_string(), r.data["steps"][r.data["currentStep"].as_str().unwrap_or("fields")]["visits"].as_array().map(|a| a.len()).unwrap_or(1)); // the editor clip of this visit is not the walkthrough: the app window is the camera
crates/aiflow-bridge/src/altium.rs+29−1@@ -59,7 +59,16 @@ pub fn to_kicad_state(a: &Value) -> Value { } impl AltiumEda {- fn read(&self, t: &dyn Transport) -> Value { t.send("altium_routing_state", &json!({"expectPath": self.path})) }+ fn read(&self, t: &dyn Transport) -> Value {+ let r = t.send("altium_routing_state", &json!({"expectPath": self.path}));+ // the schematic step leaves its sheet in front, and Altium reads only the current PCB: the+ // first placement read failed this way for 2 minutes of retries (2026-10-20). Open the board, read again.+ let not_current = r["errorCode"].as_str() == Some("no_current_board")+ || r["data"]["error"].as_str().map_or(false, |e| e.contains("no current PCB board"));+ if !not_current { return r; }+ let _ = t.send("altium_show_2d_board", &json!({"filePath": self.path}));+ t.send("altium_routing_state", &json!({"expectPath": self.path}))+ } /// The guard every edit carries: the board, and the revision the flow last read. fn guard(&self, args: &Value) -> Value { let mut g = json!({"expectPath": self.path});@@ -427,6 +436,25 @@ mod tests { assert_eq!(b.zones().len(), 1); } + #[test]+ fn a_read_with_the_schematic_in_front_opens_the_board() {+ let sent = RefCell::new(Vec::new());+ let t = |v: &str, a: &Value| {+ sent.borrow_mut().push(v.to_string());+ let shown = sent.borrow().iter().any(|s| s == "altium_show_2d_board");+ if v == "altium_routing_state" && !shown { return json!({"status": "error", "error": "", "data": {"ok": false, "error": "no current PCB board"}}); }+ json!({"success": true, "revision": "r3", "path": a["filePath"]})+ };+ let e = AltiumEda { path: "C:/b.PcbDoc".into() };+ assert_eq!(e.read(&t)["revision"], json!("r3"));+ assert_eq!(*sent.borrow(), vec!["altium_routing_state", "altium_show_2d_board", "altium_routing_state"]);+ // any other failure is answered as is, without touching the documents+ let other = |v: &str, _a: &Value| { sent.borrow_mut().push(v.to_string()); json!({"status": "error", "errorCode": "agent_busy"}) };+ sent.borrow_mut().clear();+ assert_eq!(e.read(&other)["errorCode"], json!("agent_busy"));+ assert_eq!(sent.borrow().len(), 1);+ }+ #[test] fn edits_go_to_altium_in_its_own_frame() { let sent = RefCell::new(Vec::new());
docs/release-0.2.12.mdadded+11@@ -0,0 +1,11 @@+# adom-aiflow 0.2.12++Fixes from a fresh end-to-end Altium run (Claude in Hydrogen on DESKTOP-BAR6S0F, 2026-10-20). The run went from prompt to `finish` in 71 min and delivered at 78 min.++- **`tour fields` never leaves the machine that runs the container.** The Fields URL is a localhost proxy (`http://{{port}}.localhost:47083/`), so only the machine running the container can open it. When pup's window didn't appear on the laptop, the old hint said "`tour fields --on <that machine>`" without naming the machine. The AI guessed the user's own workstation and filmed a blank page there for 12 minutes. Under Hydrogen's local mode the host is now known (the container's hostname):+ - with no `--on`, the tour films on the host;+ - `--on` any other machine is refused before anything opens;+ - the failure hint names the host and says to retry there.+- **A window that never shows gets one retry** in a fresh pup session before the tour gives up.+- **Altium: a board read opens the board when the schematic is in front.** Altium reads only the current PCB. The schematic step leaves its sheet in front, so the first `place land` failed after 2 minutes of retries ("refused at part 1 of 30: null"), and the placement clip filmed the schematic. The Altium backend now opens the board (`altium_show_2d_board`) and reads again. Verified live: the same case now lands in 2.9 s.+- Goes with altium-bridge 0.2.54 (`no_current_board` error code, and STEP occurrences named after their designators so `step2glb --molecule` anchors an Altium board). AI Flow works with 0.2.53 too: it also recognises the agent's own message.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.11",+ "version": "0.2.12", "title": "AI Flow", "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.", "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.11",+ "version": "0.2.12", "title": "AI Flow", "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad, Fusion 360 and Altium Designer today (the same steps and gates through each tool's bridge); Adom's own web apps next.", "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",