← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.56"+version = "0.1.57" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.56"+version = "0.1.57" dependencies = [  "roxmltree",  "serde",@@ -42,7 +42,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "serde",@@ -51,7 +51,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "serde",@@ -70,7 +70,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "serde",@@ -79,7 +79,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.56"+version = "0.1.57" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.56"+version = "0.1.57" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.56"+version = "0.1.57" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+5−2
@@ -37,7 +37,9 @@ When the human asks for a board in Fusion ("a 12 V to 5 V buck converter using t the whole flow below is yours: they should never have to spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.11.33 or later. Hydrogen stays full screen behind Fusion; keep Fusion windowed (about 90 % of the screen), never maximized, so the human sees both and a screen recording-catches Fusion's 3D view.+catches Fusion's 3D view. `step` brings the window each step works in to the front: Hydrogen for intake,+design, simulation, sourcing, the library, current, thermal and fields (the chat and its webviews), Fusion+for the schematic, placement, routing, pours, 3D and finish; the board is framed by its outline.  1. **The parts, from their wiki component pages.** Design the circuit (aiflow-circuit-design), then find    each part's component page on the Adom wiki (a reference design on the wiki, such as a molecule page,@@ -53,7 +55,8 @@ catches Fusion's 3D view. 3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> [--fixed <fixed.json>] --name <Design>`    starts the design with the fab's rules, draws the schematic in labelled groups, checks it pin by pin,    puts back any 3D model Fusion dropped when a part was added, and saves the design.-4. **Simulate before the board.** Run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;+4. **Simulate before the board (required: `step placement` waits for a `step simulation` after the+   schematic).** `step simulation`, then run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;    ngspice in the container is the default): `adom-spice start`, `adom-spice study <study.json>`,    `adom-spice run ngspice`, `adom-spice show` (it opens in a Hydrogen webview; press its fullscreen    button so the human sees it large). Check start-up, ripple and the load step against the
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+19−1
@@ -3643,6 +3643,18 @@ fn main() {             let known: Vec<&str> = known.iter().map(String::as_str).collect();             let mut clip_stopped = false;             let prev = r.data["currentStep"].as_str().map(str::to_string);+            // a Fusion project simulates the drawn schematic, in front of the human, before the board: placement+            // waits until a simulation visit has started after the last schematic visit+            if name == "placement" && !*back && dir.join("fusion").join("project.json").is_file() {+                let last = |s: &str| r.data["steps"][s]["visits"].as_array().and_then(|a| a.iter().filter_map(|v| v["at"].as_str()).last()).map(str::to_string);+                if let Some(sch) = last("schematic") {+                    if last("simulation").map(|sim| sim < sch).unwrap_or(true) {+                        err("simulate the schematic before the board: no simulation since the schematic was drawn", &[+                            "`step simulation`, then run the circuit in the Adom SPICE dashboard and show it to the human: `adom-spice start`, `adom-spice study <study.json>`, `adom-spice run ngspice`, `adom-spice show` (a Hydrogen webview; widen its pane so it fills the window).".into(),+                            "Check start-up, ripple and the load step against the requirements; change values in the schematic when a measurement misses, and say what changed. Record it: `evidence add --kind simulation --file <result.json>`. Then `step placement`.".into()]);+                    }+                }+            }             record_usage(&mut r, &format!("step:{name}"));             r.step_set(name, *back, why.as_deref());             // per-step clips: the previous step's recording stops, this step's starts (window capture of the editor)@@ -3678,9 +3690,15 @@ fn main() {                 let fusion_run = aiflow_bridge::fusion::active().is_some();                 // a Fusion project before its board exists (library, schematic): film Fusion, never a KiCad window                 let fusion_project = !fusion_run && dir.join("fusion").join("project.json").is_file();+                // the window the step works in comes to the front: Hydrogen for the human-facing steps (the+                // chat, the parts pages, the SPICE dashboard, the fields app), Fusion for the design itself+                if fusion_run || fusion_project {+                    const HYDROGEN_STEPS: [&str; 9] = ["intake", "design", "simulation", "sourcing", "library", "current", "thermal", "fields", "export"];+                    br.front(if HYDROGEN_STEPS.contains(&name.as_str()) { "hydrogen" } else { "fusion" });+                }                 let hwnd = if name == "fields" { None }                     else if fusion_project { br.fusion_hwnd() }-                    else if fusion_run { let h = br.pcb_editor_hwnd(); let _ = br.raw_call("fusion_view", &json!({"action": "fit", "space": "2d"})); h }+                    else if fusion_run { let h = br.pcb_editor_hwnd(); br.fusion_fit_board(); h }                     else { r.data["captures"].as_array().and_then(|c| c.iter().rev().find_map(|e| e.get("hwnd").and_then(|h| h.as_i64()))).or_else(|| br.pcb_editor_hwnd()) };                 let disk = disk_check(&dir, &br, name);                 if let Err(e) = &disk {
crates/aiflow-bridge/src/fusion.rs+30−1
@@ -160,7 +160,7 @@ pub fn translate(verb: &str, args: &Value, doc: &str, call: &dyn Fn(&str, &Value             if verb == "kicad_open_board" { json!({"success": hwnd.is_some(), "hwnd": hwnd}) }             else { json!({"success": true, "data": {"editors": hwnd.map(|h| vec![json!({"kind": "pcb_editor", "hwnd": h}), json!({"kind": "3d_viewer", "hwnd": h})]).unwrap_or_default()}}) }         }-        "kicad_send_key" => { let _ = call("fusion_view", &json!({"action": "fit", "space": "2d"})); json!({"success": true}) }+        "kicad_send_key" => { fit_board(doc, call); json!({"success": true}) }         "kicad_select_net" => call("fusion_electron_run", &json!({"command": format!("SHOW '{}'; WINDOW FIT;", args["net"].as_str().unwrap_or(""))})),         "kicad_select_clear" => call("fusion_electron_run", &json!({"command": "SHOW;"})),         "kicad_open_3d_viewer" => { let r = call("fusion_show_3d_board", &json!({"documentName": doc})); json!({"success": r["success"], "hwnd": main_hwnd(call)}) }@@ -184,6 +184,35 @@ pub fn translate(verb: &str, args: &Value, doc: &str, call: &dyn Fn(&str, &Value     } } +/// Hydrogen's main window on the box (the human's workspace: chat, webviews).+pub fn hydrogen_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> {+    let w = call("desktop_list_windows", &json!({}));+    let out = w.get("output").and_then(|o| o.as_str()).and_then(|s| serde_json::from_str::<Value>(s).ok()).unwrap_or(w.clone());+    let list = out["data"]["windows"].as_array().or(out["windows"].as_array()).cloned().unwrap_or_default();+    list.iter().find(|x| x["image"].as_str() == Some("hydrogen.exe") && x["kind"].as_str() == Some("normal")).and_then(|x| x["hwnd"].as_i64())+}++/// Frame the board, not everything: Fusion's own fit includes any stray item off the board (a part not+/// placed yet, a text), which shrinks the board to a thumbnail. The view is set to the board outline+/// (the dimension layer) with a margin; plain fit when the outline cannot be read.+pub fn fit_board(doc: &str, call: &dyn Fn(&str, &Value) -> Value) -> bool {+    let src = call("fusion_board_source", &json!({"expectDocument": doc}));+    let text = src["source"].as_str().or_else(|| src["data"]["source"].as_str()).unwrap_or("");+    let mut xs = Vec::new(); let mut ys = Vec::new();+    for w in text.split("<wire ").skip(1) {+        let w = &w[..w.find('>').unwrap_or(w.len())];+        if !w.contains("layer=\"20\"") { continue; }+        let num = |k: &str| w.split(&format!("{k}=\"")).nth(1).and_then(|r| r.split('"').next()).and_then(|v| v.parse::<f64>().ok());+        for (a, b) in [("x1", "y1"), ("x2", "y2")] { if let (Some(x), Some(y)) = (num(a), num(b)) { xs.push(x); ys.push(y); } }+    }+    if xs.is_empty() { let _ = call("fusion_view", &json!({"action": "fit", "space": "2d"})); return false; }+    let (x0, x1) = (xs.iter().cloned().fold(f64::MAX, f64::min), xs.iter().cloned().fold(f64::MIN, f64::max));+    let (y0, y1) = (ys.iter().cloned().fold(f64::MAX, f64::min), ys.iter().cloned().fold(f64::MIN, f64::max));+    let m = ((x1 - x0).max(y1 - y0) * 0.12).max(1.0);+    let cmd = format!("WINDOW ({:.3}mm {:.3}mm) ({:.3}mm {:.3}mm);", x0 - m, y0 - m, x1 + m, y1 + m);+    call("fusion_electron_run", &json!({"command": cmd}))["success"].as_bool().unwrap_or(false)+}+ /// Fusion's main window on the test box. pub fn main_hwnd(call: &dyn Fn(&str, &Value) -> Value) -> Option<i64> {     let w = call("desktop_list_windows", &json!({}));
crates/aiflow-bridge/src/lib.rs+10
@@ -190,6 +190,16 @@ impl Bridge {     /// Fusion's main window on the box (a Fusion project films it before the board exists too).     pub fn fusion_hwnd(&self) -> Option<i64> { fusion::main_hwnd(&|v, a| self.raw_call(v, a)) } +    /// Bring Fusion or Hydrogen to the front, so the human (and a screen recording) sees the window the+    /// step works in: Fusion for the schematic and the board, Hydrogen for its webviews and the chat.+    pub fn front(&self, which: &str) -> bool {+        let h = if which == "fusion" { self.fusion_hwnd() } else { fusion::hydrogen_hwnd(&|v, a| self.raw_call(v, a)) };+        match h { Some(h) => self.raw_call("desktop_bring_to_front", &serde_json::json!({"hwnd": h, "reason": format!("show {which} for this step")}))["success"].as_bool().unwrap_or(true), None => false }+    }++    /// Frame the live Fusion board by its outline.+    pub fn fusion_fit_board(&self) -> bool { match fusion::active() { Some(d) => fusion::fit_board(&d, &|v, a| self.raw_call(v, a)), None => false } }+     pub fn pcb_editor_hwnd(&self) -> Option<i64> {         let r = self.call("kicad_window_info", &json!({}));         let d = r.get("data").cloned().unwrap_or(r.clone());
docs/fusion-lane.md+1−2
@@ -72,8 +72,7 @@ its flank and the 5 x 5 mm inductor stood on edge, both Y-up models with a `rota    reply. Parts bound under older rules are bound again on the next `fusion library`.  The check is a backstop, not a proof. Look at the board in 3D before calling it done: `fusion_show_3d_board`,-then `fusion_take_screenshot` (Windows window capture cannot see Fusion's 3D viewport and shows a stale-frame), and every part must sit on its pads as its datasheet drawing shows.+or `adom-aiflow fusion view3d`, and every part must sit on its pads as its datasheet drawing shows.  A failing `analyze` prints `why:` lines with the cause of each field-screen neck and the spec change that fixes it: a via whose copper ring is the narrowest copper (`viaPower`), or another net's track cutting a
docs/release-0.1.57.mdadded+8
@@ -0,0 +1,8 @@+# AI Flow 0.1.57++From the third take of the Fusion buck demo: the run worked, but the human could not see half of it.++- **The right window comes to the front.** On a Fusion project, `step` brings Hydrogen forward for intake, design, simulation, sourcing, the library, current, thermal and fields (the chat and its webviews: the parts pages, the SPICE dashboard, the fields app) and Fusion forward for the schematic, placement, routing, pours, 3D and finish. Before, Fusion sat over Hydrogen for the whole board and the dashboards opened behind it; the live schematic was drawn behind Hydrogen. `fusion view3d` brings Fusion forward itself.+- **The board is framed by its outline.** Fusion's own fit includes anything off the board (a part not placed yet), which left the board a thumbnail in a corner of the view for the whole routing step. Steps now set the view to the board outline with a margin.+- **Simulation before the board is required.** On a Fusion project, `step placement` refuses until a `step simulation` has started after the last `step schematic`, with the Adom SPICE dashboard commands in the hint. Suggesting it was not enough: a run went straight from the schematic to the board.+- Removed a stale hint that said Windows capture cannot see Fusion's 3D view; a screen recording catches it with Fusion windowed.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.56",+  "version": "0.1.57",   "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 today; Altium, Fusion and 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+2−4
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.56",+  "version": "0.1.57",   "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 today; Altium, Fusion and 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.",@@ -44,9 +44,7 @@     "run.jsonl",     "thermal analysis"   ],-  "dependencies": {-    "adom/adom-lbr": "*"-  },+  "dependencies": {},   "scripts": {     "install": "./install.sh",     "uninstall": "./uninstall.sh"
skills/adom-aiflow/SKILL.md+5−2
@@ -37,7 +37,9 @@ When the human asks for a board in Fusion ("a 12 V to 5 V buck converter using t the whole flow below is yours: they should never have to spell out a step. Read `docs/fusion-lane.md` once; the Fusion bridge must be 1.11.33 or later. Hydrogen stays full screen behind Fusion; keep Fusion windowed (about 90 % of the screen), never maximized, so the human sees both and a screen recording-catches Fusion's 3D view.+catches Fusion's 3D view. `step` brings the window each step works in to the front: Hydrogen for intake,+design, simulation, sourcing, the library, current, thermal and fields (the chat and its webviews), Fusion+for the schematic, placement, routing, pours, 3D and finish; the board is framed by its outline.  1. **The parts, from their wiki component pages.** Design the circuit (aiflow-circuit-design), then find    each part's component page on the Adom wiki (a reference design on the wiki, such as a molecule page,@@ -53,7 +55,8 @@ catches Fusion's 3D view. 3. **The schematic, live.** `adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> [--fixed <fixed.json>] --name <Design>`    starts the design with the fab's rules, draws the schematic in labelled groups, checks it pin by pin,    puts back any 3D model Fusion dropped when a part was added, and saves the design.-4. **Simulate before the board.** Run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;+4. **Simulate before the board (required: `step placement` waits for a `step simulation` after the+   schematic).** `step simulation`, then run the circuit in the Adom SPICE dashboard (adom/adom-spice-skillpack;    ngspice in the container is the default): `adom-spice start`, `adom-spice study <study.json>`,    `adom-spice run ngspice`, `adom-spice show` (it opens in a Hydrogen webview; press its fullscreen    button so the human sees it large). Check start-up, ripple and the load step against the
tools/fusion-project.py+6−3
@@ -521,7 +521,7 @@ def cmd_library(a):         ('3D turned upright before binding: ' + '; '.join('%s (%s)' % (m, n) for m, n in turned.items()) + '.') if turned else '',         'Check the 3D yourself before going on: a model can be authored Y-up while Fusion and the board are Z-up, and a part bound that way lies on its side (an electrolytic can on its flank, an inductor on edge). '         'This step already applied each footprint\'s own model transform and checked every model against its footprint, but look anyway: once the board exists, '-        '`fusion_show_3d_board` then `fusion_take_screenshot` (Windows window capture cannot see Fusion\'s 3D viewport), and every part must sit flat on its pads the way its datasheet drawing shows. '+        '`adom-aiflow fusion view3d` (a still from Fusion\'s own renderer, then a live orbit), and every part must sit flat on its pads the way its datasheet drawing shows. '         'A part that does not: say which, and fix its model or its footprint\'s (model ... (rotate ...)) entry, then run `fusion library` again.',         'Next: adom-aiflow fusion schematic --netlist <netlist.json> --spec <spec.json> --name <Design> (the design starts from this library). New parts later: run `fusion library` again with them; they join this same library.']) @@ -672,7 +672,7 @@ def cmd_schematic(a):     ok('schematic drawn live in %d group(s), every pin on its net; the board followed with %d parts; saved as %s; board written to %s' % (len(plan['groups']), n, name, out),        [('3D: Fusion dropped the 3D models of %d part(s) when they were added; they were put back (the design was imported again with every binding on it): %s.' % (len(restored), ', '.join(restored))) if restored else '',         ('3D: %d part(s) still show as flat placeholder boxes: %s. Read <run>/fusion/restore3d/ and fusion_check_dialogs.' % (len(lost), ', '.join(lost))) if lost else '',-        'Look at the board in 3D before going on: `adom-aiflow fusion view3d` renders it with Fusion\'s own renderer (Windows capture cannot see Fusion\'s 3D viewport), and every part must sit on its pads the way its datasheet shows.',+        'Look at the board in 3D before going on: `adom-aiflow fusion view3d` saves a still from Fusion\'s own renderer and orbits the live view, and every part must sit on its pads the way its datasheet shows.',         'Next: simulate it before the board: the Adom SPICE dashboard (adom-spice start / study / run ngspice / show, full screen in a Hydrogen webview); change values in the schematic if a measurement misses; record it with `evidence add --kind simulation`.',         'Then the board: adom-aiflow start --board %s --spec <spec.json> --target %s --remote-board fusion:%s --engine <you> --prompt-time <paste time>' % (out, TARGET or '<box>', name)]) @@ -746,9 +746,12 @@ def cmd_view3d(a):     """The board in 3D, for the human and for the check: Fusion's 3D PCB, an iso view, a still from Fusion's     own renderer saved in the run (read it: every part must sit flat on its pads), then a slow live orbit     the step clip and the screen recording catch. Keep Fusion windowed (never maximized): a full-screen-    window is presented past the Windows compositor and a screen recording sees a stale frame."""+    window can leave a screen recording on a stale frame."""     st = load_state(); name = a.design or st.get('design')     if not name: err('which design? pass --design <name>')+    w = ab('desktop_list_windows', {}); wl = ((w.get('data') or w).get('windows') or [])+    fh = next((x['hwnd'] for x in wl if x.get('image') == 'Fusion360.exe' and x.get('kind') == 'normal' and 'Autodesk Fusion' in (x.get('title') or '')), None)+    if fh: ab('desktop_bring_to_front', {'hwnd': fh, 'reason': 'show the board in 3D'})     r = ab('fusion_show_3d_board', {'documentName': name})     if r.get('success') is False: err('could not show %s in 3D: %s' % (name, r.get('error')), ['Open the design (it must be the active document) and run this again.'])     time.sleep(a.settle)