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.32
10 files changed
+305−23
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.2.31"+version = "0.2.32" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.2.31"+version = "0.2.32" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.2.31"+version = "0.2.32" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.2.31"+version = "0.2.32" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+12−3@@ -78,7 +78,7 @@ The plan JSON shape (full reference: docs/plan-schema.md): If the binary should do this step itself, file it (see Give it back). -## No step is skipped+## Steps in order, and deliberate deviations A run carries its lane's required steps in order (`flows/board.json` `sequence`; Fusion: intake, rules, design, test-plan, sourcing, library, schematic, simulation, placement, routing, thermal-vias, pours,@@ -88,8 +88,17 @@ visit (the `proofs` in the flow file: `rules use` for rules, `testplan check` fo moves` for placement, `thermal-vias land`, `outline round`, `tour nets`, `silkscreen-audit --record` (Fusion: `fusion silkscreen`), `export` (Fusion: `fusion export`) ...). Going back is a return: `step <k> --back --why "<what has to change>"`. It is filmed, it draws an arc on the timeline, and every later step becomes a redo that must be done again in order.-`finish` refuses until the whole chain is in order. If a step truly does not apply, ask the human; never-jump past it.+`finish` refuses until the whole chain is in order, or until you accept what it lists on purpose.++**When a check is wrong for this board, deviate on purpose instead of stopping.** AI Flow has not seen+every kind of board. When a step does not apply, or `finish` refuses something that is right for this+design, record it: `adom-aiflow deviate "<item>" --why "<why this board is right anyway>"`. The item is+the one the refusal names (`gate`, `heatsink vias`, `analyze-thermal`, `step test-plan`, ...); the+refusal's hint lists them. A deviation is bound to the saved board and spec (any change to either+expires it), `finish` then completes as "finished ... except N recorded deviation(s)" and lists each one+with its reason in the summary, and `deviate --list` shows them. Fix what you can first: a deviation is+for a check that is wrong about this board, never for a board you have not finished, and you tell the+human about every one. Better still, fix the check in AI Flow and send the PR (Contribute to AI Flow). **Pattern boards (0.2.15, issue #84).** A passive copper-pattern board (a reflector disc, a carrier plate: no parts, no BOM, no schematic, no electrical load) takes the `pattern` sequence: `intake --kind pattern`
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/deviations.rsadded+116@@ -0,0 +1,116 @@+//! Deliberate deviations: the way past a check that is wrong for this board. AI Flow is a helper,+//! not a wall (John, 2026-11-01): a board unlike anything AI Flow has seen must still get built.+//! `deviate "<item>" --why` records one finish item, or one step to skip (`step <name>`), that the+//! AI accepts on purpose, bound to the saved board and spec it was judged on. `finish` then+//! completes and lists every deviation in its summary and its answer; nothing is hidden, and a+//! deviation expires the moment the board or the spec changes.+use serde_json::{json, Value};++/// A reason has to say something about the board, not "skip".+pub const MIN_WHY: usize = 20;++/// The item a finish line or a step blocker names: the text before its first ':' or ' (', lowercase.+pub fn key(line: &str) -> String {+ let end = [line.find(':'), line.find(" (")].into_iter().flatten().min().unwrap_or(line.len());+ line[..end].trim().to_lowercase()+}++fn bound(d: &Value, board_sha: &str, spec_sha: &str) -> bool {+ d["boardSha256"] == board_sha && d["specSha256"] == spec_sha+}++/// The deviations recorded on this exact board and spec.+pub fn live(data: &Value, board_sha: &str, spec_sha: &str) -> Vec<Value> {+ data["deviations"].as_array().into_iter().flatten().filter(|d| bound(d, board_sha, spec_sha)).cloned().collect()+}++/// The deviations recorded on an earlier board or spec: they no longer apply.+pub fn expired(data: &Value, board_sha: &str, spec_sha: &str) -> Vec<Value> {+ data["deviations"].as_array().into_iter().flatten().filter(|d| !bound(d, board_sha, spec_sha)).cloned().collect()+}++/// Record (or replace) the deviation for `item`. `known` are the items the last refusal named.+pub fn record(data: &mut Value, item: &str, why: &str, thread: &str, board_sha: &str, spec_sha: &str, at: &str, known: &[String]) -> Result<Value, String> {+ let item = key(item);+ if why.trim().chars().count() < MIN_WHY {+ return Err(format!("--why needs a real reason (at least {MIN_WHY} characters): why this board is right even though the check refuses it"));+ }+ if !known.contains(&item) {+ return Err(if known.is_empty() {+ "nothing to deviate from yet: run `finish` (or the `step` that refused) first; deviate names one of the items it lists".into()+ } else {+ format!("\"{item}\" is not an item the last refusal named; it named: {}", known.join(", "))+ });+ }+ let rec = json!({"item": item, "why": why.trim(), "aiThread": thread, "at": at, "boardSha256": board_sha, "specSha256": spec_sha});+ let list = data["deviations"].as_array().cloned().unwrap_or_default();+ let mut list: Vec<Value> = list.into_iter().filter(|d| d["item"] != item.as_str()).collect();+ list.push(rec.clone());+ data["deviations"] = json!(list);+ Ok(rec)+}++/// Remove the deviation for `item`; whether one was there.+pub fn clear(data: &mut Value, item: &str) -> bool {+ let item = key(item);+ let Some(list) = data["deviations"].as_array().cloned() else { return false };+ let kept: Vec<Value> = list.iter().filter(|d| d["item"] != item.as_str()).cloned().collect();+ let removed = kept.len() != list.len();+ data["deviations"] = json!(kept);+ removed+}++/// Split refusal lines into those still open and those a live deviation accepts (with its reason).+pub fn split(lines: Vec<String>, live: &[Value]) -> (Vec<String>, Vec<Value>) {+ let mut open = Vec::new();+ let mut accepted = Vec::new();+ for line in lines {+ let k = key(&line);+ match live.iter().find(|d| d["item"] == k.as_str()) {+ Some(d) => accepted.push(json!({"item": k, "line": line, "why": d["why"], "aiThread": d["aiThread"], "at": d["at"]})),+ None => open.push(line),+ }+ }+ (open, accepted)+}++/// The hint every refusal that deviations can answer carries.+pub fn hint(items: &[String]) -> String {+ format!("If the check, not the board, is wrong (AI Flow has not seen a design like this), accept it on purpose instead of stopping: `adom-aiflow deviate \"<item>\" --why \"<why this board is right anyway>\"` (items here: {}). It is recorded, bound to this board and spec, and shown in the finish summary; tell the human about each one. Fixing the check in AI Flow and sending a PR is better still.", items.join(", "))+}++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn keys_come_from_the_refusal_lines() {+ assert_eq!(key("gate: 0 new DRC errors and 0 unconnected (run gate)"), "gate");+ assert_eq!(key("place verify (saved board or spec changed; verify again)"), "place verify");+ assert_eq!(key("land route"), "land route");+ assert_eq!(key("heatsink vias: U1 has 2 of 6 (thermal-vias plan, land)"), "heatsink vias");+ assert_eq!(key("step routing (never done)"), "step routing");+ assert_eq!(key("native DRC: the live full DRC has 2 error(s)"), "native drc");+ }++ #[test]+ fn a_deviation_is_deliberate_named_and_bound_to_the_board_and_spec() {+ let mut d = json!({});+ let known = vec!["gate".to_string(), "heatsink vias".to_string()];+ assert!(record(&mut d, "gate", "skip", "t", "b1", "s1", "now", &known).is_err());+ assert!(record(&mut d, "pours", "the pours are deliberately absent on this RF board", "t", "b1", "s1", "now", &known).is_err());+ assert!(record(&mut d, "gate", "a reason that is long enough here", "t", "b1", "s1", "now", &[]).unwrap_err().contains("finish"));+ record(&mut d, "Heatsink vias", "LEDs at 20 mW each; the Fields solve shows +3 C", "t", "b1", "s1", "now", &known).unwrap();+ let lines = vec!["heatsink vias: D1 has 0 of 6 (thermal-vias plan, land)".to_string(), "gate: 1 new DRC error".to_string()];+ let (open, accepted) = split(lines.clone(), &live(&d, "b1", "s1"));+ assert_eq!(open, vec!["gate: 1 new DRC error".to_string()]);+ assert_eq!(accepted.len(), 1);+ assert_eq!(accepted[0]["why"], "LEDs at 20 mW each; the Fields solve shows +3 C");+ // a changed board or spec: the deviation no longer applies+ let (open, _) = split(lines.clone(), &live(&d, "b2", "s1"));+ assert_eq!(open.len(), 2);+ assert_eq!(expired(&d, "b1", "s2").len(), 1);+ assert!(clear(&mut d, "heatsink vias"));+ assert!(live(&d, "b1", "s1").is_empty());+ }+}
crates/adom-aiflow/src/main.rs+71−4@@ -55,6 +55,7 @@ mod gate_eval; mod route_variants; mod impedance_cmd; mod reviews;+mod deviations; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -259,6 +260,8 @@ enum Cmd { Impedance { #[arg(value_parser = ["table", "check"])] action: String, #[arg(long)] board: Option<String>, #[arg(long)] spec: Option<String>, #[arg(long)] plan: Option<String>, #[arg(long)] json: bool }, /// Record a change to the spec's budgets (loads, hot, rise limits, switch-node length, net widths, fab) after placement began. Budgets are frozen at placement; this is the only way to move one, and it stays on the record. SpecAmend { #[arg(long)] why: String },+ /// Accept one item `finish` (or a `step`) refused, on purpose, when the check rather than the board is wrong: `deviate "<item>" --why "..."` (the item as the refusal names it, e.g. "gate", "heatsink vias", "step routing"). Recorded, bound to the saved board and spec, and listed in the finish summary; `--list` shows them, `--clear` removes one.+ Deviate { item: Option<String>, #[arg(long)] why: Option<String>, #[arg(long)] list: bool, #[arg(long)] clear: bool }, /// Declare the step you are working on (placement, routing, pours, current, thermal, capture, finish, or your own). --back marks a return to an earlier step; --why says what sent you back. Step { name: String, #[arg(long)] back: bool, #[arg(long)] why: Option<String> }, /// The end of the run as the human sees it: the moment you say "done, here is your video". Needs finish first and the video file.@@ -2647,6 +2650,22 @@ fn print_next(cli: &Cli) { if let Some(r) = Run::open(&run_dir(cli)) { if let Some(l) = next_step_line(&r, cli) { println!("{l}"); } } } +/// The saved board's and spec's hashes: what a deviation is bound to.+fn run_hashes(r: &Run) -> (String, String) {+ (sha256_file(&r.current_board()), sha256_file(r.data["spec"].as_str().unwrap_or("")))+}++/// The flow's steps not yet proven (never done, redone, unproven, or with a failed proof), in order.+fn pending_steps(r: &Run) -> Vec<String> {+ let flow: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();+ let seq = r.data["sequence"].as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect()).unwrap_or_else(|| sequence::sequence_of(&flow, &sequence::sequence_key(&r.dir, &r.data)));+ let states = sequence::states(&r.dir, &r.data, &flow, &seq);+ states.iter().filter(|s| {+ let failed = flow["proofs"][s.name.as_str()].as_array().is_some_and(|keys| keys.iter().filter_map(Value::as_str).any(|key| continuation::failed_proof(&r.data["outcomes"][key])));+ matches!(s.status, sequence::Status::Todo | sequence::Status::Redo) || !s.unproven.is_empty() || failed+ }).map(|s| s.name.clone()).collect()+}+ fn continuation_report(r: &Run, evidence: Option<&Path>) -> Value { let flow: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap(); let seq = r.data["sequence"].as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect()).unwrap_or_else(|| sequence::sequence_of(&flow, &sequence::sequence_key(&r.dir, &r.data)));@@ -2655,6 +2674,13 @@ fn continuation_report(r: &Run, evidence: Option<&Path>) -> Value { let failed = flow["proofs"][s.name.as_str()].as_array().is_some_and(|keys| keys.iter().filter_map(Value::as_str).any(|key| continuation::failed_proof(&r.data["outcomes"][key]) || (!r.data["outcomeAt"][key].is_null() && r.data["outcomes"][key].is_null()))); matches!(s.status, sequence::Status::Todo | sequence::Status::Redo) || !s.unproven.is_empty() || failed }).map(|s| s.name.clone()).collect();+ let (bs, ss) = run_hashes(r);+ let accepted = &r.data["deviatedSteps"];+ let live = deviations::live(&r.data, &bs, &ss);+ let pending: Vec<String> = pending.into_iter().filter(|p| {+ let finish_accepted = accepted["boardSha256"] == bs.as_str() && accepted["specSha256"] == ss.as_str() && accepted["steps"].as_array().is_some_and(|a| a.iter().any(|s| s == p.as_str()));+ !finish_accepted && !live.iter().any(|d| d["item"] == format!("step {p}").as_str())+ }).collect(); let stop = evidence.map(|p| std::fs::read_to_string(p).map_err(|e| e.to_string()).and_then(|s| serde_json::from_str::<Value>(&s).map_err(|e| e.to_string()))); continuation::assess(&r.data, &pending, stop) }@@ -4254,6 +4280,30 @@ fn main() { }; match impedance_cmd::command(action, &board_file, &spec_v, plan_p.as_deref(), *as_json) { Ok((m, h)) => ok(&m, &h), Err((m, h)) => err(&m, &h) } }+ Cmd::Deviate { item, why, list, clear } => {+ let mut r = load_run(&cli);+ let (bs, ss) = run_hashes(&r);+ if *list {+ let (live, old) = (deviations::live(&r.data, &bs, &ss), deviations::expired(&r.data, &bs, &ss));+ let fmt = |d: &Value| format!("{}: \"{}\" ({}, {})", d["item"].as_str().unwrap_or(""), d["why"].as_str().unwrap_or(""), d["aiThread"].as_str().unwrap_or(""), d["at"].as_str().unwrap_or(""));+ ok(&format!("{} deviation(s) on this board and spec{}{}", live.len(), live.iter().map(|d| format!("\n {}", fmt(d))).collect::<String>(), if old.is_empty() { String::new() } else { format!("\n{} expired (the board or spec changed since):{}", old.len(), old.iter().map(|d| format!("\n {}", fmt(d))).collect::<String>()) }), &[]);+ return;+ }+ let t = thread(&cli);+ let item = item.clone().unwrap_or_else(|| err("deviate needs the item, as the refusal names it: `deviate \"gate\" --why \"...\"`", &["`deviate --list` shows the recorded ones.".into()]));+ if *clear {+ let had = deviations::clear(&mut r.data, &item);+ r.save().unwrap();+ ok(&format!("{}", if had { format!("removed the deviation for {}", deviations::key(&item)) } else { format!("no deviation recorded for {}", deviations::key(&item)) }), &[]);+ return;+ }+ let known: Vec<String> = r.data["lastRefusalItems"].as_array().into_iter().flatten().filter_map(|v| v.as_str().map(str::to_string)).collect();+ let w = why.clone().unwrap_or_else(|| err("deviate needs --why: why this board is right even though the check refuses it", &[]));+ let rec = deviations::record(&mut r.data, &item, &w, &t, &bs, &ss, &now(), &known).unwrap_or_else(|e| err(&e, &[]));+ r.log("deviate", rec.clone());+ r.save().unwrap();+ ok(&format!("deviation recorded for {}: \"{}\"", rec["item"].as_str().unwrap_or(""), rec["why"].as_str().unwrap_or("")), &["It holds for this saved board and spec only; any change to either expires it. Run the refused command again (finish, or the step). Tell the human about every deviation: finish lists them.".into()]);+ } Cmd::SpecAmend { why } => { let t = thread(&cli); let r = load_run(&cli);@@ -4539,8 +4589,20 @@ fn main() { for g in project_lib::recheck(&parts_v) { missing.push(format!("parts: project library: {g}")); } if let Some(rec) = live_rec.take() { r.outcome(verified::LIVE_DRC, rec); r.save().ok(); } r.outcome("finish-checks", json!({"powerInputs": label_checks["powerInputs"], "testPoints": label_checks["testPoints"], "lines": check_lines})); r.save().ok();+ let (bs, ss) = run_hashes(&r);+ let mut items: Vec<String> = missing.iter().map(|m| deviations::key(m)).collect();+ items.dedup();+ let (missing, deviated) = deviations::split(missing, &deviations::live(&r.data, &bs, &ss));+ r.data["lastRefusalItems"] = json!(items);+ r.save().ok(); if !missing.is_empty() {- err(&format!("not finished: {}", missing.join("; ")), &["A board with any of these open is not a result. Keep going: the hints of the failing stage say what to change.".into()]);+ let open: Vec<String> = missing.iter().map(|m| deviations::key(m)).collect();+ err(&format!("not finished: {}", missing.join("; ")), &["Keep going: the hints of the failing stage say what to change.".into(), deviations::hint(&open)]);+ }+ if !deviated.is_empty() {+ // finish-guard honours the steps these deviations left unproven, on this board and spec only+ let before_finish: Vec<String> = pending_steps(&r).into_iter().take_while(|s| s != "finish").collect();+ r.data["deviatedSteps"] = json!({"steps": before_finish, "boardSha256": bs, "specSha256": ss}); } if let Some(target) = r.data["target"].as_str().map(str::to_string) { let brs = Bridge { ai_thread: t.clone(), target };@@ -4564,10 +4626,11 @@ fn main() { if let Some(rel) = led_states::record_in_release(&solved_board, lf, &sha) { eprintln!("ledStates recorded in {}", rel.display()); } } r.data["summary"]["notApplicable"] = json!(not_applicable);+ r.data["summary"]["deviations"] = json!(deviated); if nothing_to_source { r.data["summary"]["notApplicable"].as_array_mut().unwrap().push(json!("sourcing check: not applicable (no footprint in the BOM)")); } r.log("finish", json!({"minutes": r.elapsed_minutes(), "steps": Value::Object(r.step_report())})); r.save().unwrap();- ok(&(format!("finished: {} from the prompt to a board passing the configured screens, {:.1} min\npours: {}\n{}{}", r.data["engine"], r.elapsed_minutes(), pourverify::mode_label(r.outcomes().get("land-pours").unwrap_or(&Value::Null)), step_table(&r), if lib_parts.is_empty() { String::new() } else { format!("\nparts from a project library (no public component page): {}", lib_parts.join("; ")) }) + &na_text(&r.data["summary"]["notApplicable"]) + &if reviewed.is_empty() { String::new() } else { format!("\nreviewed exceptions and assumptions:\n {}", reviewed.join("\n ")) } + "\n" + &verified::message(&checked)), &["Tell the human exactly the verified list above; never call the board ready to fabricate unless every item says yes, and even then advise a human review.".into(), "The run is not delivered yet: cut the video, then `adom-aiflow deliver --video <mp4> --message \"<what you tell the human>\"`; the clock stops there, not here.".into(), "Add your token and dollar accounting to run.json under tokens and usd; the manifest is the row on the chart.".into(), give_back_hint()]);+ ok(&(format!("finished: {} from the prompt to a board {}, {:.1} min\npours: {}\n{}{}", r.data["engine"], if deviated.is_empty() { "passing the configured screens".to_string() } else { format!("passing the configured screens except {} recorded deviation(s), listed below", deviated.len()) }, r.elapsed_minutes(), pourverify::mode_label(r.outcomes().get("land-pours").unwrap_or(&Value::Null)), step_table(&r), if lib_parts.is_empty() { String::new() } else { format!("\nparts from a project library (no public component page): {}", lib_parts.join("; ")) }) + &na_text(&r.data["summary"]["notApplicable"]) + &if reviewed.is_empty() { String::new() } else { format!("\nreviewed exceptions and assumptions:\n {}", reviewed.join("\n ")) } + &if deviated.is_empty() { String::new() } else { format!("\nfinished with {} recorded deviation(s) (checks this board did not meet, accepted on purpose):\n {}", deviated.len(), deviated.iter().map(|d| format!("{}: {} (why: {})", d["item"].as_str().unwrap_or(""), d["line"].as_str().unwrap_or(""), d["why"].as_str().unwrap_or(""))).collect::<Vec<_>>().join("\n ")) } + "\n" + &verified::message(&checked)), &["Tell the human exactly the verified list above; never call the board ready to fabricate unless every item says yes, and even then advise a human review.".into(), "The run is not delivered yet: cut the video, then `adom-aiflow deliver --video <mp4> --message \"<what you tell the human>\"`; the clock stops there, not here.".into(), "Add your token and dollar accounting to run.json under tokens and usd; the manifest is the row on the chart.".into(), give_back_hint()]); } Cmd::Capture { action, label, reason } => { let mut r = load_run(&cli);@@ -5712,12 +5775,16 @@ fn main() { err(&format!("{name} comes before {} in the flow: going back is a return", seq[cp]), &[format!("`step {name} --back --why \"<what has to change>\"`: the return is filmed and shown on the timeline as a loop, and every later step is redone after it.")]); } } let st = sequence::states(&dir, &r.data, &flow_full, &seq);- let b = sequence::blockers(&st, name);+ let (bs, ss) = run_hashes(&r);+ let live = deviations::live(&r.data, &bs, &ss);+ let b: Vec<String> = sequence::blockers(&st, name).into_iter().filter(|x| !live.iter().any(|d| d["item"] == format!("step {}", deviations::key(x)).as_str())).collect(); if !b.is_empty() {+ r.data["lastRefusalItems"] = json!(b.iter().map(|x| format!("step {}", deviations::key(x))).collect::<Vec<_>>());+ r.save().ok(); let first = b[0].split(' ').next().unwrap_or("").to_string(); err(&format!("not yet: {name} needs every required step before it done, and these are not:\n {}", b.join("\n ")), &[ format!("`step {first}` and do it: a step is done when its own command has run during its latest visit (flows/board.json proofs). No step of the flow can be skipped."),- "If a step truly does not apply to this board, say so to the human and ask; never jump past it.".into()]);+ format!("If a step truly does not apply to this board, skip it on purpose: `adom-aiflow deviate \"step {first}\" --why \"<why it does not apply>\"`, then `step {name}` again. finish lists it, and you tell the human.")]); } } }
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.2.31",+ "version": "0.2.32", "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.31",+ "version": "0.2.32", "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+12−3@@ -68,7 +68,7 @@ The plan JSON shape (full reference: docs/plan-schema.md): If the binary should do this step itself, file it (see Give it back). -## No step is skipped+## Steps in order, and deliberate deviations A run carries its lane's required steps in order (`flows/board.json` `sequence`; Fusion: intake, rules, design, test-plan, sourcing, library, schematic, simulation, placement, routing, thermal-vias, pours,@@ -79,8 +79,17 @@ moves` for placement, `thermal-vias land`, `outline round`, `tour nets`, `silksc (Fusion: `fusion silkscreen`; Altium: `altium silkscreen`), `export` (Fusion: `fusion export`; Altium: `altium export`), `fusion|altium library` and `fusion|altium schematic` ...). Going back is a return: `step <k> --back --why "<what has to change>"`. It is filmed, it draws an arc on the timeline, and every later step becomes a redo that must be done again in order.-`finish` refuses until the whole chain is in order. If a step truly does not apply, ask the human; never-jump past it.+`finish` refuses until the whole chain is in order, or until you accept what it lists on purpose.++**When a check is wrong for this board, deviate on purpose instead of stopping.** AI Flow has not seen+every kind of board. When a step does not apply, or `finish` refuses something that is right for this+design, record it: `adom-aiflow deviate "<item>" --why "<why this board is right anyway>"`. The item is+the one the refusal names (`gate`, `heatsink vias`, `analyze-thermal`, `step test-plan`, ...); the+refusal's hint lists them. A deviation is bound to the saved board and spec (any change to either+expires it), `finish` then completes as "finished ... except N recorded deviation(s)" and lists each one+with its reason in the summary, and `deviate --list` shows them. Fix what you can first: a deviation is+for a check that is wrong about this board, never for a board you have not finished, and you tell the+human about every one. Better still, fix the check in AI Flow and send the PR (Contribute to AI Flow). **Pattern boards (0.2.15, issue #84).** A passive copper-pattern board (a reflector disc, a carrier plate: no parts, no BOM, no schematic, no electrical load) takes the `pattern` sequence: `intake --kind pattern`
tests/deviation-workflow.pyadded+81@@ -0,0 +1,81 @@+"""Deliberate deviations end to end: finish and step refuse with their items, `deviate` accepts one+on purpose with a reason, bound to the saved board and spec; finish completes and lists each one;+a changed board expires them. No CAD, bridge or dashboard."""+import json, os, pathlib, subprocess, sys, tempfile++binary = str(pathlib.Path(sys.argv[1]).resolve())+BOARD = """(kicad_pcb (version 20241229) (generator "test") (general (thickness 1.6))+ (layers (0 "F.Cu" signal) (2 "B.Cu" signal) (25 "Edge.Cuts" user) (37 "F.SilkS" user))+ (net 0 "") (net 1 "A") (net 2 "B")+ (gr_rect (start 0 0) (end 30 20) (layer "Edge.Cuts"))+ (footprint "R:R0603" (layer "F.Cu") (at 10 10) (property "Reference" "R1" (at 0 -1.5) (layer "F.SilkS"))+ (pad "1" smd rect (at -0.8 0) (size 0.8 0.9) (layers "F.Cu") (net 1 "A"))+ (pad "2" smd rect (at 0.8 0) (size 0.8 0.9) (layers "F.Cu") (net 2 "B")))+ (footprint "R:R0603" (layer "F.Cu") (at 20 10) (property "Reference" "R2" (at 0 -1.5) (layer "F.SilkS"))+ (pad "1" smd rect (at -0.8 0) (size 0.8 0.9) (layers "F.Cu") (net 1 "A"))+ (pad "2" smd rect (at 0.8 0) (size 0.8 0.9) (layers "F.Cu") (net 2 "B"))))+"""+WHY = "test fixture: this board is right even though the check refuses it"++with tempfile.TemporaryDirectory(prefix="aiflow-deviation-") as td:+ p = pathlib.Path(td)+ env = dict(os.environ, AIFLOW_DASHBOARD="off", AIFLOW_NO_PART_SEARCH="1", AIFLOW_NO_PART_LOOKUP="1")+ (p / "board.kicad_pcb").write_text(BOARD)+ (p / "spec.json").write_text("{}")++ def call(*args):+ return subprocess.run([binary, "--run", "run", "--ai-thread", "deviation-test", *args], cwd=p, env=env, capture_output=True, text=True, timeout=120)++ def items():+ return json.loads((p / "run" / "run.json").read_text())["lastRefusalItems"]++ r = call("start", "--board", "board.kicad_pcb", "--spec", "spec.json", "--engine", "test")+ assert r.returncode == 0, r.stdout + r.stderr++ # deviate before any refusal: nothing to name+ assert "finish" in call("deviate", "gate", "--why", WHY).stdout++ # a step past undone steps refuses, names them, and offers the deliberate skip+ r = call("step", "routing")+ assert r.returncode == 2 and "deviate \"step " in r.stdout, r.stdout+ skipped = items()+ assert skipped and all(i.startswith("step ") for i in skipped), skipped+ for i in skipped:+ assert call("deviate", i, "--why", WHY).returncode == 0+ assert call("step", "routing").returncode == 0, "a deliberately skipped step still blocked"++ # finish refuses with its items and the deviate hint+ r = call("finish")+ assert r.returncode == 2 and "adom-aiflow deviate" in r.stdout, r.stdout+ open_items = items()+ assert open_items, "finish recorded no items"+ # a reason is required, and only named items can be deviated+ assert call("deviate", open_items[0], "--why", "skip").returncode == 2+ r = call("deviate", "not-an-item", "--why", WHY)+ assert r.returncode == 2 and open_items[0] in r.stdout, r.stdout+ # one item accepted: finish still refuses the others, and no longer names that one+ assert call("deviate", open_items[0], "--why", WHY).returncode == 0+ r = call("finish")+ refused = r.stdout.split("\n")[0]+ assert r.returncode == 2 and not any(seg.strip().lower().startswith(open_items[0]) for seg in refused.removeprefix("ERROR: not finished:").split(";")), refused+ for i in items():+ assert call("deviate", i, "--why", WHY).returncode == 0, i+ r = call("finish")+ assert r.returncode == 0 and "recorded deviation(s)" in r.stdout and "except" in r.stdout, r.stdout + r.stderr+ summary = json.loads((p / "run" / "run.json").read_text())["summary"]+ assert summary["deviations"] and all(d["why"] == WHY for d in summary["deviations"])+ assert "deviation(s) on this board and spec" in call("deviate", "--list").stdout++ # the board changes: every deviation expires and finish refuses again+ data = json.loads((p / "run" / "run.json").read_text())+ current = data["boards"][-1] if data.get("boards") else data["board"]+ cp = pathlib.Path(current if isinstance(current, str) else current["path"])+ cp = cp if cp.is_absolute() else p / cp+ cp.write_text(cp.read_text().replace("(at 20 10)", "(at 21 10)"))+ r = call("deviate", "--list")+ assert "0 deviation(s)" in r.stdout and "expired" in r.stdout, r.stdout+ assert call("finish").returncode == 2, "a deviation outlived the board it was recorded on"++ # clear removes one+ assert "removed" in call("deviate", open_items[0], "--clear").stdout+ print("PASS: step and finish name their items, deviate needs a named item and a reason, finish completes listing every deviation, a changed board expires them, clear removes one")