app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← Commit history
0.1.91: NEXT line after intake/start/prompt/step/stage end, and an over-budget step nag (issue #44)
Every intake, start, prompt, stage end and step now ends with
"NEXT: adom-aiflow --run <run> --ai-thread "<thread>" step <next> (<what it does>)".
flows/board.json gains per-step expected minutes; past twice that, every run command
(stderr) and finish-guard (text and JSON "warning") warn that the run is still in the
step and to declare the step it is actually on. Warn only, never a block.
SKILL.md: declare every step even when your own scripts do its work, and a documented
high-speed lane (take route=ai, stage start/end route around the AI's router, gate and
land route --plan, evidence add --kind calcs for impedance and skew).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
10 files changed
+160−21
Cargo.lock+10−10@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-aiflow"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-analyze", "aiflow-board",@@ -25,7 +25,7 @@ dependencies = [ [[package]] name = "aiflow-analyze"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "serde",@@ -35,7 +35,7 @@ dependencies = [ [[package]] name = "aiflow-board"-version = "0.1.90"+version = "0.1.91" dependencies = [ "roxmltree", "serde",@@ -44,7 +44,7 @@ dependencies = [ [[package]] name = "aiflow-bridge"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "serde",@@ -53,7 +53,7 @@ dependencies = [ [[package]] name = "aiflow-copper"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "aiflow-grid",@@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "aiflow-grid"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "serde",@@ -72,7 +72,7 @@ dependencies = [ [[package]] name = "aiflow-place"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "serde",@@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aiflow-pours"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "aiflow-copper",@@ -91,7 +91,7 @@ dependencies = [ [[package]] name = "aiflow-router"-version = "0.1.90"+version = "0.1.91" dependencies = [ "aiflow-board", "aiflow-grid",@@ -101,7 +101,7 @@ dependencies = [ [[package]] name = "aiflow-run"-version = "0.1.90"+version = "0.1.91" dependencies = [ "serde", "serde_json",
Cargo.toml+1−1@@ -14,7 +14,7 @@ members = [ ] [workspace.package]-version = "0.1.90"+version = "0.1.91" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
SKILL.md+16−3@@ -12,6 +12,8 @@ Install: `adom-wiki pkg install adom/adom-aiflow` (one Rust binary; `service-kic Every state-changing command takes `--ai-thread "<your thread name>"`. Every run lives in one directory (`--run DIR`, default `./aiflow-run`) with a `run.json` manifest: the clock, the plan, the decisions, every stage's start and end, the outcomes, the capture markers and the summary. The manifest is the comparison; nothing counts that is not in it. +Declare every step as you enter it, even when you do that step's work with your own scripts; the timeline, the clips and the video depend on it. `intake`, `start`, `prompt`, `stage end` and every `step` end with a `NEXT:` line naming the next required step and its exact command. A run that sits in one step for more than twice that step's expected minutes (`minutes` in flows/board.json) gets a WARNING on every command and in `finish-guard`; it never blocks, it means you are probably doing later steps' work undeclared.+ ## Continue to the requested finish line Keep the original deliverable active across status questions, dashboard changes, tool repairs and other supporting requests. Completing a subtask is not a reason to end the turn. Report progress briefly, then execute the next available engineering action. On resume, read the run and project completion plan, verify the live target, and continue the earliest incomplete gate.@@ -39,10 +41,10 @@ The guard is read-only and suitable for a host's pre-stop integration. It does n The flow is a file, `flows/board.json`: the steps in order (intake, rules, test-plan, components, libraries, models, placement, routing, thermal-vias, pours, outline, current, thermal, fields, nets, silkscreen, 3d, finish, export; the `sequence` per lane is enforced, see No step is skipped), who does each, what the binary offers at each, and the steps that come later (components, libraries, schematic, simulation before the board; moleculize, paste, probe after it). `models` (kicad_model_check) refuses to go on while any footprint lacks its 3D model, so the board renders as it will be built. `nets` (`tour nets`) selects each key net as a whole through the IPC API, pours, tracks, vias and pads together, and frames it with Zoom to Selected Objects, one net at a time on camera. `3d` (`tour 3d`) opens the 3D Viewer and runs the walkthrough by View-menu commands (kicad_3d_view: fit, top, tilt, orbit, zoom, swing, front, bottom, flip), no mouse, with the viewer window as the step's camera. `plan` prints the flow. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly.-2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). `place pack` lays each `spec.chains` entry (a series chain in electrical order, such as a divider) on one line with the shared-net pads facing and says which chains were honoured; when a wish lists several rotations it picks the one whose pads land nearest their net partners. Routing is the binary's grid router by default; say `take route=ai` if you route yourself. Pours, gate, landing, measurement, both analyses and the finish are the binary's.+2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). `place pack` lays each `spec.chains` entry (a series chain in electrical order, such as a divider) on one line with the shared-net pads facing and says which chains were honoured; when a wish lists several rotations it picks the one whose pads land nearest their net partners. Routing is the binary's grid router by default; say `take route=ai` if you route yourself (high-speed boards: see High-speed boards below). Pours, gate, landing, measurement, both analyses and the finish are the binary's. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>] [--sch design.kicad_sch]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly. `--sch` records the schematic the board came from, so `finish` requires a passing `netlist check`. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>] [--page <owner/slug>]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly. When the work begins before any board exists, open the run with `intake` instead (next section); `start` on that run attaches the board and keeps its clock.-2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). Routing is the binary's grid router by default; say `take route=ai` if you route yourself. Pours, gate, landing, measurement, both analyses and the finish are the binary's.+2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). Routing is the binary's grid router by default; say `take route=ai` if you route yourself (high-speed boards: see High-speed boards below). Pours, gate, landing, measurement, both analyses and the finish are the binary's. 3. **`stage start <name>` / `stage end <name>`** around any stage you do yourself, so the clock and the manifest see it. 4. **`route [--passes N]`**: the grid router on the current board with the spec's rules (wide nets, planes, per-net vias, Kelvin taps). If it closes short, the ERROR names the pins and the blockers and the three levers: more passes, back to placement (`place pack` the parts it names or the test points and indicators in the escape band, `place check`, route again), or take the stage yourself. 5. **`gate`**: KiCad's DRC offline on the plan, inherited errors separated. Nothing lands until it passes.@@ -53,6 +55,17 @@ The flow is a file, `flows/board.json`: the steps in order (intake, rules, test- 7. **`measure`**: KiCad's filled copper per layer, the ablation metric. **`analyze current`** and **`analyze thermal`**: IPC-2221 on the narrowest conductor of every loaded net, pour and via capacity, copper and vias at every hot tab. A loaded net whose pads all sit on one poured outer layer is carried by that layer's pour; its vias only spread heat. Hot parts take a theta ceiling (`maxThetaCPerW`) or a rise budget (`hotMaxRiseC` spec-wide, `maxRiseC` per part) against the stated watts, so write the dissipation physically (I2R at the hot Rds(on) over the conduction duty, plus switching) and say so in the spec. A FAIL names the net or part and the fix; fix it (pours, vias, widths, or placement), land, measure and analyze again. 8. **`finish`**, then **`deliver --video <mp4> --message "..."`**: `finish` refuses until the gate passed, routing and pours landed, copper measured, both analyses passed, and the live board validates at 0 unconnected with no new errors; and, when they apply, the board still equals the recorded schematic, still conforms to the molecule, still carries the spec's fab rules, and every 3D orientation warning is fixed or acknowledged. Then it stamps the end and writes the summary: minutes from the prompt, minutes per stage, decisions, copper, live DRC. Add your token and dollar accounting to the manifest under `tokens` and `usd`. +### High-speed boards: your own router, inside the flow++A board with differential pairs, impedance control or length matching (PCIe, OCuLink, USB 3, Ethernet) is beyond the grid router, so you route it yourself, but the work still happens inside the flow. Declare each step as you reach it (`step rules`, `step components`, ... `step routing`); your own schematic, placement or router scripts do the step's work, they do not replace the step. For routing:++1. `take route=ai`, then `step routing` and `stage start route`.+2. Run your own router (wrap it as `adom-aiflow exec -- python3 my_router.py ...` so its time is measured), write its result in the plan JSON shape, then `gate --plan <plan.json>` and `land route --plan <plan.json>`; `land route` is the routing step's proof.+3. `stage end route`.+4. Record the numbers: `evidence add --kind calcs --file design/impedance.json` with the stackup, the trace width and gap per layer for the target impedance, the propagation delay per layer, and the skew per pair and per lane group after routing.++If the binary should do this step itself, file it (see Give it back).+ ## No step is skipped A run carries its lane's required steps in order (`flows/board.json` `sequence`; Fusion: intake, rules,@@ -330,7 +343,7 @@ Custom manual stages default to AI. `take custom-stage=ai` or `take custom-stage A pour's area or presence is not proof of current capacity or acceptable temperature. `analyze current` and `analyze thermal` now require both the heuristic checks and a matching Adom Fields solve. Run `adom-fields analyze --board <current saved board> --spec <run spec> --out <run>/fields`, then `analyze current --fields <run>/fields/fields.json` and `analyze thermal --fields <run>/fields/fields.json`. Fields records SHA-256 identities for the exact inputs. Changed boards or specs invalidate the result; unsolved loaded nets or hot parts are not a pass. `finish` rechecks the evidence. This is an engineering screen under the solver's documented assumptions, not junction-temperature certification. -Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET`, default ConfRoomROG); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking.+Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET` names the desktop); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking. Component library footage: keep the full `library-tour` separate. A reviewed five-second `library-overview` artifact may enter compose, with `--review` binding the exact selected STEP/GLB variants, current board, clip and native readback/screenshot. See [the review contract](https://wiki.adom.inc/adom/adom-aiflow/files/docs/library-overview-review.md). Never claim native model matching from a tour alone.
bin/adom-aiflow⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+41−2@@ -1981,6 +1981,32 @@ fn project_dir_of(r: &Run) -> PathBuf { src.parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from(".")) } +/// The run's required steps: its recorded sequence, else the lane's sequence from flows/board.json.+fn run_sequence(r: &Run, flow: &Value) -> Vec<String> {+ let seq: Vec<String> = r.data["sequence"].as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect()).unwrap_or_default();+ if seq.is_empty() { sequence::sequence_of(flow, sequence::lane(&r.dir, &r.data)) } else { seq }+}++/// The over-budget nag for the current step (warn only, never a block).+fn overdue_warning(r: &Run) -> Option<String> {+ let flow: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();+ sequence::overdue_warning(&r.data, &flow, &run_sequence(r, &flow), aiflow_run::parse(&now()).unwrap_or(0))+}++/// The NEXT line: the exact command for the next required step, with its one-line description.+fn next_step_line(r: &Run, cli: &Cli) -> Option<String> {+ let flow: Value = serde_json::from_str(include_str!("../../../flows/board.json")).unwrap();+ let seq = run_sequence(r, &flow);+ let next = sequence::next_required(&r.dir, &r.data, &flow, &seq)?;+ let thread = cli.ai_thread.clone().or_else(|| r.data["aiThread"].as_str().map(str::to_owned)).unwrap_or_else(|| "<thread>".into());+ let does = sequence::short_does(&flow, &next);+ Some(format!("NEXT: adom-aiflow --run {} --ai-thread \"{thread}\" step {next}{}", run_dir(cli).display(), if does.is_empty() { String::new() } else { format!(" ({does})") }))+}++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}"); } }+}+ 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::lane(&r.dir, &r.data)));@@ -1998,10 +2024,13 @@ fn main() { // A host stop hook must not open windows, start captures or append timing events. if let Cmd::FinishGuard { json: as_json, stop_evidence } = &cli.cmd { let r = load_run(&cli);- let report = continuation_report(&r, stop_evidence.as_deref());+ let mut report = continuation_report(&r, stop_evidence.as_deref());+ let warning = overdue_warning(&r);+ if let (Some(w), Some(o)) = (&warning, report.as_object_mut()) { o.insert("warning".into(), json!(w)); } if *as_json { println!("{}", serde_json::to_string_pretty(&report).unwrap()); } else { println!("{}: {}", report["decision"].as_str().unwrap_or("continue"), report["reason"].as_str().unwrap_or(""));- println!("Next: {}", report["nextAction"].as_str().unwrap_or("review remaining scope")); }+ println!("Next: {}", report["nextAction"].as_str().unwrap_or("review remaining scope"));+ if let Some(w) = &warning { println!("WARNING: {w}"); } } std::process::exit(if report["mayStop"] == true { 0 } else { 2 }); } let dir = run_dir(&cli);@@ -2017,6 +2046,9 @@ fn main() { let idx = r.turn_begin(&command_line()); let _ = r.save(); let _ = TURN.set((dir.clone(), idx));+ // the over-budget nag: a run that sits in one step far past its expected minutes is most+ // likely doing later steps' work undeclared (issue #44). Warn only; `step` itself is the fix.+ if !matches!(cli.cmd, Cmd::Step { .. }) { if let Some(w) = overdue_warning(&r) { eprintln!("WARNING: {w}"); } } // the clip guard: a recording that has outrun its step's budget is a mistake, not evidence. // Stop it here, on whatever command comes next, before it eats the disk (one once ran // nine hours on a static editor because deliver did not stop it).@@ -2188,6 +2220,7 @@ fn main() { let _ = TURN.set((dir.clone(), idx)); let steps: Vec<String> = r.data["steps"].as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default(); ok(&format!("board attached to the intake run at {} for engine {engine}; the clock still counts from the prompt, {pt} ({:.1} min so far; steps before the board: {})", dir.display(), r.elapsed_minutes(), if steps.is_empty() { "none".into() } else { steps.join(", ") }), &[differs.unwrap_or_default() + &format!("Next: adom-aiflow plan --run {} (the stages this board needs and who can take each).", dir.display()), "Re-run `sourcing check --bom <csv>` now: the check before the board could not diff the BOM against the board's references.".into()]);+ print_next(&cli); return; } let pt = prompt_time.clone().unwrap_or_else(now);@@ -2207,6 +2240,7 @@ fn main() { if spec_v.get("molecule").is_some() { start_hints.push("The spec is a molecule: `adom-aiflow molecule check` before placement; place check, gate and finish run it too.".into()); } start_hints.extend(["Then name the run's page once: `report --page <owner/slug> --user-machine <the human's box> --push --refresh` (or `--surface webview` when you run inside Adom Hydrogen and the human watches a workspace tab). From then on the run's own README (steps, times, returns, every step's clip and screenshots) rebuilds, pushes and reloads in the human's browser at every step change, by itself.".into(), "Record the paste time exactly: --prompt-time 2026-09-14T15:04:05Z when you start late.".into()]); ok(&format!("run started for engine {engine} at {}; the clock started at the prompt, {pt}", dir.display()), &start_hints);+ print_next(&cli); } Cmd::Intake { prompt_time, brief, requirements, engine, target, page } => { let t = thread(&cli);@@ -2231,7 +2265,9 @@ fn main() { "Record the design record as it lands: `evidence add --kind calcs --file design/calcs.json`, `evidence add --kind simulation --file sim/<results>.json` (a failing simulation is refused), `evidence add --kind vendor-model --file <result>` for a vendor PSpice/LTspice run (adom/adom-spice-skillpack).".into(), "When the board exists: `start --board B.kicad_pcb --spec spec.json --engine <you> [--target <box> --remote-board <path>]` on this same --run attaches it; the clock stays at the prompt.".into(), if r.data["page"].is_null() { "Name the run's page once (`clips --page <owner/slug>` or `report --page ...`) so every clip lands on docs/clips.md as it stops.".into() } else { String::new() },+ "Declare every step as you enter it, even when you do that step's work with your own scripts; the timeline, the clips and the video depend on it.".into(), ]);+ print_next(&cli); } Cmd::Sourcing { what, bom, profile, qty, board, requirements } => { thread(&cli);@@ -2457,6 +2493,7 @@ fn main() { r.stage_end(name, res, note.as_deref()); r.save().unwrap(); ok(&format!("stage {name} ended at {}", now()), &[format!("Minutes so far per stage: {}; {:.1} min since the prompt.", Value::Object(r.stage_minutes()), r.elapsed_minutes())]);+ print_next(&cli); } } Cmd::Place { what, wish, moves, out, pause_ms } => {@@ -3409,6 +3446,7 @@ fn main() { r.data["clock"]["lastPromptTime"] = json!(at); r.save().unwrap(); ok(&format!("prompt {n} at {at}: the AI's time on it counts from here"), &["Continue the original requested scope after supporting tasks and status updates. Use `done --message \"...\"` only at completion, an explicit pause, or a genuine blocking handoff; it stops session accounting, not the engineering run.".into(), "Every step, clip and drawing you make until then belongs to this session on the run page.".into(), "Measure external work with adom-aiflow exec -- <program> <args>. Unwrapped long gaps are unobserved and may undercount active work, not proof of idle time.".into()]);+ print_next(&cli); } Cmd::FinishGuard { .. } => unreachable!("handled before side effects"), Cmd::Done { message, stop_evidence } => {@@ -4204,6 +4242,7 @@ fn main() { let hint = if *back { format!("Return to {name} recorded{}. Every command from here is charged to {name} until the next `step`.", why.as_ref().map(|w| format!(": {w}")).unwrap_or_default()) } else if known.contains(&name.as_str()) { format!("Every command from here is charged to {name} until the next `step`.") } else { format!("{name} is your own step name (the known ones: {}); it is charged like any other.", known.join(", ")) }; let back_hint = if *back { "If the binary sent you back for its own fault (a wrong refusal, a missing lever, a rigid rule), that is a bug for the page, not a design lesson: file it so the next run does not pay for it.".to_string() } else { String::new() }; ok(&format!("step {name}{}{}{}", if *back { " (back)" } else { "" }, prev.map(|p| format!(", leaving {p}")).unwrap_or_default(), if clip_note.is_empty() { String::new() } else { format!("; {clip_note}") }), &[hint, record_hint, back_hint, report_note, clips_note]);+ print_next(&cli); } Cmd::Deliver { video, message, accept_suspect } => { thread(&cli);
crates/adom-aiflow/src/sequence.rs+47@@ -137,6 +137,36 @@ fn does_of(flow: &Value, name: &str) -> String { .unwrap_or_default() } +/// The step's one-line description (flows/board.json `short`, else the start of its `does`).+pub fn short_does(flow: &Value, name: &str) -> String { does_of(flow, name) }++/// The next required step after `current`: the one after it in the lane's sequence, or, when the+/// current step is not in the sequence (an own step name), the first required step not yet done.+pub fn next_required(dir: &Path, data: &Value, flow: &Value, seq: &[String]) -> Option<String> {+ let current = data["currentStep"].as_str().unwrap_or("");+ if let Some(p) = position(seq, current) { return seq.get(p + 1).cloned(); }+ states(dir, data, flow, seq).into_iter().find(|s| s.status != Status::Done).map(|s| s.name)+}++/// Expected wall minutes for one visit of a step (flows/board.json `minutes`, default 45).+pub fn expected_minutes(flow: &Value, name: &str) -> u64 {+ flow["minutes"][name].as_u64().unwrap_or(45)+}++/// The over-budget warning: the current step's latest visit started more than twice its expected+/// minutes ago (wall clock). None when within budget, delivered, or no step is open. Read-only.+pub fn overdue_warning(data: &Value, flow: &Value, seq: &[String], now_secs: u64) -> Option<String> {+ if !data["clock"]["deliveredTime"].is_null() { return None; }+ let current = data["currentStep"].as_str()?;+ let at = last_visit(data, current).and_then(|t| aiflow_run::parse(&t))?;+ let min = now_secs.saturating_sub(at) / 60;+ let expected = expected_minutes(flow, current);+ if min <= expected * 2 { return None; }+ let ahead: Vec<String> = position(seq, current).map(|p| seq.iter().skip(p + 1).take(3).map(|s| format!("`step {s}`")).collect()).unwrap_or_default();+ let examples = if ahead.is_empty() { "`step design`, `step routing`".to_string() } else { ahead.join(", ") };+ Some(format!("still in {current} after {min} min (expected about {expected} min); declare the step you are actually on ({examples}, ...), even when your own scripts do that step's work. The timeline, the clips and the video depend on it."))+}+ /// The timeline the dashboard draws: every required step with its status, and every return as a loop. pub fn timeline(dir: &Path, data: &Value, flow: &Value, seq: &[String], minutes: &serde_json::Map<String, Value>) -> Value { let st = states(dir, data, flow, seq);@@ -246,4 +276,21 @@ mod tests { let st = states(Path::new("/nonexistent"), &d, &flow, &seq()); assert!(blockers(&st, "b").is_empty()); }++ #[test]+ fn next_and_overdue() {+ let flow = json!({"minutes": {"a": 10}, "short": {"b": "the b step"}});+ let seq: Vec<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();+ let d = json!({"currentStep": "a", "clock": {"deliveredTime": null}, "steps": {"a": {"visits": [{"at": "2026-01-01T00:00:00Z"}]}}});+ assert_eq!(next_required(Path::new("/nonexistent"), &d, &flow, &seq).as_deref(), Some("b"));+ assert_eq!(short_does(&flow, "b"), "the b step");+ let t0 = aiflow_run::parse("2026-01-01T00:00:00Z").unwrap();+ assert!(overdue_warning(&d, &flow, &seq, t0 + 20 * 60).is_none(), "twice the budget is still in budget");+ let w = overdue_warning(&d, &flow, &seq, t0 + 21 * 60).unwrap();+ assert!(w.contains("still in a after 21 min") && w.contains("`step b`"), "{w}");+ assert_eq!(expected_minutes(&flow, "zzz"), 45);+ let mut done = d.clone();+ done["clock"]["deliveredTime"] = json!("2026-01-01T01:00:00Z");+ assert!(overdue_warning(&done, &flow, &seq, t0 + 999 * 60).is_none());+ } }
flows/board.json+27@@ -471,6 +471,33 @@ "finish": "0 unconnected, 0 errors, every check passed", "export": "manufacturing files written and checked" },+ "minutes": {+ "_": "Expected wall minutes for one visit of each step. Past twice this, every command and finish-guard warn that the run is still in that step (warn only, never a block): work done with the AI's own scripts still belongs to the step it does. A step not listed here expects 45 minutes.",+ "intake": 20,+ "rules": 10,+ "design": 45,+ "test-plan": 20,+ "sourcing": 30,+ "library": 30,+ "components": 30,+ "libraries": 30,+ "models": 30,+ "schematic": 45,+ "simulation": 30,+ "placement": 45,+ "routing": 60,+ "thermal-vias": 15,+ "pours": 20,+ "outline": 10,+ "current": 15,+ "thermal": 15,+ "fields": 20,+ "nets": 10,+ "silkscreen": 30,+ "3d": 15,+ "finish": 20,+ "export": 20+ }, "phases": { "Plan": [ "intake",
package.json+1−1@@ -1,7 +1,7 @@ { "slug": "adom-aiflow", "type": "app",- "version": "0.1.90",+ "version": "0.1.91", "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.90",+ "version": "0.1.91", "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.",
skills/adom-aiflow/SKILL.md+16−3@@ -12,6 +12,8 @@ Install: `adom-wiki pkg install adom/adom-aiflow` (one Rust binary; `service-kic Every state-changing command takes `--ai-thread "<your thread name>"`. Every run lives in one directory (`--run DIR`, default `./aiflow-run`) with a `run.json` manifest: the clock, the plan, the decisions, every stage's start and end, the outcomes, the capture markers and the summary. The manifest is the comparison; nothing counts that is not in it. +Declare every step as you enter it, even when you do that step's work with your own scripts; the timeline, the clips and the video depend on it. `intake`, `start`, `prompt`, `stage end` and every `step` end with a `NEXT:` line naming the next required step and its exact command. A run that sits in one step for more than twice that step's expected minutes (`minutes` in flows/board.json) gets a WARNING on every command and in `finish-guard`; it never blocks, it means you are probably doing later steps' work undeclared.+ ### Before every final answer: executable finish guard Run `adom-aiflow --run <run> finish-guard --json` before ending a turn. Exit 2 / `decision: continue` means give a short progress update in commentary and immediately execute the next useful action. Do not send a final checkpoint and wait for “keep going.” This applies after skill/code changes, releases, dashboard work, diagnostics, status questions and context recovery. Run this check again after any later work changes the state.@@ -29,10 +31,10 @@ The guard is read-only and suitable for a host's pre-stop integration. It does n The flow is a file, `flows/board.json`: the steps in order (intake, rules, test-plan, components, libraries, models, placement, routing, thermal-vias, pours, outline, current, thermal, fields, nets, silkscreen, 3d, finish, export; the `sequence` per lane is enforced, see No step is skipped), who does each, what the binary offers at each, and the steps that come later (components, libraries, schematic, simulation before the board; moleculize, paste, probe after it). `models` (kicad_model_check) refuses to go on while any footprint lacks its 3D model, so the board renders as it will be built. `nets` (`tour nets`) selects each key net as a whole through the IPC API, pours, tracks, vias and pads together, and frames it with Zoom to Selected Objects, one net at a time on camera. `3d` (`tour 3d`) opens the 3D Viewer and runs the walkthrough by View-menu commands (kicad_3d_view: fit, top, tilt, orbit, zoom, swing, front, bottom, flip), no mouse, with the viewer window as the step's camera. `plan` prints the flow. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly.-2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). `place pack` lays each `spec.chains` entry (a series chain in electrical order, such as a divider) on one line with the shared-net pads facing and says which chains were honoured; when a wish lists several rotations it picks the one whose pads land nearest their net partners. Routing is the binary's grid router by default; say `take route=ai` if you route yourself. Pours, gate, landing, measurement, both analyses and the finish are the binary's.+2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). `place pack` lays each `spec.chains` entry (a series chain in electrical order, such as a divider) on one line with the shared-net pads facing and says which chains were honoured; when a wish lists several rotations it picks the one whose pads land nearest their net partners. Routing is the binary's grid router by default; say `take route=ai` if you route yourself (high-speed boards: see High-speed boards below). Pours, gate, landing, measurement, both analyses and the finish are the binary's. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>] [--sch design.kicad_sch]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly. `--sch` records the schematic the board came from, so `finish` requires a passing `netlist check`. 1. **`start --board B.kicad_pcb --spec spec.json --engine <you> --prompt-time <UTC of the paste> [--target <box> --remote-board <path on the box>] [--page <owner/slug>]`**: copies the board into the run and starts the clock at the prompt, not at your first command. Record the paste time honestly. When the work begins before any board exists, open the run with `intake` instead (next section); `start` on that run attaches the board and keeps its clock.-2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). Routing is the binary's grid router by default; say `take route=ai` if you route yourself. Pours, gate, landing, measurement, both analyses and the finish are the binary's.+2. **`plan`**: the stages this board needs and who can take each. Placement is yours (the binary helps: `place pack --wish`, `place check --moves`, `place land --moves`). Routing is the binary's grid router by default; say `take route=ai` if you route yourself (high-speed boards: see High-speed boards below). Pours, gate, landing, measurement, both analyses and the finish are the binary's. 3. **`stage start <name>` / `stage end <name>`** around any stage you do yourself, so the clock and the manifest see it. 4. **`route [--passes N]`**: the grid router on the current board with the spec's rules (wide nets, planes, per-net vias, Kelvin taps). If it closes short, the ERROR names the pins and the blockers and the three levers: more passes, back to placement (`place pack` the parts it names or the test points and indicators in the escape band, `place check`, route again), or take the stage yourself. 5. **`gate`**: KiCad's DRC offline on the plan, inherited errors separated. Nothing lands until it passes.@@ -43,6 +45,17 @@ The flow is a file, `flows/board.json`: the steps in order (intake, rules, test- 7. **`measure`**: KiCad's filled copper per layer, the ablation metric. **`analyze current`** and **`analyze thermal`**: IPC-2221 on the narrowest conductor of every loaded net, pour and via capacity, copper and vias at every hot tab. A loaded net whose pads all sit on one poured outer layer is carried by that layer's pour; its vias only spread heat. Hot parts take a theta ceiling (`maxThetaCPerW`) or a rise budget (`hotMaxRiseC` spec-wide, `maxRiseC` per part) against the stated watts, so write the dissipation physically (I2R at the hot Rds(on) over the conduction duty, plus switching) and say so in the spec. A FAIL names the net or part and the fix; fix it (pours, vias, widths, or placement), land, measure and analyze again. 8. **`finish`**, then **`deliver --video <mp4> --message "..."`**: `finish` refuses until the gate passed, routing and pours landed, copper measured, both analyses passed, and the live board validates at 0 unconnected with no new errors; and, when they apply, the board still equals the recorded schematic, still conforms to the molecule, still carries the spec's fab rules, and every 3D orientation warning is fixed or acknowledged. Then it stamps the end and writes the summary: minutes from the prompt, minutes per stage, decisions, copper, live DRC. Add your token and dollar accounting to the manifest under `tokens` and `usd`. +### High-speed boards: your own router, inside the flow++A board with differential pairs, impedance control or length matching (PCIe, OCuLink, USB 3, Ethernet) is beyond the grid router, so you route it yourself, but the work still happens inside the flow. Declare each step as you reach it (`step rules`, `step components`, ... `step routing`); your own schematic, placement or router scripts do the step's work, they do not replace the step. For routing:++1. `take route=ai`, then `step routing` and `stage start route`.+2. Run your own router (wrap it as `adom-aiflow exec -- python3 my_router.py ...` so its time is measured), write its result in the plan JSON shape, then `gate --plan <plan.json>` and `land route --plan <plan.json>`; `land route` is the routing step's proof.+3. `stage end route`.+4. Record the numbers: `evidence add --kind calcs --file design/impedance.json` with the stackup, the trace width and gap per layer for the target impedance, the propagation delay per layer, and the skew per pair and per lane group after routing.++If the binary should do this step itself, file it (see Give it back).+ ## No step is skipped A run carries its lane's required steps in order (`flows/board.json` `sequence`; Fusion: intake, rules,@@ -315,7 +328,7 @@ Custom manual stages default to AI. `take custom-stage=ai` or `take custom-stage A pour's area or presence is not proof of current capacity or acceptable temperature. `analyze current` and `analyze thermal` now require both the heuristic checks and a matching Adom Fields solve. Run `adom-fields analyze --board <current saved board> --spec <run spec> --out <run>/fields`, then `analyze current --fields <run>/fields/fields.json` and `analyze thermal --fields <run>/fields/fields.json`. Fields records SHA-256 identities for the exact inputs. Changed boards or specs invalidate the result; unsolved loaded nets or hot parts are not a pass. `finish` rechecks the evidence. This is an engineering screen under the solver's documented assumptions, not junction-temperature certification. -Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET`, default ConfRoomROG); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking.+Placement checks compare individual native DRC findings with the baseline, not just their counts. Conservative courtyard bounding boxes guide packing but do not override native courtyard checks. Project `.kicad_pro` and `.kicad_dru` constraints follow board snapshots. When present, they require a native `kicad-cli` (optionally set `ADOM_AIFLOW_KICAD_CLI`); the single-file headless service must not silently discard them. In a container without native KiCad, `ADOM_AIFLOW_KICAD_CLI=adom-aiflow-kicad-cli-remote` (installed with AI Flow) runs `pcb drc` on a desktop's KiCad through Adom Bridge (`KICAD_REMOTE_TARGET` names the desktop); a stock distro kicad-cli that lacks `pcb drc --refill-zones` is ignored. The gate refills zones natively when it has a native kicad-cli, so zone fills are KiCad's own, not the raster estimate, and the current and thermal analyses read the board's own copper when the routing plan leaves nets out (an adopted, already-routed board). Manufacturing-rule warnings, including hole spacing, remain blocking. Component library footage: keep the full `library-tour` separate. A reviewed five-second `library-overview` artifact may enter compose, with `--review` binding the exact selected STEP/GLB variants, current board, clip and native readback/screenshot. See [the review contract](https://wiki.adom.inc/adom/adom-aiflow/files/docs/library-overview-review.md). Never claim native model matching from a tour alone.