Agent workflows

Drive Rosette with AI coding agents like OpenCode, Claude Code, or Cursor.

Most Rosette designs are written by a person and an AI coding agent working together. This guide is the practical playbook for that workflow: what rosette init puts in your project for the agent, how the prompt-build-check loop runs, and where things go wrong.

For the design philosophy behind this approach, the Agent-Driven Design article covers the "why." This page is the "how."

What rosette init gives the agent

When you run rosette init and pick an AI tool (OpenCode or Claude Code), the project layout is set up so an agent has everything it needs to read your code, write new designs, and verify them on its own.

my-chip/
├── AGENTS.md            # or CLAUDE.md, agent instructions
├── rosette.toml         # project config: layers, DRC, DFM
├── components/          # editable component library
├── designs/             # your design scripts
├── output/              # GDS build artifacts
├── .agents/skills/      # focused skills (or .claude/skills/)
└── .rosette/
    ├── index.md         # compact task-to-context router
    ├── contracts/       # focused API contracts by task
    ├── api.pyi          # complete API fallback
    ├── cli.json         # complete CLI fallback
    └── manifest.json    # generated reference provenance

Each piece plays a specific role:

  • AGENTS.md / CLAUDE.md: short instruction file the agent reads on startup. It sends the agent to the compact index, requires semantic project layers, keeps composition-oriented designs thin, and allows one-off exploratory geometry to remain local to a design.
  • .rosette/index.md: tells the agent which focused contract, skill, project configuration, and local source apply to its current task.
  • .rosette/contracts/: task-specific API slices for layout composition, routing, verification, and component authoring. They are generated from the complete contract, so signatures have one source of truth.
  • .rosette/api.pyi: complete API fallback for uncommon operations not in a focused contract. It is not loaded by default.
  • .rosette/cli.json: complete CLI fallback for exact commands, flags, exit behavior, and JSON output schemas. Agents read only the relevant command.
  • rosette.toml: layer definitions, DRC rules, DFM config. The physical constraints of your design, machine-readable. Agents pick semantic layer names from [layers] instead of guessing GDS numbers.
  • components/: editable Python source for waveguides, bends, MMIs, grating couplers, ring resonators, and more. The agent reads the docstrings and signatures, then composes them. The generic template includes the full catalog; the blank template starts with shared helpers for components you add later.
  • Focused skills: routing strategy, the build-check-inspect loop, and component-authoring conventions. Skills are installed for both templates.

Restore references after cloning

.rosette/ is gitignored because its references match the installed Rosette version. After a fresh clone, run uv run rosette update to recreate the index, task contracts, and complete fallbacks. Do not rerun rosette init in an existing project.

Pick a tool when you init

rosette init accepts --tool agents (also opencode, codex, or cursor) or --tool claude if you want to skip the interactive prompt. Pass --tool agents,claude for both harnesses or --tool none to skip agent files. The same instruction body and skills are projected to AGENTS.md plus .agents/skills/, or CLAUDE.md plus .claude/skills/.

Keep contracts and runtime aligned

Run project commands through uv run rosette, not a global rosette or ro executable. If a canonical import from a generated contract fails, do not add a compatibility shim. Run uv run rosette update and align the installed package before continuing. Rosette checks both package version and structural API identity.

The loop

The workflow is short. You describe the design in plain language, the agent writes the script, and then it iterates against the build and check commands until they pass.

prompt -> agent reads .rosette/index.md and rosette.toml
       -> agent loads the task contract, skill, and relevant components/
       -> agent writes designs/<name>.py
       -> uv run rosette build designs/<name>.py
       -> uv run rosette check designs/<name>.py
       -> agent reads violations, fixes, repeats

Every Rosette design exports a top-level Cell named design. That's the convention agents follow:

# designs/loopback.py
from rosette import Cell, Layer
from rosette.project import load_layer_map
from rosette.routing import Route
from components import grating_coupler

layers = load_layer_map()
gc = grating_coupler(layers.silicon.layer, waveguide_width=0.5)

gc_in  = gc.at(0,   0)
gc_out = gc.at(0, 127)

route = Route(layers.silicon.layer, width=0.5, bend_radius=10.0)
route.start_at_port(gc_in.port("opt"))
route.to(40, 0)
route.to(40, 127)
route.end_at_port(gc_out.port("opt"))

design = Cell("loopback")
design.add_ref(gc_in)
design.add_ref(gc_out)
design.add_ref(route.to_cell("route").at(0, 0))

The agent then runs the verify steps:

uv run rosette build designs/loopback.py
uv run rosette check designs/loopback.py

A passing run looks like this:

drc  designs/loopback.py  22 rules, 4 polygons

  passed (0.8ms)

checks  designs/loopback.py  4 ports, 1 connections

  passed (0.1ms)

And a failing run is just as readable:

drc  designs/foo.py  22 rules, 8 polygons

  FAIL  Lsilicon.no_overlap on 1/0, 1/0: Forbidden overlap (449.488 um²) at (-12.0, -12.0) to (12.0, 12.0) (within 'ring')
  FAIL  Lsilicon.allowed_angles on 1/0: Edge angle 95.6 deg not in allowed angles [0.0, 90.0]
  ...

  65 violations (65 errors) in 2.8ms

That's all the agent needs. Each violation has a rule name, the offending geometry, and a coordinate. The agent reads the output, figures out which parts of the design need to change, and edits the script.

Machine-readable output

The prose above is tuned for humans. For a fully reliable verify loop — no ANSI stripping, no regexing severities out of prose that might reword — add --json to check, drc, or dfm. The command then emits a single JSON object on stdout (and nothing else), with a stable, versioned schema:

uv run rosette drc designs/foo.py --json
{
  "schema": 2,
  "command": "drc",
  "design": "designs/foo.py",
  "passed": false,
  "elapsed_ms": 12.4,
  "summary": { "violations": 1, "errors": 1, "warnings": 0 },
  "violations": [
    {
      "severity": "error",
      "rule_name": "min_spacing",
      "rule_type": "spacing",
      "layer": "1/0",
      "layer2": null,
      "message": "spacing 0.180 < 0.200",
      "cell_name": "ring",
      "cell_name2": "ring",
      "bbox": [[12.34, 5.12], [12.52, 5.30]]
    }
  ],
  "suppressed": 0,
  "skipped_cells": 0,
  "waived": 0
}

The agent reads passed directly instead of matching prose, and each bbox is in microns — the same coordinate space as rosette shot, so the agent can render the offending region with uv run rosette shot --bbox <xmin,ymin,xmax,ymax> to see what went wrong.

uv run rosette check --json returns one combined object with drc, checks, and dfm keys (the last is null unless --include-dfm is passed) plus a top-level passed. Exit codes are unchanged in JSON mode: a failing run still exits 1 alongside "passed": false.

In every JSON mode, read passed to gate the loop — it is present on every object, including skips and errors. Each slot in the combined check object (drc, checks, dfm) also carries its own passed, plus skipped/error flags when it didn't produce a normal result.

Config-error handling matches the human check flow. A bad DRC config (or missing rosette.toml) is fatal: check emits a top-level error object ({"schema": 2, "command": "check", "passed": false, "error": "..."}) and exits 1. A bad [dfm] section or checks config is non-fatal under check — its slot becomes {"command": ..., "passed": false, "skipped": true, "error": true, "reason": ...} while the top-level passed reflects only DRC and connectivity. (The standalone uv run rosette dfm --json treats the same DFM config error as fatal and exits 1.) A dfm slot of {..., "error": false, "reason": ...} is a benign skip — no [dfm] section configured.

`rosette build` alone is not enough

A passing build only means the GDS file was written. It does not mean the design is physically correct. Always follow up with uv run rosette check (or use uv run rosette build --check for a build with a DRC pre-check). The agent instructions in AGENTS.md already say this; it's worth knowing yourself so you can call it out if the agent skips the check step.

Prompting tips

The agent file gives the agent a baseline. A few things you can do on top of that to get better results.

Be specific about intent. "Design a ring resonator" is fine. "Design a 10 um radius ring resonator with a 200 nm gap, single bus, with grating couplers on both ends of the bus" gives the agent the constraints it needs to make the right decisions. The more constraints you state up front, the fewer iterations you'll need.

Let the agent read first. A good agent reads .rosette/index.md, rosette.toml, the selected task contract, and relevant components/*.py before writing code. If your agent has a habit of jumping straight to writing, prepend something like "Read the Rosette index and relevant component source first" to your prompt.

Ask for the check loop explicitly. If the agent stops at "the build passed," ask it to run uv run rosette check and resolve any violations. After a few iterations most agents pick up the pattern and run checks without prompting.

Surface foundry constraints in rosette.toml. If your DRC rules are right, the agent will catch its own mistakes. Tighten your [drc.layers.*] constraints to match your actual process; vague defaults produce vague designs. See the Design rule checking guide.

Keep prompts in plain English. Rosette's API surface is small enough that an agent rarely needs nudging on which class to use. Save the API hints for cases where you genuinely need a specific approach (for example, "use Euler bends" or "place the array as a single AREF").

Picking an agent tool

Rosette is tool-neutral. The same instructions, task contracts, skills, and verify commands work with any agent that can read files and run shell commands.

  • OpenCode reads AGENTS.md. Run uv run rosette init --tool opencode.
  • Claude Code reads CLAUDE.md. Run uv run rosette init --tool claude.
  • Cursor and other editor-integrated agents generally read either file or can be pointed at one manually.

The harness adapter writes both the instruction file and the matching skill directory. Run uv run rosette update after changing harness files or upgrading Rosette.

Common pitfalls

Hardcoded layer numbers

Agents sometimes write Layer(1, 0) directly instead of load_layer_map().silicon.layer. This works, but it bypasses your project's layer stack and breaks if you renumber. The instruction file tells the agent not to do this; if it slips through, ask the agent to switch to load_layer_map().

Stopping at a passing build

A green uv run rosette build only means the GDS was written. The design can still have unconnected ports, DRC violations, or auto-reduced bends. Always end the loop on uv run rosette check, not on uv run rosette build.

Stale API assumptions

Agents trained months or years ago will confidently write APIs that don't exist or have moved. The agent file points them at .rosette/index.md and focused contracts; if you see hallucinated method names, prompt the agent to reload the relevant contract. The complete api.pyi remains available as a fallback.

Skipping `components/`

For photonic designs, the components/ library is where the real domain knowledge lives (port conventions, fiber pitch, taper lengths). An agent that ignores it and rolls its own waveguide will produce plausible but subtly wrong layouts. If your agent does this, ask it to read components/ first or use the relevant component directly.

See also

On this page