Start here for topopt. Checks that the topopt server and the machine are ready, then takes the user from a part (described in words, or STL and YAML files) to a printable STL through check, run, dashboard and result. Use when the user wants a lighter or stronger 3D-printed part, or asks to start, set up or try topopt.
26 stars
0 votes
0 copies
0 views
Added November 7, 2026
ai-agentspythonrustgoshellbashspringawsgitbackend
Works with
claude code
claude desktop
terminal
cli
mcp
Security analysis
F34/100
criticalPipes output to a shell interpreter
mediumUses curl or wget to download content
criticalDownloads and executes remote scripts — classic supply chain attack
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Start Here?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/chartergroup-start-here)
---
name: start-here
description: Start here for topopt. Checks that the topopt server and the machine are ready, then takes the user from a part (described in words, or STL and YAML files) to a printable STL through check, run, dashboard and result. Use when the user wants a lighter or stronger 3D-printed part, or asks to start, set up or try topopt.
---
# topopt: start here
topopt makes a part lighter. You give it the space, the mounting and the loads. It removes the material that does little work and writes a printable STL. It runs on this computer.
This file is a path with facts. Skip, reorder or repeat a step when the user's request needs it. You decide the steps. Ask the user when an answer changes the part (the load, the mounting, the size). If the user does not say what the part holds (an object, a mass or a force), ask for it before the run, also when the user says "run it". Install or build something only after the user agrees.
## Talking to the user
These are offers. Use the ones that the request needs.
- A request with no numbers ("a pen", "a shelf bracket"): offer three ways and let the user pick: the full list of questions (the intake items of the `lead-design` role); the 2 or 3 questions that change the part most (the load, the mounting, the size); or a first design from stated guesses (origin `inferred` in `record_brief`). Every way keeps the load question: in the third, ask the user to confirm the guessed load before the run. In Claude Code, one `AskUserQuestion` call with three options can carry the offer; another client gets a short list. A user who says "just run it" can skip the offer.
- Say early whether and how topology optimization helps for this object. A thin shell, or a part with no load path, may not fit.
- For a small detail (not the load, the mounting or the size), choose a sensible value and say so.
- Before the first run, call `environment` (Step 1) and tell the user the device and the time, and that the user can pick another solver or device (`settings.solver`, `settings.gpu_device`; Step 1). The time is an estimate, not a limit: `check_problem` gives the start estimate (Step 3); then `run_status` shows the progress.
- Name the work folder in your first reply. Answer in the user's language. Use plain words first (the share of the block that stays, how much the part flexes, how fine the grid is) and give the technical numbers when the user asks.
- Look at the picture before you report, and say in one or two lines what it shows: the regions before the first run (Step 3), the part after each run (Step 5).
- After a run, call the finishing pass (Step 5), then say what was checked and what was not (Step 5).
## Facts to tell the user (when they matter)
- Target: a Mac with Apple silicon. Linux x86_64 also works. An Intel Mac (or an x86_64 Python under Rosetta) is best effort: tested on Linux only, with an older lock, `auto` gives `amg`, and the time estimate is not calibrated. On Apple silicon, a native arm64 Python is the fast path. Windows does not work.
- `uv` is not a prerequisite. The launcher uses the user's `uv` if it is uv 0.6.8 or newer, and skips an older one on the PATH. Else the setup downloads a pinned uv release from the official uv GitHub releases (about 20 MB, once, into the plugin data folder) and checks its SHA256. uv gets Python 3.13 itself. If that download does not work, the official installer is `curl -LsSf https://astral.sh/uv/install.sh | sh` (or `brew install uv`). For a uv that is too old, the fix is an update of that uv with the tool that installed it: `brew upgrade uv`, `pip install --upgrade uv`, or `uv self update` for a uv from the install script.
- First start: the server downloads Python 3.13 and the packages: about 200 MB (150 MB of packages on Linux x86_64, measured) and about 500 MB on disk. Measured: 23 s on a fast link, so 1 to 3 minutes is normal. Claude Code waits only 30 s for a server, so the launcher connects at once with all the tools, and a tool call answers with the setup status until the setup is done. The setup goes on in the background, detached from the server, so a client that ends the process group or the process tree of the server (Codex does the first each time it closes the server) does not stop it. Where neither `setsid` nor `perl` exists, the detach is only partial, and `doctor` says which way this machine uses. In a client that still stops the setup, the user can run `sh scripts/topopt-env.sh sync` in a terminal, in the repo root, before the first start (`docs/clients.md`). Other clients wait for other times. Later starts take 1 to 2 s.
- Fast solver on a Mac: Apple Accelerate, through a small C shim. Building it needs the Xcode Command Line Tools (`xcode-select --install`). Without a working shim, `settings.solver: auto` runs the AMG solver and prints a warning with the build command. AMG is slower. `check_problem` gives the time estimate for the solver that runs, and `environment` gives it for each solver. No full AMG run is measured on a Mac. The measured hook times (one Linux machine, one M1) are in the README.
- Runs, STLs and results go in `runs/` in the work folder. With the plugin, that is the folder where `claude` started. With the Claude Desktop extension, it is the Work folder field in Settings > Extensions > topopt (empty: `~/Documents/topopt`); a change needs a full quit of Claude (Cmd+Q on a Mac). `environment` shows the folder in use. When a reply has `work_folder_note`, tell the user that text.
## When a tool fails or a check refuses
The table "First-run problems" in `docs/reference/install.md` maps the message of a failed tool or of a refused check to its cause and its fix. In a checkout, the file is `docs/reference/install.md` in the repo root. In the plugin, it is `${CLAUDE_PLUGIN_ROOT}/docs/reference/install.md`. Look for the message in the first column, and give the user the fix in plain words, with its command. When the message is not in the table, the doctor (Step 0) and `check_problem` (Step 3) give the facts.
## Step 0. Is the server connected?
Look for the tools of the server `topopt`, for example `check_problem` (Claude Code with the plugin names them `mcp__plugin_topopt_topopt__...`; another client adds its own prefix). If they are there, go to step 1.
If a topopt tool answers `topopt is still setting up its Python environment`, the first setup runs: tell the user, wait the time that the text gives, and do not call the tool again at once. The tools work in the same session when the setup is done.
If the tools are not there, run the doctor first. It changes nothing, it installs nothing, and it works with a broken or a missing environment:
1. Run the doctor. Use this prefix for every setup command (Claude Code fills in the two paths; in a checkout, run `sh scripts/topopt-env.sh doctor` in the repo root, and use `status` and `sync` in the same way):
`CLAUDE_PLUGIN_DATA="${CLAUDE_PLUGIN_DATA}" sh "${CLAUDE_PLUGIN_ROOT}/scripts/topopt-env.sh" doctor`
It prints the facts of the setup: uv, the environment folder, the stamp, a cut setup, a running setup and an import test of the packages. When the environment runs, it adds the report of `topopt doctor`: the checkout (git or not, the revision, how far it is behind `origin/main` as of the last fetch, and when that fetch was), the packages, the known environments and which client uses which, the offers and the next commands. It ends with the command to run next. In a checkout venv, `topopt doctor` (or `.venv/bin/python -m topopt doctor`) gives the second report alone, and `--json` gives it as data. A copy of topopt that is older than this command says so in the report. Updating the copy is then the first step.
2. Read it and tell the user the facts. A checkout that is behind `origin/main` is a fact to tell: the update command is in the report, and the user decides. The numbers are as of the last fetch; the report gives the fetch command. Run a fix command only after the user agrees.
3. The row `uv` says `not found:` and `the setup downloads uv ...`: there is nothing to install, the setup gets it (see 5). The row can also name a uv on the PATH that is too old (older than uv 0.6.8): the setup skips it and downloads its own. If the launcher says that the automatic download did not work (offline, a platform with no pinned uv, a failed SHA256 check, or a `TOPOPT_UV` that does not run), tell the user the reason. When the message also says `The uv on PATH is too old`, offer the update of that uv with the tool that installed it (the commands above): a new uv from the install script does not help while the old one comes first on the PATH. Otherwise offer the official installer (the command above). Run it only after the user says yes. Then the user runs the `sync` command (5), and the tools work in the same session (6).
4. It says the setup is running: wait for it. `sync` waits until the running setup ends, or wait a minute and run `status` again. The log is `setup.log` in the same folder as the environment.
5. It says not ready and no setup is running: tell the user what happens (a download of about 200 MB when the uv cache is empty, 1 to 3 minutes), get a go-ahead, then run the same command with `sync` in place of `doctor`. Give the shell call a 10-minute timeout, if the client lets you set one. A setup that stopped (a network error, a killed process) leaves a marker, and the next `sync` installs the packages again. One fact: an environment that an older launcher left cut has no marker, `status` calls it ready, and a tool fails with an `ImportError`. The import test of the doctor finds it. Then run the command that the doctor prints at the end: it makes the marker file and runs `sync`, which installs the packages again. This needs the network, also when the uv cache is warm; with `UV_OFFLINE=1` in front of the command, uv works offline when its cache has every wheel (`docs/clients.md`). In Codex, a command that you run has no network by default: the user can run `sync` in their own terminal, or approve the command outside the sandbox.
6. When it says ready: the tools work in the same session, because the launcher hands its open connection to the Python server. If a tool still is not there, ask the user to reconnect the server. In Claude Code, type `/mcp` and reconnect `topopt` (or `/mcp reconnect all`). Another client has its own way (restart the client, or turn the server off and on: `docs/clients.md`). You cannot do this yourself.
## Step 1. The machine
Call `environment` before the first run. Read the platform, the solver backends, what `auto` means here, the RAM and the cores. Tell the user the facts that matter, and the device that will run.
The device that will run is the solver that `auto` picks first here (`auto_solver`: `gpu-mg` on a GPU that passes a short self-test, else `cholesky`, `pardiso` or `amg` on the CPU). `gpu_solver` says which: `used`, the `device`, the self-test and, when no GPU is used, `why` and `to_use_the_gpu`. `opencl.devices` lists the OpenCL devices (`kind` tells a GPU from a CPU device; an empty list means that `gpu-mg` cannot run here). Tell the user the solver and the device. If `gpu-mg` fails during a run, `auto` goes on with the CPU solver (`auto_choice.cpu_solver`) from the last checkpoint, and `run.json` and `run.log` say so. Tell the user that they can pick another solver or device. `auto` stays the default until the user asks. In the YAML, `settings.solver` takes `auto`, `cholesky`, `pardiso`, `amg` or `gpu-mg`. `topopt run` and `topopt check` take `--solver` with the same values, for one command. `run_topopt` has no solver argument: edit the YAML, then call `check_problem`. For `gpu-mg`, `settings.gpu_device` is `auto`, a number from the list that `topopt check` prints (the `index` in `opencl.devices`), or a part of the device name.
If `recommendations` has an offer (`build_accelerate_shim` on a Mac, `install_pypardiso` on Linux), tell the user the reason, the speed-up and the license note, and ask. Run the command only after a yes. If the user says no, the run goes on with AMG: `auto` does that without a change to the YAML. The shim is built in the plugin folder. A plugin update needs a new build.
The answer has `checkout` too: git or not, the revision, and how far the code is behind `origin/main` as of the last fetch (`last_fetch_utc`; nothing is fetched). Tell the user when the code is behind, and give `update_command`.
`gpu_solver` is a fact, not an offer. When `used` is false and `to_use_the_gpu` has a step (for example the install command of pyopencl), tell the user the reason and the step, and ask before you run a command. `settings.solver: gpu-mg` asks for the GPU solver on any OpenCL device, also a CPU device, and it has no fallback (`run_topopt` has no solver argument: edit the YAML; `--solver gpu-mg` does it for `topopt run` and `topopt check`). The speed of gpu-mg is measured on GPUs. `gpu_solver.measured` has the figures of one machine (the hook example on a MacBook Air M4: seconds per iteration of the default solver against gpu-mg); another machine and another GPU can differ. `topopt gpu-check` measures this machine (one command, one report to share), and `check_problem` gives the time and memory estimates for a problem. A gpu-mg run that stops with `did not reach the relative residual` names its options in the error: `settings.gpu_tol` (the target of each solve), `settings.gpu_backward_error` (a solve at the FP64 rounding floor is accepted at this backward error; 0 turns the rule off), `settings.gpu_max_iter` (case B: the PCG iterations of the first pass, default 1000), a stiffer start design, or another solver. The residual history of the failed solve is in `gpu_mg.jsonl` of the run folder. What gpu-mg takes: springs, joints and per-axis supports (a `fixed` entry with `axes`), alone or together, with or without a `fixed` region. In its case C (a device without 64-bit float) the joint term is computed in 32-bit float and the inner PCG removes the rigid modes of each body and 6 harmonic columns for each bonded patch, and solves them in 64-bit on the host (`run.log` says so): `check` prints the beam over the bond stiffness on its softest mode, with the cases that were measured on pocl and on one Intel Arc iGPU (one host each; revision 133e5a1; a cube that only a joint holds solved at 9.9e-12 and above with the rigid modes removed, and at 3.3e-7 and above without; a part with two tube joints and no fixed region solved with the harmonic columns and stopped without them) and the memory of the columns, which the memory estimate does not count. These are facts, not a limit. A run that stops gives the ratio and the options: another solver, `settings.gpu_case: B` on a device with 64-bit float, or a stiffer beam (a larger section, a shorter beam).
## Step 1b. Preferences
The reply of `environment` has the key `preferences`. It tells whether the user saved their answers in the file `topopt-preferences.yaml` in the work folder.
- `exists` is false: ask the user once, in one message, for five things. The printer: the model only, for example "Bambu P1S". The user does not give the print bed or the printer type; see "The printer model" below. The filaments the user has: the names only, for example "Bambu PLA Basic"; see "The filaments" below. The material, when the user gives no filament (a name such as PETG, and E in MPa, nu and density in g/cm3 if the user knows them; the resource `topopt://materials/reference` has typical values). The quality, which is the voxel size in mm: a smaller voxel gives a finer part and a slower run, and `check_problem` gives the time for a part. The outputs after a run: `renders` (images), `step` (a STEP file) and `verify` (a stress check); the final STL and the part card are made in every run. The user can answer "keep the defaults": then call `save_preferences` with no section, so that nobody asks again. Otherwise call `save_preferences` with the answers.
- `exists` is true: use the values and do not ask again. The user can change them at any time. A new answer is a new call of `save_preferences`, or an edit of the file by hand. The server reads the file at each call, so a change is live at once.
- `problems` is not empty: tell the user what is wrong. The server uses no value from the file until it is fixed. `save_preferences` can repair a wrong key. It does not change a file that is not valid YAML.
- A preference does not change a run by itself. `build_regions` writes it into the YAML only for a block that the spec does not have, and says so (`preferences_facts`). `check_problem` names a difference between the YAML and the file. The problem YAML stays the record of a run.
### The printer model
The user names the printer. The tool knows the print bed (`bed_mm`), the build height and the printer type. Call `find_printer` with the name that the user said, and read `status`.
- `match`: show the user what the profile gives (the print bed, the build height, the printer type) and say which values are `null` (unknown). Then call `save_preferences` with `print={"printer_model": <id>}`. A YAML with `print.printer_model: <id>` gets the bed and the printer type from the profile, and the print report compares the part height with the build height. A `bed_mm` or a `printer_type` that the YAML gives wins over the profile.
- `ambiguous`: offer the `candidates` and the facts in `differs`, and let the user pick one. Do not choose for them.
- `none`: the printer is not in the catalog. If `suggestions` has names, ask whether the user means one of them (a suggestion is not taken by itself). If not, follow the steps below.
A printer that is not in the catalog:
1. Look up the page of the maker (a web search). Take these values from that page: the printer type (`printer_type`: `fdm` for a filament printer; `save_printer` needs it), the bed size, the build height, the highest nozzle and bed temperature, the enclosure (`open`, `enclosed` or `heated`), the nozzle material and the extruder.
2. Show the user the values with the address of the page, and ask whether they are right. A value that you cannot find stays unknown (`null`): ask the user for it, and leave it `null` if the user does not know. A client with no web access asks the user for the values.
3. After the user agrees, call `save_printer` with the values and their source: `source_url` (the page) and `opened` (the date), or `source: "user"` when the user said the values. A value from another page can have its own address in `field_sources`. The tool writes `topopt-printers.yaml` in the work folder.
4. Then call `save_preferences` with the new id as `printer_model`.
The user can also skip all of this: give `print.bed_mm` and `print.printer_type` in the YAML, as before.
### The filaments
The user names the filaments they have; the catalog knows the modulus, the density and the print needs of each. Ask once, after the printer: "Which filaments do you have?" (the names, or the label of the spool). Call `find_material` with each name and read `status`.
- `match`: show the user what the profile gives (`derives`: E, which is the lower of the XY and Z modulus, and the density; nu stays the assumed 0.35, because no data sheet gives it) and say which values are `null` (unknown). The numbers of a data sheet are for printed test bars of one maker, not for the part.
- `ambiguous`: offer the `candidates` and the facts in `differs`, and let the user pick one. A family alone ("PLA") fits many filaments, so the answer is `ambiguous`. Do not choose for them.
- `none`: the filament is not in the catalog. If `suggestions` has names, ask whether the user means one of them (a suggestion is not taken by itself). If not, follow the steps below.
Keep the answers: `save_preferences` with `filaments={"available": [<ids>]}`. If the user has chosen the filament for the part, `material={"filament": <id>}` keeps it as the material too. A YAML with `material.filament: <id>` gets E and density from the profile; a key that the YAML gives wins, and `topopt check` says where each number came from.
Then show what the printer can print: call `printer_materials` (it takes the printer of the preferences, or `printer=<name>`). Say the answer as facts: each filament is `ok`, `care` (it can work, and the reason says what to do or know), `no` (a stated need is above a stated limit of the printer as it ships) or `unknown` (a value is missing), and each reason names the two values that it compares. Start with the filaments of the user (the key `available`); list the others when the user asks. The states are facts, not a stop: the user may have changed the nozzle or the printer, and a `no` does not stop a run. The user decides.
A filament that is not in the catalog:
1. Look up the data sheet of the maker (a web search). Take these values from it: the family, the tensile modulus and strength in the layer plane (`E_xy_mpa`, `strength_xy_mpa`) and across the layers (`E_z_mpa`, `strength_z_mpa`), the density, the nozzle and bed temperature ranges, whether an enclosure and a hardened nozzle are needed, and the drying setting. A run needs a modulus (the tensile one; if the sheet gives only a bending modulus, give that as `bending_modulus_xy_mpa` or `bending_modulus_z_mpa`) and the density.
2. Show the user the values with the address of the sheet, and ask whether they are right. A value that you cannot find stays unknown (`null`): ask the user for it, and leave it `null` if the user does not know. A client with no web access asks the user for the values.
3. After the user agrees, call `save_material` with the values and their source: `source_url` (the sheet) and `opened` (the date), or `source: "user"` when the user said the values. A value from another page can have its own address in `field_sources`. The tool writes `topopt-filaments.yaml` in the work folder.
4. Then use the new id in `save_preferences`.
The user can also skip all of this: give `material.E`, `material.nu` and `material.density` in the YAML, as before.
This is an offer. If the user wants to go on at once, ask later, or ask only what the part needs.
## Step 2. The part
- The user describes a part in words: use the skill `part-to-topopt` (`.claude/skills/part-to-topopt/SKILL.md`; the same text is in `.agents/skills/part-to-topopt/SKILL.md`. A client that does not list the skill can read that file). It builds region STLs and a problem YAML (the tool `build_regions`) and gives the assumptions to state. A sketch or a photo with no size gives proportions only: the skill asks for one real size (Step 1, item 4) before the run.
- The user has an STL of the part itself ("here is my part", "keep the screw holes"): use the skill `part-to-topopt`, section "Your own STL". It reads the file with `find_features`, asks which holes take screws and what is mounted, and builds the regions with the `stl` item of `build_regions`.
- The user has the region STL files and a problem YAML: write or fix the YAML (see CONTRACTS.md section 2 in the `code_folder` that `environment` returns).
- The user wants a quick try: `examples/hook/hook.yaml` is the T-shirt hook. `check_problem`, `environment` and `run_topopt` find a relative path like this one in the plugin folder, when it is not in the work folder.
- Files in the plugin folder are replaced by a plugin update. To change an example, copy its folder (the YAML and the STLs) to the work folder first, for example `runs/<name>-input/`, and change the copy.
Before the first run, write the load inventory (the section "Load inventory, verify and ask, buckling" of the skill `part-to-topopt`; a simple part needs one or two lines). Say what you are not sure of. If an answer changes the part, ask the user; if you go on with a guess, write it as a guess (origin `inferred`) and say it again at the end.
State your assumptions before the run: the loads and where they come from, the mounting, the size, the material, the volume fraction, the voxel size. The tools return `assumptions` and `derived` for this.
Record the brief with `record_brief`: the text (the goal, the loads, the mounting, the material, the sizes) and the assumptions, each with its origin (`user`, `inferred` or `default`). The tool makes a case: the YAML, the STL files it names, the brief and the runs. `case_status` shows the brief, the input hashes, the runs and what changed since the brief. Say the brief to the user and go on, or wait for an answer when the answer changes the part. A missing load changes the part. A YAML outside the work folder (an example in the plugin folder) gets no case file: copy it first.
## Step 3. Check
Call `check_problem` with the YAML path. It loads and voxelizes the problem and runs nothing. Read the grid, the free dofs, the time estimate for the solver that runs, the memory estimate, `lost_force` and the warnings. Tell the user the time (an estimate, not a limit) and the warnings that matter. A guard (`max_grid_voxels`, `max_active_voxels`, the memory check) stops a run that cannot work. Its message names the override and the other options. You decide with the facts: the memory estimate against the RAM (a run above the RAM can freeze the machine). Raise the guard or take a larger voxel, and tell the user what you chose, why and the price (`part-to-topopt`, Step 4).
Look at the problem before the first run. Call `render_part` with `yaml_path` (the problem YAML). It draws the regions and one force arrow for each force of each load case, and it runs no solve. Say in one or two lines what the picture shows: is each load on the face that the user means, does its arrow point the right way, is the fixed region where the part is held, does a keep-out cover the hole or the space that it is for. The arrow length does not show the force size. The numbers of `check_problem` can look fine while a load sits on the wrong face or a region is in the wrong place, and a picture shows this in a second. The cost is a few seconds (the tool text gives 2 to 4 s for a part of 85,000 triangles). Draw the YAML again after each change of the YAML or of a region STL, before the next run. This look is your own check. It is not the `renders` output of the preferences (Step 1b), which is about the images that the user gets. A client that cannot show images: call the tool with `inline=false`, read the facts (the bounding box of each region, `load_arrows`, the missing files), and tell the user that you did not see the picture.
## Step 4. Run
Call `run_topopt` with the YAML path. It returns at once and gives `dashboard_url` (usually `http://127.0.0.1:8765/`). Give the URL to the user. The dashboard shows the shape while it forms. On a desktop, the browser can open by itself. One run goes at a time.
A run makes all the passes of `settings.iterations`. `stop_rules=true` (or `settings.stop_rules` in the YAML) lets it end earlier, when the objective has settled. The rules are off by default.
If the YAML has a case file and no current brief (none recorded, or an STL file or a requirement key of the YAML changed since), `run_topopt` returns `awaiting_user`. The reply has the facts (the changed files and keys), the options and the override `proceed_without_brief=true`. Nothing starts. Record the brief and call again. Give the override only after the user agrees: the case log records it. A change of a method setting only (for example `settings.solver` or `settings.voxel_size`) does not stop the run: `brief_facts` names the key with both values, and you tell the user what changed, why and the price. A YAML with no case file runs as before. The reply has `brief_stated`.
## Step 5. Watch and report
Before you report, answer the final review checklist of the `lead-design` role (`team_guide` with `lead-design`).
Nothing calls you back when the run ends: no client starts a new turn when a background job ends. A promise of a later report cannot be kept (for example "I will report when the run finishes"). There are two choices. Keep calling `run_status` in this turn until the state is `done`, `error` or `stopped`, or `process_alive` is false, then report. Or end the turn with a plain line that tells the user what to ask to get the result (for example "what is the result?").
Call `run_status` from time to time. Each call returns at once. In a client with a shell, wait between calls (for example `sleep 20` in Bash). In a client without a shell, call `run_status` again in the same turn. The check gave the start estimate of the time; `run_status` shows the pass and the time so far.
To end a run that is still going (the user changed their mind, the numbers look wrong), call `stop_run`. It writes a request and returns at once. The run reads the request at the end of its current pass, saves the checkpoint and ends with `state` `stopped` (no `result/`). A pass can take a long time, so the stop is not at once; `run_status` shows `stop_request` while the request waits. The reply of `stop_run` has `state`, the facts and the options (a run in its post step has no pass left to stop: the reply says so and writes nothing). A run that an older build started ignores the request: the `note` says so. To continue a stopped run, see the next paragraph (`resume=true`; the inputs did not change, so no `allow_changed_inputs`).
A run that was stopped (by `stop_run`, or because the process was killed or the machine was off) leaves a checkpoint of the last finished pass. `run_status` shows it as `checkpoint`. To continue the run, call `run_topopt` with the same YAML and `out_dir`, and `resume=true`. If the YAML or a region file changed since the checkpoint, the reply names the changes. Tell the user, and use `allow_changed_inputs=true` only if they want the new inputs. Without `resume`, a new run in a folder that holds the checkpoint of an unfinished run deletes it: the reply has `earlier_checkpoint` then. Tell the user, and use `resume=true` if they want to continue the old run.
To add passes to a run that is done (or stopped), call `run_topopt` with `continue_from` (the run folder), `more_iterations` (how many passes to add, 1 or more, no upper limit) and a NEW `out_dir`. The new run starts from the last checkpoint of the parent (pass K) and has K + `more_iterations` passes. The parent folder does not change, and `out_dir` cannot be the parent or inside it. The new folder gets a copy of the snapshots (the reply facts give the bytes). The reply has `continue`: the pass K, the pass count, whether pass K runs again (a finished parent: yes, with its update step; a parent that did not finish gives a fork that starts at pass K + 1), the step settings and the facts. Tell the user the pass count and the facts that matter. If the parent ended at a stop rule and the rules are on, the rule is judged again at pass K and can end the new run there: the facts give the options (`stop_rules=false`, or other thresholds). A change of the YAML or a region file is a refusal with the changes; use `allow_changed_inputs=true` only if the user wants the new inputs. `resume=true` is for finishing a run in its own folder. Step 6 says when a continuation fits a change of the problem.
When `state` is `done`, call `run_result`. Tell the user the mass before and after, the stiffness for each load case (`n/a` with a reason when a case has a moment, a remote force or a pressure), the run time, the solver and the path of `final.stl`. The `verdict` in the metrics says whether the run converged, reached the iteration limit or stopped at a rule, and whether the constraint rows are met. For the state `iteration_limit`, say it in plain words: "the run stopped at its pass limit", not "not converged". `converged` (the objective has settled) and `feasible` (the constraint rows are met) are two facts, and a run can have one without the other. The text gives each test with its value, its threshold and met or not met. The default thresholds are not tuned for a part: give the numbers to the user, and do not call a run bad because of the label. `lost_force` and the warnings are facts to include. The dashboard result panel has a Refine button: a page with a stress overlay and a tool to place marks. The stress is a display of the voxel model. It is not a safety check. For a run with a stress limit, read `stress.cases.<case>.summary` in the `run_result` answer. Give the user both facts: the relaxed row of the optimizer (met or not) and the voxel estimates of the part stress against the limit (the grey and the thresholded design give a range). "Met" is about the relaxed measure. Neither number is a check of the printed part. To change the part from the marks or from a comparison, see Step 6.
After each run, look at the part before you report. Call `render_part` with the run folder (with no argument it takes the last run). It draws `final.stl` with the regions, and the facts give the share of back faces and the missing files. Say in one or two lines what you see: the main members, a part that is cut off, a region with no material around it, magenta back faces (a flipped or open surface). The numbers of a run can look fine for a wrong part, so the look comes before the report. A change that has no run gets the same look: give `stl_paths` for the `edited.stl` of `apply_marks`. `section` cuts the part open. The `inline=false` fallback of Step 3 works here too. A render is not a stress check or a fit check.
Every finished run already has the print check: `result/print_check.json`, and `run_result` shows `print_check` (the counts of pass, fail and unknown, the names of the failed checks, and the unknown checks that have a limit). It reports and stops nothing. Read it first. To check the part for printing with other limits or another build direction, or to check an STL file, call `print_check` (second call). It runs no solve. Each check (closed mesh, bodies, closed voids, bed fit, overhang, support estimate, plate contact, wall thickness) has a value, a limit with its source, a state (pass, fail or unknown) and a reason. The limits are keys of the YAML `print` block, such as `min_wall_mm`; a check with no limit is `unknown`. Tell the user the facts that matter. `strict` is an opt-in gate and stops nothing. A print pass is not a stress check.
After each successful run, call the finishing pass before the final report to the user: the role `finish` (the sub-agent `topopt:finish-engineer`, or `team_guide` with `finish`; one agent that does all the roles does the pass itself). It looks at `final.stl` with `render_part` (the views `front` and `fl2:-1.333,-1,0.373`, `regions` `off`), removes visible voxel steps with the tools that exist, runs the print check again, and reports what it changed, the print check and the renders before and after, the price in mass and stiffness, and what it did not fix. Tell the user that report with the result. The cheap routes (the look and the print state) run after every run. A route that costs a run, such as the fine pass, is an offer to the user with the estimate. A part with no visible steps and no print finding can end the pass with a report of what was looked at and no change. The pass changes a method setting freely, with a report; a user requirement it tightens with a report and its price, and loosens only with the consent of the user.
Steps on the finished part are a known limit of the voxel method in this version, not a fault of the input. Where a surface runs a few degrees off a grid plane, each one-voxel step stretches into a long flat tread (the voxel size divided by the tangent of the tilt: a step every 8.9 mm at 0.5 mm and 3.2 degrees). Long, thin parts show them most; curved or steep faces give short steps, which the mesh step smooths or makes lower. A step is one voxel high and is already in the run's numbers: the stiffness comes from the voxel model, and `mass_final_g` is the mass of `final.stl`. For long steps, cheapest first: on the faces of a closed design space, if `metrics.mesh.envelope_fill` shows that the fill did not run, `settings.envelope_fill` `auto` or `on` in a post-only resume (minutes; the stiffness at the same mass falls a few percent, more on a coarse grid); on member surfaces, a `smooth` mark with `apply_marks` (seconds; the mass change is reported; its effect on a long step is not measured, and the stiffness change is known only with a `stiffness` or `stress` rule of `acceptance`); a finer voxel with `fine_from` (a whole run; the estimate is in `run_result.fine_pass`), which makes the steps lower but can still show them. `settings.mesh_method` is no route for long steps. A better smoothing of long steps on member surfaces is planned, with no date. Nothing is measured on fatigue or stress at a step edge.
To check the stress of the finished part with another mesh and element, call `verify_run` with the run folder (`estimate_only` is the default: it meshes nothing and gives the memory and time; `estimate_only=false` starts a background job that `verify_status` shows). It needs the extra `verify`, refuses a YAML with a moment, a remote force, a pressure or a per-axis support, and gives pass or fail for an `allowable_mpa` that the user gives. With `gate=true` it gives pass or fail for the limits that the YAML already has (`stress_limit`, `buckling.min_factor`, `limits` rows), read as limits of the tet value; `allowable_mpa` then replaces the stress limits of the YAML, and a larger value loosens them (only with the consent of the user). A joint or a spring of the YAML holds the part like a clamp: the result then has `supports` (the bond force and moment of each joint, the force of each spring), the press-fit check is skipped for a joint, and the buckling load factors are for buckling of the part, not of the beams. Tell the user what the check does not cover.
After the first finished run of a part, offer the stress check once. It answers "will it break under these loads?" (`verify_run`). Say what it costs: the extra `verify`, and the time that `verify_run` gives first with `estimate_only`. Say that a yes or no needs a limit: an allowable stress from the user (`allowable_mpa`), or the limits that the YAML has (`gate=true`). The user decides. When the job is done, `verify_status` has `plain`: one plain line for each load case, with the source of the limit, and with `gate=true` the line `gate: PASSED` or `gate: FAILED (...)`. Give these lines to the user as they are.
To give the finished part to a CAD program, call `export_step` with the run folder (`estimate_only` first for a large mesh). It needs the extra `step` (`estimate_only` does not); without it the reply has the install command. The call returns when the file is written, and a large export holds it for minutes, so read the estimate first. It wraps `final.stl` (or `part_only.stl` with `part_only`) as a closed solid in a STEP file, `result/final.step`. The faces are the facets of the mesh: a CAD user can cut holes in it and join it to other parts, and cannot edit its surfaces. The report gives the faces, the file size, the sampled distance to the STL, the volume change and the checks of the model and of the file read back; tell the user the tolerance and the distance.
After a run, say what was checked and what was not (stress, fatigue, the printed part), the files the user has, and what the user can ask next (Step 6). A printable part is not a certified part.
## Step 6. Refine from feedback (optional)
The user says what they see: "too heavy", "extra film here", "rough", "grey". The lever cards say which key or tool changes it. Read the MCP resource `topopt://levers` (or run `topopt levers <key or word>`; the resource `topopt://levers/<key or word>` gives the cards of one key or word): one card for each YAML key and each engine tool. A card gives the kind of the lever, the cost of one try, the words of the user, the fact in the result that detects the symptom, and the measured evidence with its part class. The cost has four layers: a new run, a continued or branched run, the post step only (`run_topopt` with `resume=true` and `allow_changed_inputs=true` on a finished run: seconds, for the keys that the cards mark), and a local edit of the finished part. Pick the cheapest layer that can work, compare the result with `compare_runs`, and tell the user what you changed, why, and the price.
Who decides (guidance; no tool refuses a change):
- A method setting (penal, beta schedule, move limit, solver and tolerances, voxel size, iterations, robust, mesh method, smoothing): you change it without asking, and you report what, why and the price.
- A user requirement (loads, supports, keep-outs, interfaces, material, stress limits and safety factors, a stated mass or volume target, print limits, symmetry): you may tighten it without asking, with a report and its measured price. You loosen it only with the user's consent, and you bring the facts.
- A key that can be either (for example `settings.min_member_mm`, a print rule or a look lever) follows its origin in `record_brief`: `user` is a requirement; `inferred` and `default` are yours.
**A change after a finished run.** The user asks for a change to a part that has a run. A test on three parts (hook, bracket, ladle) measured the routes below. These are facts and offers. The user's requirement decides (Who decides, above).
- Less volume: offer "continue from the last run": `run_topopt` with `continue_from`, `more_iterations` and `allow_changed_inputs=true` (Step 5). Measured at -20 % volume only: 44 to 55 % fewer passes than a fresh run, and a compliance at equal volume of 0.94 to 1.00 of the fresh run. No other size is tested.
- One factor on all the forces, with no stress, displacement or compliance limit: the design is the same, and a new run is not needed. A fresh run gave a byte-identical `final.stl` on all three parts. With a stress, displacement or compliance limit, or a different factor for each load case, a run is needed.
- A load direction turned by 10 degrees or more (less than 10 degrees is not measured), or a change of a requirement (the minimum member, the print rules): run fresh. A continuation keeps the old topology (at 30 degrees and equal volume, the compliance was +7.5 % on the bracket and +17 % on the hook, in its worst load case) and the old thin members (with the minimum member at 2 voxels, the thinnest member of the hook and the ladle was 69 % and 80 % of the fresh run's). A key that only the post step reads, such as `print.build_direction`, keeps its cheaper route (layer 3, above).
- After a continuation, tell the user the volume and the compliance of the new run, and that a continuation is not proven equal to a fresh run. Compare a compliance with another run only at equal volume.
The marks are points of emphasis that the user places on the Refine page and downloads as `marks_<name>_<time>.json` (units mm, the frame of `final.stl`). `marks_to_hints` edits no mesh; `apply_marks` edits the finished part. Three tools turn feedback into a next run, and a fourth edits the part:
- `run_history` lists the earlier runs with their settings and main metrics, and names a missing or partial file as a fact. `compare_runs` puts two runs side by side: the setup diff, the metrics diff and the voxel labels that changed. It also says as facts whether the YAML and STL files on disk can be the ones that ran (a newer file, a YAML value that differs from `run.json`), because `run.json` holds no hash.
- `marks_to_hints` reads the marks file and writes a NEW folder with a proposed YAML (the YAML of the run, with the changes), region STLs and `hints.json`. For each mark it gives the facts (position, radius, distance to the surface, voxels touched, overlap with the fixed, load and keep-out regions) and the options. A remove mark becomes a keep-out region, an add mark a forced-solid region, a smooth mark stays a fact because the YAML has no key for it. `options` change the choice (`skip`, a smaller `radius_scale`, `per_mark`). A keep-out from a point mark is a hard void, so a smaller radius is a fair option. The first mark wins an overlap. The reply is short; `hints.json` has the full facts, and `detail` gives them in the reply. The report says if the proposed YAML loads and gives the target volume before and after.
- `apply_marks` applies the marks to `final.stl` in a NEW folder (`edited.stl`, `apply_marks.json`): a remove mark is a cut that protects the parent, an add mark a closing, a smooth mark a fairing (iso for a point, fair for a stroke; `smooth_mode` tube rebuilds a whole strut and is asked for). It gives facts for each mark (volume and mass change, the clip by the frozen regions, thickness near the mark, parent check, bodies, stress near the mark) and no verdict. No FE run is made on the edited part. Marks made for another `final.stl` are refused unless `allow_stale` is set. The other route is `marks_to_hints` and a new run; you choose and tell the user why.
- Read the report, tell the user which option the YAML uses and why, draw the proposed YAML (Step 3), then call `run_topopt` with it and a new `out_dir`, and `compare_runs` with the first run. To change the part in another way, change the spec or the YAML and run again.
- To cut the thin film at the contact faces of `part_only.stl`: `metrics.part_only.contact` gives, for each removed body, `thinnest_wall_mm` and `area_below_voxel_mm2` (the contact area with a wall below the voxel size), and a note names the bodies with a thin wall. The thinnest wall is the lower end of the film, not a value to use. The wall thickness to cut off is a print or a look decision of the user. Write it as `settings.part_only_min_film_mm` in a NEW YAML file in the folder of the old YAML (the region STL paths are relative to that folder). Call `run_topopt` with that YAML, `out_dir` set to the old run folder (or to a copy of it, to keep the old result), `resume=true` and `allow_changed_inputs=true`: the post step repeats and the optimization does not. Then read `thin_area_mm2`, `cut_area_mm2` and `thin_area_after_mm2` for each body in `metrics.part_only.contact`, and change the value if the film is still there. `compare_runs` with the first run shows the key when the two YAML files differ.
- `ratchet_run` judges candidate edits of the finished part (marks or STL files) with the rules of the YAML block `acceptance`, and keeps the ones that pass; call it with `estimate_only` first, then `ratchet_status`. Rules are the owner's: add a rule only when the user names it and its limit. `ratchet_approve` is the opt-in owner gate. A mark candidate is judged by the stress rule like any other; if verify refuses its surface, the verify error is in the log.
- `cleanup_list` finds the spurs, bumps, uneven struts, flat webs and loose bodies of the finished part and writes them as a marks file (`marks.json` in a NEW folder, with the facts of each candidate); it edits nothing and gives no verdict. Call it with `estimate_only` first if the part is large. Read `applicability` before you trust the strut candidates (a shell or plate part gives sheet fragments). Then `ratchet_run` with `source` `marks:<marks_rel>` judges each mark with the rules of the YAML. A part with a loose body gets the body marks only: judge them first, then list the `acc_01` folder of that ratchet root.
## Roles
The plugin has five sub-agents: `topopt:lead-design-engineer` (talks to the user, states the assumptions, reviews the result), `topopt:cad-engineer` (regions, shapes, geometry), `topopt:fea-engineer` (loads, supports, solver, run), `topopt:print-engineer` (can it be printed?) and `topopt:finish-engineer` (smooths the finished part and checks printing again; Step 5). A simple part needs none of them. Use them when a separate context helps, for example for a complex part.
A client without these sub-agents gets the same five role texts from the tool `team_guide`: call it with `lead-design`, `cad`, `fea`, `print` or `finish` (with no role it lists them). One agent can read the roles that it needs and do the work itself.