polyweave is a Claude Code plugin for making 3D assets — geometry, surface and motion. You state what an asset has to satisfy; it searches for the parameters that satisfy it, measuring against your own reference rather than against an opinion. On top of Blender, Godot and a generative mesh service, on your machine, against files in your own repository.
# the agent has a drawing and a name, and no numbers at all > polyweave accept mascot --from docs/design/art/ui/mascot.png wrote assets/mascot.accept.toml 3 predicates, 2 parameters to search > polyweave search mascot job j_7f3a stage=queued job j_7f3a stage=rendering rung=sphere 18/64 elapsed_s=54 job j_7f3a stage=rendering rung=preview 61/64 elapsed_s=212 done score 0.94 light=2.8 form=1.9 sheet .polyweave/sheets/mascot-7f3a.png (5 candidates) trace .polyweave/traces/mascot-7f3a.json (64 samples, 41 cached) > polyweave bake mascot --rung final done 1024x1024 saturation_p99=0.87 delta_e=1.4 silhouette_iou=0.981 accept: 3/3 predicates pass rung=final > polyweave bake shelf --rung final post.render-uniform the render is a single colour, so nothing was lit -> the model 'shelf' carries no material. Set `material` on it, or pass `glaze: {roughness: 0.22}` to apply the default.
Four calls: turn a drawing into a spec, search for the parameters that satisfy it, take the verdict at the final rung, and — on the last one — get a failure that names the door instead of a traceback. The commands are the surface docs/specs/tool-surface.md fixes; the values are illustrative.
polyweave is at design stage. What this page describes is the product docs/ROADMAP.md and docs/specs/ specify: 36 lines across eight blocks, none of them shipped.
Every measurement quoted here was taken in Cottony, the game the backlog was drawn from, and describes the cost polyweave exists to remove. None of it is a benchmark of polyweave, because there is nothing yet to benchmark — and the line that has to ship before any of this becomes a measurement is on the roadmap too, at the very end.
Cottony is a real game, and it is where nearly every symptom in this backlog was measured: a fourteen-field render rig found by hand at two minutes a sample, a silhouette bought for thirty credits, a percentile that disagreed with a mean, and two runs of one unchanged scene that differed in 29,696 pixels. The backlog was not imagined and then justified — it was read off a project that already hurt.
What is expensive is everything between having a mesh and knowing it is right — and today that is a person changing one number at a time and looking at a two-minute render. Six measurements from Cottony, each of which put a line on the backlog.
Cottony's render rig has fourteen tuned fields, and every one of them was found by rendering, looking, and changing it by hand. It is the single largest cost in making an asset, and it is the cost this plugin exists to remove.
A board matched the concept art's mean saturation to within 0.01 and still looked washed out. The whole difference sat at the 99th percentile, which is why a measurement here is a distribution and never a single number.
A wide low cap was sent to the generative service and a tall dome on a long stem came back. A silhouette check against the drawing would have said so before a credit moved.
Two runs of one unchanged scene. A path-traced bake is not byte-reproducible, so equality is the wrong question and a tolerance is the right one — and any usable distance metric has to sit above that noise floor.
Blender's EXACT boolean returns an empty mesh with no error when its target was bevelled. The silence cost a full render to locate, which is why every operation here asserts its own output before returning.
The rig's parameters live in a thousand-line module, so every caller pays a file read to find out what it may set. A surface an agent has to read the implementation to use is one it will get wrong on the first call.
One file beside the asset says what makes a render of it correct. A search reads it, proposes values for the parameters the file permits, bakes at the cheapest rung that can answer, measures the result and scores how comfortably each predicate passed. What comes back is the winning values, a contact sheet and a trace of everything it rejected.
min, max and target are the only comparisons a predicate has. Anything needing more is a measure that does not exist yet, and the honest response is to add the measure rather than widen the grammar.[search.<param>] does not name is not searched, whatever the optimiser would like. It can tune the exposure; it cannot decide the asset should be twice as large.id, so a result can be argued with. An anonymous predicate is one nobody can discuss.A surface is read on a sphere, and a sphere renders in three seconds. Nothing in the old pipeline made the cheap look the default, so the expensive one is what got run — every time, for every judgement, including the ones a sphere could have settled.
subject means the pixels where alpha clears the floor, so a prop is measured over its own pixels and not diluted by whatever background it happens to sit on.luma_bands takes the display size and downscales before counting, which is the only honest way to ask that question.A measure named in an acceptance spec has to be one of these. An unknown name is a refusal — spec.unknown-measure — never a warning, because a spec that accepts any string is a spec that silently checks nothing.
| Measure | Range | What it is |
|---|---|---|
saturation_{p1,p50,p99,mean,std} | 0–1 | HSL saturation over the region, returned as a set and never as one number |
luma_{p1,p50,p99,mean,std} | 0–1 | Relative luminance, sRGB-weighted |
hue_spread | 0–1 | Circular standard deviation of hue, weighted by saturation |
alpha_coverage | 0–1 | Fraction of the region above the alpha floor |
silhouette_iou | 0–1 | Intersection over union of the alpha mask against a reference drawing |
silhouette_centroid_offset | px | Distance between the two mask centroids |
silhouette_bbox_delta | px | Largest per-edge difference between the bounding boxes |
region_colour | Lab | Mean CIELAB colour over the region |
delta_e | 0–100 | CIEDE2000 distance to a target: under 2 is a difference you have to look for |
distance | 0–1 | Perceptual distance between two renders, above the sampler's own noise |
changed_fraction | 0–1 | Fraction of pixels differing by more than a stated amount |
luma_bands | count | Distinct luminance bands surviving a downscale to the size it will be seen at |
One measure governs the whole surface: a caller gets a call right on the first attempt, and answers a failure, without opening an implementation file. Where a rule does not serve that, the rule is wrong.
stage is a short vocabulary, so a caller branches on a word rather than parsing a message..polyweave/jobs/, so a session that ends mid-render can be told what happened by the next one.job.worker-gone.describe() returns each parameter's type, range, default and one sentence, read from the implementation, so the documentation and the code cannot drift.capabilities() says which renderer and version are here, whether an engine is reachable, which offscreen route works, whether a service key is present and what budget is left — so a caller plans against it rather than discovering a missing binary three calls later.A success returned over a result nobody checked is a failure this plugin exists partly to remove. Before returning, each operation checks what must be true of what it produced. A failed assertion is an error, never a warning, and its code names the assertion.
| Produces | Asserted before it returns |
|---|---|
| A mesh | at least one face; finite bounds; no NaN in any vertex |
| A boolean result | face count is not zero where both operands had faces |
| A render | not a single uniform colour; not fully transparent; dimensions as requested |
| A texture | not fully transparent; not a single uniform colour |
| A download | byte length matches the declared length; sha256 recorded |
| A capture | the named artefact exists on disk and is a readable image |
A traceback says where the code gave up, not what the caller should do instead — which is the one thing needed to retry without another round trip.
{
"code": "render.no-material",
"message": "the model 'mascot' carries no material, so the rig has nothing to light",
"remedy": "set `material` on the model, or pass `glaze: {roughness: 0.22}` to apply the default",
"detail": "…traceback, for a human…"
}code is stable and namespaced by area, and does not change once published. remedy is the call that closes it, with the arguments filled in wherever they are derivable; where the choice is a judgement the tool cannot make, it names both doors and what separates them. message never contains a traceback. detail may.
A fetch draws on a real balance. The ceiling is a person's to set; the plugin's job is to make a spend bounded and visible, and never to decide one is worth it. That is a constraint on this project, not a feature of it.
Godot exits zero on a script error and non-zero on a clean quit. The only honest signal is a line the script printed and no error in the log — and every project that has driven it headless has written that parser again.
capabilities() names the offscreen route that works on this machine.Every generative mesh arrives as a static surface, and rigging one by hand is the step that keeps character animation out of reach entirely. Everything below follows from fixing that one thing.
Cottony's tray, star, ball and props are four modules of imperative geometry code, and the shape each one describes is not readable without running it. A declaration is the same shape in a file a person can read, a search can reach and a reviewer can argue with.
Stated in docs/specs/tool-surface.md so that no other spec has to repeat them and no ingest has to invent one.
the engine's convention, because the engine draws the result
unless the asset declares pixels_per_unit
the base of the silhouette, not the middle of the box
linear values live inside a renderer and never in an interface
radians nowhere a caller can see
a cache in a home directory is state a colleague cannot reproduce
Each block on the roadmap is a group of lines that share a failure. These pages carry the lines themselves — the symptom in the roadmap's own words, and the measurement behind it.
Handles for anything slow, a post-condition on every operation, typed errors that name the remedy, a surface that describes itself and this machine, and configuration resolved per call.
6 linesA surface is read on a sphere in three seconds, a render returns its measurements in the same answer, a distribution is reported as a set rather than a mean, and a sheet shows an asset beside its siblings.
5 linesWhat counts as a correct render, written as a file a search can aim at; a search that finds the rig numbers instead of a person finding them by hand; a cache so nothing renders twice; and a trace of everything the search rejected.
4 linesA silhouette gate before the credit moves, a ceiling agreed once instead of a question per fetch, the mesh kept because the service deletes it at seventy-two hours, and a fetched asset normalised into this project's axes.
6 linesA Godot run judged by what the script printed rather than by an exit code that means nothing, a capture that works where there is no screen, a scale derived instead of tuned, and a runner pinned so two machines agree.
4 linesA fetched mesh rigged so it can be posed at all, motion as a clip rather than a second static render, curves as text a diff can show, and one source for both a 2D sprite and a 3D scene.
4 linesA shape stated as data rather than written as a program: a vocabulary read off real assets, numbers a search can reach, a review before anything is built, and a way back to code for the shape no format can state.
5 linesFive constraints bind the project, and they are governed alongside the backlog: a proposal is read against them before it becomes a line. The sharpest is the third.
The caller here is an agent in a terminal, so a surface that needs a person at a screen to operate is one the agent cannot use at all.
Those three already do the work; what is missing is the loop around them, so this orchestrates and measures and never re-implements a renderer, an engine or a mesh generator.
A fetch draws on a real balance, so the ceiling is a person's to set and the plugin's job is to make a spend bounded and visible, never to decide one is worth it.
Cottony is the first consumer and not the specification, so anything it needs that a second project would not is configuration, and a default that cannot be overridden is a defect.
Everything runs on the developer's machine against files in their own repository, because a plugin that needs an account is one that fails on the day the account does.
Cottony is the first consumer and the test of whether the configuration boundary holds — an adoption that needs a fork proves that it does not. It is also where the claim gets settled: nothing today records how long a correct asset took before and after, so there is no way to tell a real improvement from a rearrangement of the same work.
Until that line ships, every number on this page is Cottony's cost and not polyweave's saving. That is the honest state of it, and it is why the benchmark is on the roadmap rather than in the pitch.