Repository navigation
feat: post-mortem fixes: per-side support spread, silhouettes, prime tower, project edits, re-sliceable exports, calibration (0.4.0) - #6
Merged
Conversation
arp -an (numeric, no reverse DNS) with a 2 s timeout; the 8883 probe in a pool of 16 threads, 0.5 s per host and 2 s for the sweep; paho connect_timeout 3 s; the TLS handshake 5 s (paho cannot change keepalive on an open connection, so PinnedSSLSocket sets it), keepalive 10 s; and the whole read (config, discovery, MQTT) under a 15 s budget in a worker thread. On expiry the error names the step: "timed out after 15 s in mqtt". PrinterService times each step and reports it through on_step. pgrep gets a 2 s timeout.
doctor starts with a shambu section (version from __version__, package
path, install kind from direct_url.json, commit, expected, match). A
mismatch prints every section, then MISMATCH and the exact install line,
and exits 3; a malformed value is exit 2. --printer-timeout S (default 15)
sets the read budget. Each step goes into timings_s and one stderr line.
The printer error has its IP redacted. A missing Studio binary no longer
hides the other sections (exit 3 after them).
sweep --write-spec OUT.json [--pick NAME] writes the chosen variant as a
full spec with the input's outdir and a _sweep provenance key, and ends
with 'wrote OUT.json: variant ... (Studio estimate ...; baseline ...)'.
It refuses the input path, an unknown --pick, and --no-slice without
--pick (exit 2). The JSON becomes {results, written} only with the flag.
The shambu skill runs doctor with --expect-version 0.4.0. The setup skill installs the v0.4.0 tag, refreshes the marketplace before /plugin update, and reinstalls the tag in place of uv tool upgrade. The README marks the checkout install as development only. A test keeps every version literal in both skills equal to shambu.__version__.
- RepairNozzleStats patch (build and patch --repair-nozzle-stats): extruder_nozzle_stats [] or #0 becomes <nozzle_volume_type>#<count> per extruder, filament_extruder_variant / filament_self_index stay in block form, filament_volume_map / filament_nozzle_map get one entry per filament. A CLI re-slice of an unsliced export exited -100 "No valid nozzle found". - set_filament_map no longer collapses filament_extruder_variant to one entry per filament. - verify FAILs a file the CLI cannot re-slice; inspect prints the reason; RunResult.summary() appends the -100 exception line from stderr. - effective_filaments counts process-level support, interface, wall and infill filaments; InterfaceReach checks the interface extruder's reach with the brim margin plus 3 mm, and the packer honours it. - tower.py: Studio's prime-tower footprint estimate, auto placement in the shared reach with fallbacks, the TowerClear rule. Per-plate wipe_tower spec key (auto, a spot, or false); per-plate wipe_tower_x/y vectors; tower size written to the process; conflicting sizes across plates are an error. - spec: unknown keys become warnings (shambu spec lists them).
…rface filament 03-3mf-format section 8 (the -100 chain and the per-extruder keys), 06-slicer-rules sections 5 and 10 (process-level support filaments, the tower table and the plate-14 numbers), 07 row 21, 00-findings section 10.
- sweep --write-spec writes through an exclusive, uniquely named temporary file, so an input named <OUT>.tmp can no longer be truncated and removed. A failed write or rename removes the temporary file and raises BuildError with the destination and reason (one JSON error, exit 3). - The port probe uses daemon threads, so a blocked probe never delays the process exit. A probe that raises is logged and counted closed. A nonzero arp exit is logged. - Each printer read has its own attempt state. An expired attempt never calls on_step or writes timings during a later read. - running_pids returns None (unknown, with a warning) when pgrep fails or times out; pgrep exit 1 stays an empty list. Doctor shows 'unknown'. - StudioCLI.version raises StudioError for a malformed Info.plist or a missing CFBundleShortVersionString, so doctor still prints every section. - --printer-timeout must be finite, above 0 and under TIMEOUT_MAX. --expect-version uses fullmatch, so a trailing newline is exit 2. - printer --wait 8 gives a 15 s budget (wait + 7). A non-tag requested_revision shows as 'git revision <name>'. - README install lines pin v0.4.0, and the version-literal test reads the README too.
…te footprints, bottom-left packer
The 0.3.0 checker refused the plate-14 layout that printed: a 0.25 x height support
margin on all four sides, summed between neighbours, held off the bed edge, on
bounding boxes, packed in rows.
- layout.support_spread replaces support_margin: a per-side Spread (probe, estimate,
spec, none). Tree estimate min(22, max(8, 0.10 x height)), normal 5 mm.
- Clearance replaces NoOverlap: silhouette gap vs max(facing spreads) + 1 mm.
Under 1 mm is FAIL body; short against an estimate is a WARN ("the slice
decides"), against a probe or spec value a FAIL margin. Brims leave the pair rule.
- InsideBed drops tree spread (Studio clips trees to the bed); ExtruderReach checks
only the sides where the reach is narrower than the bed.
- mesh.silhouette / hull_2d and layout.RasterFootprint / BoxFootprint.
- BottomLeftPacker for by-layer plates (0 degrees); ShelfPacker kept for by-object
plates and filament groups. Placers return Placement(plates, unplaced) and never
raise; unplaced parts get one PLACEMENT NOT DONE line and skip geometric rules.
- Check.kind; report lines FAIL body overlap / FAIL|WARN margin rule / PLACEMENT NOT
DONE; to_json adds check_kinds.
- probe.py: check/build --probe-supports slice each supported part alone, parse
Support extents per side, cache under <outdir>/.probe.
- Exit -101 names the two spec keys from Studio's log; exit -21 explains Studio's
24 mm arrange spacing.
Outside the layout files: spec.py gains only PartSpec.support_spread (four
non-negative numbers, parsed and written back); facade.build passes probe_supports
through; cli.py adds --probe-supports to check and build; test_pipeline.py follows
the new margin fields and overlap text.
…redicate - extruder_nozzle_stats_new wins when non-empty (BambuStudio.cpp:4151-4155): reslice_problems, the repair and verify use the effective key; only empty or zero-count entries are rewritten, a repaired _new is copied to the plain key, and valid High Flow stats stay byte-identical. - tower.plan_project picks ONE size configuration that every auto plate accepts (fallback over all plates together); a fixed tower pins it; the blocking plate FAILs by name. Each step recomputes the footprint. - The obstacle test is a parameter (tower.Clearance, BoxClearance by default) for a silhouette binding. - tower.needs_support (enable_support, enforce_support_layers, raft_layers) drives effective_filaments and interface_uses; Smooth timelapse plans a tower on a one-filament plate.
…verb, --no-check second pass, studio escape hatch - edits.py: ReplaceMesh, MoveObject, SetTower, AddFilament as Patches with a touches declaration; EditSet saves to a temporary archive, re-opens and verifies it, then renames. - threemf.Project.save() refuses a change to an undeclared entry (bytes, additions, deletions), any 3D/* add/remove/reorder and any _rels change. - build: two_pass_slice runs checked passes per plate in their own folders and a --no-check pass only after an overridable -101 or -100 (slicing-warning line); build --slice --no-check. - cli: patch --replace-mesh/--move/--tower/--add-filament, shambu slice, shambu studio --. - studio: CliCommand.parse for the escape hatch.
… fixes Design review: - The shambu skill checks `shambu --version` first, then runs doctor with --expect-version. The setup skill says a usage error on --expect-version means a CLI older than 0.4.0. - doctor builds the shambu section first; a missing binary or a bad plist never suppresses it. - A subprocess test proves a stalled printer read (daemon worker thread) does not keep the process alive past its timeout. - --write-spec keeps "loaded" filaments and records in _sweep the presets and colours the measurement used, the Studio version and the sweep command that reproduces it; the written line ends 'measured with ...'. A mesh a variant moved under the sweep folder is copied to <OUT stem>_meshes/ beside the written spec. Rubric review: - The probe uses min(64, hosts) threads and a window of max(2 s, rounds x 0.5 s), capped by what is left of the read timeout. A window that ends early logs and reports 'N of M hosts probed'. - Discovery is a protocol with with_deadline(), timings and shortfall; resolve() no longer branches on ArpProbe. - A raising on_step callback is logged; the read completes. - The written spec takes the replaced file's mode, else 0666 under the umask. --write-spec checks the folder is writable before the sweep (exit 3) and refuses the --variants file and links of the input (exit 2). - One name for the read limit: timeout (READ_TIMEOUT_S, timeout_s). ArpProbe.sweep_s is probe_window_s. The printer's first step is 'credentials'. install_kind names 'uv tool' only when uv-receipt.toml proves it. Dead OSError handler removed; doctor's studio and profiles sections catch concrete types; rubric item 13 lists the seven documented noqa: BLE001 and why.
…, bounded read on a daemon thread)
…d brims, no export of unplaced parts - Copies: shambu's `count` copies are separate model objects (live: two <object> entries and two build items), so Studio's first-instance conflict check (ConflictChecker.cpp:244-245, PrintApply.cpp:140-160) sees every copy. Documented; test that a -101 on copy 2 names "<key> #2". LINE = 1.0 is described as shambu's policy: Studio refuses any 0.01 mm collinear overlap or crossing. - Export: Plan.unplaced / Plan.exportable. --keep-going overrides advisory checks, never an unplaced part or a placeholder filament. - Probe: keeps the support centreline cells as a mask in the footprint frame; pair_check uses it when present. Cache key includes the Studio version; each probe slices at the bed centre in .probe/<cache key>/ with its own log. SpreadProbe.measure returns ProbeResult(spread, mask). - layout.needs_support (Print.hpp:492-494) drives support_spread, the probe and support-filament pinning; a raft-only part gets a spread. - Filament groups get the union of their members' outlines; fixed parts keep theirs. - A brim past the bed or a nozzle's reach is a WARN brim (Brim.cpp:1011-1138); model or support crossings stay FAIL. The packer clips a brim only when it must. - REVIEW-RUBRIC item 5 names the new strategies; sweep.py notes that WARN is ok=True.
- Interface spread (brim margin + 3 mm) on all four sides of the packing margin; InterfaceReach reports the coordinate that fails (x or y), names extruder N (left/right nozzle). - verify: tower-planned keys compared against the resolved process, plus the tower size and each plate's wipe_tower_x/y; wipe_tower false with dual-material-interface verifies. - Nozzle stats: validated and repaired only where the machine needs them (multi-extruder or multi-nozzle, BambuStudio.cpp:6648), length checked against the extruder count; Studio's _new precedence throughout. - RepairNozzleStats never adds variant keys to a project without them, runs before the map patch, and a no-op patch keeps the archive byte-identical (save skips an unchanged project, copies to a new path). - _origins: no origin when the footprint is larger than the area (math.floor, inverted bounds), inside() guard; faster box clearance (grown rects once, per-column narrowing). - Tower sizes compared after resolving against the process; spec.warnings reach the plan; a fixed spot outside the reach is reported once (TowerClear); provisional reserve grown by the 3 mm clearance. - tower._f raises BuildError on an unparsable value; interface_uses raises on a bad slot; part-level support slots count only with support on. - Removed the dead ReserveClear rule, Context.reserve and layout.WIPE_TOWER; one name for the interface spread; tower.py Design paragraph; recipe text qualifies the 41 mm tower to the 218 mm example.
- replace-mesh refuses a shared mesh resource, compares canonical triangles (connectivity and winding) for the no-op guard, keeps an intentional sink, verifies before the rename. - add-filament: explicit key schema (Studio's filament_options_with_variant), slot presets built from the project's own settings, normalised values from a scratch export with the new preset alone (an N+1 export writes the new variant block first), flush matrix from a second export; one-entry vectors written out, ambiguous shapes refused. - two_pass_slice: a checked pass per plate, each pass in its own folder; only -101 or a -100 with Studio's slicing-warning line is overridden; slice pins filament_prime_volume. - move: the clearance test is one function (edits.CLEARANCE) for the footprint work to bind. - REVIEW-RUBRIC item 11: the touches contract; reference 03 and 00 sections.
…trict tower verify, stats per extruder) - Project.save: an unchanged project saved to another path goes through the temp file and os.replace. - command() writes wipe_tower_x/y whenever a plate has a planned tower (Smooth timelapse, one filament). - Tower verify requires every key planning wrote, finite numbers, and one wipe_tower_x/y value per plate (Studio's get_at falls back to the first). The test fake now writes what the CLI writes. - Nozzle-stat repair iterates the extruder count, broadcasts the last nozzle_volume_type, and takes each extruder's own nozzle count. - Column narrowing keeps rects within the 10 mm scoring radius; BoxClearance.grown is computed once.
- printer.py: Status gains read_at (UTC), speed_level (spd_lvl), speed_percent (spd_mag), stages (stg) and task_id. to_json shows the IP as <ip> and the masked serial only; printer filaments --json too. watch polls at most every 60 s while preparing, in the first 15 minutes of RUNNING and under 15 minutes left. With max_errors, a failed read yields a WatchError (IP redacted) and the watch goes on. - printer watch --log FILE writes one JSON line per poll (no IP, no serial) and marks the final state. --gcode3mf puts the file's M73 track in a header line. --max-errors (default 10, then exit 3) and --max-polls are new. - calibrate.py (pure): parse_m73, pairs_from_log, fit (whole-print or span ratio, startup offset, flags) and combine (a range, or a point estimate only for n >= 3 Standard-speed prints within 0.15). - shambu calibrate --log ... --add --show stores prints in ~/Library/Application Support/shambu/calibration.json (conf.py, atomic). - report: "Studio estimate H h M min", then the measured range or a calibrated time when calibration data exists for the printer model and material. - references 04 (time and job fields, stage ids, the watch log) and 06 (Studio's estimate against the printer: D.7 0.751, E7a 1.054).
…key, swept spreads, pocket candidates - probe: an E-only unretraction is not an extrusion (the cradle's -x spread read 9.1 for 14.9 mm); CACHE_FORMAT 3. The cache key hashes the complete resolved machine, filaments, process and part settings, slot, map, bed and Studio version. I/O errors become StudioError with the path; the version fallback is logged. Design paragraph in the docstring. parse_support_spread removed. - packer: plate 1 keeps its index when it holds only fixed parts, so a part pushed to plate 2 is unplaced and never exported. Candidates include the internal corners of placed outlines (a U's pocket), tried in score order. ShelfPacker applies x_max_of with an unplaced reason. - pair rule: the outline grown by the four spreads is tested against the whole neighbour, then side by side to name the side; the message gives the exact gap. - layout.clipped_at_bed: one classification; a raft keeps its bed margin on the estimate, spec and probe paths. - -101: names with spaces are split on known CLI names; shared CLI names are reported as ambiguous; an unreadable log is reported as such. - report: no "part(s) fit" line beside an unplaced part; a footprint that falls back to the box warns with the reason; check_kinds holds the kind and check_levels the severity. - naming: Placed.margin is Placed.brim, Placed.reserve is Placed.keep_out, "spread" and "brim" in messages, "line allowance" for LINE. Removed Spread.toward, Spread.turned, WIPE_TOWER, gap_between, hull_2d and the Placement shim.
…, bottom-left packer, support probe, tagged report lines)
- runs.py (new, behind facade methods): add-filament scratch exports, the verify slice (exit 0 and a readable log required), apply_edits, slice_project, studio_raw; cli.py only parses argv and prints. facade.build takes no_check; Pipeline.export takes it. - threemf.save(verify=): the verify hook runs on the temporary archive before the rename, so edits.py writes no file; EditSet resolves relative moves to absolute targets once. - slice: refuses an output that is the input (or a link to it), exports into <stem>.slice/ and replaces the output only after success; the combined pass is classified too. - edits: shared meshes inside one object and mesh_shared refused; anchor min keeps a sink; plate filament maps for reach; every instance of a neighbour checked; tower footprint against each used extruder; Hybrid nozzles match Standard; finite numbers; malformed data and refusals are EditError (exit 1); dry run checks the touches. - add-filament validates the whole schema, writes out one-entry vectors, reports every fallback and checks that the colour and extruder landed. - escape hatch: every path option, symlink and assemble-list model path stays in the tree. - naming: checks skipped / skippable, verify slice, brim, bounding boxes, min corner.
Identifiers: - A watch error is a category and a fixed message (error_category); it never quotes the exception, so no IP, IPv6, host or masked serial reaches the log, stdout or the logger. - Calibration prints follow a fixed schema (calibrate.validate_print): no other key, no IP, host or masked serial in a name, the log's file name and not its path, finite positive numbers. The refusal message is fixed. - printer raw prints public_raw(): no _topic, net, wifi_signal, ipcam or serial keys, secret keys redacted, text naming the IP or serial replaced. Fit: - Logs are read as sessions, one per header; each session is fitted against its own track. Lines must be JSON objects with typed fields; a clock that goes back, a changed task_id or a layer that goes back rejects the log. - Each print has a mid-print rate (layer 11 and up) and a startup offset (the intercept), fitted apart. A prediction is offset + rate x Studio minutes. - Flags come from every poll: speed level and percent, the printer departing from M73 R by more than 5 % (research C), pause (subtracted), FAILED, start and end. A point estimate needs 3 prints with none of them and rates that span less than 0.15, with float tolerance at both boundaries. - parse_m73 reads ;LAYER:n, checks unnumbered markers against the layer count, needs real M73 P R lines, and strips CRLF. CalibrationError is a BambuError. Storage and CLI: - The plate G-code is streamed from the zip (threemf.open_text_entry). - update_calibration() locks a file beside the calibration file around load-change-save. Write, lock and log failures are DataFileError (exit 3). - watch: --wait-for-start; the start window needs a real PREPARE or an early first poll; after an error the interval follows the last good poll; no sleep after the last poll. status --json ends_at is a dated UTC time. - Names: poll (a log line), print (a stored entry); the report line reads "plate 1: Studio estimate ...". Fixture renamed calibration_prints.json.
- slice: a fresh mkdtemp run folder beside the output, never an existing folder by name; the export is read back and summarised before it replaces the output. - add-filament: per-filament keys from Studio's filament key list plus the project-only keys, so a key the preset omits is extended with a WARN; a one-entry project-only vector takes the supplied value. - EditSet: each relative move resolves just before it applies; moves verify against the object's id, size and z range captured just before the move. - dry run and save share Project.materialise() before the touches check. - escape hatch: an assemble list is rewritten into the run folder with absolute paths. - the verify slice keeps its log on failure and names it.
…two-pass --no-check, slice and studio verbs
- Start evidence must be observed (a PREPARE poll, or a first RUNNING poll at layer 0); a missing layer is not a start. A layer poll without the printer's countdown sets the blocking flag countdown_not_logged. - The calibration file stores a session fingerprint (header, first and last poll) and the printer model and material only: no file name, no preset labels. Dedupe is on the fingerprint, so one session imported under three names is one print. IPs are found with ipaddress (::1, ports, zones); the serial, masked serial and access code from Studio's config are scrubbed from stored names and from calibrate's output. - Display names accept any printable text (brackets, slashes, Unicode); a name that cannot be stored is dropped, not fatal; a zero countdown ratio is valid. - calibrate handles each session on its own: a session with no print is skipped with a note, and the later sessions still count. - watch turns a ConfError (no access code) into one `setup` record and stops. - A corrupt zip member is a ProjectError; parser errors pass through. - A close failure after a write failure no longer hides it; a close failure alone is a DataFileError; temp-file cleanup is best effort. - _finite() treats an int too large for a float as not finite.
…d storage, streaming errors)
…zle stats per filament
… filament classifier, per-filament reach bounds, tower edits on the tower model
…, refuse an unsliced slice export, filesystem errors exit 3
…tline WARN kind, patch protocol defaults, dead code, tower type names, packer wording)
…n, changelog lines
…ing, open and discover verbs, tower fallback order, trigger queries
… 0.22 x height, slice names the -101 pair, tower checks after add-filament and per plate, inspect positions, calibrate span, per-volume and range filament scope, header scrub as a structure, dropped plates)
…e or leaves no tower spot (--no-probe keeps the estimate)
…and the cradle with at
… parts trigger the probe, one overflow line, one sweep probe cache
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Before:
shambu checkgrew every supported part by a quarter of its height on all four sides, summed two neighbours' margins, tested bounding boxes only and packed with a shelf packer that failed closed, so it refused as "three plates" a layout that Bambu Studio sliced and printed. The hand-back showed Studio's kinematic estimate as the time.patchcould change settings only, so a mesh swap, a second filament, a move or the prime tower meant unzipping the 3MF by hand. Every unsliced export carried an emptyextruder_nozzle_statskey, so Studio's CLI could never re-slice it (exit -100 "No valid nozzle found", read as "exit 156"). Three versions of the tool ran in one session and the doctor could not say which.After (0.4.0): per-side support spread (tree estimate
min(26, max(8, 0.22 x h))mm from four measured parts, 5 mm normal;--probe-supportsmeasures it with a solo slice and keeps a support mask, andcheck/build/sweepprobe automatically when the estimate would split a plate or leave no tower spot,--no-probeturns that off), the larger spread of a pair plus a 1 mm line allowance instead of the sum, raster silhouettes instead of boxes, a bottom-left packer with pocket candidates, tagged report lines (FAIL body overlap,WARN margin rule,WARN brim,PLACEMENT NOT DONE) and the slice as the final clash test with the two objects named on exit -101. An unplaced part never reaches Studio. Awipe_towerspec key withautoplacement from a port of Studio's tower size estimate, aTowerClearrule, process-level interface filaments counted for reach,RepairNozzleStatson every export and a verify that refuses a non-re-sliceable file.patch --replace-mesh / --add-filament / --move / --tower / --repair-nozzle-stats / --dry-runwith declaredtouchesand a save guard,shambu slice, a two-pass--no-check, and a loggedshambu studio --escape hatch.doctor --expect-version, bounded printer discovery,sweep --write-spec. Timestampedprinter watch --log,shambu calibrate(two measured prints disagree at 0.75 and 1.05 x Studio, so the hand-back says "Studio estimate" and a point estimate needs three qualifying prints). No IP or serial in any JSON, log or file. Skill text, references, eight evals and an error doc rewritten to match.Why
Post-mortem of the G2 rev E plates (2026-10-20 to 10-21),
docs/errors/2026-10-21-three-plates-and-uncalibrated-estimates.md. The research corrected three of its claims: the doctor never hung (atimeoutwrapper macOS does not have), exit 156 was the empty stats key not space, and D.7 was PETG at a standard-flow nozzle. Every new rule cites its Bambu Studio v02.08.02.61 source line inskills/shambu/references/00-findings.mdsections 10 to 12 (ConflictChecker.cpp, Brim.cpp, TreeSupport.cpp, PartPlate.cpp, WipeTower.cpp, BambuStudio.cpp, PresetBundle.cpp, ToolOrdering.cpp, PrintConfig.cpp, Print.hpp) or the measured plate-14 runs.Reviewed as it went: a Codex design review, six Codex worktree reviews with verification rounds, seven rubric reviews against REVIEW-RUBRIC.md and the skill-authoring rules, and three Codex rounds on the merged branch; every accepted finding has a test. Empirical replay of every shambu use that failed in the post-mortem session, on the actual specs, STLs and projects: the seven "three plates" specs check clean and slice (the printed layout: 17 h 58 / 478.9 g against Studio's 18 h 0 / 480.68 g), the hand mesh swap through
--replace-meshgives the same 17 h 2, the cradle project re-slices at 18 h 0, the PLA-interface plate builds and slices with a placed tower,--add-filamenton the printed project slices after--tower, the seed that exited -102 gets its reach FAIL and slices once moved,--keep-goingexports nothing on a placement failure, the sweep winner is written and built, and plates 11 and 12 are unchanged.Deferred with a note in the references: the packer does not find the nested one-plate layout of the five real plate-14 parts without
at(the cradle sits in the feeler's pocket; Studio's own arrange exits -21 on the same parts), rotation trials and a Studio-arrange layout fallback, a global candidate budget for dense concave plates, the tower size order (Studio's default first; the 22 mm tower saved 5 h on plate 14 but is a stability risk at 218 mm without a rib wall, so it is an option viawipe_tower.width), and the real-CAD release loop from CONTRIBUTING.md on the data/ parts.Checklist
make checkpasses (ruff, mypy, pytest).__version__insrc/shambu/__init__.pyandversionin.claude-plugin/plugin.jsonbumped together to 0.4.0.CHANGELOG.mdupdated (the 0.4.0 entry).pushallonly).