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 provenanceEach 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, repeatsEvery 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.pyA 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.8msThat'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. Runuv run rosette init --tool opencode. - Claude Code reads
CLAUDE.md. Runuv 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
- Installation: set up a project and pick an agent tool
- Core concepts: the mental model the agent works from
- Design rule checking: tighten
your
rosette.tomlso checks catch real problems - Agent-Driven Design: the philosophy behind this workflow, with worked examples
- An Accidental Convergence: why code-driven photonic design fits LLMs well