agentleFS
chartergroupchartergroup/topopt/skills/start-here/SKILL.md

start-here

Skill26 starsChanged 16 days agoMITView on GitHub

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.

  • Pipes a download into a shell
  • Installs packages

11 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 6 are distinctive, and 5 are very specific to this project.

  • 0

    Common

    Most of the 9 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    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 …

    line 21
  2. Distinctive:

    After a run, call the finishing pass (Step 5), then say what was checked and what was not (Step 5).

    line 22
  3. Very specific:

    Look for the tools of the server topopt, for example check_problem (Claude Code with the plugin names them mcp__plugin_topopt_topopt__....

    line 38
  4. Very specific:

    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 …

    line 40
  5. Distinctive:

    If the tools are not there, run the doctor first.

    line 42
  6. Distinctive:

    Run the doctor. Use this prefix for every setup command (Claude Code fills in the two paths.

    line 44
  7. Very specific:

    Call environment before the first run.

    line 55
  8. Very specific:

    Call run_topopt with the YAML path.

    line 136
  9. Very specific:

    Before you report, answer the final review checklist of the lead-design role (team_guide with lead-design).

    line 144
  10. Distinctive:

    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 …

    line 172
  11. Distinctive:

    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 …

    line 189

“Common” is measured: kept by at least 6 in 10 of the 9 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.