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
0.1.29: pin complete canonical library viewer and preserve unowned zones
14 files changed
+116−74
Cargo.lock+10−10Cargo.toml+1−1README.md+2bin/adom-aiflowcrates/adom-aiflow/src/main.rs+21−2crates/aiflow-bridge/src/lib.rs+44−54docs/library-overview-review.md+2docs/release-0.1.29.md+9package.json+1−1page.json+2−2skills/adom-aiflow/SKILL.md+4tools/fetch-library-viewer.sh+10tools/library-tour.html+5−3tools/library-tour.py+5−1Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -23,7 +23,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.28"+version = "0.1.29" dependencies = [ "serde", "serde_json",@@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.28"+version = "0.1.29" dependencies = [ "serde", "serde_json",@@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.28"+version = "0.1.29" dependencies = [ "serde", "serde_json",@@ -48,7 +48,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-board", "aiflow-grid",@@ -58,7 +58,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-board", "serde",@@ -67,7 +67,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-board", "serde",@@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-board", "aiflow-copper",@@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.28"+version = "0.1.29" dependencies = [ "aiflow-board", "aiflow-grid",@@ -96,7 +96,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.28"+version = "0.1.29" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.28"+version = "0.1.29" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
README.md+2@@ -175,3 +175,5 @@ A pour's area or presence is not proof of current capacity or acceptable tempera Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. Manufacturing-rule warnings, including hole spacing, remain blocking. Latest maintenance: [0.1.28 lifecycle and native IPC readiness](docs/release-0.1.28.md).++[0.1.29: reliable component tours and explicit zone ownership](docs/release-0.1.29.md).
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+21−2@@ -1941,8 +1941,27 @@ fn main() { let zones = aiflow_pours::resolve_outline(&b, read_json(&zp)["zones"].as_array().unwrap_or(&Vec::new())); r.stage_start("land-pours", Some("binary"), None); let before = br.zone_state(&rb, true);- let inherited = inherited_zone_ids(&r);- let results = br.land_zones(&rb, &zones, *pause_ms, &inherited);+ let mut owned: Vec<String> = r.data["ownedZoneIds"].as_array().into_iter().flatten().filter_map(|v|v.as_str().map(str::to_string)).collect();+ // Legacy successful operation replies are evidence of creation, not an inference.+ if let Some(groups)=r.outcomes().get("land-pours").and_then(|o|o["groups"].as_array()) {+ for g in groups.iter().filter(|g|g["ok"]==true) {+ for id in g["itemIds"].as_array().into_iter().flatten().filter_map(Value::as_str) {+ if !owned.iter().any(|x|x==id) { owned.push(id.to_string()); }+ }+ }+ }+ let results = br.land_zones(&rb, &zones, *pause_ms, &owned);+ for (_,reply) in &results {+ if Bridge::ok(reply) {+ for id in reply["removedIds"].as_array().into_iter().flatten().filter_map(Value::as_str) { owned.retain(|x|x!=id); }+ }+ if Bridge::ok(reply) || reply["mutated"]==true {+ for id in reply["itemIds"].as_array().into_iter().flatten().filter_map(Value::as_str) {+ if !owned.iter().any(|x|x==id) { owned.push(id.to_string()); }+ }+ }+ }+ r.data["ownedZoneIds"]=json!(owned); if let Some((_, rep)) = results.iter().find(|(n, _)| n == "removed-stale") { eprintln!("{}", rep["message"].as_str().unwrap_or("")); }
crates/aiflow-bridge/src/lib.rs+44−54@@ -12,19 +12,21 @@ fn retryable_read(reply: &Value) -> bool { !matches!(reply["errorCode"].as_str(), Some("board_mismatch" | "invalid_socket" | "invalid_path" | "api_server_off" | "unsupported_kicad" | "no_pcb_frame" | "permission_denied")) } -/// Shoelace area of a zone entry's `polygon` ([[x, y], ...]).-fn polygon_area_of(z: &Value) -> f64 {- let pts: Vec<(f64, f64)> = z.get("polygon").and_then(|p| p.as_array()).map(|a| a.iter().filter_map(|q| Some((q[0].as_f64()?, q[1].as_f64()?))).collect()).unwrap_or_default();- if pts.len() < 3 {- return 0.0;- }- let mut a = 0.0;- for i in 0..pts.len() {- let (x1, y1) = pts[i];- let (x2, y2) = pts[(i + 1) % pts.len()];- a += x1 * y2 - x2 * y1;+/// Only IDs returned by successful flow operations establish ownership. Names,+/// area and absence from the original board do not establish ownership.+fn owned_zones_to_replace(live: &Value, planned: &[Value], owned: &[String]) -> Result<Vec<String>, String> {+ let rows=live["zones"].as_array().ok_or("zone readback has no zones array")?;+ let mut replace=Vec::new();+ for row in rows {+ let id=row["id"].as_str().ok_or("zone readback has no item ID")?;+ if row["type"] == "teardrop" { continue; }+ if owned.iter().any(|x|x==id) { replace.push(id.to_string()); continue; }+ let name=row["name"].as_str().unwrap_or("");+ if !name.is_empty() && planned.iter().any(|p|p["name"].as_str()==Some(name)) {+ return Err(format!("planned zone {name:?} conflicts with unowned live ID {id}; preserve it and choose a distinct flow zone name, or reconcile it explicitly through the native bridge"));+ } }- (a / 2.0).abs()+ Ok(replace) } #[derive(Clone)]@@ -201,61 +203,30 @@ impl Bridge { (replies, ok, None, traces.len()) } - /// Land zones grouped by net (one undo step per group, lowest priority first), converging the- /// live board on the plan: a live zone with a planned zone's name and outline area (within 2 %)- /// is already landed and is skipped; a live zone carrying one of the plan's names but another- /// outline is stale (the spec changed) and is removed first. The revision is read fresh before- /// every group because KiCad's refill after a commit changes the board again after the bridge- /// has answered.- pub fn land_zones(&self, board: &str, zones: &[Value], pause_ms: u64, inherited_ids: &[String]) -> Vec<(String, Value)> {+ /// Replace only explicitly owned zones, never infer ownership from a name or area.+ /// Until native full-configuration reconciliation is available, replace owned+ /// zones rather than incorrectly treating equal-area outlines as equivalent.+ pub fn land_zones(&self, board: &str, zones: &[Value], pause_ms: u64, owned_ids: &[String]) -> Vec<(String, Value)> { let live = self.zone_state(board, false);+ if !Bridge::ok(&live) { return vec![("zone-readback".into(),live)]; } let mut out = Vec::new();- let names: Vec<String> = zones.iter().filter_map(|z| z.get("name").and_then(|n| n.as_str()).map(str::to_string)).collect();- let mut consumed = vec![false; zones.len()];- let mut stale: Vec<String> = Vec::new();- for l in live.get("zones").and_then(|z| z.as_array()).cloned().unwrap_or_default() {- let (Some(name), Some(id)) = (l.get("name").and_then(|n| n.as_str()), l.get("id").and_then(|i| i.as_str())) else { continue };- let kind = l.get("type").and_then(|t| t.as_str()).unwrap_or("copper");- let inherited = inherited_ids.iter().any(|i| i == id);- // the flow owns every copper or keepout zone the board did not start with; a zone it- // owns that the plan no longer describes (renamed, reshaped, dropped) is stale- if inherited || kind == "teardrop" || (name.is_empty() && !names.is_empty() && inherited_ids.is_empty()) {- continue;- }- if !names.iter().any(|n| n == name) {- stale.push(id.to_string());- continue;- }- let area = l.get("outlineAreaMm2").and_then(|a| a.as_f64()).unwrap_or(0.0);- let hit = zones.iter().enumerate().position(|(i, z)| !consumed[i] && z.get("name").and_then(|n| n.as_str()) == Some(name) && {- let pa = polygon_area_of(z);- (pa - area).abs() <= 0.02 * pa.max(area).max(0.5)- });- match hit {- Some(i) => consumed[i] = true,- None => stale.push(id.to_string()),- }- }+ let stale=match owned_zones_to_replace(&live,zones,owned_ids) {+ Ok(ids)=>ids,+ Err(reason)=>return vec![("zone-ownership".into(),json!({"success":false,"errorCode":"zone_ownership_conflict","message":reason}))],+ }; if !stale.is_empty() { let Some(rev) = self.revision(board) else { return vec![("removed-stale".into(), json!({"success": false, "errorCode": "no_revision"}))] }; let r = self.call("kicad_remove_zone", &json!({"filePath": board, "expectedRevision": rev, "itemIds": stale})); let n = stale.len(); let good = Bridge::ok(&r);- out.push(("removed-stale".into(), json!({"success": good, "removed": n, "errorCode": r["errorCode"], "message": format!("{n} live zones the flow had landed are no longer in the plan (renamed, reshaped or dropped) and were removed before landing"), "reply": r})));+ out.push(("removed-stale".into(), json!({"success": good, "removed": n, "removedIds": stale, "errorCode": r["errorCode"], "message": format!("{n} explicitly owned live zones were removed before applying the full requested configuration"), "reply": r}))); if !good { return out; } std::thread::sleep(std::time::Duration::from_millis(pause_ms)); }- let skipped = consumed.iter().filter(|c| **c).count();- if skipped > 0 {- out.push(("already-landed".into(), json!({"success": true, "skipped": skipped, "message": format!("{skipped} zones were already on the live board (same name and outline) and were not landed again")})));- } let mut groups: Vec<(String, Vec<Value>, u64)> = Vec::new();- for (i, z) in zones.iter().enumerate() {- if consumed[i] {- continue;- }+ for z in zones { let key = z.get("net").and_then(|n| n.as_str()).unwrap_or("keepout").to_string(); let prio = z.get("priority").and_then(|p| p.as_u64()).unwrap_or(0); if let Some(g) = groups.iter_mut().find(|g| g.0 == key) {@@ -331,6 +302,25 @@ impl Bridge { mod tests { use serde_json::json; + #[test]+ fn zone_ownership_is_never_inferred_from_name_or_board_age() {+ let live=json!({"zones":[{"id":"foreign","name":"manual","type":"copper"},{"id":"ours","name":"power","type":"copper"}]});+ let plan=vec![json!({"name":"power","polygon":[[0,0],[1,0],[1,1],[0,1]]})];+ assert_eq!(super::owned_zones_to_replace(&live,&plan,&["ours".into()]).unwrap(),vec!["ours"]);+ assert!(super::owned_zones_to_replace(&live,&plan,&[]).unwrap_err().contains("unowned"));+ assert!(super::owned_zones_to_replace(&live,&[],&[]).unwrap().is_empty());+ }++ #[test]+ fn equal_area_is_not_equivalent_zone_geometry() {+ let live=json!({"zones":[{"id":"ours","name":"power","type":"copper","outlineAreaMm2":1.0}]});+ for polygon in [json!([[0,0],[1,0],[1,1],[0,1]]),json!([[10,10],[11,10],[11,11],[10,11]])] {+ let plan=vec![json!({"name":"power","polygon":polygon})];+ assert_eq!(super::owned_zones_to_replace(&live,&plan,&["ours".into()]).unwrap(),vec!["ours"]);+ }+ assert!(super::owned_zones_to_replace(&json!({"success":false}),&[],&[]).is_err());+ }+ #[test] fn permanent_identity_refusal_is_not_retried() { for code in ["board_mismatch", "invalid_socket", "api_server_off", "no_pcb_frame"] {
docs/library-overview-review.md+2@@ -23,3 +23,5 @@ The review JSON is explicit human/AI review evidence, not an automatic visual-qu Use the same selection manifest accepted by `tools/library-tour.py build`: target `eda`, `mpnMarking` on/off, and each component's `selectedModel` with variant, STEP and GLB paths and hashes. Paths are relative to the containing manifest or absolute. All model hashes are checked. Registration and composition both recheck the board, clip, selection, and native evidence. A pending render review or an old screenshot must not be marked passed. Review the visible native bodies, pin-one indicators, orientation, selected marking/light variants and the tour contact sheet/playback before attesting. If anything changes, redo the affected review and use `--replaces <old clip>` to retain the history without repeating the old overview. The full tour does not enter the final cut automatically. No static wiki/title cards are added. This does not validate electrical function, footprint dimensions or redistribution rights; those reviews and provenance remain on the reusable global component pages.++Tours use the complete pinned canonical viewer release, served locally with the tour. On a viewer load error, rebuild with `tools/library-tour.py build --manifest <selection.json> --out <tour-dir>` and inspect the explicit error before recording. Native variants and tour selection must agree, including pin-one markers and the chosen LED/MPN state.
docs/release-0.1.29.mdadded+9@@ -0,0 +1,9 @@+# AI Flow 0.1.29++Component tours use the complete pinned canonical Adom 3D Viewer 0.7.1, including its hashed module chunks, served with the tour. The build fetches that release if it is absent. A failed viewer import is caught and shown instead of leaving the app at Loading. Hydrogen receives a restart recipe for the tour server.++The final ESC library integration was exercised with all 40 marked component selections: selected STEP hashes match actual native board bindings; native render and component-tour contact sheets were reviewed; the short overview registered with hashed review evidence and composed into a four-second clip. The detailed tour stays separate.++Zone ownership is now explicit. Only item IDs returned by successful flow operations may be replaced. Zones introduced by a person or another tool are preserved, even when absent from the original board. A conflicting unowned name refuses with an explanation. Name plus area is no longer treated as equivalent geometry: the conservative fallback replaces explicitly owned zones using the full requested plan, rather than silently skipping a reshaped or reconfigured zone. It is not atomic native reconciliation; removal and creation remain separate native undo operations and a failure must be inspected. Full native reconciliation remains on KiCad Bridge issue 101.++Tests cover foreign-zone preservation, unowned name conflicts, moved equal-area polygons and invalid readback.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.28",+ "version": "0.1.29", "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−2@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.28",+ "version": "0.1.29", "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.",@@ -86,4 +86,4 @@ "name": "John Lauer", "email": "[email protected]" }-}+}
skills/adom-aiflow/SKILL.md+4@@ -162,3 +162,7 @@ Component library footage: keep the full `library-tour` separate. A reviewed fiv ## Native editor readiness `capture open` must confirm native IPC for the exact board, not only a visible window. KiCad may give its single IPC server to the first editor. On `board_mismatch`, read `capture-open-ipc.json` or `place-land-log.json`, save and gracefully close only your task-owned conflicting editors, and reopen the intended board first. Never close another user's work or restart a shared bridge. Permanent identity refusals are not retryable. Reject a static failed take and record the reason before re-taking it.++Tours use the complete pinned canonical viewer release, served locally with the tour. On a viewer load error, rebuild with `tools/library-tour.py build --manifest <selection.json> --out <tour-dir>` and inspect the explicit error before recording. Native variants and tour selection must agree, including pin-one markers and the chosen LED/MPN state.++Zone ownership comes only from successful native creation item IDs recorded by this run. Never infer ownership because a zone was absent from intake, has a familiar name, or has the same area. Preserve unowned zones and resolve name conflicts explicitly. Current fallback replaces owned zones using separate remove/create operations; inspect a partial failure before resuming.
tools/fetch-library-viewer.shadded+10@@ -0,0 +1,10 @@+#!/bin/bash+# Fetch the complete pinned canonical viewer, including its hashed module chunks.+set -euo pipefail+root="$(cd "$(dirname "$0")/.." && pwd)"+version=0.7.1+tmp="$(mktemp -d)"+trap 'rm -rf "$tmp"' EXIT+(cd "$tmp" && adom-wiki release download adom/adom-3d-viewer "$version")+mkdir -p "$root/vendor/3d-viewer"+tar -xzf "$tmp/adom-3d-viewer-babylon9-$version.tar.gz" -C "$root/vendor/3d-viewer"
tools/library-tour.html+5−3@@ -1,11 +1,13 @@ <!doctype html><meta charset="utf-8"><title>Component library tour</title>-<link rel="stylesheet" href="https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/style.css">+<link rel="stylesheet" href="vendor/3d-viewer/style.css"> <style>body{margin:0;background:#151b23;color:#e9eff8;font:15px system-ui}header{padding:14px 20px;display:flex;gap:15px;align-items:center}button,select,a{background:#263647;color:#dcefff;border:1px solid #4e647d;border-radius:5px;padding:8px;text-decoration:none}#stage{width:100%;aspect-ratio:16/9;position:relative;max-height:85vh}#status{color:#b6c5d7}#error{color:#ffb09a;padding:10px}#viewer{position:absolute;inset:0}canvas{outline:none}small{padding:0 20px;display:block;color:#b6c5d7}</style> <header><b>Component library</b><button id="overview">All components</button><button id="detail">Record full tour</button><button id="short">Record 5s overview</button><select id="pick"></select><a id="wiki" target="_blank">Component wiki</a><span id="status">Loading…</span></header><small id="policy"></small><div id="stage"><div id="viewer"></div></div><div id="error"></div> <script type="module">-import 'https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260915a';-const B=window.BABYLON,V=window.Adom3DViewerBabylon9;const state=window.libraryTour={ready:false,recording:false,error:null,mode:'overview',completed:[]};const status=document.querySelector('#status');+const state=window.libraryTour={ready:false,recording:false,error:null,mode:'overview',completed:[]};const status=document.querySelector('#status'); try{+ await import('./vendor/3d-viewer/adom-3d-viewer-babylon9.esm.js');+ const B=window.BABYLON,V=window.Adom3DViewerBabylon9;+ if(!B||!V)throw Error('Canonical viewer did not expose its expected API'); const data=await(await fetch('tour.json')).json();state.manifest=data;document.querySelector('#policy').textContent=`${data.components.length} reusable components · MPN marking ${data.mpnMarking} · Preview sizes normalized for inspection; original CAD is unchanged. Detailed tour is separate; the final video uses the 5s overview.`; const viewer=V.init(document.querySelector('#viewer'),{zUp:true,showGround:false,showViewCube:false});window.tourViewer=viewer;await viewer.loadModel(data.components[0].glb);const scene=viewer.getScene(),engine=viewer.getEngine(),camera=viewer.getCamera();viewer.clearScene();camera.lowerRadiusLimit=.1;camera.upperRadiusLimit=200;camera.minZ=.01;camera.maxZ=500;const groups=[]; for(let i=0;i<data.components.length;i++){
tools/library-tour.py+5−1@@ -6,6 +6,10 @@ def digest(path):return hashlib.sha256(path.read_bytes()).hexdigest() if a.action=='build': if not a.manifest:p.error('--manifest is required for build') src=json.loads(a.manifest.read_text());assert src.get('eda') in ['kicad','altium','fusion'],'explicit target EDA required';assert src.get('mpnMarking') in ['on','off'],'resolve the marking preference before building the selected library'+ vendor=pathlib.Path(__file__).resolve().parent.parent/'vendor/3d-viewer'+ if not (vendor/'adom-3d-viewer-babylon9.esm.js').exists():+ subprocess.run(['bash',str(pathlib.Path(__file__).with_name('fetch-library-viewer.sh'))],check=True)+ shutil.copytree(vendor,a.out/'vendor/3d-viewer',dirs_exist_ok=True) rows=src.get('components',[]);assert rows,'empty component library';seen=set();a.out.mkdir(parents=True,exist_ok=True);(a.out/'models').mkdir(exist_ok=True);result=[] for r in rows: assert r['mpn'] not in seen,'duplicate component identity';seen.add(r['mpn']);selected=r['selectedModel'];expected='mpn' if src['mpnMarking']=='on' else 'plain';assert selected['variant']==expected,f"{r['mpn']}: tour/board variant mismatch; no silent plain fallback"@@ -26,7 +30,7 @@ elif a.action=='serve': def log_message(self,*args):pass print(f'Library tour serving on http://localhost:{a.port}/',flush=True);http.server.ThreadingHTTPServer(('127.0.0.1',a.port),Handler).serve_forever() elif a.action=='record':- control=pathlib.Path.home()/'.adom/hydrogen-control-url';assert control.exists(),'Hydrogen control unavailable; open the tour in Pup and use the recording controls, then verify';base=control.read_text().strip();url=f'http://localhost:{a.port}/';subprocess.run(['adom-cli','hydrogen','webview','open-or-refresh','--name','Component library tour','--url',url],check=True)+ control=pathlib.Path.home()/'.adom/hydrogen-control-url';assert control.exists(),'Hydrogen control unavailable; open the tour in Pup and use the recording controls, then verify';base=control.read_text().strip();url=f'http://localhost:{a.port}/';subprocess.run(['adom-cli','hydrogen','webview','open-or-refresh','--name','Component library tour','--url',url,'--ai-thread',a.ai_thread or 'AI Flow component library','--reason','Review and record the selected native component library','--restart',__import__('shlex').join([sys.executable,__file__,'serve','--out',str(a.out),'--port',str(a.port),'--ai-thread',a.ai_thread or 'AI Flow component library']), '--restart-cwd',str(a.out)],check=True) def evaluate(js,waitForFrame=False): q={'target':url,'js':js};r=json.load(urllib.request.urlopen(urllib.request.Request(base+'/eval-in',data=json.dumps(q).encode(),headers={'Content-Type':'application/json'}),timeout=20)); if not r.get('ok') and waitForFrame and 'frame/context not found' in str(r.get('error')):return {'ready':False}