open source · 100% in the browser · no backend

Code becomes a circuit.

Simulate analog circuits right in the browser, describe them in TypeScript and let AI agents iterate until it works — with diagnostics that point to the line and simulation-verified hints.

v(C1) — RC charge · 1 V/div · 1 ms/div● tran

The loop that made vibecoding work — now for electronics

Compile → simulate → assert → structured diagnostic. A bench .bench.ts describes circuit + spec; forge closes the cycle in seconds, without leaving the terminal.

1

Write

Describe the circuit and the spec in a bench: components with named pins (anode, +), free parameters via tb.param and assertions as tests.

2

Verify

forge verify climbs the lint → op → tran ladder with fail-fast. Lint analyzes the topology as a graph before the solver — singular matrix never reaches you.

3

Fix

Every failure carries a stable code, a source line, and a simulation-verified hint. Apply it with --set, run again, go green.

forge — zsh
$ forge verify bench/led.bench.ts
  ✓ lint  pass
  ✗ op    fail
        A301 a1: D1.i within [8m, 12m] — measured 28.039m (2.34x)
          at bench/led.bench.ts:22
          hint (verified): R1 in [261, 316] passes all op assertions — try R1 = 280
FAIL · exit 1
$ forge verify bench/led.bench.ts --set R1=280
  ✓ lint  pass
  ✓ op    pass
PASS · exit 0

A real simulator, with modern-compiler ergonomics

A custom numeric engine in TypeScript, validated against analytical solutions — and a diagnostic layer built for whoever iterates: human or agent.

Real simulation in the browser

Modified Nodal Analysis + Newton-Raphson, DC operating point and transient with implicit Euler. 100% local, no backend, no cloud token.

Errors that teach

Stable code (F105), source location (at bench/led.bench.ts:10) and a note with the physics behind the problem — rustc style, detected by graph lint before the solver runs.

Simulation-verified hints

Declare tb.param('R1', …) and, when an assertion fails, the platform sweeps the range, simulates each point, and returns the interval that provably passes.

Built for LLM agents

Exit codes with semantics (0/1/2/3), deterministic --json (two runs diff cleanly) and a run ledger in .forge/ against context attrition.

Custom components

defineComponent({ pins, params, stamp, onStep }) creates components in code — including nonlinear ones with internal state, in the same engine as the built-ins.

Visual editor + code

Canvas with animated current and an oscilloscope, automatic wire routing with A*, multi-file editor with highlighting. The canvas is a projection of the code — never the other way around.

Examples ready to open

Each example opens straight in the editor — tweak the values, turn on the simulation and watch the current flow.

Voltage divider

Two resistors in series: the "hello world" of Ohm's law, with a probe on the middle node.

Open in the simulator →

RC low-pass filter

Resistor + capacitor: exponential charge in the transient and the time constant τ = RC.

Open in the simulator →

Resonant RLC

R, L and C in series: damped oscillation, ringing and the natural frequency of the tank.

Open in the simulator →

SPDT switch

A 3-terminal switch toggles between two LEDs — click it with the simulation running.

Open in the simulator →

MCU + LEDs

A chip drives two LEDs through series resistors — the classic blink, at the circuit level.

Open in the simulator →

Built for agents

The bench is the complete artifact: circuit + free parameters + assertions. --json returns a deterministic RunRecord that the agent consumes without parsing text.

bench/led-driver.bench.tsTypeScript
import { bench, VoltageSource, Resistor, LED } from 'circuit-forge';

export default bench('led-driver', (tb) => {
  const v1 = tb.add('V1', new VoltageSource(5));
  const r1 = tb.add('R1', new Resistor('330'));
  const d1 = tb.add('D1', new LED());

  v1.pin('+').connect(r1.pin('1'));
  r1.pin('2').connect(d1.pin('anode'));
  d1.pin('cathode').connect(tb.gnd);
  v1.pin('-').connect(tb.gnd);

  tb.name('led', d1.pin('anode'));

  // sizing is the platform's job, not the agent's
  tb.param('R1', { min: '100', max: '10k', scale: 'log' });

  tb.expect.op('D1.i').toBeWithin('5m', '15m'); // lit, without burning out
  tb.expect.op('led').toBeWithin(1.8, 2.6);     // healthy Vf
});
forge verify --json · RunRecord (resumo)JSON
{
  "schema": "forge-run/0.1",
  "bench": "led-driver",
  "engine": "[email protected]",
  "stages": {
    "lint": { "verdict": "pass" },
    "op": {
      "verdict": "fail",
      "assertions": [{
        "id": "a1", "probe": "D1.i",
        "check": { "op": "within", "min": 0.008, "max": 0.012 },
        "measured": 0.028039,
        "margin": { "ratio": 2.34 },
        "at": "bench/led.bench.ts:22",
        "hint": {
          "kind": "verified-param-range",
          "param": "R1",
          "passingRange": [261.016, 316.228],
          "suggestedValue": 261.016
        }
      }]
    },
    "tran": { "verdict": "skipped" }
  },
  "verdict": "fail",
  "firstFailure": "a1",
  "exitCode": 1
}

A vibecoding loop for electronics: the agent writes, forge simulates, the diagnostic points to the line.