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
Publish 0.1.26
35 files changed
+40−5
bin/adom-aiflowdocs/component-sourcing.md+1docs/library-tours.md+1docs/silkscreen-priority-pass.md+1docs/silkscreen.md+1hydrogen-widget.json+1install.shpage.json+9−3tools/aiflow-widget.py+1tools/integrate-rust-sections.py+1tools/library-tour.html+1tools/library-tour.py+1tools/native-text-bounds.py+1tools/silkscreen-associations.py+1tools/silkscreen-contact-policy.py+1tools/silkscreen-dashboard/SKILL.md+1tools/silkscreen-dashboard/font-sources.json+1−1tools/silkscreen-dashboard/icon.svg+1−1tools/silkscreen-dashboard/server.py+1tools/silkscreen-dashboard/settings-cog-outline.svg+1tools/silkscreen-kicad-mask.py+1tools/silkscreen-layout.py+1tools/silkscreen-leaders.py+1tools/silkscreen-obstacle-check.py+1tools/silkscreen-pairs.py+1tools/silkscreen-preflight.py+1tools/silkscreen-sections.py+1tools/test-silkscreen-contact-policy.py+1tools/test-silkscreen-events.py+1tools/test-silkscreen-kicad-mask.py+1tools/test-silkscreen-layout.py+1tools/test-silkscreen-obstacle-check.py+1tools/test-silkscreen-sections.py+1uninstall.shwidgets/README.md+1bin/adom-aiflow⋯ 1 unchanged line ⋯
docs/component-sourcing.md+1@@ -33,3 +33,4 @@ Read the shared wiki-component skill. Use manufacturer, exact MPN and function i Use exact per-asset permission evidence. An explicit rights-holder authorization can permit publishing a derivative of their own asset; document the statement, date, scope and affected hashes. Do not silently replace the upstream package license or apply that authorization to unrelated manufacturer CAD. Independently generated replacements disclose drawing dimensions, undimensioned approximations, comparison scope and the generator. A native board can hide an existing model through appearance filters. KiCad's models-not-in-position-file setting is one example. Do not change BOM or position flags merely to make a model visible. Native model visibility, binding and document verification belong in the EDA bridge. AI Flow orchestrates and records; send bridge deficiencies to the owning bridge.+
docs/library-tours.md+1@@ -29,3 +29,4 @@ After opening or recreating a Hydrogen tour tab, retry a missing frame during th bounded readiness wait. A frame-not-found result before mounting is not a renderer failure. Actual loading/recording errors must still fail; verify decoded frames and advancing playback time on the published wiki video.+
docs/silkscreen-priority-pass.md+1@@ -15,3 +15,4 @@ The order below is explicit: a lower priority cannot buy a collision, incorrect 9. **Evidence.** Replay all required groups and real rework with active text sizes. Isolate faces and show the bottom from below (or explicitly mirrored from above). Record 1920×1080; frame the complete active group independently of manual inspection camera movement. Include final native EDA proof. Inspect contact sheets and actual webview playback. Never describe the replay as recorded internal AI reasoning or omit unresolved decisions from the report. Report separately: coverage, geometric validity, native verification, visual association/readability, source handoff and actual shared-package release. Passing DRC alone is not production acceptance.+
docs/silkscreen.md+1@@ -154,3 +154,4 @@ Prefer readable documentation blocks in open board interior, not crowded rim spa ## Full-board process recording For a short silkscreen population video, prefer a fixed full-board camera with Follow placement off. Preserve the selected follow setting when recording; do not silently enable it. Record every accepted placement group at a readable event cadence, top and bottom separately, with only the active face visible and the bottom shown from below. Keep the whole board inside the frame. Accelerate the recorded footage with ffmpeg to the requested duration rather than skipping placement events. Distinguish event replay from live solver timing. Finish with refreshed native EDA 3D evidence; for a one-minute film, reserve about ten seconds for that proof. Close-up follow mode remains optional for detailed review.+
hydrogen-widget.json+1@@ -20,3 +20,4 @@ "refresh": "Local JSON every 4 seconds while visible, 30 seconds while hidden.", "docs": "widgets/README.md" }+
install.shpage.json+9−3@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.25",+ "version": "0.1.26", "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.",@@ -79,5 +79,11 @@ "label": "Compare engines", "prompt": "run the same board flow on Fable and Codex and show me the comparison" }- ]-}+ ],+ "org": "adom",+ "tag": "insiders",+ "author": {+ "name": "John Lauer",+ "email": "[email protected]"+ }+}
tools/aiflow-widget.py+1@@ -120,3 +120,4 @@ else: self.send_response(200);self.send_header('Content-Type',kind);self.send_header('Cache-Control','private, max-age=31536000, immutable' if kind=='image/png' and ('/thumb/' in self.path or file.parent.name=='widget-previews') else 'no-store');self.end_headers();self.wfile.write(body) def log_message(self,*args):pass server=http.server.ThreadingHTTPServer(('127.0.0.1',a.port),H);(pathlib.Path.home()/'.adom/aiflow-widget.pid').write_text(str(os.getpid()));print('AI Flow widget listening',a.port,flush=True);server.serve_forever()+
tools/integrate-rust-sections.py+1@@ -24,3 +24,4 @@ arm=''' } ''' s=s.replace('match &cli.cmd {','match &cli.cmd {'+arm,1);a.main.write_text(s);print('Added native Rust section command without replacing existing commands')+
tools/library-tour.html+1@@ -30,3 +30,4 @@ try{ document.querySelector('#overview').onclick=all;document.querySelector('#pick').onchange=e=>one(+e.target.value);document.querySelector('#detail').onclick=()=>state.record('detail');document.querySelector('#short').onclick=()=>state.record('overview');state.ready=true;all(); }catch(e){state.error=String(e);document.querySelector('#error').textContent=String(e);status.textContent='Failed';} </script>+
tools/library-tour.py+1@@ -52,3 +52,4 @@ elif a.action=='verify': for kind in ['detail','overview']: raw=a.out/'recordings'/(kind+'.webm');assert raw.exists(),f'missing {kind} recording';mp4=raw.with_suffix('.mp4');subprocess.run(['ffmpeg','-y','-v','error','-i',str(raw),'-an','-vf','fps=24,scale=1280:720','-c:v','libx264','-preset','veryfast','-crf','28','-pix_fmt','yuv420p','-movflags','+faststart',str(mp4)],check=True);meta=json.loads(subprocess.check_output(['ffprobe','-v','error','-show_format','-show_streams','-of','json',str(mp4)]));seconds=float(meta['format']['duration']);assert mp4.stat().st_size<10_000_000,'clip exceeds page cap; reduce bitrate before publishing';assert int(meta['streams'][0]['nb_frames'])<=seconds*25+2,'unexpected frame duplication';expected=5 if kind=='overview' else 2*len(src['components']);assert abs(seconds-expected)<3,f'{kind}: wrong duration {seconds}';sheet=raw.with_name(kind+'-sheet.png');subprocess.run(['ffmpeg','-y','-v','error','-i',str(mp4),'-vf',f'fps=1/{max(seconds/12,.1)},scale=320:-2,tile=4x3','-frames:v','1',str(sheet)],check=True);clips.append({'kind':kind,'raw':str(raw),'file':str(mp4),'seconds':seconds,'sha256':digest(mp4),'contactSheet':str(sheet),'visualReview':'pending','useInFinal':kind=='overview'}) (a.out/'clips.json').write_text(json.dumps({'selectionSha256':src['selectionSha256'],'mpnMarking':src['mpnMarking'],'clips':clips,'finalMaximumSeconds':120,'notes':'Review both contact sheets and playback before registration; only the overview belongs in the final cut by default.'},indent=2));print(json.dumps(clips,indent=2))+
tools/native-text-bounds.py+1@@ -23,3 +23,4 @@ for i,l in enumerate(m['labels']): b=[min(v[0] for v in boxes),min(v[1] for v in boxes),max(v[2] for v in boxes),max(v[3] for v in boxes)] out.append(dict(index=i,text=l['text'],box=b,offset=[(b[k]-(l['x'] if k%2==0 else l['y']))/l['size'] for k in range(4)])) a.out.write_text(json.dumps({'sourceSvgSha256':hashlib.sha256(a.svg.read_bytes()).hexdigest(),'layer':a.layer,'method':'nearest matching native stroked-text group; inspect mapping before layout','bounds':out},indent=2));print('Native SVG text bounds:',len(out))+
tools/silkscreen-associations.py+1@@ -44,3 +44,4 @@ def associate(data): return dict(placements=children,graphics=graphics,unresolvedAssociations=unresolved) if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=associate(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(r,indent=2));print(len(r['graphics']),'pointers;',len(r['unresolvedAssociations']),'association reviews')+
tools/silkscreen-contact-policy.py+1@@ -63,3 +63,4 @@ def apply_policy(spec,geometry,enabled=True): return spec if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--geometry',required=True);p.add_argument('--out',required=True);p.add_argument('--no-contact-borders',action='store_true');a=p.parse_args();r=apply_policy(json.loads(Path(a.input).read_text()),json.loads(Path(a.geometry).read_text()),not a.no_contact_borders);Path(a.out).write_text(json.dumps(r));print(json.dumps(r['contactPolicyStats']))+
tools/silkscreen-dashboard/SKILL.md+1@@ -83,3 +83,4 @@ Prefer readable documentation blocks in open board interior, not crowded rim spa ## Full-board process recording For a short silkscreen population video, prefer a fixed full-board camera with Follow placement off. Preserve the selected follow setting when recording; do not silently enable it. Record every accepted placement group at a readable event cadence, top and bottom separately, with only the active face visible and the bottom shown from below. Keep the whole board inside the frame. Accelerate the recorded footage with ffmpeg to the requested duration rather than skipping placement events. Distinguish event replay from live solver timing. Finish with refreshed native EDA 3D evidence; for a one-minute film, reserve about ten seconds for that proof. Close-up follow mode remains optional for detailed review.+
tools/silkscreen-dashboard/font-sources.json+1−1@@ -7,4 +7,4 @@ "url": "https://adom.inc/fonts/familjen-grotesk/familjen-grotesk-latin-600-normal.woff2", "bytes": 11232 }-]+]
tools/silkscreen-dashboard/icon.svg+1−1@@ -1 +1 @@-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#e6edf3" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 16V8h4M7 12h3M14 8h3M15.5 8v8M14 16h3"/></svg>+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#e6edf3" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 16V8h4M7 12h3M14 8h3M15.5 8v8M14 16h3"/></svg>
tools/silkscreen-dashboard/server.py+1@@ -163,3 +163,4 @@ def watch(): else:previous=time.time() if time.time()-last>settings['ttlHours']*3600:shutdown();return threading.Thread(target=watch,daemon=True).start();signal.signal(signal.SIGTERM,lambda *_:threading.Thread(target=shutdown,daemon=True).start());print(json.dumps({'port':server.server_port}),flush=True);server.serve_forever();save();reg.unlink(missing_ok=True)+
tools/silkscreen-dashboard/settings-cog-outline.svg+1@@ -8,3 +8,4 @@ <circle cx="12" cy="12" r="3"/> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> </svg>+
tools/silkscreen-kicad-mask.py+1@@ -60,3 +60,4 @@ def obstacles(root,clearance=0.0): return result if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--board',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=obstacles(parse(Path(a.board).read_text()));Path(a.out).write_text(json.dumps(r));print(len(r),'native mask obstacles; custom pads',sum(o['sourceShape']=='custom' for o in r))+
tools/silkscreen-layout.py+1@@ -92,3 +92,4 @@ if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);p.add_argument('--events');a=p.parse_args();EVENT_FILE=a.events;EVENT_ID=uuid.uuid4().hex;r=solve(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(r,indent=2)); for item in r['placements']:emit('unresolved' if item.get('unresolved') else 'placement',id=item['id'],candidate=item,reason='No feasible candidate in the supplied search set' if item.get('unresolved') else 'Selected by font, locality and collision constraints; native review pending') emit('complete',unresolved=r['unresolved'],nativeVerified=False,method=r['method']);print('OK: silkscreen candidate layout; '+str(len(r['placements'])-len(r['unresolved']))+' placed, '+str(len(r['unresolved']))+' unresolved');print('Hint: Inspect fitted-body visibility, verify native text extents/DRC, and refresh BOTH native views before acceptance. This planner does not mutate the board.');raise SystemExit(2 if r['unresolved'] else 0)+
tools/silkscreen-leaders.py+1@@ -63,3 +63,4 @@ def prepare(spec): return spec if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();s=prepare(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(s,indent=2))+
tools/silkscreen-obstacle-check.py+1@@ -20,3 +20,4 @@ def check(data): return dict(schemaVersion=1,passed=bool(inventory) and not missing and not collisions,coverageErrors=[] if inventory else ['Whole-board inventory is empty'],missingObstacles=missing,collisions=collisions,inventoryItems=len(inventory),obstacles=len(obs),marks=len(data['marks']),nativeVerified=False,note='Conservative rectangle audit only; geometry provenance, fitted transforms, label association and native 2D/3D review remain required.') if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True,type=Path);p.add_argument('--out',required=True,type=Path);a=p.parse_args();result=check(json.loads(a.input.read_text()));a.out.write_text(json.dumps(result,indent=2));print(('OK' if result['passed'] else 'ERROR')+f": {len(result['coverageErrors'])} coverage errors; {len(result['missingObstacles'])} missing obstacles; {len(result['collisions'])} collisions");raise SystemExit(0 if result['passed'] else 2)+
tools/silkscreen-pairs.py+1@@ -59,3 +59,4 @@ def generate(d): return dict(bounds=list(outline.bounds),labels=groups,clearance=.025,solver='milp',solverSeconds=60,milpPerFont=5,candidatesPerLabel=1000,fontWeight=20,distanceWeight=3) if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();Path(a.out).write_text(json.dumps(generate(json.loads(Path(a.input).read_text()))))+
tools/silkscreen-preflight.py+1@@ -41,3 +41,4 @@ def check(data): return dict(passed=not errors,errors=errors,labels=len(placements),graphics=len(data.get('graphics',[])),obstacles=len(obstacles),nativeVerified=False) if __name__=='__main__': p=argparse.ArgumentParser();p.add_argument('--input',required=True);p.add_argument('--out',required=True);a=p.parse_args();r=check(json.loads(Path(a.input).read_text()));Path(a.out).write_text(json.dumps(r,indent=2));print(('OK' if r['passed'] else 'ERROR')+': '+str(len(r['errors']))+' preflight findings');raise SystemExit(0 if r['passed'] else 2)+
tools/silkscreen-sections.py+1@@ -68,3 +68,4 @@ if __name__=='__main__': Path(a.out).write_text(json.dumps(dict(placements=out,unresolved=missing,sections=reports),indent=2)) print(('ERROR' if missing else 'OK')+f': {len(out)} documentation labels in ordered sections; {len(missing)} unresolved') raise SystemExit(2 if missing else 0)+
tools/test-silkscreen-contact-policy.py+1@@ -17,3 +17,4 @@ a=m.apply_policy(dict(labels=labels),dict(outline=mapping(box(0,0,10,10)),bodies bs=[l['candidates'][0]['sharedBorderBounds'] for l in a['labels']] assert len({(round(b[0],5),round(b[2],5),round(b[3]-b[1],5)) for b in bs})==1 print('PASS: unequal text widths retain shared border rails and row height')+
tools/test-silkscreen-events.py+1@@ -4,3 +4,4 @@ spec=dict(bounds=[0,0,10,10],obstacles=[dict(box=[0,0,2,2])],labels=[dict(id='C1 with tempfile.TemporaryDirectory() as td: m.EVENT_FILE=str(pathlib.Path(td)/'events.jsonl');m.EVENT_ID='test';r=m.solve(spec);assert r['unresolved']==['C2'];events=[json.loads(l) for l in pathlib.Path(m.EVENT_FILE).read_text().splitlines()];assert any(e.get('reason')=='intersects a reserved obstacle' for e in events);assert any(e.get('reason','').startswith('clears') for e in events);assert all(e['solveId']=='test' for e in events);assert r['nativeVerified'] is False print('PASS: real rejection/feasible events, solve identity, unresolved MILP result and native-review boundary')+
tools/test-silkscreen-kicad-mask.py+1@@ -16,3 +16,4 @@ try:m.primitive(['gr_unknown']) except ValueError:pass else:raise AssertionError('unsupported geometry accepted') print('PASS: complete custom mask, rotated coordinates, margin once, unsupported geometry refused')+
tools/test-silkscreen-layout.py+1@@ -24,3 +24,4 @@ for roles,font in [([{'role':'reference','size':.8}],.9),([{'role':'reference',' x=c(0,font);x['children']=roles;g['candidates'].append(x) r=m.solve({'bounds':[-1,-1,6,3],'labels':[g]});assert not r['unresolved'];assert r['placements'][0]['font']==.4 print('PASS: required pair roles and reference/value hierarchy cannot be traded for score')+
tools/test-silkscreen-obstacle-check.py+1@@ -14,3 +14,4 @@ class Audit(unittest.TestCase): d=dict(inventory=[dict(id='C4',kind='body',layers=['F.SilkS'])],obstacles=[dict(id='C4',kind='body',layer='F.SilkS',box=[0,0,2,1])],marks=[dict(id='label',layer='B.SilkS',box=[.1,.2,.3,.6])]) self.assertTrue(m.check(d)['passed']) unittest.main()+
tools/test-silkscreen-sections.py+1@@ -16,3 +16,4 @@ class Sections(unittest.TestCase): p,missing,_,_=m.place_sections([dict(id='blocked',rows=[[0],[1]],anchor=[1,1])],[dict(text='a',role='note')]*2,lambda i,z,a:[-z,-z,z,z],box(0,0,2,2),box(0,0,2,2),GeometryCollection(),'F.SilkS') self.assertEqual(p,[]);self.assertEqual(missing,[0,1]) if __name__=='__main__':unittest.main()+
uninstall.shwidgets/README.md+1@@ -31,3 +31,4 @@ activity underneath; the expanded heading retains that identity. Polling updates only changed content. Existing image and selector nodes survive unchanged polls, new images decode before replacement, and unchanged sizing messages are suppressed. Hash-addressed cached images use private immutable caching; the live feed does not.+