Skip to content

feat: post-mortem fixes: per-side support spread, silhouettes, prime tower, project edits, re-sliceable exports, calibration (0.4.0) - #6

Merged
Shavvimal merged 44 commits into
mainfrom
fix/post-mortem-layout-estimates
Oct 21, 2026
Merged

Shavvimal merged 44 commits into
mainfrom
fix/post-mortem-layout-estimates

Conversation

@Shavvimal

Copy link
Copy Markdown
Owner

What

Before: shambu check grew 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. patch could 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 empty extruder_nozzle_stats key, 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-supports measures it with a solo slice and keeps a support mask, and check/build/sweep probe automatically when the estimate would split a plate or leave no tower spot, --no-probe turns 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. A wipe_tower spec key with auto placement from a port of Studio's tower size estimate, a TowerClear rule, process-level interface filaments counted for reach, RepairNozzleStats on every export and a verify that refuses a non-re-sliceable file. patch --replace-mesh / --add-filament / --move / --tower / --repair-nozzle-stats / --dry-run with declared touches and a save guard, shambu slice, a two-pass --no-check, and a logged shambu studio -- escape hatch. doctor --expect-version, bounded printer discovery, sweep --write-spec. Timestamped printer 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 (a timeout wrapper 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 in skills/shambu/references/00-findings.md sections 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-mesh gives 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-filament on the printed project slices after --tower, the seed that exited -102 gets its reach FAIL and slices once moved, --keep-going exports 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 via wipe_tower.width), and the real-CAD release loop from CONTRIBUTING.md on the data/ parts.

Checklist

  • make check passes (ruff, mypy, pytest).
  • Tests added or changed for the behaviour.
  • __version__ in src/shambu/__init__.py and version in .claude-plugin/plugin.json bumped together to 0.4.0.
  • CHANGELOG.md updated (the 0.4.0 entry).
  • No printer serial, access code, IP address or personal path in the diff.
  • Printer code stays read-only (subscribe + pushall only).

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.
…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.
- 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.
… 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)
…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)
… parts trigger the probe, one overflow line, one sweep probe cache
@Shavvimal
Shavvimal merged commit 68cc260 into main Oct 21, 2026
5 checks passed
@Shavvimal
Shavvimal deleted the fix/post-mortem-layout-estimates branch October 21, 2026 22:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant