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
AI Flow 0.1.64: Fusion library names uppercase (ADD cannot find a lowercase part name); a stranded native Fusion dialog is named when Fusion reports busy
7 files changed
+45−29
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.63"+version = "0.1.64" dependencies = [ "serde", "serde_json",@@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.63"+version = "0.1.64" dependencies = [ "roxmltree", "serde",@@ -42,7 +42,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "serde",@@ -51,7 +51,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "aiflow-grid",@@ -61,7 +61,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "serde",@@ -70,7 +70,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "serde",@@ -79,7 +79,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "aiflow-copper",@@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.63"+version = "0.1.64" dependencies = [ "aiflow-board", "aiflow-grid",@@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.63"+version = "0.1.64" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.63"+version = "0.1.64" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow⋯ 1 unchanged line ⋯
docs/release-0.1.64.mdadded+4@@ -0,0 +1,4 @@+# AI Flow 0.1.64++- **Fusion library names are uppercase.** Fusion's ADD command cannot find a part whose library name has lowercase letters: the 0.5 mm test point (`Adom_TestPoint_D0.5mm`) failed to place on every attempt while every all-uppercase MPN placed, and a take of the buck demo fell back to the old 1.0 mm pad (no 3D, so a black box). `fusion library` now gives adom-lbr uppercase device and package names, binds the 3D under them, and every package-name lookup (3D injection, the board's restore, the missing-3D check) compares without case. ORIENT_RULES 3 rebuilds parts made under the old names. The MPN attribute and the wiki page are unchanged.+- **A stranded native dialog is named.** When Fusion reports its main thread busy, `fusion` steps look for a native Open, Save or Export window left by an interrupted step and say so in the hint; the bridge cannot see those dialogs, and the run otherwise waits on a Fusion that will not answer.
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.63",+ "version": "0.1.64", "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+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.63",+ "version": "0.1.64", "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.",
tools/fusion-project.py+28−16@@ -113,8 +113,19 @@ def ab(verb, args, timeout=600): except Exception: return {'success': False, 'error': (p.stdout + p.stderr)[-400:]} o = d.get('output') if isinstance(o, str) and o.startswith('{'):- try: return json.loads(o)+ try: d = json.loads(o) except Exception: pass+ # Fusion busy for a long time is often a native file dialog (Open, Save, Export) left on screen by an+ # interrupted step: the bridge cannot see it, so name it (2026-10-07: a stopped schematic left "Open")+ if verb.startswith('fusion_') and 'busy' in str(d.get('error') or d.get('errorCode') or '').lower():+ w = run(['adom-bridge', '--target', TARGET, '--ai-thread', THREAD, 'desktop_list_windows', '{}'], timeout=60)+ try:+ wd = json.loads(w.stdout); wo = json.loads(wd['output']) if str(wd.get('output', '')).startswith('{') else wd+ stuck = [x.get('title') for x in ((wo.get('data') or wo).get('windows') or []) if x.get('image') == 'Fusion360.exe' and (x.get('title') or '').strip() in ('Open', 'Save', 'Save As', 'Export', 'Import')]+ except Exception:+ stuck = []+ if stuck:+ d['_hint'] = 'Fusion has a native %s dialog open (left by an interrupted step); it blocks every command. Cancel it on the desktop (or ask the human to), then retry.' % ' / '.join(stuck) return d @@ -556,7 +567,7 @@ def service_pose(step, fp): return None -ORIENT_RULES = 2 # bump when the orientation rules change: parts bound under older rules are bound again+ORIENT_RULES = 3 # 3: Fusion library names are uppercase (ADD cannot find a lowercase name). Bump when the orientation rules change: parts bound under older rules are bound again def silk_text(t):@@ -636,7 +647,7 @@ def cmd_library(a): pdir = STATE / 'pages' / re.sub(r'[^A-Za-z0-9._-]', '_', mpn) out_lbr = lib_dir / 'parts' / (mpn + '.lbr') if w.get('footprint'): # a board-only part (test pad) from a footprint file- r = run(['adom-lbr', 'generate', '--fp', w['footprint'], '--name', mpn, '--package-name', mpn, '-o', str(out_lbr)])+ r = run(['adom-lbr', 'generate', '--fp', w['footprint'], '--name', mpn.upper(), '--package-name', mpn.upper(), '-o', str(out_lbr)]) if 'Lint PASSED' not in r.stdout + r.stderr: err('%s: adom-lbr generate failed: %s' % (mpn, (r.stdout + r.stderr)[-300:])) st['parts'][mpn] = {'lbr': str(out_lbr), 'step': None, 'boardOnly': True, 'numbers': {}, 'lbrVersion': lbr_ver} added.append(mpn); say('part %s: board only (footprint %s)' % (mpn, Path(w['footprint']).name)); continue@@ -658,7 +669,7 @@ def cmd_library(a): elif choice in DEVICE_SYMBOLS: use = STATE / 'symbols' / (choice + '.kicad_sym'); use.parent.mkdir(parents=True, exist_ok=True); use.write_text(DEVICE_SYMBOLS[choice]) else: use = sym- args = ['adom-lbr', 'generate', '--fp', str(fp), '--part-metadata', str(pdir / 'page.meta.json'), '--name', mpn, '--package-name', mpn, '-o', str(out_lbr)]+ args = ['adom-lbr', 'generate', '--fp', str(fp), '--part-metadata', str(pdir / 'page.meta.json'), '--name', mpn.upper(), '--package-name', mpn.upper(), '-o', str(out_lbr)] if use: args[2:2] = ['--sym', str(use)] if choice in DEVICE_SYMBOLS: args += ['--symbol-layout', 'keep'] r = run(args)@@ -700,27 +711,28 @@ def cmd_library(a): send(sorted(stage.glob('lib/*')), bdir + '/lib'); send(sorted(stage.glob('models/*')), bdir + '/models'); send([combined], bdir) say('binding real 3D to %d part(s) in Fusion (about a minute each)' % len(todo)) r = ab('fusion_build_library_3d', {'lbrPath': bdir + '/' + combined.name, 'outLbrPath': bdir + '/' + name + '_3d.lbr', 'capture': True,- 'parts': [{'package': m, 'lbrPath': bdir + '/lib/' + m + '.lbr', 'modelPath': bdir + '/models/' + m + '.step'} for m in todo]}, timeout=300)+ 'parts': [{'package': m.upper(), 'lbrPath': bdir + '/lib/' + m + '.lbr', 'modelPath': bdir + '/models/' + m + '.step'} for m in todo]}, timeout=300) t0 = time.time(); res = {} while time.time() - t0 < 60 * (5 + 3 * len(todo)): time.sleep(20) pr = ab('fusion_prove_result', {}).get('result') or {}- if pr.get('boundLbr', '').endswith(name + '_3d.lbr') and pr.get('partsDone') == len(todo) and all(x.get('package') in todo for x in pr.get('parts', [])):+ if pr.get('boundLbr', '').endswith(name + '_3d.lbr') and pr.get('partsDone') == len(todo) and all((x.get('package') or '').upper() in {m.upper() for m in todo} for x in pr.get('parts', [])): res = pr; break if pr.get('stage'): say(' 3D: %s' % pr.get('stage')) if not res: err('the 3D build did not finish in time', ['Read fusion_prove_result; run `adom-aiflow fusion library` again: parts already bound are kept and only the rest are built.'])+ back = {m.upper(): m for m in st['parts']} for x in res.get('parts', []):- if x.get('success') and x.get('wip_urn'): st['parts'][x['package']]['urn'] = x['wip_urn']+ if x.get('success') and x.get('wip_urn'): st['parts'][back.get(x['package'].upper(), x['package'])]['urn'] = x['wip_urn'] save_state(st) # inject every binding (this build's and earlier ones) into the library, then put it on the box- urns = {m: p['urn'] for m, p in st['parts'].items() if p.get('urn')}+ urns = {m.upper(): p['urn'] for m, p in st['parts'].items() if p.get('urn')} # keyed by Fusion's (uppercase) package name t = combined.read_text() t = re.sub(r'(?s)<packages3d>.*?</packages3d>', '', t); t = re.sub(r'(?s)<package3dinstances>.*?</package3dinstances>', '', t) p3 = ''.join('<package3d name="%s" urn="" wip_urn="%s" locally_modified="yes" type="model"><description>%s</description><packageinstances><packageinstance name="%s"/></packageinstances></package3d>' % (m, u, m, m) for m, u in urns.items()) if p3: t = t.replace('</packages>', '</packages><packages3d>' + p3 + '</packages3d>', 1) def dev(mt): d = mt.group(0); pk = re.search(r'package="([^"]+)"', d.split('>', 1)[0])- u = urns.get(pk.group(1)) if pk else None+ u = urns.get(pk.group(1).upper()) if pk else None return d.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % u, 1) if u else d t = re.sub(r'(?s)<device [^>]*package="[^"]+"[^>]*>.*?</device>', dev, t) combined.write_text(t); send([combined], bdir)@@ -861,13 +873,13 @@ def cmd_schematic(a): if n != len(nl['components']): err('the board has %s of %d parts: schematic/board sync is off' % (n, len(nl['components'])), ['Start from a clean Fusion session (close half-finished imported designs) and run this again.']) # 3D on the board: a part added live arrives without the library's 3D binding (Fusion then draws a flat # placeholder box in the 3D PCB). Count them and put them back before the design is saved.- want3d = {m for m, p_ in st['parts'].items() if p_.get('urn')}+ want3d = {m.upper() for m, p_ in st['parts'].items() if p_.get('urn')} def missing3d(): cur = ab('fusion_get_app_state', {}); cur = (cur.get('data') or cur).get('activeDocument') or name sb = ab('fusion_board_source', {'expectDocument': cur}) if not sb.get('source'): err('could not read the board to check its 3D: %s' % sb.get('error'), ['Show the board (fusion_show_2d_board) and run this again.']) return sorted(re.search(r'name="([^"]+)"', e).group(1) for e in re.findall(r'<element [^>]*>', sb.get('source') or '')- if (re.search(r'package="([^"]+)"', e) or [None, None])[1] in want3d and 'package3d_urn' not in e)+ if ((re.search(r'package="([^"]+)"', e) or [None, ''])[1] or '').upper() in want3d and 'package3d_urn' not in e) lost = missing3d(); restored = [] if lost: restored = restore_3d(st, name, a.project, a.folder)@@ -895,7 +907,7 @@ def restore_3d(st, name, project, folder): flat placeholder box). The design is exported, every schematic part and board element of a package that has a model gets its package3d_urn, the library gets its packages3d, and the design is imported again. Returns the parts that were restored."""- urns = {m: p['urn'] for m, p in st['parts'].items() if p.get('urn')}+ urns = {m.upper(): p['urn'] for m, p in st['parts'].items() if p.get('urn')} # keyed by Fusion's (uppercase) package name lib = st['library']; bdir = box_dir() + '/design'; loc = STATE / 'restore3d'; loc.mkdir(parents=True, exist_ok=True) for ext in ('sch', 'brd'): r = ab('fusion_export_eagle_source', {'outputPath': '%s/%s-src.%s' % (bdir, name, ext)})@@ -910,17 +922,17 @@ def restore_3d(st, name, project, folder): L = L.replace('</packages>', '</packages>' + p3, 1) def dev(md): D = md.group(0); pk = re.search(r'package="([^"]+)"', D.split('>', 1)[0])- if not pk or pk.group(1) not in urns or 'package3dinstances' in D: return D- return D.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % urns[pk.group(1)], 1)+ if not pk or pk.group(1).upper() not in urns or 'package3dinstances' in D: return D+ return D.replace('</connects>', '</connects><package3dinstances><package3dinstance package3d_urn="%s"/></package3dinstances>' % urns[pk.group(1).upper()], 1) return re.sub(r'(?s)<device [^>]*>.*?</device>', dev, L) return re.sub(r'(?s)<library [^>]*>.*?</library>', one, t) restored = set() brd = fix_lib((loc / (name + '-src.brd')).read_text()) def el(mt): e = mt.group(0); pk = re.search(r'package="([^"]+)"', e)- if 'package3d_urn' in e or not pk or pk.group(1) not in urns: return e+ if 'package3d_urn' in e or not pk or pk.group(1).upper() not in urns: return e restored.add(re.search(r'name="([^"]+)"', e).group(1))- return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk.group(1)] + ('/>' if e.endswith('/>') else '>')+ return e[:-1].rstrip('/') + ' package3d_urn="%s"' % urns[pk.group(1).upper()] + ('/>' if e.endswith('/>') else '>') brd = re.sub(r'<element [^>]*>', el, brd) sch = fix_lib((loc / (name + '-src.sch')).read_text()) devpkg = {}