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.2.44
10 files changed
+180−32
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.43"+version = "0.2.44" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.43"+version = "0.2.44" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.43"+version = "0.2.44" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.43"+version = "0.2.44" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+9−3@@ -162,9 +162,15 @@ the library, simulation, current, thermal, fields and export, Fusion for everyth 1. **Intake and the timeline.** `intake`, `timeline open`. Offer the Claude usage widget in Hydrogen's title bar (Widgets, Claude usage) so the human sees the percent used as the run goes. 2. **Design rules, before anything is drawn.** Ask which fab builds the board. Default 3rdPartyFab (our- own process, say only "3rdPartyFab"), JLCPCB or any pack `rules list` shows. `rules use --profile- 3rdPartyFab [--layers 2]` installs the KiCad rules, loads the same pack into the open Fusion design and- writes the choice into the spec. Tell the human the rules in plain words.+ own process, say only "3rdPartyFab"), JLCPCB or any pack `rules list` shows. Then ask which rule set:+ the fab's **published** limits (the default), a **variant** someone published on top of it (`rules+ list` names them, e.g. `circuit-forge-strict`; say who made it and why), or the human's **own+ preferences** (`--prefs prefs.json`: `{"silkMinTextHeight": {"mm": 0.6}, "minTraceWidth": {"mm": 0.2}}`,+ checked on the board as warnings that never block). Never choose a stricter set for them; they can+ switch any time (`rules use --variant <name> --force`, or back to published with `rules use --force`).+ `rules use --profile 3rdPartyFab [--layers 2] [--variant <name>] [--prefs <file>]` installs the KiCad+ rules, loads the same pack into the open Fusion design and writes the choice into the spec. Tell the+ human the rules in plain words. A set others should share goes on adom/pcb-design-rules as a profile. 3. **The circuit** (aiflow-circuit-design): datasheet math and values. 4. **The test plan** (skill aiflow-test-plan). Think through how the built board will be tested on the probing workcell and write tests.json; `testplan check --tests tests.json --netlist <netlist.json>
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+47−11@@ -56,6 +56,7 @@ mod route_variants; mod impedance_cmd; mod reviews; mod deviations;+mod prefs; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -153,7 +154,11 @@ enum Cmd { Testplan { #[arg(value_parser = ["check"])] action: String, #[arg(long)] tests: Option<String>, #[arg(long)] netlist: Option<String>, #[arg(long)] add: bool }, Rules { #[arg(value_parser = ["use", "install", "check", "list"])] action: String, #[arg(long)] profile: Option<String>, #[arg(long)] board: Option<String>, /// replace an existing .kicad_dru #[arg(long)] force: bool, /// copper layers (default: the profile's -<n>L suffix, else the board's copper layer count, else 2)- #[arg(long)] layers: Option<u32> },+ #[arg(long)] layers: Option<u32>,+ /// `rules use`: a named variant of the fab's pack on adom/pcb-design-rules (`<fab>-<n>L-<variant>`, e.g. circuit-forge-strict); the published pack when omitted+ #[arg(long)] variant: Option<String>,+ /// `rules use`: the human's own preferences, a JSON file {"silkMinTextHeight": {"mm": 0.6}, "minTraceWidth": {"mm": 0.2}}: kept in the spec (fab.preferences) and checked on the board as warnings that never block (not written into the KiCad rules, where a preference would replace the fab's limit)+ #[arg(long)] prefs: Option<String> }, /// Start the clock at the prompt before any board exists: the run opens with the `intake` step and no board; `start --board` attaches it later on the same clock. Intake { #[arg(long)] prompt_time: String, /// the brief (a file: the human's message, a transcript, a spec); a requirements JSON is recorded as the requirements too #[arg(long)] brief: Option<String>, /// requirements.json (aiflow-intake): fab.target picks the sourcing profile@@ -4420,6 +4425,12 @@ fn main() { // 0.2.18: every LED's on/off state, and a programmable part's interface where the workcell reaches it let led_check = led_states::check(&solved_board, &board_now()); check_lines.push(led_states::summary(&solved_board, &board_now(), &led_check));+ // the human's own preferences: listed, never blocking (they are taste, not the fab's limits)+ let pref = spec_soft(&r)["fab"]["preferences"].clone();+ if pref.is_object() {+ let found = prefs::findings(&board_now(), &pref);+ check_lines.push(if found.is_empty() { "preferences: all of the human's own preferences met".to_string() } else { format!("preferences (warnings, not blocking): {}", found.join("; ")) });+ } let led_file = match led_check { Ok(p) => p, Err(p) => { missing.push(led_states::gate_message(&p)); None } }; let prog_check = programming::check(&solved_board, &board_now(), &spec_soft(&r)); check_lines.push(programming::summary(&solved_board, &board_now(), &spec_soft(&r), &prog_check));@@ -5191,7 +5202,7 @@ fn main() { if mode == "netlist" { hints.push("Run it again once the board is placed (`testplan check`): a test point under a part or out of tether reach is a gap again. finish runs it too.".into()); } ok(&body, &hints); }- Cmd::Rules { action, profile, board, force, layers } if action == "use" || action == "list" => {+ Cmd::Rules { action, profile, board, force, layers, variant, prefs } if action == "use" || action == "list" => { let page = "adom/pcb-design-rules"; let list = || -> Vec<String> { let o = std::process::Command::new("adom-wiki").args(["api", "/api/v1/pages/adom/pcb-design-rules/files?path=kicad"]).output().ok();@@ -5202,7 +5213,10 @@ fn main() { if action == "list" { let l = list(); if l.is_empty() { err(&format!("could not read the rule packs on {page}"), &["Check `adom-wiki whoami` and the wiki's health, then retry.".into()]); }- ok(&format!("{} rule packs on {page} (<fab>[-<process>]-<n>L):\n {}", l.len(), l.join("\n ")), &["`rules use --profile 3rdPartyFab` (the default) or `--profile <fab>[-<process>]`; --layers picks the copper count.".into()]);+ // published packs end in -<n>L; anything after that is a variant someone published on top of one+ let is_variant = |n: &str| n.rsplit_once("L-").is_some_and(|(a, _)| a.rsplit('-').next().is_some_and(|d| d.chars().all(|c| c.is_ascii_digit()) && !d.is_empty()));+ let (vars, pubs): (Vec<String>, Vec<String>) = l.into_iter().partition(|n| is_variant(n));+ ok(&format!("{} published rule packs on {page} (<fab>[-<process>]-<n>L), the fab's own limits:\n {}{}", pubs.len(), pubs.join("\n "), if vars.is_empty() { String::new() } else { format!("\n{} variant(s) people published on top of a pack (<pack>-<variant>, opt-in):\n {}", vars.len(), vars.join("\n ")) }), &["Ask the human which fab and which set before choosing: the published pack (the default, the fab's own limits), a variant, or their own preferences (`rules use --prefs <file.json>`). `rules use --profile <fab> --layers <n> [--variant <name>]`.".into()]); return; } let mut r = load_run(&cli);@@ -5214,13 +5228,24 @@ fn main() { let layers: u32 = layers.or(asked_layers).or_else(|| checks::board_copper_layers(&cb)).unwrap_or(2); let slug = match asked_base.as_str() { "fab" | "adom-3rdpartyfab" => "adom-3rdpartyfab".to_string(), o => o.to_string() }; let shown = if slug == "adom-3rdpartyfab" { "3rdPartyFab".to_string() } else { slug.clone() };- let file = format!("{slug}-{layers}L.kicad_dru");+ let variant = variant.clone().map(|v| v.trim().to_ascii_lowercase()).filter(|v| !v.is_empty());+ let file = match &variant { Some(v) => format!("{slug}-{layers}L-{v}.kicad_dru"), None => format!("{slug}-{layers}L.kicad_dru") }; let o = std::process::Command::new("adom-wiki").args(["repo", "show", page, &format!("kicad/{file}")]).output(); let text = o.ok().filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).to_string()).filter(|t| t.contains("(rule")).unwrap_or_else(|| { let l = list();- err(&format!("no {layers}-layer rule pack for {shown} on {page} ({file})"), &[format!("Packs there: {}. Pick one with --profile and --layers.", if l.is_empty() { "(could not list)".into() } else { l.join(", ") })])+ let near: Vec<&String> = l.iter().filter(|n| n.starts_with(&format!("{slug}-{layers}L"))).collect();+ err(&format!("no {} for {shown} on {page} ({file})", match &variant { Some(v) => format!("{layers}-layer variant {v:?}"), None => format!("{layers}-layer rule pack") }), &[format!("For {shown} {layers}L there: {}. All packs: `rules list`.", if near.is_empty() { "nothing".into() } else { near.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", ") })]) });- let marked = format!("# adom-aiflow rules: {slug} {layers}L (from {page})\n{text}");+ // the human's own preferences: kept in the spec and checked by AI Flow as warnings, never written into the+ // KiCad rules (KiCad applies one rule per constraint, so a preference rule would replace the fab's limit)+ let mut prefs_rec = Value::Null;+ if let Some(pf) = prefs {+ let pv: Value = serde_json::from_str(&std::fs::read_to_string(pf).unwrap_or_else(|e| err(&format!("{pf}: {e}"), &[]))).unwrap_or_else(|e| err(&format!("{pf} is not JSON: {e}"), &[]));+ prefs::validate(&pv).unwrap_or_else(|e| err(&format!("{pf}: {e}"), &["A stricter set others should share belongs on adom/pcb-design-rules as a profile (README, Published limits and profiles).".into()]));+ prefs_rec = pv;+ }+ let body = text.clone();+ let marked = format!("# adom-aiflow rules: {slug} {layers}L{} (from {page})\n{body}", variant.as_ref().map(|v| format!(" variant {v}")).unwrap_or_default()); let rd = dir.join("rules"); let _ = std::fs::create_dir_all(&rd); std::fs::write(rd.join(&file), &marked).unwrap_or_else(|e| err(&e.to_string(), &[])); let mut placed = vec![rd.join(&file).display().to_string()];@@ -5262,28 +5287,39 @@ fn main() { } } // the choice: in the spec when there is one, and in the run (an intake run has no spec yet)- r.data["fabRules"] = json!({"profile": shown, "slug": slug, "layers": layers, "file": file});+ r.data["fabRules"] = json!({"profile": shown, "slug": slug, "layers": layers, "file": file, "variant": variant}); if let Some(sp) = r.data["spec"].as_str().map(str::to_string) { if let Ok(mut v) = std::fs::read_to_string(&sp).map_err(|_| ()).and_then(|t| serde_json::from_str::<Value>(&t).map_err(|_| ())) { if !v["fab"].is_object() { v["fab"] = json!({}); } v["fab"]["rules"] = json!(slug);+ match &variant { Some(x) => v["fab"]["rulesVariant"] = json!(x), None => { if let Some(o) = v["fab"].as_object_mut() { o.remove("rulesVariant"); } } }+ if !prefs_rec.is_null() { v["fab"]["preferences"] = prefs_rec.clone(); } write_json(Path::new(&sp), &v); } } let mins: Vec<String> = text.lines().filter(|l| l.starts_with("# Min") || l.starts_with("# Copper to") || l.starts_with("# Annular")).map(|l| l.trim_start_matches("# ").to_string()).take(6).collect();- r.outcome("rules", json!({"profile": shown, "slug": slug, "layers": layers, "kicad": placed, "native": {"eda": eda.id(), "loaded": fusion_loaded}, "fusion": if eda.id() == "fusion" { fusion_loaded.clone() } else { Value::Null }}));+ r.outcome("rules", json!({"profile": shown, "slug": slug, "layers": layers, "variant": variant, "prefs": prefs_rec, "kicad": placed, "native": {"eda": eda.id(), "loaded": fusion_loaded}, "fusion": if eda.id() == "fusion" { fusion_loaded.clone() } else { Value::Null }})); r.save().unwrap();- ok(&format!("design rules: {shown}, {layers} layers{fusion_note}. KiCad rules at {}{replaced_note}", placed.join(", ")), &[+ let set = match &variant { Some(v) => format!(" (variant {v}, opt-in, stricter or different from the fab's published limits)"), None => " (the fab's published limits)".into() };+ let pref_now = if prefs_rec.is_null() { spec_soft(&r)["fab"]["preferences"].clone() } else { prefs_rec.clone() };+ let pref_lines = if pref_now.is_object() && cb.ends_with(".kicad_pcb") { Board::load(&cb).map(|b| prefs::findings(&b, &pref_now)).unwrap_or_default() } else { Vec::new() };+ let pref_note = if pref_now.is_object() { format!(", the human's own preferences kept (checked as warnings: {})", if pref_lines.is_empty() { "all met now".to_string() } else { pref_lines.join("; ") }) } else { String::new() };+ ok(&format!("design rules: {shown}, {layers} layers{set}{pref_note}{fusion_note}. KiCad rules at {}{replaced_note}", placed.join(", ")), &[ format!("Tell the human in plain words which rules are now enforced: {}.", if mins.is_empty() { "see the file".into() } else { mins.join("; ") }),- "Every later step designs to these: placement checks, routing widths and clearances, the gate's DRC and finish all read them. Design with margin above the minimums.".into()]);+ "Every later step designs to these: placement checks, routing widths and clearances, the gate's DRC and finish all read them. Design with margin above the minimums.".into(),+ "The human can switch at any time: another variant (`rules use --variant <name> --force`), back to the published pack (`rules use --force`), or their own preferences (`--prefs <file.json>`); `rules list` shows what exists.".into()]); }- Cmd::Rules { action, profile, board, force, layers } => {+ Cmd::Rules { action, profile, board, force, layers, .. } => { let run = Run::open(&dir); if run.is_some() { thread(&cli); } let board_path = board.clone().or_else(|| run.as_ref().map(|r| r.current_board())).unwrap_or_else(|| err("rules needs --board <file.kicad_pcb> outside a run", &[])); // the profile: --profile, else the spec's fab.rules, else the run's fabRules (`rules use`) let target = run.as_ref().and_then(|r| checks::rules_target(&spec_of(r), &r.data["fabRules"])); if profile.is_none() { if let Some(w) = target.as_ref().and_then(|t| t.2.clone()) { eprintln!("Warning: {w}"); } }+ if let (Some(r), Ok(b)) = (run.as_ref(), Board::load(&board_path)) {+ let pref = spec_soft(r)["fab"]["preferences"].clone();+ if pref.is_object() { for w in prefs::findings(&b, &pref) { eprintln!("Warning (the human's own preference, not blocking): {w}"); } }+ } let recorded_layers = if profile.is_none() { target.as_ref().and_then(|t| t.1) } else { None }; let profile = profile.clone().or_else(|| target.as_ref().map(|t| t.0.clone())).unwrap_or_else(|| err("no rules profile: pass --profile jlcpcb|jlcpcb-<n>L|fab or set \"fab\": {\"rules\": ...} in the spec", &[])); match action.as_str() {
crates/adom-aiflow/src/prefs.rsadded+99@@ -0,0 +1,99 @@+//! The human's own design preferences (John, 2026-11-03: "let users build their own preferences"):+//! taste on top of the fab's published limits, such as bigger or smaller silkscreen text or wider+//! tracks. They never go into the KiCad rules file: KiCad applies ONE rule per constraint to an item+//! (the last that matches), so a warning-level preference rule would replace the fab's error-level+//! limit. AI Flow checks them on the board itself and reports them as warnings that never block.+//! Stored in the spec as `fab.preferences`: {"silkMinTextHeight": {"mm": 0.6}, "minTraceWidth": {"mm": 0.2}}.+use aiflow_board::{sx, Board};+use serde_json::Value;++pub const KEYS: [&str; 2] = ["silkMinTextHeight", "minTraceWidth"];++/// Validate a preferences object; Err names the bad key.+pub fn validate(p: &Value) -> Result<(), String> {+ let Some(o) = p.as_object() else { return Err("preferences must be an object".into()) };+ for (k, v) in o {+ if !KEYS.contains(&k.as_str()) { return Err(format!("unknown preference {k:?}: the preferences are {}", KEYS.join(", "))); }+ if !v["mm"].as_f64().is_some_and(|x| x.is_finite() && x > 0.0) { return Err(format!("{k} needs {{\"mm\": <positive number>}}")); }+ }+ Ok(())+}++fn on_silk(n: &sx::Node) -> bool {+ n.nodes().any(|c| c.tag == "layer" && c.atom(1).is_some_and(|l| l.ends_with(".SilkS")))+}++fn hidden(n: &sx::Node) -> bool {+ n.items.iter().any(|i| matches!(i, sx::Item::Atom { text, .. } if text == "hide"))+ || n.nodes().any(|c| c.tag == "hide" && c.atom(1) != Some("no"))+ || n.find("effects").is_some_and(|e| e.items.iter().any(|i| matches!(i, sx::Item::Atom { text, .. } if text == "hide")) || e.nodes().any(|c| c.tag == "hide" && c.atom(1) != Some("no")))+}++/// Printed silkscreen text and its height (KiCad's font size is (size HEIGHT WIDTH)).+fn silk_texts(n: &sx::Node, owner: &str, out: &mut Vec<(String, f64)>) {+ let owner = if n.tag == "footprint" { n.nodes().find(|p| p.tag == "property" && p.atom(1) == Some("Reference")).and_then(|p| p.atom(2)).unwrap_or(owner).to_string() } else { owner.to_string() };+ for c in n.nodes() {+ if matches!(c.tag.as_str(), "fp_text" | "gr_text" | "property" | "text") && on_silk(c) && !hidden(c) {+ if let Some(h) = c.find("effects").and_then(|e| e.find("font")).and_then(|f| f.find("size")).and_then(|s| s.number(1)) {+ let label = c.atom(if c.tag == "property" { 2 } else { 2 }).or(c.atom(1)).unwrap_or("").to_string();+ out.push((if owner.is_empty() { label } else { format!("{owner} \"{label}\"") }, h));+ }+ }+ silk_texts(c, &owner, out);+ }+}++/// Warnings for the board against the preferences: one line per preference that is not met.+pub fn findings(b: &Board, p: &Value) -> Vec<String> {+ let mut out = Vec::new();+ if let Some(min) = p["silkMinTextHeight"]["mm"].as_f64() {+ let mut texts = Vec::new();+ silk_texts(&b.root, "", &mut texts);+ let small: Vec<String> = texts.iter().filter(|(_, h)| *h + 1e-9 < min).map(|(l, h)| format!("{l} {h} mm")).collect();+ if !small.is_empty() { out.push(format!("preference silkMinTextHeight {min} mm: {} silkscreen text(s) smaller: {}", small.len(), small.iter().take(8).cloned().collect::<Vec<_>>().join(", "))); }+ }+ if let Some(min) = p["minTraceWidth"]["mm"].as_f64() {+ let thin: Vec<f64> = b.copper_tracks().iter().map(|t| t.width).filter(|w| *w + 1e-9 < min).collect();+ if !thin.is_empty() {+ let narrowest = thin.iter().cloned().fold(f64::INFINITY, f64::min);+ out.push(format!("preference minTraceWidth {min} mm: {} track segment(s) narrower, the narrowest {narrowest} mm", thin.len()));+ }+ }+ out+}++#[cfg(test)]+mod tests {+ use super::*;+ use serde_json::json;++ const BOARD: &str = r#"(kicad_pcb (version 20241229) (layers (0 "F.Cu" signal) (2 "B.Cu" signal) (37 "F.SilkS" user))+ (net 0 "") (net 1 "A")+ (gr_rect (start 0 0) (end 20 20) (layer "Edge.Cuts"))+ (gr_text "BIG" (at 5 5) (layer "F.SilkS") (effects (font (size 1.5 1.5) (thickness 0.2))))+ (footprint "R:R0603" (layer "F.Cu") (at 10 10)+ (property "Reference" "R1" (at 0 -1) (layer "F.SilkS") (effects (font (size 0.6 0.6) (thickness 0.1))))+ (property "Value" "10k" (at 0 1) (layer "F.Fab") (effects (font (size 0.5 0.5))))+ (property "Datasheet" "x" (at 0 0) (layer "F.SilkS") (hide yes) (effects (font (size 0.3 0.3))))+ (pad "1" smd rect (at -0.8 0) (size 0.8 0.9) (layers "F.Cu") (net 1 "A")))+ (segment (start 1 1) (end 5 1) (width 0.15) (layer "F.Cu") (net 1))+ (segment (start 1 2) (end 5 2) (width 0.3) (layer "F.Cu") (net 1)))"#;++ #[test]+ fn preferences_validate() {+ assert!(validate(&json!({"silkMinTextHeight": {"mm": 0.6}, "minTraceWidth": {"mm": 0.2}})).is_ok());+ assert!(validate(&json!({"minClearance": {"mm": 0.2}})).unwrap_err().contains("unknown"));+ assert!(validate(&json!({"silkMinTextHeight": {"mm": 0}})).is_err());+ assert!(validate(&json!([1])).is_err());+ }++ #[test]+ fn findings_name_small_printed_text_and_thin_tracks_only() {+ let b = Board::from_text("t.kicad_pcb", BOARD.to_string()).unwrap();+ let f = findings(&b, &json!({"silkMinTextHeight": {"mm": 0.8}, "minTraceWidth": {"mm": 0.2}}));+ assert_eq!(f.len(), 2, "{f:?}");+ assert!(f[0].contains("1 silkscreen text(s)") && f[0].contains("R1 \"R1\" 0.6 mm"), "{}", f[0]); // the fab-layer Value and the hidden property do not count+ assert!(f[1].contains("1 track segment(s)") && f[1].contains("0.15 mm"), "{}", f[1]);+ assert!(findings(&b, &json!({"silkMinTextHeight": {"mm": 0.5}})).is_empty());+ }+}
flows/board.json+2−1@@ -23,7 +23,8 @@ "record": "the AI asking which fab and which design rules, the human's answer, then the rules loading in the EDA (Fusion's DRC dialog or KiCad's board setup) with the key numbers on screen", "workflow": [ "Ask the human which fab will build the board and name the choices: 3rdPartyFab (default, Adom's own process; its rules are private, fetched with an Adom login and never restated on a public page) or JLCPCB (published 2-layer capabilities). Never guess: a board drawn to the wrong process is a respin.",- "`adom-aiflow rules use --profile 3rdPartyFab` (or jlcpcb) writes the profile into the spec (fab.rules), installs the KiCad rules beside the board and, on a Fusion run, loads the Fusion DRC rules into the open design. `rules check` must pass before placement.",+ "Then ask which rule set for that fab, and say they can switch any time: the fab's published limits (the default, nothing stricter or looser than the fab states), a variant someone published on top of it (`rules list` shows them, e.g. circuit-forge-strict, with its author and why), or their own preferences (bigger or smaller silkscreen text, wider tracks: a small JSON for `--prefs`, checked as warnings, never blocking). Never pick a stricter set for them.",+ "`adom-aiflow rules use --profile 3rdPartyFab` (or jlcpcb, or any pack; `--variant <name>` for a variant, `--prefs <file.json>` for their own preferences) writes the choice into the spec (fab.rules, fab.rulesVariant, fab.preferences), installs the KiCad rules beside the board and, on a Fusion run, loads the Fusion DRC rules into the open design. `rules check` must pass before placement.", "Say the rules out loud in the chat in plain words (minimum track, clearance, via, edge clearance) only for public profiles; for 3rdPartyFab say 'the 3rdPartyFab 2-layer rules are loaded' and nothing more." ] },
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.43",+ "version": "0.2.44", "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.43",+ "version": "0.2.44", "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.",
skills/adom-aiflow/SKILL.md+10−4@@ -151,9 +151,15 @@ the library, simulation, current, thermal, fields and export, Fusion for everyth 1. **Intake and the timeline.** `intake`, `timeline open`. Offer the Claude usage widget in Hydrogen's title bar (Widgets, Claude usage) so the human sees the percent used as the run goes. 2. **Design rules, before anything is drawn.** Ask which fab builds the board. Default 3rdPartyFab (our- own process, say only "3rdPartyFab"), JLCPCB or any pack `rules list` shows. `rules use --profile- 3rdPartyFab [--layers 2]` installs the KiCad rules, loads the same pack into the open Fusion design and- writes the choice into the spec. Tell the human the rules in plain words.+ own process, say only "3rdPartyFab"), JLCPCB or any pack `rules list` shows. Then ask which rule set:+ the fab's **published** limits (the default), a **variant** someone published on top of it (`rules+ list` names them, e.g. `circuit-forge-strict`; say who made it and why), or the human's **own+ preferences** (`--prefs prefs.json`: `{"silkMinTextHeight": {"mm": 0.6}, "minTraceWidth": {"mm": 0.2}}`,+ checked on the board as warnings that never block). Never choose a stricter set for them; they can+ switch any time (`rules use --variant <name> --force`, or back to published with `rules use --force`).+ `rules use --profile 3rdPartyFab [--layers 2] [--variant <name>] [--prefs <file>]` installs the KiCad+ rules, loads the same pack into the open Fusion design and writes the choice into the spec. Tell the+ human the rules in plain words. A set others should share goes on adom/pcb-design-rules as a profile. 3. **The circuit** (aiflow-circuit-design): datasheet math and values. 4. **The test plan** (skill aiflow-test-plan). Think through how the built board will be tested on the probing workcell and write tests.json; `testplan check --tests tests.json --netlist <netlist.json>@@ -211,7 +217,7 @@ everything drawn on the design. The lane's steps are the Fusion lane's, with `al 1. **Intake and the timeline.** `intake --target <box> --eda altium ...`, `timeline open`. `--eda altium` fixes the run's step list (the brief need not say "Altium").-2. **Design rules.** Ask which fab builds the board; `rules use --profile <pack> [--layers 2]`. Before+2. **Design rules.** Ask which fab builds the board, then which rule set (the fab's published limits, a variant from `rules list`, or the human's own `--prefs`); `rules use --profile <pack> [--layers 2] [--variant <name>] [--prefs <file>]`. Before the board exists the choice is recorded; the board `altium schematic` builds carries it. 3. **The circuit** (aiflow-circuit-design), **the test plan** (aiflow-test-plan, `testplan check`), **the parts** from their wiki component pages (aiflow-sourcing). The netlist is `netlist.json`: