#The torno manual

Everything torno does, in the order a first-time user meets it: what it needs, how to start it, a walk-through, then the reference. The README is the short introduction. The same text, with a table of contents at the side, is at torno.luisg.me/manual.

Torno is a desktop workbench for making CAD parts with an AI coding agent: the agent writes build123d Python in a terminal inside the window, torno builds the model every time a file is saved and shows the parts beside the terminal. You point at the parts to say what you mean, measure them, and hold them to numbers; the agent is told what you pointed at and has tools to see what you see.

The window has three columns. On the left, the projects of the folder you opened, and under them the parts, scans and marks of the open one; the marks have the room, and the sections above them fold. In the middle, the parts in 3D, with a bar of views above them and the tools down the left edge. On the right, the terminal the agent runs in. A line at the bottom says how the last build went.

Torno is not a modeller. It has no sketcher, no feature tree and no way to edit a solid by hand. The model is the Python file; torno shows it. It uploads nothing and publishes nothing. The name is Portuguese for a bench vise, which the logo is: one holding a part. The design is in docs/superpowers/specs/2026-10-08-torno-design.md.

#Requirements

Linux or macOS. Developed on Arch with Wayland (Hyprland). macOS is tested on GitHub's macOS runners and not yet by eye.

Torno checks these when it starts, before anything fails, and says so in a block called "Ready to work" on the first screen, in a workspace with no project and on the page of a new project (below): a line for Claude Code, for uv when it is missing, and for the kernel's Python environment. When everything is there the block is one quiet line ("Claude Code and build123d 0.13.0 are ready"), and a click opens it. It looks for claude and uv on the PATH torno was started with. It also runs claude --version and claude auth status, once at start and within 3 seconds each (nothing written, nothing of your Claude Code settings read), to say its version and whether you are signed in, and uv --version; under test it only looks on the PATH and never runs claude. "Check what torno needs" in the folder menu opens the same lines in a dialog and asks again ("Check again"). The first time, torno's own Python environment is made by uv and about 900 MB is fetched, so that first run needs the network; until then the block says "Not made yet", and "Make it now" makes it (uv sync on torno's own kernel project, never on a workspace) with uv's lines shown as it goes, so that the first build does not have to. A workspace with its own pyproject.toml is only reported on: whether its .venv is there. Torno never makes one.

#Install

git clone https://github.com/luisg0nc/torno.git
cd torno
pnpm install

#Run

scripts/torno ~/projects    # open that folder as the workspace
scripts/torno               # open the workspace opened last, or the first screen
pnpm start [folder]         # the same, from the repository
scripts/torno --print-command [folder]   # the command it would start, one argument a line, the folder first as TORNO_WORKSPACE=…; starts nothing
scripts/torno --print-desktop            # Linux only (macOS: see below). The desktop entry for this copy of torno; writes nothing
scripts/torno --install-desktop          # writes it: ~/.local/share/applications/torno.desktop, and nothing else
scripts/torno --uninstall-desktop        # removes that file
TORNO_OZONE=wayland scripts/torno        # or x11: the platform Electron is told to draw on

scripts/torno works from any directory, and through a symlink. It installs the dependencies if node_modules is missing, builds the app if it has not been built since the sources last changed, and starts it. A build that fails is tried again at the next start. It knows a build by app/out/.built, a hash of what the sources held when its last build that succeeded began: a checkout or a touch that leaves every source as it was builds nothing, and any change does. Only the launcher writes it: after a pnpm build or a test run of your own on changed sources, the first start from the launcher builds again (some ten seconds, written into app/out).

With no folder to open, the first screen says what torno is, says what is ready and what is missing (see Requirements), draws how a folder of projects is laid out and offers "Open folder" (an empty one is fine) and "Open the example"; coming back, it lists the folders opened lately first. The example is one small project, an enclosure for a sensor board, made to be shown: a model with three parts (a body, a lid and a clip), a brief written as a person would, two marks, a dimension that fails and two saved views. It is copied to a temporary folder. That copy is removed when torno quits, with whatever was made in it, and the window says so under the workspace's name for as long as it is open; one that a crash left is removed at the next start. To keep what you made in it, press "Keep this as a folder…" under the workspace's name: choose a name in the dialog and torno copies the whole example, with the marks, the views and the chat, to a new folder of that name, whole or not at all, and opens it in place of the copy. It never writes into a folder that exists, so a name that is taken is refused, and the new folder is added to the folders opened lately. A folder given to scripts/torno is added to the folders opened lately, as one opened from the window is.

Once a folder is open, the folder button at the top left opens a menu with the same ways to another: the folders opened lately (one that is not found is struck and can be removed), "Open folder…" and "Open the example", and "Check what torno needs".

Left alone, Electron chooses whether to draw on Wayland or X11. If the window does not come up, or comes up wrong, TORNO_OZONE=wayland or TORNO_OZONE=x11 has the launcher pass --ozone-platform for it. Any other value is refused, headless among them: Electron crashes with it here.

On macOS Electron has one platform: TORNO_OZONE is checked but then ignored, with a line on stderr, and --print-desktop, --install-desktop and --uninstall-desktop write nothing and exit 2 (make torno.app with pnpm dist:mac instead). The launcher hashes the sources with sha256sum or, failing that, shasum -a 256, which a Mac has.

--install-desktop (Linux) writes one file and only when you run it: torno.desktop under $XDG_DATA_HOME/applications (~/.local/share when that is not set), after printing the path and the entry. It refuses to run as root. The entry starts this script, and names its icon, by their whole paths: run it again after moving the repository. It names no platform: a launcher that should use one needs TORNO_OZONE in the environment it starts programs with. Started from the entry (--from-desktop), what goes wrong is also shown as a desktop notice (notify-send), or, where there is none, written to ~/.local/state/torno/launcher.log: nobody reads what a launcher prints.

Only one torno runs at a time. Starting it again brings the window that is already open to the front and says so on stderr; given a folder, that window opens it, asking first if a session there is in the middle of its work.

On Linux torno has no menu. The window is closed through the window manager, which ends torno and the sessions in it. When that would cut something short, torno asks first, in a dialog over the window ("Close torno?", with "Keep it open" in focus) that names each project and what would be cut short, as a link to the project: a session working or waiting for an answer, a prompt written and not sent, a region painted on a scan and not kept, a scan being imported or an export being written. With none of that it closes at once. Ctrl+Q, Ctrl+W, F11 and the zoom shortcuts do not exist. For work on torno itself, pnpm dev runs the app from the Vite dev server, where F5 and Ctrl+R reload the window.

On macOS the menu bar is global, so torno has one there: the app menu (About, Hide, Quit), Edit and Window. Copy, paste, cut, select all and undo (⌘C, ⌘V, ⌘X, ⌘A, ⌘Z) come from the Edit menu, in fields and in the terminal. ⌘Q asks first, as closing the window does ("Close torno?" while something would be cut short); the Dock's Quit and a logout do not ask. There is no Close item, no reload and no zoom. Run from the repository, the menu bar names the app "Electron" (the bundle of node_modules/electron); a packaged app is "torno". On a Mac, and on Linux, torno asks your login shell for its PATH at start, so uv and claude are found when torno is started from the Finder, the Dock or a launcher.

On macOS, pnpm dist:mac makes torno.app in app/dist/: mac-arm64/torno.app on Apple silicon (and a zip beside it), mac/torno.app on an Intel Mac (pnpm dist:mac -- --x64). Copy it to /Applications and open it from the Finder or the Dock. It carries the kernel's source, torno's skills for Claude Code, the icon and the example, and still needs uv and claude installed; it finds them through your login shell's PATH (above). Its Python environment for a workspace with no pyproject.toml is made at the first build of one, in ~/Library/Application Support/torno/kernel-env, and never inside the app. The app is signed ad hoc and not notarized: one built on your own Mac opens as it is; one that was downloaded (the zip a CI run uploads, say) is quarantined, so right-click it and choose Open, or run xattr -dr com.apple.quarantine torno.app. Updating means building again.

On Linux, pnpm dist:linux makes torno-<version>.AppImage and torno-app-<version>.tar.gz in app/dist/ (make the AppImage executable and run it, or unpack the tar.gz and run torno-app). They carry the same files as the Mac app, are not signed, and need uv and claude as well (found through your login shell's PATH); the kernel's Python environment is made in ~/.config/torno/kernel-env, never inside the package. There is no Windows build. pnpm pack:check builds the unpacked folder alone, app/dist/linux-unpacked, to check the packaging with TORNO_PACKAGED=… pnpm -C app exec playwright test e2e/packaged.spec.ts.

#Five minutes with torno

This walk-through uses the example that torno carries, in a temporary copy, so nothing of yours is touched. It needs nothing from the repository, so it works the same in a packaged app. The example is an enclosure for a sensor board: a body, a lid and a clip. The lid rattles, which is what you will fix.

  1. Start torno with no folder (scripts/torno, or open the app) and press "Open the example" on the first screen. The window opens on a page of cards with one project, enclosure. The copy is removed when torno quits, and the window says so under the workspace's name.

  2. Click enclosure. It is built (a second or two; longer the very first time, while uv makes torno's own Python environment, which the status line says: "Making torno's Python environment, first time only") and drawn: the body, the lid on it and the clip round the front. Drag to turn it, drag with the right button to pan, scroll to zoom, F to fit. 1 to 4 are the iso, front, right and top views. The window says how the pointer moves the view once, in a line at the foot of the view that goes at your first move of the camera, and keeps it, with the keys, in the sheet of keys (?), where the pointer is the first group. Point at a tool in the palette for what it does; with a tool in hand that same line says what it waits for next.

  3. Read what is there. The Marks list has three marks the example came with: M1, a pin on the lid for a label; M2, an arrow at the cable notch, which says the notch is too narrow for the plug; and M3, a dimension: the gap between the lid's lip and the wall, which should be 0.2 mm. Its row says Fails, because the gap is 0.35 mm. Click a row and the view goes to it. The line at the bottom counts it (1 dimension fails). Open brief.md in the project's folder to read what the person who made it asked for.

  4. Press E. The parts are drawn apart, for looking only. Press E again to put them together.

  5. Under the bar of views, click "Lid fit". The camera moves to the lip, the clip is hidden and a section is cut through the lid and the body, so that you see the gap. "Cable side" looks at the notch from behind. "+ Save view" keeps how you are looking under a name of your own.

  6. Press P and click the top of the body, near a corner. A pin is left there, numbered M4, and its note opens: type "rounder here" and press Enter. The pin is red: it is yours. Press Esc to put the Pin tool down.

  7. Press M, hold the pointer on the outer wall of the body and press T: the wall under the pointer is measured by the kernel, exactly, 2.4 mm, and stays on the part as a dimension. Type 2.4 under "Should be" and press Enter: the row says Passes. Enter gave the keys back to the view: press Esc to put the tool down.

  8. Press X. A plane cuts the parts in half at once, open towards you; a bar appears under the view. Drag the arrow on the plane, or use the arrow keys, to move it. "Off" in the bar takes the cut away, and Esc puts the tool down.

  9. In the pane on the right, type "make the notch 14 mm wide and the gap 0.2 mm" and press Enter. A session starts with your prompt, and your marks go with it as text: the face, the point, your note, the measured value and whether it passes. From then on they show as sent. The agent can ask torno for a picture (look), a measurement (measure) and a cut (section), and it edits enclosure.py; every save is built and drawn. When it is done, M3 says Passes. Then send /mcp__torno__review, which has the agent go through every open mark against the build.

  10. "Export…" at the bottom right (or Ctrl+Shift+E) writes one HTML file that someone without torno can open: it needs nothing else.

  11. To keep what you made, press "Keep this as a folder…" under the workspace's name and choose a name. Otherwise it is removed when torno quits. Press ? for every key, and W for the page of projects. Then open your own folder with the folder button at the top left.

Torno wrote only in the temporary copy, and in the one folder you chose if you kept it.

#Projects and builds

A folder is a project when it holds a .py file; dotfiles and __init__.py do not count, and folders whose names start with . or _, and common, tests and out, are never projects. The model is the one .py file that is not a test (test_*.py, *_test.py, conftest.py). Of several, it is the one named after the folder, with - and _ taken as the same: helmet-stand/helmet_stand.py. Once a project is open, torno keeps building the model it chose for as long as that file exists. A folder where no model can be chosen, or a model with none of the forms below, is listed with the reason it cannot be built.

With a project open in a folder of several, the list of projects is folded to one row, the switcher: the open project's name, and "All projects W" beside the title. A click on the name, or Enter, opens a menu of every project (a dot for a session that works or waits, the reason under one that cannot be built) with "New project" at its foot; the arrows, Home and End move in it, Enter opens one, Esc puts it away. When a session of another project waits for you, the switcher has its dot and says so ("bracket needs you"). The "Projects" title unfolds the whole list in place. With no project open, or the page of projects up, the list is whole.

The kernel imports the model file as a module (if __name__ == "__main__" does not run) and takes the first of:

  1. Params() with assembly(p) (layout assembly) and printables(p) or printable(p) (layout print), each returning a dict of name to part.
  2. Params() with functions named make_<part>(p), defined in the model file and callable with the parameters alone: each one that returns a solid is the part <part> (layout main).
  3. build123d shapes in module-level variables (layout main).

In the print layout a model usually poses every part for the bed at the origin. When parts of that layout overlap seen from above, torno sets them side by side along X, 10 mm apart, in the order the model gave them; each keeps its own Y and Z. The picture, the bounding boxes and scene all show the parts where torno put them, and scene says how far each was moved (arranged). The model and what it exports are not changed.

Checks the model declares. A model with Params() may also define checks(p), a list of declarations that torno measures after every build, beside the dimensions you place yourself:

def checks(p):
    return [
        {"name": "lid fits the tub", "kind": "clearance", "a": "lid", "b": "tub", "min": p.clearance - 0.05, "max": p.clearance + 0.05},
        {"name": "tub is one body", "kind": "solids", "a": "tub", "equals": 1},
        {"name": "walls", "kind": "thin", "a": "tub", "min": 1.2, "layout": "print"},
        {"name": "fits the bed", "kind": "bed", "size": [256, 256, 256]},
    ]

A declaration is data, not a function: torno measures it with the kernel's own queries, so each result says whether it is exact. name and kind are required; a and b are part names or group names (a group stands for its parts; never a face number, which changes with the model), layout is optional (the first layout, assembly before print, that has the parts; print for the kinds about printing when it has them). The kinds are clearance (the least distance, mm), overlap (the volume shared, nothing unless told), solids (how many a part has), volume (mm³), thin (a wall under min mm: found by sampling, so no result is a proof that there is none), overhang (faces facing down steeper than angle, 45 unless told), mesh (the part's mesh is closed) and bed (the layout fits size). What is wanted is min and/or max, equals, or expected with tolerance or percent. A declaration that is not one fails the build and says what is wrong, as a bad groups(p) does. A check that fails never fails the build, and never holds the agent back when it would stop; it is told with the build (What the agent is told) and listed in the window under Checks. torno never writes checks(p).

Names for faces and edges. A face number (lid/F12) changes whenever the topology does. A model with Params() may also define tags(p, parts), which names faces and edges, and a name holds across builds:

def tags(p, parts):
    lid = parts["lid"]
    return {"lid": {"top": lid.faces().sort_by(Axis.Z)[-1], "screw_holes": lid.faces().filter_by(GeomType.CYLINDER)}}

It is called once for each layout, with the dict of parts that layout's function returned, and returns part name to tag name to a face, an edge or a list of them, taken from those very shapes. A tag name is a letter or _ and then letters, digits, _, . or -. The ref of a tag is lid/@top: it names one face or edge (several, lid/@screw_holes, are taken whole by highlight and ask; where one face or edge is needed, a tag of several is refused, naming them). describe, measure, highlight and the dimensions take it, scene lists each part's tags with the faces and edges they are now, and the pointer says lid/F12 · @top. A mark you place on a face or an edge that has a tag keeps the tag, and each build looks for it first: the mark follows the face by its name when the numbers shift, or the face moves. If the build has lost the tag (or it names several faces now) the mark is found by its point, as any mark is, and keeps the tag for a build that brings it back. A tag that names nothing the part has (a shape of another part, a face taken before an edit, an empty list) fails the build and says which. torno never writes tags(p, parts).

Bought parts. A printed part is usually made around something bought: a screw, a bearing, a board. A model says so with an optional references(p) beside assembly (name to shape, in the assembly's frame):

def references(p: Params):
    return {"608": Pos(0, 0, 4) * SingleRowDeepGrooveBallBearing(size="M8-22-7", bearing_type="SKT")}

A reference is drawn in a neutral grey, listed in Layers with the tag "bought", and picked and measured like a part. It is an obstacle: check clearance, overlap and interference take it against the parts (never one reference against another), and checks(p) and groups(p) may name it. It is not made: it is in no print layout, not counted by check bed, has no mass, takes no thin, overhang or mesh check, and is in no STL or 3MF; a STEP holds it only when ticked in the export sheet. Its name is its own (not a part's, no /, not of the form M<n>), and a model with only a print layout cannot have one. Standard parts come from bd_warehouse, which torno's own kernel environment has (simple=True for a screw); a vendor's STEP is read by the model itself (import_step) and returned among the references, as torno reads no file of its own accord. A reference that is a library's compound of many solids is taken as the solids it holds, in one shape.

Builds. Saving a file in the project's folder (dotfiles, out/, cache/, .torno/ and __pycache__/ are ignored, as are build/, dist/, node_modules/, .venv/ and .git/) or in the workspace's common/ starts a build 300 ms later. So does saving a file that the model imported from another project (uses in scene and state.md: box/box.py for a lid that imports the box's maker), as far as the last good build found it; torno reads this from what the model imported and never asks you to declare it. A project that imports one that imports it back shows the other in uses on both cards; Python decides whether that works. Saving a .py file during a build cancels that build and starts another. Any other file changing during a build may be the model's own doing, so the build is left to finish and one more follows it. A build that runs longer than 600 s is stopped; TORNO_BUILD_TIMEOUT sets another limit. The line at the bottom says building with the seconds elapsed, built with the time it took and the revision (r3), or failed. A failed build keeps the last good parts on screen, and its status opens the error with the traceback.

History of builds. Every build that worked is kept under .torno/history/ (see Files torno writes): a copy of the Python files it was made from, its parts' numbers, and a small picture. A kept build has an id of its own, h41, which goes on counting across runs of torno; the revision (r3) is of one run and is told beside it. The latest 100 are kept, and every one you star. A build that fails the model's own checks is kept too, marked: it is often the one to compare against. A rebuild of sources the newest kept build was made from is not kept again. History in the status line opens the builds in a strip, newest first: the picture, the id, how long ago, and what changed from the build before in a line (tub changed, lid same). Choose one for its facts, Show the source to read the Python it was made from, read only, Star to keep it, Show over the current build to see it drawn translucent, in grey, over the build on show (for looking only: nothing is measured from it, a picture for the agent does not show it, and a section, parts drawn apart or another layout put it away; the status line says it is there and has Stop), or Ask the agent to go back to this build, which puts a request in the message box of the session (or starts one) naming the copies, and sends nothing. torno never puts a file back: the agent edits the project's files, under your permission rules, and the copies are not to be edited. What a kept build had then (checks that failed, parts with a defect, how your dimensions stood when it was checked) is as of then, never how the project stands now. A build can be shown over the current one only if it was made in this run and its meshes are still held: they are kept in memory, never on disk, within a budget of 64 MB for the builds before the newest, the oldest let go first. The agent reads the same with history, and compares any two with diff.

A failed fillet or chamfer. build123d and OpenCascade say only that a fillet or a chamfer could not be made, naming neither the edges nor the radius. When a build fails inside one, the kernel tries it again in parts before it reports the failure, for 5 s at most (less when the build's own limit is nearer, and it says when it ran out): each edge alone at the radius asked; if each works alone, the pairs, those that share a face first; and the largest radius that works for all of them, and for each edge that fails alone, to 0.001 mm. The error says it under its message, such as fillet R6 of 8 edges: each works alone; 4 pairs of adjacent edges fail (edges 4-5, 4-6, 5-7, 6-7: 12 mm apart, two R6 need more); the 8 together work up to R5.999, then a line for each edge of the failures. The shape being filleted is not a part on screen, so its edges are numbered by their place in the call, from 0, and their ends are in its own frame (local); one that is exactly an edge of one part of the last good build (its ends, its length and the points along it) is also named by that ref, as of that build. Nothing is retried in the model. The agent is told the same with the result of its edit and in scene, and can try a fillet on the last good build with try_fillet before it edits the model again.

A failed boolean. A cut, a fuse, an intersect or a split that fails (a StdFail_NotDone, or Null TopoDS_Shape object) is looked into the same way, within the same 5 s, by looking at its operands: nothing is run again. The error says which of these it found, such as cut failed: tool is a face, not a solid: an operand that is empty, one that is not a solid, one that is not valid (OpenCascade's check, and what its argument analyzer finds, such as a face that crosses itself), a tool that ends flush with the face it cuts instead of overshooting it (faces of the two that lie on one plane facing the same way), faces that lie on each other, shapes further apart than they should be, and shapes that only touch. Then a line for each operand (its type, whether it is valid, its faces and its box) and for each pair of faces that lie on each other, with the area they share. The operands are not parts on screen: their faces are numbered by their place in the operand, from 0 (tool F5), and their numbers are in the operand's own frame (local). A distance is OpenCascade's and is told with ≈; a count of points of one lying inside the other is of points sampled on its vertices and the middles of its edges, so it can say that none was found, never that none is there.

The window. On the left, the list of projects, then the layers, the parameters, the print profile and the scans of the open project, then its marks. Projects, Layers, Parameters, Print profile and Scans each fold by their title; folded, the title says what matters (Layers 12 · 3 hidden, Scans 1 · unaligned). Marks never folds: it keeps room for five rows at least, and the sections above give way first. Drag the line above Marks up for more (Up and Down with it in focus), double click it for the shares again. Drag the right edge of the column to make it wider (Left and Right with it in focus), double click it for the default width; the column is never narrower than that, and leaves the view its room beside the session pane. What is folded, where the line is and how wide the column is are kept by the window, not in the folder. The layers (parts) of the open project each have a visibility switch, and for a model of ten parts or twenty they are also the way to the part: the pointer on a row lights the part in the view, a click on its name selects it (as a click with Select would, so "Insert" and "Note" work on it) and a double click brings it into view, the target on the row shows only that part, and when some parts are switched off the title says 3 hidden · Show all. After the name, a muted number counts the open marks on the part (in the colour of a failing check when one of them is one); a click on it lists only those marks in Marks, under a line Only marks on lid with a cross that brings the rest back. That is a view of the list: the agent is told every mark as before. Parameters lists the fields of the model's Params (folded to its title at first; a model with none has no section): each field's name, its default in the monospace face (four digits at most, the whole value on hover) and its comment as the model has it, and, for a default the model works out (an expression, not a number written in), that it is worked out in the model. The model's comments should begin with where a size comes from (brief:, M3:, S1 ≈:, ref <file>:, object:, datasheet <name>:, person:, derived: or assumed:), and under the title a line counts the sizes that say assumed and those that say nothing, their names on hover; it is what the model says, not something torno has checked. torno never writes the model. A field whose default is a number or a flag written in the file (wall: float = 2, lid: bool = True), or a Literal["round", "square"], has a control under its name: type a number and press Enter, or let go of a slider (the model gives one with field(default=2.0, metadata={"min": 1, "max": 5, "step": 0.2})), tick the box, or choose from the list. The project is built again with that value, and the row says differs from the file with a "Reset" beside it; "Reset all" and "Ask the agent to bake it in" (which puts a request in the session's prompt, never sent) are under the title while any value is tried. What you try is kept in .torno/params.json and the file always wins: if the model's default for that field changes, your value is let go of and the row says so, and a value the model no longer offers is let go of the same way. The agent is told, with every prompt, which values the build is made with. The model only sees a tried value where it is given p: a helper that makes Params() itself does not. With a session running, a click on a row puts Params.wall in its prompt at the cursor, never sent, so you can write "make Params.wall 2.4" around it; with no session the rows say to start one. Checks lists what the model declares in its checks(p) (see Projects and builds), under Parameters and folded at first to a title that says how many fail (Checks 3 · 1 fails; a model with none has no section): each check's own name, how it stands in a word (pass, fails, not measured, not run), what was measured and what was wanted, and, for a thin wall, that it was found by sampling. They are the model's, marked with the agent's colour; your dimensions are marks. A check the agent made easier to pass, or removed, since it was first declared is said under the rows (walls: min 1.2 → 0.6 mm (since r7)). A model that says which of its parts belong together (an optional groups(p) beside assembly, group name to part names) has them listed under a header each, which folds the group, hides or shows all of it, or, on "Show only", shows that group alone in the layout; ask the agent to group parts and it edits groups(p). In the viewport, drag to orbit (about the point of a part under the pointer when the drag starts), drag with the right or middle button to pan, scroll to zoom, F to fit the selection (pressed again, all the parts); a triad in the bottom left corner shows which way X (red), Y (green) and Z (blue) run in the project's own frame, as the agent's pictures do, and in the bottom right corner are a scale bar (orthographic) or the step of the grid, grid 5 mm (perspective); the bar at the top switches layout (assembly, print), view and projection. The folder button (top left) opens a menu of the folders opened lately (the open one marked "current"), "Open folder…" and the example; opening another ends the sessions of this one, so torno asks first, in a dialog that names the projects, if one of them is working or waiting for an answer. A folder that cannot be opened is said under the folder's name, and the workspace stays as it was.

"Rebuild", in the line at the bottom beside how the build stands, builds the open project again from its sources as they are (Ctrl+Shift+B from anywhere). It is always there, and it is the main button when the last build failed for a reason outside the model (the build took too long, stopped, or was cancelled) or was let go to make room: building the same sources again may work. After an error in the model it stays quiet, since the same sources give the same error. While a build runs it reads "Building…" and takes no press: that build is of the sources as they are. The parts on screen are older than that build, so a click cannot pick them from the first moment; they fade only when it has run for 0.6 s, so a quick save does not make them blink. A build that fails fades them at once.

When a build lands after another of the same project, the status line says which parts it changed: "tub changed, lid same", or "no part changed" when a save made no difference to what is drawn. A part counts as changed when its shape is another or it stands elsewhere or turned. Hover the words for the lines diff gives Claude Code, for example "tub: volume 6280 → 6410 mm³ (+130, +2.1 %), size 40 × 40 × 40 → 40 × 40 × 42 mm"; a number the kernel cannot give exactly has "≈". For about a second and a half the parts that changed are also shown as ghosts: their old edges in the muted colour, where they stood, and their new edges in the accent. They go by themselves, fading once, or at once on any key or click. The parts that did not change are not touched. There is no ghost while a section is on or the parts are drawn apart, nor in a picture for Claude Code.

"Build failed" opens the error: what happened in words ("Python error in the model", "The build took too long (timeout)", "The build process stopped (crash)"), the message, why a fillet or a chamfer failed, the traceback and what the model printed. It says who is told: Claude Code is told this with your next prompt, or was told after its own edit, so there is no need to paste it into the chat. "Ask Claude Code to fix it" puts a short request in the message box, opens the session pane if it was away, and never sends it: you read it and press Enter. With no session running it reads "Start a session to fix it" and starts one with the request waiting in its box. After a failure outside the model "Rebuild" is the main button of the sheet instead. "Copy" takes the whole error as text. The line above the box of the chat says "build failed" among what goes with the next prompt.

A part the build says is broken (not valid, inside out, not closed, or with a mesh that disagrees with its volume) has the word beside its name in the layers, with every defect it has in its tooltip, and the line at the bottom counts such parts ("1 part has a defect"). The agent is told the same after its edit (What the agent is told).

When the project is in a git work tree, the status line and the card say what has changed since the last commit: "3 files changed since a1b2c3d", with the files in the tooltip (the .py files and the others of the project's folder, and common/; not .torno/), or "No changes since a1b2c3d". It is read when the project is opened, when a build ends, and when the window is focused again. torno only reads: it runs git status with --no-optional-locks, which leaves the index as it is, gives it five seconds, and never commits, adds or changes anything. Without git, or outside a repository, nothing is said. Commits are the agent's: ask for one in the chat (the Marks panel offers a request, see Marks).

The pointer is told what it is on: a face (lid/F12), or an edge within reach of it (lid/E3), with a line from the kernel on what that is, such as plane, normal +Z, z = 20, 1180 mm²; a face or an edge that the model names (tags(p, parts), see Projects and builds) is told with its name, lid/F12 · @top. Hidden parts are never picked. While the parts on screen are older than the model (a build is running, or the last one failed) nothing is picked, and the pointer says why.

#Making a new project

"New project" starts one from what you have. It is at the foot of the list of projects (of the switcher's menu when the list is folded), in the header of the page of projects and in its empty state: a folder with no project in it leads with "Make your first project", "New project" and, where torno runs from its repository, "open the example to look around". The way for models you already have (a folder such as part/part.py, and the note on pyproject.toml and uv) is in a fold, "Already have build123d models?". The page asks for a name and "What is it?" (what it is for, what it must fit or hold, sizes you know, how it will be made), and then for what you have, each with a note for Claude:

At most 6 pictures go with the prompt, photos and doodles first, then the thumbnails of other projects; a picture beyond that, or one that fails the checks (PNG or JPEG, 2000 px at most, 3.75 MB, so that it is under 5 MB as the API takes it; a PNG over that is sent as a JPEG), is named in the prompt as not attached, with the file in reference/ to read. Notes are kept to a line each and the description to 20,000 characters.

The name is the folder's and its model's: letters, digits, - and _ (no dots), starting with a letter or digit, at most 64 characters, and not one that is there already (compared without regard to case, and a-b is a_b). Torno refuses a name that would stand in for a library (json, typing, numpy, build123d, any module of Python's standard library), since a model's folder leads the import path and its file would be imported in the library's place; also Python keywords, the workspace's own common, tests, out and docs, tools, reference, and a name whose model file would be taken for a test (test_*, *_test). The page says why while you type.

"Create" writes one new folder in the workspace and nothing else: <name>/<module>.py (a placeholder, a 20 mm cube, to be replaced), brief.md (your words and a line for each reference with its note) and reference/. The .torno/ folder is made as for any project. It is made in a hidden temporary folder beside it and renamed into place as the last step, so it is whole or not there, and it never goes into or over a folder that exists (an empty one of that name is used). A temporary folder a crash left is swept away at the next opening of the workspace once it is ten minutes old, unless it holds files in reference/ (your doodles and STEP files may be nowhere else): such a leftover, a hidden .<name>.torno-new-<hex> folder in the workspace, is yours to look at and delete. Then torno opens the project, imports the scans, starts Claude Code in a chat and sends one prompt with everything. The prompt asks Claude to list the sizes that decide fit or function and say where each comes from (your words, a note, a scan, another project, or nowhere), and to ask you for those that come from nowhere in one round (a card in the chat, at most four questions, each with a typical value among its options; "build with assumptions" is an answer) before it builds. What it builds without an answer is marked "assumed" in the model, and its reply lists those. If Claude Code cannot start (the command is not found, or the trust question stands, or the CLI starts and leaves at once because it is signed out or too old), the project is made and open anyway: the pane says why and the text of the prompt waits in the message box for you to send. Where torno already knows Claude Code is not found or not signed in, the page says so above "Create", before the folder is made. The pictures are not sent then: the text says each is "not attached" and names the file in reference/ (and the .torno/thumb.png of another project) for Claude to read. If the project was made but the workspace does not list it, the scans were not imported, and the page says so and where the folder is. While "Creating…" shows, Esc, Cancel and Discard do nothing.

While the page is up the window is about the project being made: no project of the list is selected (folded, the list reads "New project"), the Marks section gives way, and the status line says "New project" with the build123d version alone. The session pane says the first prompt Claude Code will get once you create the project, as it will be sent and read-only, filled in as you go: your own words in ink and what torno adds in grey, and under it a thumbnail of each photo and doodle, with "attached" or "named only" (only the first 6 pictures go with the prompt). Change the words on the left; the prompt follows. A session of the project that was open that is still running stays as it is, a question waiting in it included, under a note saying whose it is: it goes on, and the new project gets its own. Cancel or Esc brings everything back as it was.

Beside "Create", "Claude Code on" picks the model and the effort the new project's session starts on. Both start at "Your default", which passes nothing: Claude Code goes by your own settings. A choice holds for that session alone (torno starts the CLI with --model and --effort) and is never written to your settings; the page keeps it for the next project you make in this window. The models are the ones Claude Code offered in the last chat (kept in torno's own data), or its aliases, Opus, Sonnet and Haiku, before any chat has run; the efforts (Low, Medium, High, Extra high, Max) are the ones the chosen model takes, and a model that takes none leaves the effort off. Both can be changed in the chat afterwards.

The doodle board (the same board as the chat's "Draw on view") has a thin pen, a thick pen and an eraser, "Undo" and Ctrl+Z, "Clear", and "Done" and "Cancel". The doodle board fills the window and has a light grid under the ink; hold Shift while drawing for a straight line from where the stroke began. (A drawing on the view has no grid: its ground is the picture.) Esc puts the board away; if strokes were drawn since it was opened it asks "Discard this doodle?" first. A doodle can be opened again from its card and changed.

What you have entered is a draft, kept while the window lives: you can leave the page, open another project and come back. Esc on the page leaves it, asking first when something is in it; so does leaving the workspace, and so does closing the window ("A new project is set up here and not made yet"). Nothing of the draft is written until "Create".

#The agent

Each project has its own session: Claude Code running in the project's folder. It opens in a chat, a pane of torno's own, or in a terminal when you switch to it. It keeps running while another project is shown. A session that works or that waits for your answer (a permission, a question, a plan) says so wherever you are, not only in its own pane. In the list of projects its row has a dot after the name: a quiet one while it works, and, in the warning colour with the words "needs you", while it waits. Folded, the list's one row has the dot of any session of the folder that works or waits, and names the project of one that waits ("bracket needs you"); its menu has a dot on each row as the list does. Nothing is shown for a session that is idle or ended. When the session pane is hidden, its button has the same dot for the open project's session. And when any session of the folder waits for you, the status line has a button "bracket needs you" (with "and N more" if several do, the first in the list named): it opens that project, shows the pane and takes you to the card. If the window is not the one in use when a session starts to wait, its frame flashes (the dock icon bounces on a Mac), once for each question; torno posts no system notification. "New session" starts one, in place of the session running there, if any; torno asks first if that one is in the middle of its work. "End" ends the session and starts none, asking first in the same way. "Resume" continues the last conversation Claude Code had there. In the chat view the empty pane has the box already: write, press Enter, and a session starts with your prompt. Where a conversation is on file its last turns are shown above the box, under "Last conversation, 2 days ago", with "Continue" (the default) and "New conversation" beside "Send"; the buttons "New session" and "Resume" start one with no prompt and leave what you wrote in its box, unsent. If the folder has Claude Code settings of its own, the question is asked first and your prompt waits in the box. The empty pane has no "Attach view" or "Draw on view": those are of a session. Ctrl+` moves between the session and the viewport; F6 and Shift+F6 go round the parts of the window, and Ctrl+Shift+D shows or hides the session pane. In a terminal Shift+Enter is a new line, Ctrl+Shift+C and Ctrl+Shift+V copy and paste.

#What goes well, and what does not

What torno and the agent do well, and where you have to help. This comes from how the loop has gone on real projects, not from a promise.

Goes well:

Needs more from you:

Does not go well:

In every case the answer is the same: give a measured number, a scan or a dimension mark, in place of a description. How to work with the agent is in the README's guide.

#The chat

The chat is your own claude, under your own login, started in its headless mode (JSON on pipes instead of a screen). Torno draws what it says and does; nothing of Claude Code's settings is read or changed (torno lists the paths and sizes of the instruction files Claude Code reads, never their text). Enter sends, Shift+Enter is a new line, and a prompt sent while the agent is at work waits its turn ("Queued"; "Stop" interrupts the turn, and what was queued still runs after it; Ctrl+C does the same in the box when nothing is selected there).

"Chat · Terminal" under the header switches the same conversation between the two: the session is ended (torno asks first if it is busy) and started again, resuming it. With no session it sets what the next one will be; the choice is kept in .torno/agent.json. What the chat showed is kept as text in .torno/chat/, and a resumed conversation shows it under "Earlier in this conversation"; pictures are never written. A revision counts from 1 in every run of torno, so r12 of an earlier part of the conversation may name another build now: the chip's tooltip says so there, and in the current run it says what the build was made from ("sources 9f3e1a2b4c5d, on a1b2c3d with changes": a hash of the .py files it read, the commit then checked out, and whether files differed from it), also under "Show text" of the build line. .torno/builds.jsonl keeps the same for each build. The photos and doodles you gave when you made the project are shown again from reference/, by the name the log keeps (one you have since deleted says reference/doodle-1.png is no longer there); a picture a tool gave says "Picture of an earlier build, not kept", and any other "Picture not kept".

#What Claude Code is given

Torno runs Claude Code, and changes nothing of its settings: everything it gives it is on the command line, in the environment, or in a file of torno's own that holds no secret. It reads none of its settings either. Of the instruction files Claude Code reads on its own, it lists the paths and sizes (next paragraphs) and never their text.

.torno/agent.json keeps the view, chat or terminal, with "agent": "claude". A file that names another agent is not followed: Claude Code is used, in the chat, and the pane says so, with "Use Claude Code" to write the file anew. One that names Copilot CLI, which torno also ran until 16 October 2026, says that torno no longer supports it. The .torno/prompt.md and .torno/agent/ such sessions left are neither read nor removed.

Claude Code is also given torno's skills, a plugin that comes with torno (app/resources/agent-plugin) and is loaded for that session only, with --plugin-dir: nothing is installed in your Claude Code. A skill costs a line until the agent decides to read it. torno:torno-model is the model file contract in full (which file, Params, assembly, printables, make_<part>) with a template; torno:build123d the idioms of build123d 0.13, the failures that leave a valid but wrong part (a boolean that lost volume, an edge taken from an older shape), the usual OCCT errors (fillets, booleans, StdFail_NotDone; of a failed fillet, to read why and try one with try_fillet before editing again), an API reference of build123d 0.13.0 that the agent searches offline in any workspace, and the sizes of screw, nut and insert holes and how to use bd_warehouse (which torno's kernel has) and references(p) for bought parts; torno:scans and torno:dimensions what the instructions would otherwise say of scans and of dimensions; torno:printing the rules of a part for an FDM printer (fits per side, walls as multiples of the line width, overhangs, holes, orientation and the bed), with typical numbers that are only assumptions until you give your printer and material (the /printable prompt reads it first). So Claude Code's instructions are shorter: they keep what torno is, refs, marks, the loop and the model file in short, and name the skills.

Claude Code is also given a subagent, torno-reviewer, on its command line (--agents; nothing is written to .claude/agents/). It has only the tools that read (scene, look, marks, measure, check, section, describe, diff, the scan tools, and Read). It runs on Sonnet, whatever model the session uses (TORNO_REVIEWER_MODEL=inherit gives it the session's, or a model name another), and stops after 20 turns. It reads brief.md, checks the current build against what you asked, every open mark and every dimension, numbers first and at most two pictures, and ends with one line each that begins HOLDS, FAILS or UNCHECKED, with the number and the ref. It changes nothing. Claude Code is told to call it after a change of several steps, before saying it is done.

The status follows the hooks Claude Code posts: UserPromptSubmit, PreToolUse and PostToolUse mean working, Stop idle (and so does StopFailure, which ends a turn an error of the API cut short, and is never held back), PermissionRequest and a Notification that asks something needs input. A Notification that only says Claude Code has been waiting at its prompt for a while (or has signed in) leaves the status as it was. In a terminal, the first time Claude Code runs in a folder it asks whether you trust the folder; that question is its own, and it is answered there. Run headless, as the chat runs it, Claude Code skips that question, so torno asks instead: before the first chat in a project whose folder, or a folder above it up to your home, has settings Claude Code would load (.mcp.json, .claude/settings.json, .claude/settings.local.json), the pane names those files and asks "Start the chat anyway?". "Start the chat" remembers the folder (its real path, in torno's own data beside the recent folders, never in the workspace) and does not ask again, whatever those files later say; "Use the terminal" starts Claude Code in the terminal view, which asks its own question. Your own ~/.claude is never one of the settings files looked at for it.

/model typed in a terminal session of torno's changes the model of that session and also your default for every Claude Code session from then on: Claude Code writes it to ~/.claude/settings.json, torno's or not. The model menu of the chat does not; nor does starting torno with TORNO_AGENT_CMD="claude --model sonnet", which saves nothing.

#What the agent is told

With every prompt, a few lines:

[torno] bracket · build ok · r3 (1.2 s) · layout assembly · parts: base, post.
Scans: S1 Bay.glb, aligned in print by 4 pairs (RMS 2.1 mm, worst 3.4 mm), shown at 60 %.
Selected: base/F5 (plane, normal +Z, z = 2.5, 1200 mm²), base/E5 (line, 30 mm)
Marks from the user (drawn in look pictures with these numbers):
M3 pin on the edge lid/E40 between lid/F12 and lid/F40 (line, 40 mm) at (12, 40.5, 20): "this edge is ugly"
M4 thickness 8 mm wall, along the normal, plate/F2 at (30, 0, -10) (expected 10 ± 0.05: FAIL, 2 under)
Dimensions (measured on r3):
M1 distance 6 mm between planes, plate/F2 to plate/F4 (expected 8 ± 0.05: FAIL, 2 under)
1 pinned dimension passes
Open marks: M2 (moved), M6 (unchecked)

When Claude Code has edited the model, the answer to that edit says how the build ended, the same first lines and the dimensions. Between them, of a build that worked, torno says what the build did to the parts and what is wrong with them, which it checks at every build from the numbers the kernel worked out of the shape:

[torno] tub · build ok · r8 (0.9 s) · layout assembly · parts: tub, lid.
Changed since r7: tub: volume 28400 → 26100 mm³ (-2300, -8.1 %), faces 14 → 22, solids 1 → 2; 1 part unchanged.
Check: tub is 2 solids (was 1); the face or edge numbers of tub may name others now (describe refs again).
Dimensions (measured on r8):
M4 thickness 2 mm wall, along the normal, tub/F2 at (30, 0, -10) (expected 3 ± 0.05: FAIL, 1 under)

When Claude Code would stop, torno holds it back for what the turn itself did: once for each reason, and twice in a turn at most. Every reason is for a build the turn caused: one that began after the turn first changed a source, by an edit of a .py of the project or a Bash command after which one is newer. The first build of a project you have just opened, a save of yours in your own editor before the agent's first edit, and any build in a turn that changed no source are told to the agent as ever but never hold it back. It first waits, up to twenty seconds, for such a build that is still running and for the dimensions to be measured. Then, by the first that applies:

  1. The build is still running. It is asked to read the result with scene and to look.
  2. The build after the one on show failed (The build after r7 failed: ...), and it is asked to fix it or say it is still broken.
  3. A part has a defect the build the turn began with had not (above).
  4. A dimension that passed on the build the turn began with fails on the latest: M4 passed on r7 and fails on r9 (6 mm, expected 8 ± 0.05). Fix it, or say why it must change. A dimension that already failed when the turn began never holds it back; the agent may rightly stop to ask about it.
  5. A build the turn caused ended after its last look: r9 was built during this turn, and you have not looked at it since. This is however the file was written, by the agent's editing tool or through Bash. A look by the reviewer counts. When nothing in the parts changed since the turn began (a comment, a rename: every part is the same shape, by the kernel's hash of it, in the same place), there is nothing to look at and it does not hold. A volume, size and count that stayed as they were do not say so: a hole moved is another shape.

A reason already held is not waited for again: a Stop after one that was held for a running build does not wait again.

A hold for a defect or a dimension that finds the build unlooked says so too, and counts for both. A hold is said to the agent and changes no status.

The answer waits for the dimensions to be measured on the new build, ten seconds at most, and ends with what the model printed: Model printed (last 2 lines): and the end of it, fifteen lines and a thousand characters at most, with a note when earlier output was cut, and nothing when the model printed nothing. It is the output of the build that ended last, failed or good, and build123d's own warnings are in it. So a print() in the model is how the agent sees a value, since it may not run the model itself; scene gives the same as output. state.md does not carry what the model printed.

After a build, the agent is also told which sizes the model's comments say are assumed or have no source (Assumed: wall 2, lip 3 (2 of 14 sizes); no source: 4), only when that set is other than after the build before.

.torno/state.md says the same as text, for an agent without torno's tools: the build, the layouts and parts, where each size of Params comes from, the saved views, the scans and every open mark. Its first line says when it was written: nothing keeps it current once torno has quit.

#Tools

Torno's MCP server (on 127.0.0.1, a port of its own choosing, answering only requests that carry the token of the project's session) gives the agent of a project these tools for that project. They answer from the last build that worked, say which revision that is, and say so when a newer build is running or has failed. A tool given a key it does not take is refused, naming the keys it takes.

The tools that only read (scene, look, marks, describe, measure, section, check, diff, history, try_fillet, try_params, features, scans, scan_clash, scan_deviation) say so, and Claude Code may call several at once. The server also tells Claude Code at the start, in a few lines, what each tool is for. Claude Code's tool search keeps an MCP server's tools out of the model's sight until it searches for them; five are loaded from the start because nearly every turn uses them: scene, look, marks, describe and measure. The others are found by that search.

Tool What it gives
mcp__torno__scene The build status and error, what the model printed in the last build (output), the layouts, every part of one layout (the one you have on screen, or layout; the parts of the others are counted) with its size, bounding box, volume and whether it is visible, the model's groups of parts, your camera, section and explode, the saved views, the scans, how many dimensions pass, fail and are not measured, and the checks the model declares with how each stands. It notes when the parts of the print layout were spread along X. Over 200 parts, or when the list would not fit, a part is its name, size and group only (partsTruncated); describe gives the rest.
mcp__torno__look A picture from the renderer you are looking at, with the marks drawn. With no arguments it is your view, cut by your section or drawn apart as you have it. It takes view (iso, front, back, left, right, top, bottom, the name of a saved view, or a camera), layout, parts, size (long edge, 1280 by default, 2000 at most), marks (false leaves them out), section ('current', null, or a plane), explode (0 to 1, or {amount, axis, scale}; null together), scans, scanColour and frame. With focus (a ref such as lid/F12 or lid/E3, a part, a group, a mark such as M4, or a box {min, max} in mm) and margin (a share of the box, 0.3 by default) it frames a detail up close: from the side view names (front, iso, …; without one, the way your camera looks), at the picture's whole resolution, with the parts drawn together. The caption says what was drawn, and, with a focus, what was framed and when the middle of it is hidden behind other parts. Also saved under .torno/views/.
mcp__torno__marks Your marks and the agent's as data, each with a line of text. It lists the marks that are not resolved, 50 at a time; status (resolved too), ids (["M4"], any status), limit (200 at most) and next say which. A stroke is given as a summary of its points (count, first, last, box) unless ids names it. What was left out is said (omitted, next).
mcp__torno__describe The geometry of a part, a face or an edge (lid, lid/F12, lid/E3, or one the model names, lid/@top), or of a group of parts by its name: the box of its parts and their volume. Of a face or an edge it also gives a selector: a build123d expression on part, such as part.faces().filter_by(Axis.Z).sort_by(Axis.Z)[-1], that the kernel evaluated on this build and that gave exactly that shape, as robust by geometry (a direction, an extreme, a radius, a place) or by order (an index into a sort, which the next edit may break), and selectorFrame, which says in whose frame it is.
mcp__torno__highlight Tints faces or edges in your viewport with a note, to ask "this one?", until you click or a minute passes. A group's name tints all of its parts.
mcp__torno__ask Asks you to choose and waits: a card above the box of the chat (under the view for a terminal session) with a button for each answer, and the refs of an answer light up in your view while the pointer is on its button. If it allows, you may answer with the one face or edge you select in the view instead, or with "None of these". The agent gets back what you chose, as refs. The session shows Needs input meanwhile; the question ends by itself after 10 minutes with "not answered", and when the turn ends.
mcp__torno__show Changes your camera, layout, visible parts (a group's name stands for its parts), section, explode and scans to show you something, or puts a saved view on screen by its name; with focus and margin, as look takes them, it puts your camera on a ref, a mark or a box. For now only: what .torno/scene.json keeps stays yours. Refused while you hold a button in the viewport.
mcp__torno__save_view Proposes a saved view: it appears in your strip marked "proposed" until you keep or discard it. Nothing on screen changes.
mcp__torno__add_mark Leaves a mark of the agent's, in green: a pin, a pick, an arrow, a shape, or with kind: 'dimension' a dimension, measured at once and on every build like yours. A point that is not on the part is refused, saying how far off it is.
mcp__torno__resolve_mark Resolves a mark with a line on what was done, which you then confirm or reopen. A dimension of yours with an expected value is refused: it is your check, and only you resolve it.
mcp__torno__measure An exact number from the geometry. See Measuring and dimensions.
mcp__torno__section The outlines a plane cuts of the parts, with their areas, and a 2D drawing of the cut. See Section.
mcp__torno__check Whether two parts overlap, how far apart they are, whether a part's mesh is closed, and whether it prints: overhangs, thin walls, the fit on a bed.
mcp__torno__diff What changed between the last two successful builds (or the two given as from and to: a revision of this run, or a build kept in the history by its id, h41): for each layout the parts added and removed, and for each part its volume (and its change in percent), box size and min corner before and after, with whether each is exact, and its face, edge and solid counts, a hint that refs may name other faces now. The answer to an edit gives the same of each part that changed. builds says what each was made from (a hash of the sources, the commit, whether files differed from it, and of a kept build its id and the checks, defects and dimensions it had then, which are as of then), and a note says when the two were made from the same sources.
mcp__torno__history The builds kept of the project (see Files torno writes, history/), newest first: each with its id (h41), when, its revision if it was made in this run, what changed from the build before it in a line, the checks of the model that failed and the parts with a defect (a build that fails its checks is kept all the same), how your dimensions stood when it was checked, and whether you starred it. With id, one build, with the paths of the copies of its Python files, to read when going back to it. torno never writes a file back.
mcp__torno__expect Called before an edit: what it should change, as checks (a part's volume, faces, edges or size as a value or a change with a tolerance; a measure with the number it expected; unchanged parts). The next successful build is held against it, and the answer to the edit says in a line how many checks were met and which were not, with the numbers (≈ where the kernel's is not exact). Kept in memory for that one build, told once, no mark, and it never holds a turn back. A ref to a face of a part whose face count changed is told as moved and not measured. scene gives the last verdict.
mcp__torno__try_fillet Tries a fillet (or a chamfer, kind: 'chamfer') of radius mm of some edges of a part (part, edges as refs such as lid/E3) on the last good build and throws it away: whether it works, and when it does not, the edges that fail alone or in pairs and the largest radius that works, as a failed build's error says it. It changes nothing.
mcp__torno__try_params Builds the model with other values of its Params (set, the fields scene lists under params as editable: {"wall": 2.4}) in a worker of its own and throws it away, then says whether it built (or the model's error), what changed against the last build part by part, and the measurements (measure: bbox, volume, area, mass or distance of whole parts by name) and checks (check: as the check tool, parts by name) asked for, all worded as those tools word them and said to be of the trial. It changes nothing: not the model, not the values you are trying in the window, not the build on screen; it is made on top of your values, and refused when there is no memory for a second build or while a build runs. To keep a value the agent edits the model.
mcp__torno__features The holes, bosses, fillets, chamfers and patterns of a part, found on its geometry (part, and kinds to ask for some of hole, boss, fillet, chamfer, pattern): for a hole its diameter, axis, where it opens and the face it opens in, depth, whether it is through or blind (and its bottom), and a counterbore, countersink or rounded mouth; for a boss its diameter and height; for a fillet its radius, sweep and length and whether it rounds an outside edge or an inside corner; for a chamfer its size; for a pattern how many holes or bosses alike there are and whether they lie in a row, a grid or on a circle, with the step; each with the faces it is made of and the faces and edges it joins, as refs highlight takes. Diameters, radii, angles and axes are exact, and so are depths between planes and round faces; a depth taken between freeform faces says it is not (≈). Holes and bosses need a cylinder, cone or torus that goes all the way round: a slot end is neither a hole nor a fillet.
mcp__torno__scans The scans of the project and how each is aligned.
mcp__torno__scan_clash What of a scan is inside a part.
mcp__torno__scan_deviation How far a part lies from a scan.

There is no tool that exports, imports or aligns a scan, or writes a file: those are yours.

#Prompts

The same server offers requests that come up again and again as MCP prompts. Claude Code lists them as slash commands, and the chat's command menu shows them when you type /:

Command What it asks the agent
/mcp__torno__review Go through every open mark and every unmet dimension against the current build: measure, look, and resolve the marks that are done.
/mcp__torno__printable Check the print layout for 3D printing: thin walls, overhangs, closed meshes, holes too narrow to print, and whether it fits your bed, given as an argument (/mcp__torno__printable 256x256x256, in mm).
/mcp__torno__explain Describe the model for someone new to it: its parts, its Params and how the parts relate.

#Marks

The tools, each with its key (all keys are under Keys):

Key Tool What it does
V Select Click selects a face or an edge, and a double click its part; the choice in the corner (Any, Face, Edge, Part) says what a click takes. Shift-click adds or takes out, Esc clears. "Insert" puts the selected refs in the session's prompt without sending it; "Note" leaves a mark on each.
P Pin Click a part: a pin there, and its note opens beside it. A pin on an edge is a pin on that edge: the pointer snaps to an edge within reach, and the agent is told which edge it is and the two faces that meet there.
A Arrow Drag from a part: an arrow of that length. With Shift it runs along the surface normal or a principal axis.
D Draw Press and draw. On a locked view (an orthographic front, back, left, right, top or bottom, not turned since; a padlock shows on its button) the stroke is a profile in millimetres on the plane that faces you. With Draw in hand a standard view you choose is taken in orthographic, so it is locked at once, and perspective comes back when you turn the view; in perspective the pointer says "Ortho to draw a profile on the XZ plane". On any other view it lies on the surface. Alt-drag turns the camera with this tool in hand.
S Shape Click places a box, cylinder, sphere or plane, to add, remove, keep out or refer to. A gizmo moves it (G), turns it (R) or sizes it (T), and its row has the exact numbers.
X Section Cuts the parts by a plane. See Section.
M Measure Measures; Enter keeps a dimension. See Measuring and dimensions.
L, B Align scan, Region of a scan See Scans.

Esc drops what is being dragged, then puts the tool down for Select. In a note, Enter saves and Esc leaves it for later. A note is also kept when you leave it any other way, such as Ctrl+` to the terminal: what you typed goes with the prompt you type next.

Every mark has a number (M1, M2, never reused), a note, and a row in the Marks list. Yours are drawn in red, the agent's in green. A mark is a draft until it has been carried to the agent (see The agent), then sent, then resolved by you or by the agent, with a line on what was done. Clicking a row makes its mark the active one and opens the row; the camera goes to the view the mark was placed from only when the mark is out of view, and the reticle at the end of the row ("Go to its view") always goes there. The pointer on a row, or the keys' focus in it, rings the mark's chip in the view, more faintly than the active one's, and lights the face it is on, as a mention of it in the chat does.

The chips in the view answer the pointer too. On a chip the pointer is a hand, the mark's row lights, and its note is read out beside the pointer on one line (M1 too thin here; for a dimension its numbers, M3 40 mm, expected 42 ± 0.05, 2 under). A click on a chip makes its mark the active one and brings its row into view, opened, without moving the camera; for a mark of yours not yet sent, its note opens beside it to be written. A click on a chip is the chip's whatever tool is in hand: no pin is left under it. A drag that starts on a chip turns the camera with Select in hand, as anywhere else; with Draw, Arrow, Shape, Region or another tool that drags, it goes to the tool once it has moved past a click, so a stroke may begin under a chip (and the camera stays still).

#Measuring and dimensions

Lengths are millimetres, angles degrees, areas mm², volumes mm³ and masses grams; Z is up.

The Measure tool (M). A strip in the corner of the viewport chooses what two clicks measure (Distance or Angle) and what a double click on a hole, a cylinder or a circular edge measures (Diameter or Radius). T has no button: with the pointer on a face it measures the wall there, from that point along the inward normal to where the part ends. With Shift a click is a bare point, tied to no face. While the second click is pending the pointer reads out an estimate from the mesh, marked ≈. What is measured is never that: torno asks the kernel, which measures on the exact geometry and says how (6 mm between planes, axis to axis, point to plane, minimum). A distance between two parallel flat faces is the distance between their planes, between two cylinders or circles with parallel axes the distance between the axes, and of anything else the least distance. An angle is between flat faces, straight edges and the axes of cylinders, cones, tori and circles (between axes of two bores, axis to plane of a bore and a face); a face that is flat, or an edge that is straight, within 1e-6 mm but stored as a spline or an extrusion is taken too, and its angle is not said to be exact. Before the second click of an angle the pointer says, in place of the estimate, when an end is taken by its axis (no estimate from the mesh), only if it is flat or straight (a spline), or not at all (a sphere); the click is sent all the same. When the kernel refuses (an angle to a sphere), its reason is read out at the pointer. Esc drops a first click; the tool stays in hand.

A measurement leaves nothing. The kernel's value is read out at the pointer and stays in the strip as Last: 6 mm between planes (a distance adds Δx, Δy and Δz, from the kernel's two points) and drawn in the view in ink, between those points, with its value at the middle, until the next measurement, Esc with nothing under way, another tool, a new build or another project. It is no mark, and the agent is not told of it: not with the next prompt, not in state.md, not in a picture. To keep it, press Enter or "Keep as a dimension" in the strip: it becomes a dimension, measured again by main as it is kept, and the strip says Kept as M4.

Dimensions. What you keep of a measurement is a dimension: a mark (M4) with a value, drawn on the part and listed with the marks. Its row, once clicked, shows the value, how it was taken, and under "Should be" two fields: what is expected (=) and the tolerance (±), in mm or degrees. With an expected value the dimension is a check: it passes when the value is within the tolerance of it (0.05 mm, or 0.1° for an angle, when you give none) and fails otherwise, and the row says Passes or Fails: 2 under. A failing one is ringed on the part and in the agent's pictures, its chip saying by how much it misses after the cross (40 mm ✗ 2 under), and the line at the bottom counts those that fail and those with no value.

When the project has checks, a line heads the Marks list: Checks on r12: 3 pass · 1 fails · 1 not measured. Each count is a button that goes to the first such check. A dimension with no expected value is not counted: it was kept to be read, and stays in the group its status puts it in.

measure (the agent's) takes kind, a, for a distance or an angle b, for a mass density, and layout. An end is a ref (lid/F12, lid/E3, or a whole part where the kind takes one), the number of a mark (M3: what it is on, only if the last build's check confirmed it; a dimension is refused, with a line that says to name its end), {point: [x, y, z]}, or {ref, point}. Each reply is JSON with a reading: the answer in one line.

kind Of Reply
distance a and b mm, the two points it is between, and how
angle a and b degrees: between the outward normals of two flat faces (0 to 180), or between two straight edges or axes, or a straight edge or axis and a flat face (0 to 90); the axis of a cylinder, cone, torus or circle is taken as a line, and a face flat or an edge straight within 1e-6 mm as a plane or a line, not exact
radius, diameter a cylindrical, spherical or toroidal face, or a circular edge mm
thickness a face of a solid, at a point of it (the centre of the face when none is given) mm, along the inward normal to where the part ends; moved says how far the point was from the face before it was laid on it
bbox a part, a face or an edge min, max, size
area a part or a face mm²
volume a part that is a solid mm³
mass a part that is a solid grams, with the density used (g/cm³; the material's of the project's print profile when it has one, else 1.24, PLA, and the reply says which)

check takes kind, a, for two parts b, and layout; a and b are names of whole parts. interference and bed take neither. overhang, thin and bed ask whether it prints: they are of the print layout when the model has one and no other is named.

kind Reply
overlap the volume two solid parts share, in mm³, and the box it lies in
clearance the least distance between two parts and the points it is between; 0 when they touch, and when they overlap, which overlapping: true then says. With fit (press, sliding or loose) and a material in the print profile, the reply also says whether the gap is within that fit's band per side, how far outside it, and whose numbers they are
interference no part: every pair of the layout's solid parts at once. The pairs that share volume (interfering, each with its volume and the box it lies in, largest first), the pairs that only touch (touching) and how many are clear. Pairs whose boxes are apart are clear without a cut; the others are cut in order of how much their boxes overlap, for 20 s at most, and sampling says how many were measured when the time runs out. Not answered in a layout torno spread for display
mesh whether the mesh the part is drawn and exported with is closed (watertight), its triangles, the number of openEdges and where up to 20 of them are
overhang with angle (degrees from vertical; the profile's material's, else 45, when none is given): the faces of a that face down steeper than that, each a ref (tray/F7) with its area and steepest lean, and their whole area; faces on the bed are left out. Planes are exact; a curved face is judged by its normals at sampled points and is not
thin with min (mm; the profile's least wall when none is given): the faces of a where the wall, measured along the inward normal at sampled points of each face, is under min, with the thinnest of each and of the part; always approximate, and sampling says what was measured
bed with size ([x, y, z] mm; the profile's printer's bed when none is given), no part: whether the layout's box fits the bed, its size, and by how much it is over along each axis; exact only when every part's box is

The print profile is the printer (bed, nozzle, layer) and the material (density, shrink, the gap per side of a press, a sliding and a loose fit, least wall, overhang angle) a project is made for. Choose them under "Print profile" in the left column. The printers are yours, kept in torno's settings (profiles.json, next to config.json; "Add a printer…" makes one); PLA, PETG and ABS come with typical values, and a material of your own goes in that file (materials: name, density, and optionally shrink, fits, minWall, overhang). A choice is copied into <project>/.torno/profile.json, so the project travels with the numbers it was checked against. With a profile, check bed, thin and overhang and measure mass take their values from it when the call gives none, and the reading says "by your profile (Bambu X1C, PETG)"; a new conversation is told the profile in one line, state.md holds it, and the skill printing writes Params from it, not from memory. These are your numbers, not measured: a verdict against them is never a claim that the part prints.

What is exact, what is approximate and what is refused is under Limits and exactness.

#Section

X cuts at once when nothing is cut yet: a plane through the middle of the parts, across the axis you look most along, open towards you (the half taken away is the one on your side). A bar appears at the bottom of the viewport: X, Y and Z put the plane across that axis, again open towards you, "From face" puts it on the next flat face you click, the slider and the number beside it say where the plane is, as the agent is told: across an axis its place in the object frame (y = 20; type a number to move it), else how far along its normal it is from the middle of the parts (12 mm along the normal). The tick on the slider and "Middle" (there while the plane is elsewhere) are the middle of the parts. "Flip" takes away the other side and "Off" shows the parts whole again. What is on the side the plane's arrow points to is not drawn. Each solid part is closed where it is cut, in its own colour a step darker, hatched with fine lines in its edge colour (in a picture for the agent too); a part that is not a closed solid (a scan, an open shell) is cut open. Drag the arrow on the plane to move it; a drag anywhere else turns the camera. The arrow keys move it by 1 mm, with Shift by 0.1 mm. On a cut face there is nothing of the model to pick, and the pointer says so; a mark on what was cut away stays, drawn faint. The bar stays while there is a section, whichever tool is in hand, and the section is kept with the project. The pictures the agent takes are cut the same way.

section (the agent's) takes plane, and optionally parts, layout, size, deflection and scan. A plane is {origin, normal}, or {axis: 'x' | 'y' | 'z', at}, or 'current' for your own section. The reply has, for each part, the outlines the plane cuts of it as closed loops of [u, v] points in mm along the plane's own axes, each loop's area (negative round a hole), the part's area, how many loops are open, and the extent of the whole cut. The outlines come from the exact geometry, as polylines within 0.01 mm of it, or within the deflection asked for (0.005 to 0.05 mm). A reply holds at most 1,500 points: beyond that the loops are thinned, and say so. Areas are the kernel's, of the true outlines, whatever was thinned. With it comes a 2D drawing of the cut on a millimetre grid, each part in its colour, with what is up in the world up on the page. To see the cut in 3D the agent calls look with a section.

#Scans

A scan is a mesh of the real thing or of where the part goes: an engine bay, a room, a machine, taken with a phone or a scanner. Torno draws it with the parts, so that you and the agent can check the model against it. It is good to a few millimetres at best and shows only what the camera saw. Nothing taken from a scan is exact, and torno never says that it is.

Importing. "Import scan…" in the Scans panel, or a file dropped on the view: GLB, glTF, OBJ, PLY or STL, with faces (a point cloud is refused). The file is only read. Torno keeps its path, size, date and hash, and makes one mesh of it in millimetres and Z up under .torno/scans/, baking a texture into colours at the vertices. The scan goes on working from that mesh when the file moves or is deleted (its row then says "source missing" or "source changed"); when the mesh is gone too, the row offers "Import again". A scan of more than 2,000,000 triangles is reduced to that many, and its row says from how many.

Units. glTF is read as metres and Y up; the others as millimetres, or metres when the whole is under 20 units across, with Y up for OBJ and Z up otherwise. Right after an import in the window, when either was assumed, a short sheet at the top of the Scans panel asks once: How is S1 read?, with the unit and the up axis and the size the scan would have read so (Its size would be 120 × 120 × 0 mm) and, under it, the size of the parts of the layout shown (The parts of assembly: about 40 × 40 × 41 mm); when the two diagonals are more than 100 times apart either way, a line in the warning colour says how many times and names the unit that would bring them near (About 1,000 times the parts: is it in millimetres?). The scan is brought into view with the parts once, right after the import and again after "Apply"; the views and F still frame the parts only. The scan is drawn in the view meanwhile. "Keep" says the guess is right, "Apply" reads it as chosen, "Later" puts the question away. Until it is answered the row has a dot after the name, and its detail says the size and what was assumed (120 × 120 × 0 mm, from m, Z up (assumed)) and asks Read as metres, Z up: right? with "Yes" and the unit and axis under it (not while the sheet is up, which asks the same). Changing either drops the scan's alignments, which torno says before it does it.

The Scans panel. Each scan (S1, S2, never reused) has a row of one line, built like a layer's: its id, its name (whole, with the path of its source, in the tooltip), how it stands in the layout on screen in one word (its sentence in the tooltip), an opacity slider when pointed at, and a switch. The arrow at the end, or a click on the name, opens its detail beneath it, one scan at a time: its size and how it was read, how it stands in a sentence, what it rests on, "Frame" (bring it into view: F and the views frame the parts only), "Align", "Rename" (its name in torno: Enter keeps it, Esc leaves it), then the regions painted on it as chips that go to the mark (M5 wiring loom), the unit and up axis (folded under "How it is read" once the reading has been answered), its colours, and last "Remove". On a row the state word gives way before the name, which keeps at least ten characters. A scan drawn solid hides what is behind it and takes the pointer; one drawn see-through lets you point at the part behind it. The section cuts a scan open like the parts.

Aligning (L). A scan is aligned in one layout at a time; in a layout where it is not, its row says unaligned (in the quiet colour until the scan has been aligned anywhere: it is only the next step) and nothing is checked against it. The detail of such a scan makes "Align" its main button, and the sheet that asks how a new scan is read offers Next: align S1, which takes up the tool on Floor. The bar at the bottom of the viewport has three steps, 1 Floor · 2 Pairs · 3 Nudge, which can follow one another. A tick on a step says it has been done for the scan in this layout, and one line under them says what the step is for. The bar opens on the first step not done: Floor for a scan with no alignment that is much larger than the parts (ten times their diagonal: a room, an engine bay), Pairs for one about their size, Pairs once a floor is down, Nudge once there are pairs. When a floor or pairs are kept, the bar goes on to the next. Any step can still be chosen, and the bar comes back to the one you chose last for that scan, in this window:

"Undo" puts back the alignment before the last one, "Clear" takes it away. What the row then says of the scan is what the agent is told of it:

Row Meaning
aligned Tied to points of the model and checked against the last build. Its detail says what it rests on: 4 pairs, RMS 2.1 mm is a fit of pairs and how well they fitted (the RMS is of the points you clicked, not of the scan); …, then moved by hand is a fit or a floor moved afterwards.
placed Tied to no point of the model: placed by the handles or typed values (by hand), laid on the ground (floor only), or pairs whose model ends were typed. No build can say whether it still holds, so none is checked.
pending Nothing is built in this run yet, or the last build has not been compared with the alignment yet. Torno never builds the model again by itself to make that check.
check The model has moved since: a point you clicked on a part is no longer on it, torno spreads a part otherwise, or the scan was imported again from a changed file. Align again, or press "Still right" in the detail.
rough Fewer than three pairs, pairs almost in a line, or pairs left more than 5 mm apart. Torno asks before it keeps one.
unaligned Not aligned in the layout on screen.
missing Its mesh is gone from the disk; the detail offers "Import again".

After every build torno looks for the model points of each alignment on their parts again. It tells what it finds; it never moves a scan by itself. A scan aligned in another layout offers "Use this alignment in layout": it is put where it stands there, as a start, and kept as placed by hand.

Regions (B). Drag over a scan to paint a part of it: a brush whose radius is in the bar ([, ]), "Erase" (E, or Ctrl held), "Box" (Shift-drag), "Grow to flat", "Undo" (Z), "Clear". What is seen is what is painted: the brush takes nothing behind a part, nor of what a section has cut away. "Keep" (Enter) makes it a mark with a number and a note, such as "wiring loom, can be pushed aside": it is how you tell the agent what a part of the scan is. A painting waits until you keep or clear it, whatever tool or project you go to meanwhile. "Edit" on a region's row takes it in hand again. Removing a scan leaves its regions as marks that say their scan is gone.

The scan tools. The agent cannot import or align a scan: you do. It can ask about one. Every reply names the revision, the layout and how the scan is aligned there, has exact: false, and a reading whose numbers follow a ≈ and which opens with whatever puts the alignment in doubt. Lengths taken from a scan are told to 0.1 mm.

Limits, each said in the reply that meets it: scan_clash checks at most 200,000 scan points near the part and tells the 8 largest clusters; both checks give themselves 10 s, and a count that was cut short is never told as a no; scanColour colours at most 60,000 points; an alignment keeps 64 pairs. Refused, with the way out: a scan that is not aligned in the layout asked; a part torno moved for display (see Limits and exactness); a scan whose mesh is gone or that is being imported; a region that no longer fits its scan's mesh.

#Explode and saved views

Explode. E, or "Explode" in the bar of views, draws the parts of the layout apart; a bar under the views has the slider, the direction (from the centre, or along X, Y or Z) and the distance (×0.5 to ×4). It is for display only: the parts are where they were built, and every pick, ref and number is of the parts as assembled.

Saved views. "+ Save view" in the strip under the bars keeps what is on screen under a name: the camera, the layout, which parts are switched off, the section or the explode (never both), and how each scan is shown. A click on a chip brings it back; the chip's menu renames it, updates it to what is on screen, or deletes it. A name is 1 to 40 characters and not that of a standard view. A project keeps 24 views; the strip shows six and the rest behind "n more". Applying a view changes no geometry and no mark and starts no build. What a view names that is gone (a part, a layout, a scan) is passed over and said on its chip.

The agent sees the views in scene, draws one with look {"view": "Web cut"} whatever is on your screen, puts one on screen with show, and proposes its own with save_view: a dashed chip that says "proposed", with "Keep" and "Discard". A proposal is the agent's until you keep it; it can replace its own proposals (12 at most) and never a view of yours.

#The page of projects

W, or "All projects" at the top of the list (beside its title once a project is open), shows every project of the folder as a card; a workspace of several projects starts there. / or a letter filters by name, the arrows move, Enter opens, Esc goes back. The head (title, buttons, filter) stays at the top as the cards scroll. "By name" or "Recently changed" orders the cards; the choice is kept for the folder. A card has:

Looking at the page builds nothing, opens nothing and writes nothing.

#Export

"Export…" in the line at the bottom (the open project), in the head of the page of projects (any of them), on a card, or Ctrl+Shift+E, writes one HTML file for someone who has no torno, or a STEP, STL or 3MF file of one project (below), or, of the project on show, a picture of the view or a drawing of its section (below). It is made from the last build of this run, so a project must have been opened first. Nothing is uploaded: the file goes where the save dialog says, which opens in .torno/exports/.

The sheet lists what would go, with counts and the size, and what is ticked is what is in the file:

In the file
always the parts of every layout, in their colours; the explode kept for each layout; the views you saved or kept
only when ticked marks and their notes (drafts go with them, and the sheet says how many); resolved marks; parts that are switched off; each scan, by its own tick, reduced to at most 60,000 triangles
never where anything is on this computer; the model's source; what a model printed; the session with the agent; state.md; a view the agent proposed and you did not keep; what was painted on a scan

The page opens from file:// with the network off: its script, style and fonts are inside it, and its policy lets it load and send nothing. It has the parts with a switch each, the layouts, an explode slider, a section (parts are drawn open, without cut faces), the saved and the standard views, the marks as labels and a list with each dimension as torno measured it, and light and dark. It measures nothing and says so. With several projects, each is unpacked when it is first shown.

A part costs about 10 bytes a triangle on top of 0.7 MB for the page itself: a project of one part and 12,000 triangles in two layouts is 1 MB. Over 12 MB the largest scan is reduced further, then left out, and the sheet says so; above 64 MB nothing is written. The file is written whole or not at all, and a name without .html gets it. Once it is saved, the sheet offers Show in folder and, for a page, Open in browser: they act on the file just written, which torno itself remembers. A project left out because it has no build of this run is a link in the sheet: it opens the project, and when its build lands the sheet comes back with it ticked beside the others.

#STEP, STL and 3MF

The sheet's format buttons write a file of the model instead, of one project:

Format What is in it Layout unless you pick another
STEP the exact shape, each part a named body in its colour assembly
STL one part, as triangles print
3MF a plate: each part a named object in its colour print

Every file is in millimetres, with the parts where the layout has them: a plate's parts are where torno set them apart, as on screen. You tick the parts that go; an STL or a 3MF is of solids alone. A part of several solids is, in a 3MF, one object made of a mesh of each; an STL of solids that touch is refused, saying to fuse them in the model or export 3MF. Their triangles are made for the file, not taken from the screen: none further than the tolerance from the faces (0.01 mm unless you type another, 0.001 to 1 mm), nor turning more than the angle (5°, 0.5 to 45°). A tolerance that would make more than 500,000 triangles is refused before anything is made: past that the kernel could not make the file in the time it has.

The kernel makes the file from the last build, and torno writes it beside the name you chose. Before the file takes that name, the kernel reads it back and checks it:

A file that fails is not saved, and the sheet says why. One that passes is named, and the sheet says what was checked: Checked: 3MF, 2 objects, 184,220 triangles, closed, volume within 0.04 % of the model. Checked, not printed. A 3MF can be opened once saved in the program your system opens 3MF files with, your slicer, when "Open it once saved" is ticked.

#A picture of the view, and a drawing of its section

The sheet is grouped by who will open the file: a page to look at (the HTML page), the CAD file (STEP, STL, 3MF), and, when the sheet is of the project on show, a picture (PNG) and a drawing of a section (SVG, PDF), which are of the view as it is on screen. Nothing is uploaded, and no agent has a tool for any of them: the save dialog opens in .torno/exports/ with a name offered, and the file is written whole or not at all (a name without the ending gets it).

#Keys

? shows this table in the window, and searches it. Above the keys the sheet lists the pointer, which has no keys:

Pointer What it does
Drag Turn the view about the point under the pointer; with Arrow or Draw in hand, a drag that starts on the part draws instead
Right drag Pan the view (a middle drag does too)
Scroll Zoom, towards the pointer
Double click With Measure in hand: measure something round, its diameter or its radius

Point at a tool of the palette and a panel beside it says, at once, what the tool does and its key. With a tool in hand (not Select) a quiet line at the foot of the view says what the tool waits for next: for Measure click the first end, then click the second end; for Arrow press on the part and drag to where it should go. Region of a scan and Align scan, with no scan in the project, are drawn quieter and say Import a scan first. The line that tells how the pointer moves the view shows the first time a project is drawn in this install, until the camera is first moved; the window remembers that in its own storage, never in a workspace.

Where Keys What it does
Everywhere ? This sheet of keys, outside a field and the terminal
Everywhere Ctrl+` To the session, and from it back to the view
Everywhere F6 To the next part of the window: projects, view, session, status line
Everywhere Shift+F6 To the part before
Everywhere Ctrl+Shift+D Show or hide the session pane
Everywhere Ctrl+Shift+E Export to one HTML file, or to STEP, STL or 3MF: the open project, or from the page of projects any of them
Everywhere Ctrl+Shift+B Rebuild: build the open project again, as the button in the status line does
Viewport V Select: click a face or an edge, double-click for its part; Shift-click adds or takes out. The choice by the selection says what a click takes
Viewport M Measure: two clicks, or a double click on a round thing; nothing is kept unless you keep it
Viewport X Section: cut the parts by a plane
Viewport P Pin: click a point and leave a note
Viewport A Arrow: drag from a point
Viewport D Draw: a stroke on the surface, or on a locked view
Viewport S Shape: place a box, a cylinder, a sphere or a plane
Viewport B Region of a scan: paint what something is
Viewport L Align scan: stand a scan where the model is
Viewport 1 2 3 4 Iso, front, right and top view
Viewport F Fit the selection in view, or the parts; again, the parts
Viewport O Perspective or orthographic
Viewport E Draw the parts apart, or together again
Viewport W All projects of the folder, on one page (there a letter filters them by name)
Viewport Esc Step back: what is under way, then the tool in hand, then the selection
With a tool in hand T Measure: the wall under the pointer
With a tool in hand Enter Measure: keep the last measurement as a dimension
With a tool in hand Left Right Up Down Section: move the plane by 1 mm
With a tool in hand Shift+Left Shift+Right Shift+Up Shift+Down Section: move the plane by 0.1 mm
With a tool in hand Tab Align, pairs: which end is asked for first
With a tool in hand Backspace Align, pairs: take the last end back
With a tool in hand [ ] Align, floor: a smaller or larger patch
With a tool in hand G R Align, by hand: move or turn the scan
With a tool in hand [ ] Region: a smaller or larger brush
With a tool in hand E Region: paint or rub out
With a tool in hand Z Region: undo the last stroke
With a tool in hand Enter Region: keep what is painted
With a shape in hand G R T Move, rotate or size the shape with its handles
Page of projects Left Right Up Down Home End Move among the cards
Page of projects Enter Open the card in focus
Page of projects / Filter by name: a letter or a figure does too
Page of projects Esc Clear the filter, then back to the open project
Page of projects Enter In the filter: open the first card
Page of projects Down In the filter: down to the cards
New project Esc Leave the page, asking first when something is in it; on the doodle board, put the board away
Session Shift+Enter A new line in the prompt, not a send
Session Ctrl+Shift+C Copy what is selected in the terminal
Session Ctrl+Shift+V Paste into the terminal
Session Enter Chat: send the prompt
Session Shift+Enter Chat: a new line in the prompt, not a send
Session Up Down Chat: choose among the / commands offered while one is typed
Session Tab Chat: complete the / command chosen
Session Ctrl+C Chat: stop the turn while Claude Code works, with nothing selected in the box
Session Enter Chat: allow what the card in focus asks (in its reason field, Enter denies)
Session Esc Chat: put away a picture shown large
Session Left Right On the edge of the session pane: wider or narrower
Session Left Right On the edge of the left column: narrower or wider
In a field, a menu or a sheet Enter Field: keep what was typed
In a field, a menu or a sheet Esc Field: leave it as it was
In a field, a menu or a sheet Shift+Enter Note: a new line
In a field, a menu or a sheet Down Right Menu: the next item
In a field, a menu or a sheet Up Left Menu: the one before
In a field, a menu or a sheet Home End Menu: the first or the last
In a field, a menu or a sheet Esc Tab Menu: close it
In a field, a menu or a sheet Up Down On the line above the marks: more room for them, or less
In a field, a menu or a sheet Esc Sheet or question: put it away
In a field, a menu or a sheet Ctrl+Z Drawing board (a doodle, or a drawing on the view): undo the last stroke
In a field, a menu or a sheet Shift Drawing board: hold Shift while drawing for a straight line from where the stroke began
For working on torno F12 Ctrl+Shift+I Developer tools

A tool in hand is offered a key before the viewport is. F12 and Ctrl+Shift+I open the developer tools whenever torno runs from the repository.

#Files torno writes

Inside a workspace torno writes only under <project>/.torno/, with one exception: "Create" on the page of a new project (see "Making a new project") writes the new project's folder, <name>/<module>.py, brief.md and reference/. Those are the project's own, in git like its model, and torno never writes into them again. It writes under .torno/ only when that is a real folder inside the project: where a link or a file is in its place (a workspace copied from elsewhere may hold one) torno writes nothing there, and says so once in the Marks panel, until a real folder is put in its place.

File What In git
scene.json how you last looked at the project: the layout, the layer visibility, the section, the explode of each layout, how each scan is shown, and the saved views (views) travels with the project
params.json the values you are trying in place of the model's Params defaults, with the default the file had; the model file is never written ignored
marks.json the marks of the project: yours and the agent's, with their notes, anchors and status; of a dimension also what is expected of it and the value last measured travels
scans.json the scans of the project: where each source file is and its hash, how it was imported, and its alignment in each layout travels
scans/<id>/mesh.bin, <mark>.mask the scan as torno draws and checks it, and the triangles of each region the meshes are ignored (scans/.gitignore): a clone has the record, and the row offers "Import again"
profile.json the printer and material the project is made for (a copy of your choice under "Print profile") travels with the project
agent.json whether Claude Code is shown as a chat or a terminal; without it, in a chat travels
chat/<conversation>.jsonl what a chat session said and did, as text (a turn's line holds the tokens it used, as numbers); no picture data, no address (a picture of reference/ by its path) ignored
session.json the conversation to resume ignored
state.md the build, the parts, the checks of the model, the views, the scans and the open marks as text ignored
thumb.png, thumb.json the card's picture, and what it is of ignored
builds.jsonl a line for each build that worked: its revision, when, the run of torno it was made in (revisions count from 1 in every run), a hash of the sources it was made from, and the commit then checked out and whether files differed from it; the last 500 ignored
history/ the builds kept: for each build that worked (not again for sources the newest kept build was made from) a copy of the Python files it was made from, its scene without the meshes, a small picture, and an index of them with an id such as h41, which counts on across runs; the latest 100 and every one you star; never read back into your files ignored
views/<n>.png the pictures and section drawings the agent asked for, the latest 20 ignored (a picture of a scan may show the real world)
exports/ where the save dialog of an export opens; empty unless you save there ignored

"Ignored" is by .torno/.gitignore (or one inside the folder), which torno adds lines to and never rewrites; a line you negate there (!state.md) is yours, and is not added against. What travels with the project in git is what is the project's: scene.json, marks.json, scans.json, profile.json and agent.json. What changes with every build or session, or is of this machine, is ignored: state.md, session.json, chat/, the thumbnail, builds.jsonl, history/, views/ and exports/. Add .torno/ to the workspace's .gitignore if you want none of it in git.

Outside the workspace:

The agent's CLI keeps its own records as it always does.

#Limits and exactness

Exact and approximate. Every number the tools give is measured by the kernel on the model's own B-rep, never on the mesh on screen. A reply says exact: true when the value is exact; otherwise the reading starts with ≈ and says approximate, and how says why.

What it costs. The volume and the box of every part are worked out during each build. On prismatic parts that is nothing: the test models build in a tenth of a second. On a freeform part it is about a second for the volume and up to a second for the box: a project of three lofted parts of about 90 faces each, whose model runs in 10 s, builds in 17 s, 5 s of it for volumes and boxes. area of the whole of such a part takes a second or two on the first ask (three integrals of every freeform face, to say how many digits agree); describe of the part takes one integral a face and says its area with ≈. Both are then kept until the next build. A question the kernel has not answered within 30 s is an error that says so.

The agent's tools are for its session alone. The MCP server and the address the agent's hooks post to listen on 127.0.0.1 alone, check that a request comes from no web page (Origin, Host), and answer 404 to any request without its session's token in the X-Torno-Session header: 32 random bytes made when the session starts, which open that session's hooks and its project's tools, and nothing once the session has ended. The token is in the agent's environment (TORNO_SESSION) and nowhere else: not in the arguments it is started with, which every user of this machine can read (ps), and not in the file --mcp-config names, which holds only the placeholder ${TORNO_SESSION} that Claude Code expands. Torno does not log it and writes it to no file. That keeps out another user of this machine, any program that only finds the port or reads the agent's command line, and the session of another project. It does not keep out a program running as you, which can read the agent's environment, nor a script the agent runs, which inherits it: such a program can call show, add_mark, resolve_mark and save_view of that project, and post its hooks, while the session lasts.

Parts that fit together. overlap, clearance and a distance between two parts are of the parts where the layout has them, so fit can only be checked in a layout that places the parts as assembled (assembly(p) in the model). In a print layout that torno spread along X the parts are where torno put them for display: there these three are refused between two parts, with a sentence that says why and names the layouts that do place both parts; a dimension between them is not kept, and one that is there already is not measurable on such a build; a scan is not checked against a part torno moved. What is of one part is answered as ever (thickness, diameter, bbox, area, volume, mass, mesh, a distance within the part, an angle), with arranged, how far torno moved the part. A project with only a print layout can therefore be measured part by part, but not checked for fit until its model has an assembly.

Memory. The last build of a project stays in memory, in a process of its own, to answer what is measured and checked of it: half a gigabyte for a small part, and many gigabytes for a large model. At most four are kept. Before a build is started the kernel looks at how much memory is free: when there is less than the last build of that project holds (2 GiB at least) and 2 GiB more, it lets go the builds of other open projects first, the one least recently used first, and then that project's own. The line at the bottom then says "The last build was let go to make room (memory): tools answer when this one lands". A project whose build was let go is built again when something is asked of it, never by a check on a timer; and a model that has just failed to build is not built again until a source changes.

#Troubleshooting

#Tests

pnpm test          # unit tests of the app (vitest), a few seconds
pnpm test:kernel   # the app against the real Python kernel and a stand-in agent (vitest)
pnpm test:e2e      # the built app in a hidden window (Playwright)
(cd kernel && uv run pytest -q)   # the kernel alone (pytest)

GitHub Actions runs the suites on Linux (.github/workflows/ci.yml, skipped for a change of only the site or the docs) and apart on macOS (.github/workflows/macos.yml, on main and weekly, not a required check), with the e2e suite only when asked.

No test opens a window, and none runs a real agent CLI: a stand-in (app/test/fake-agent.mjs) takes its place. Under test Electron keeps its own files (cache, storage, crash dumps) in a folder of the run in the temporary folder, removed once it has quit, and nothing in ~/.config/torno (on macOS ~/Library/Application Support/torno). The scans in the tests are made by the test code; none is in the repository. A test holds this file to the code: its table of keys, and the tools it names.

The evals: prompts to parts with the real Claude Code. app/evals/ measures whether a change to torno makes the agent build better parts. It is not a test: it runs the real claude under your login and spends your subscription, so no suite and no CI runs it, and it starts only when you ask three times: TORNO_EVAL=1, no CI, and spend my subscription typed once it has said the model, the cases and the budget.

pnpm build
TORNO_EVAL=1 pnpm -C app eval                        # every case, 5 times each, sonnet, $2 a run at most
TORNO_EVAL=1 pnpm -C app eval --case t1-box --repeats 1 --label try
pnpm -C app eval --compare app/evals/out/<a>.jsonl app/evals/out/<b>.jsonl   # two reports side by side

A case (app/evals/cases/<id>/case.json) is a brief, often a start model and marks, the prompts, the answers of the person it plays (by topic, with a default: "I don't know: choose, and say what you chose"), a turn limit and the checks. Each run copies the case to a new temporary folder (never a workspace of yours), starts the built app on it in a hidden window, sends the prompts through the chat with claude --model <model> --max-budget-usd <budget> --permission-mode acceptEdits, allows edits and torno's tools and denies every other request, and answers questions from the case. Then it scores the model as the agent left it: app/evals/score.py, with torno's kernel loader, checks validity, part names, solids, sizes and ratios, volume, clearance, overlap and holes on the B-rep (each pass or fail with its number, exact only where the kernel says so), and an mcp check calls a torno tool through the project's MCP address. The person's dimensions in a case's start (start/.torno/marks.json, with an expected value) are checks too, read through marks: one passes only with its verdict measured on the last build. A diff check holds the last edit (the two builds diff keeps) to the parts the case lets it change: a part with other face, edge or solid counts, or moved, fails it, and so does a part come or gone; the volume and size checks hold the rest against the start. --ablate takes parts of torno away from the agent, to measure what each is worth: skills (no plugin of skills: the whole system prompt), reviewer (no torno-reviewer subagent), stop (a Stop is never held back) and look (the tool answers that it is off) and digest (the answer to an edit leaves out what its build changed; state.md keeps it), in a comma list; --look-size <px> draws a look that names no size at that long edge (256 to 2000) in place of 1280, and --holds 1 lets a turn's Stop be held back once in place of twice. torno reads them (TORNO_EVAL_ABLATE) only when both TORNO_TEST=1 and TORNO_EVAL=1 are set, which only the evals set together: in any other run nothing is taken away or changed. The cases: three of primitives and plates (T1), four printable parts with fillets, bosses and a lid that fits (T2), two of mating parts (T3), two that leave a dimension out (asking is right), two pairs of the same edit by a mark and by text alone, and two edits that must change nothing else. The report is one JSON line per run in app/evals/out/<date>-<commit>-<label>.jsonl (ignored by git), with checks passed, the build apart, whether it asked before it first wrote the model, whether it said done while a check failed, turns, tools by name, pictures, tokens, cost and time from the CLI's own result, and a Markdown table beside it.

#Environment variables

Variable Effect
TORNO_WORKSPACE=<path> the workspace to open at start, instead of the one remembered. scripts/torno <path> sets it.
TORNO_AGENT_CMD=<cmd> the command to run for a session in place of claude, split on spaces and given Claude Code's arguments: TORNO_AGENT_CMD="claude --model sonnet"
TORNO_BUILD_TIMEOUT=<seconds> how long a build may run before it is stopped: 600 by default, a day at most
TORNO_OZONE=wayland or x11 read by scripts/torno only, on Linux: the platform Electron is told to draw on. Unset, Electron chooses. On macOS it is ignored, with a line on stderr.
TORNO_CONFIG=<path> the config file to use instead of ~/.config/torno/config.json (on macOS ~/Library/Application Support/torno/config.json)
TORNO_DEBUG=1 print to stderr the workspace opened at start, the address of the MCP server, and the command line and hooks of every session
TORNO_OUTSIDE_AGENTS=1 a spike (#54): open a socket only you can open, in your runtime folder, through which an agent torno did not start reaches one project's tools by node scripts/torno-mcp.mjs <project folder> (as a stdio MCP server in its config), after you allow it in the window. Off by default.
TORNO_EVAL=1 lets pnpm -C app eval start, which spends your subscription; it still asks before it starts, and never runs under CI
TORNO_EVAL_ABLATE=<list> what an eval run takes away from the agent (skills, reviewer, stop, look); read only with both TORNO_TEST=1 and TORNO_EVAL=1, and set by pnpm -C app eval --ablate
TORNO_EVAL_LOOK_SIZE=<px>, TORNO_EVAL_HOLDS=1 or 2 the size of a look that names none, and how many times a Stop may be held back, in an eval run; read under the same rule, set by --look-size and --holds
TORNO_EVAL_AGENT=<cmd> the agent of the evals in place of claude --model … --max-budget-usd … --permission-mode acceptEdits; the stand-in (node app/test/fake-agent.mjs) checks the runner and spends nothing
TORNO_TEST=1 create the window hidden and never show it; keep Electron's own files in a temporary folder of the run; expose the test hooks; look for claude on the PATH without running it. For tests only.

The agent inherits torno's environment without these, and without the variables a parent Claude Code session exports (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT and the like), so torno can be started from inside one. Variables you set yourself, such as CLAUDE_CODE_USE_BEDROCK, are passed on. It is given TORNO_SESSION, its session's token, which no address and no argument holds. A torno started from inside a session of another torno passes that session's TORNO_SESSION, TORNO_OBJECT and TORNO_SESSION_ID to nothing it starts: not to the agent, the kernel, the import of a scan or the checks of what torno needs.

#Not yet verified on a visible screen

Torno was built and tested in a window that is never shown, with a stand-in for the agent.

Verified on a real compositor output (Hyprland, NVIDIA; the session was locked, so not by eye). On 10 October 2026 torno ran as a real, mapped window on a Hyprland output, on Wayland and on X11, with the real agents (Copilot CLI too, which torno has since stopped running); what it drew was taken from the compositor and looked at afterwards. The first start drew the parts on the GPU at 60 frames a second on both platforms. Only one torno: a second start exited and the first opened the folder it was given. There is no menu, also on Alt. Alt-drag with the Draw tool turned the camera and drew nothing (sent as events; no binding of the compositor's was in the way). hyprctl clients showed class torno. Light and dark were legible in both themes. A large project's thumbnail was on its card at once and the window did not stall. Ctrl+Shift+E with Claude Code's prompt in focus brought up the export sheet and nothing reached the CLI; ? outside a field and the terminal brought up the sheet of keys. Left alone for five minutes with a large project open, torno's memory stayed flat and its processes idled near zero. With Claude Code a pin was told with the next prompt and turned from draft to sent, and scene, look, marks, save_view and add_mark ran without a permission question. What that run found wrong is mended: closing the window now asks first when something would be cut short, the Marks list no longer runs out of its column when a mark has moved, "Needs input" is no longer shown while Claude Code is only idle, a session can be ended from its header, and tests no longer write into ~/.config/torno. An exported file showed no parts in firefox --headless --screenshot, which takes its picture before the first frame; driven in headless Firefox instead, the page drew the parts.

The chat was driven once with the real Claude Code (Haiku, 15 October 2026) in a hidden window on a temporary copy of intake: a pin was sent with the prompt and shown under it, a torno look came back with its picture and the "Pulled from torno" line, a ref in the reply was a chip, a Write was allowed by the CLI without asking under the person's permission mode (so there was no card), and no .claude/ folder was written in the project. It was not looked at on a visible screen, so the items listed below stay to check.

What remains is yours to try once. Each says what to try and what should happen.

On the screen:

With a real agent: