Conversation
Startup cost was unmeasurable: nothing between process start, server ready and first paint carried a timestamp. src/startup-timeline.js records named marks against one epoch. Main stamps LUBAN_START_T0 into its environment; the forked server inherits it and the renderer reads it off window.process, so marks from all three processes are comparable. Each prints one ordered table at the end of its own startup. Lives at the top of src/ so every entry point reaches it with no build change - src/*.js is babel-compiled for main, and both webpack bundles resolve ../startup-timeline. No behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A literal newline had ended up inside the template literals. Same output, but the escape is what was meant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
index.jsx would not render until signin had returned and the socket had emitted 'startup'. Neither is needed by anything on the home screen, and controller.connect() has no timeout, so an unreachable backend left the user on the spinner indefinitely. Render once i18n is ready; run signin and connect afterwards. i18n keeps a 3s cap so a stalled backend delays the paint by at most that. socket-controller now buffers on/once/channel registrations made before a socket exists and replays them on connect, because components mount before connect() is called. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The API layer used origin-relative paths and socket-controller did
io.connect(''), both of which only work while the page is served by the
backend. #3 loads the app off disk over luban:// before the server
exists, so neither will hold.
backend-origin.js owns the answer. It defaults to window.location.origin
for an http(s) page, so this changes nothing today, and otherwise waits
for main to hand it over: ipcMain answers 'get-server-origin' on demand
and pushes 'server-origin' once the server is listening.
No behaviour change; plumbing for #3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the splash wait. The window loaded app/loading.html and only navigated to http://127.0.0.1:<port> once the forked server reported ready, so the user watched a GIF for however long the fork took - 1.6s warm, 12-26s cold. The luban:// handler already serves app/** off disk, so the window now navigates straight there. The protocol registration, @electron/remote and the proxy config move ahead of the first load into prepareAppEnvironment(); startToBegin() keeps only bookkeeping. That makes every API call cross-origin, so the server answers CORS for luban:// origins - including the OPTIONS preflight superagent forces by sending Authorization and Cache-Control - and socket.io gets the cors option v4 requires. Three things the move exposed: - the scheme was registered without supportFetchAPI, so i18next's requests never reached the handler and always hit the 3s cap - express mounts the app directory at both / and /worker, so worker URLs arrive with a prefix that is not on disk; the handler now strips it, and reports a missing file rather than returning a stream that throws - components call the API as they mount, about a second before the server origin arrives, so those calls resolved against luban://. defaultAPIFactory now holds each call until the origin is known First paint 3275ms -> 1503ms warm, and now precedes server-ready rather than following it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #2. DataStorage's constructor logs from module scope and createServer() logs as its first statement, so the gap between those two lines is pure require() of ./app and ./services. Across 15 launches of my own log that gap was 12-24s cold and ~0.6s warm; a cold run of this branch's parent measured 26.6s. Nothing above the listener needs either module. The port now binds on a bare http.createServer whose handler parks requests, the ready callback fires immediately, and express, the machine channels, the slicer and the task workers load on the next tick. Parked requests are replayed once the app exists, so the gap refuses nothing. DataStorage.init() moves into the deferred block too - it must run after app.js, which installs the process-wide unhandledRejection handler that its floating font download relies on. The startup table prints at 'ready', which is now before all this, so the deferred phase reports its own line. Warm: bundle required 1081ms -> 369ms, server ready 1764ms -> 982ms, services ready at 1711ms and off the critical path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #5. Behind a VPN or offline the online-resource proxies never answered. All five logged the error and returned without touching res, so the renderer's XHR stayed open until the socket gave up. They now carry a 5s response / 10s deadline timeout and always reply, 503 on failure. getUserInfoData also stops calling agent.use() per request, which accumulated Authorization plugins on the shared agent and leaked whichever token was set last into unrelated calls. downloadManager had no timeout and no catch, so an unreachable font or calibration map left a floating rejection. It now aborts at 15s and resolves a boolean - callers treat these as best-effort. whenBackendOrigin() was unbounded, noted as a known gap in #18; it now rejects after 30s so API calls fail rather than queue for ever. CaseResource ran an access probe with no timeout at all, leaving a blank frame until its 60s iframe timer fired. Bounded at 2s, matching the home page probe. It was also mounted unconditionally and merely hidden with display-none, so every launch loaded an iframe from resources.snapmaker.com even for users who never open the Library; it now mounts on first open and stays mounted after. The home page says so inline - an empty grid with a label above the local examples - rather than popping a dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #21. Sentry.init ran at module scope, before app.whenReady, on every launch: 21 integrations, the Electron crashReporter, four OpenTelemetry globals and a read of its offline envelope store - all ahead of the first window, and all useless to someone offline or behind a VPN. Nobody opted in. It is now required and initialised only when the stored enableCrashReporting flag is set, which defaults to false. debug: true is dropped from the shipped config. electron-updater was imported at module scope and updateHandle() wired its listeners during startup, though nothing checks for updates until the renderer asks 200ms after mount. The wiring moves into wireAutoUpdater(), called on first use. node-fetch is likewise required where the changelog is fetched, its only caller. The flag is exposed as a Settings > General checkbox and a Settings menu item, both stating it applies on next start, backed by get/set-crash-reporting IPC. Main reads the store before any renderer exists, so the store is authoritative. Warm, same profile: app ready 497ms -> 76ms, first paint 1413ms -> 1055ms. Startup log drops from 106 to 69 lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
machine.json held a single server.{name,address,token} triple, so
connecting to a second machine overwrote the first machine's token,
forcing a touchscreen re-auth on every switch even though tokens stay
valid until the machine powers off.
Persist server.machines as [{name, address, token, lastConnectedAt}]:
- lookup by address first, name as DHCP-reallocation fallback,
most recent record wins on multiple matches
- upsert on successful connect, keyed by address
- legacy keys migrated on first read and still written after each
connect for external readers and UI auto-select
Fixes #6
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MCP Streamable HTTP endpoint (stateless JSON-RPC over POST /mcp) on its own loopback-only HTTP server, off unless LUBAN_MCP_PORT or configstore mcpPort is set. Hand-rolled transport: the official SDK needs Node 18+ and Electron 15 embeds Node 16. Tool registry plus one read-only seed tool, get_connection_status, backed by a new ConnectionManager accessor. Closes #7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_machine_profile reports build volume, per-toolhead work ranges and authored kinematics (SM2 family: platform travels in Y, toolhead moves X/Z) - null for machines whose kinematics are not recorded, so an agent cannot guess them. get_position reads the last heartbeat cached on SstpHttpChannel and reports work and machine coordinates (machine = work - originOffset, Luban's own convention), origin offset, homed state and report age. Closes #8. Closes #9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Motion leaves the process only as a gcode file through the same prepare_print/start_print path as Start on Luban, so the controller job state machine and door interlock apply (#23). submit_gcode_job stages a file and returns a confirm_url; a human reviews extents, feeds, spindle use and warnings in a browser and approving mints a one-time code that is never returned over MCP, so a model cannot self-authorise motion. start_gcode_job requires that code, an idle machine, and consumes the token whether or not the start succeeds. stop_gcode_job needs no confirmation. validate_gcode reports statically without state. Closes #12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
capture_frame reads the local USB webcam through ffmpeg DirectShow (mcpFfmpegPath/mcpCameraDevice) or an HTTP snapshot URL (mcpCameraUrl); list_cameras enumerates sources. Every frame is stamped with the firmware-reported position it was taken at. move_and_capture performs one bounded XY move at the current Z via the direct path - no Z parameter by design, Z and compound motion go through submit_gcode_job per #23 - waits for two settled heartbeats at the target, then captures. Guards: idle machine, toolhead off, per-call travel limit (mcpMaxJogDistance), build-envelope check in machine coordinates. The transport now passes tool-supplied MCP content through, so frames return as image content rather than JSON text. Closes #10. Closes #11. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Calibrations are keyed by the machine Y they were derived at (the SM2 platform travels in Y, so a pixel-to-machine mapping is only valid at that Y) and record Z since camera height changes scale; stored as a 2x2 pixel-delta-to-mm matrix in userDataDir, surviving restarts. visual_servo executes one clamped correction step per call through the same guarded single-move path as move_and_capture - the iteration loop belongs to the calling agent - auto-selects the nearest calibration by current Y, and warns when the step itself moves Y or the calibration is off-key. Deriving the matrix stays the agent's job; the store keeps it. Closes #13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verbose toggle on the console widget: heartbeat position and status are printed on change (work and machine coordinates), and MCP tool calls are mirrored live as one line each - name, duration, ok or error - via a new mcp:activity broadcast from the server. Off by default; toggling prints its own state so the mode is always visible in the scrollback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings gains an MCP Server section: enable toggle and port box, persisted to the server configstore (mcpEnabled/mcpPort) and applied at the next start, with a status line reporting what this run is actually doing - listening address and tool count, or not running - and whether LUBAN_MCP_PORT overrides the stored settings. Served by GET/POST /api/mcp. Legacy behaviour kept: mcpPort alone still enables when the mcpEnabled flag has never been written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator-defined vocabulary: home/homing is ALWAYS machine home - G28 to the limit switches - never the work origin. Moving to work X0 Y0 is the separate goto_work_origin operation: one bounded XY move at the current Z through the same guarded path as move_and_capture, Z deliberately untouched. Both tool descriptions now state the distinction so calling agents cannot conflate them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Parked socket.io requests replay into Express once services start and 404 there, because socket.io only attaches its request interceptor at startServices - so entering Workspace during the bind-to-ready gap broke the page and every reload until services finished. A prompt 503 with Retry-After lets the client back off and retry into the working server. Hotfix at the top of the MCP stack; belongs upstream in the startup stack (#38) and drops out when fixed there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hardware-observed on the A350: G28 invalidates the work offset while the controller keeps reporting the pre-home originOffset in the heartbeat, so derived machine coordinates land outside the build volume (Z 656 on a 325mm machine) and work coordinates silently stop meaning what they did. get_position now carries a warnings field that flags out-of-volume machine coordinates and says why; the home tool result points at it and tells the caller to re-establish the work origin before trusting work coordinates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… home Operator decoded the coordinate model from Luban's own Home button with the verbose echo: the controller has G53 (machine workspace) and G54+ (workspaces), heartbeat pos is in the currently selected workspace, and machine home is (-19, 342, 328). Luban homes as G53;G28;G54 - a bare G28 leaves positions reported in an unselected workspace, which was the source of the impossible derived machine coordinates (Y 464/Z 656). - home now sends the same G53;G28;G54 sequence as Luban's button and warns that G28 also homes B (stock on the rotary rotates; observed -45 to 0 on hardware). - Every direct-path gcode an MCP tool sends is mirrored to the verbose console with the controller reply ([mcp:home] > G28 / < X:...), so the operator can see which coordinate frame each command ran in. - New query_firmware_position tool returns the raw M114 response alongside the heartbeat view - the authoritative frame check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….json The skill predates the tooling: it was written when every frame was hand-pasted and gcode hand-relayed. Rewritten around the Luban MCP surface (position-stamped capture, guarded single moves, Y/Z-keyed calibration store, one-step visual_servo, M114 frame check, human-gated jobs) and the machine semantics verified on hardware since: G53/G54 workspace model, home = (-19, 342, 328) via G53;G28;G54, origins persist across homing, toolhead-mounted camera with the platform moving under it in Y, rotary homes with G28. The vision core is unchanged and board_metrology.py is bundled. The skill degrades gracefully to the hand-relayed workflow when no MCP is live. A project-scope .mcp.json at the repo root points sessions at the local MCP endpoint (127.0.0.1:40889/mcp), so agents working in this repo get the luban server without per-command --mcp-config flags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
HH:MM:SS.mmm on every verbose-mode line - sent-command echoes, heartbeat position changes, MCP activity and gcode mirror - so the operator can measure real latencies, e.g. the ~1s gap between a commanded move and the heartbeat reporting the new position. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Driven by a real submission: a job meant to 'drop Z 20mm' was written as G90 + G1 Z-20 - an absolute move to work Z-20, not a distance, which plunges below stock whenever the origin is at stock top. Three new warnings: absolute Z below zero with spindle off (suggests the G91... G90 relative wrap), motion before any G90/G91 (inherits whatever mode the controller is in), and ending with G91 active (Luban's convention restores G90). Report also carries assumesDistanceMode and endsInRelativeMode. Verified against the real submission and four variants; legitimate cut jobs are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hardware finding: the firmware parks back at the work origin when a prepare_print/start_print job completes, so the interlocked file path cannot hold a working Z - the only path that persists a Z change is the direct one, which the operator rule reserves for explicitly requested, carefully considered moves. move_z operationalises that rule: a SINGLE absolute Z move (spindle off, homed, idle, envelope-checked, feed capped at 600) staged through the same confirm page as jobs - which now shows a DIRECT MOVE banner stating the door interlock does not apply - with current Z, target, delta, feed and the agent's stated reason. Only the operator's one-time code executes it; the result waits for two stable heartbeats and reports the persisted position. Jobs gain a kind field (file|direct) and a completed state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two live-session frame misreads - the calibration board dismissed as a cutting mat, and the endmill never identified - drove a blind 80mm descent past a usable view. The skill gains a frame-reading section: identify by evidence not remembered composition (the board is the yellow-brown surface with printed black grid and cell labels), the rig-mounted-vs-scene heuristic (frame-position invariant across moves means camera-mounted), and the endmill's near-lens blur signature. capture_frame now reports the operator-configured expectedToolRegion box (configstore mcpToolRegion) with every frame, turning tool identification into a lookup on fixed rig geometry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A live sign-flipped calibration (M = -J^-1) drove two servo steps away from the target and was caught only by manual re-measurement. The tool contract held; the gap was no automatic safeguard. visual_servo now remembers the last step per calibration+target and warns the moment the pixel error fails to shrink - one wasted step instead of several. Both tool descriptions state the convention outright (error = target - feature; M = +J^-1; J.(M.e) must reproduce +e) and the skill's derivation step now includes the sign verification protocol. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dominant field error source was hand-estimated pixel coordinates: a misread hole position caused a ~50% calibration error, and the sign bug was only caught by manual cross-checking. Captures now carry a frameId (last 12 frames cached in memory) and track_feature template- matches a patch between two frames by id - zero-mean NCC on jpeg-js (already a dependency), returning the matched pixel, shift, confidence, and a second-peak gap that warns of repetitive-grid ambiguity. Verified offline: a synthetic (17,-9) shift recovered exactly at NCC 0.92 in 211ms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…amera, batch Z Five fixes from the first full calibration session: - set_camera_calibration accepts the fitted jacobian and REJECTS a sign-flipped matrix (M.J ~ -identity) before it reaches hardware; large identity residuals warn. - visual_servo auto-select no longer hard-fails at 2mm - a single step drifts Y 2-6mm - it picks the nearest entry within 25mm and keeps the scale-may-be-off warning beyond 2mm. - New set_tool_region tool so expectedToolRegion converges from live frames instead of config edits. - Camera device choice is sticky (last-good remembered, preferred) with one retry on transient open failure; a vanished device is an error, never a silent substitution to another (possibly dead) camera. - move_z accepts z_targets (max 20): the operator approves the exact list once, each start_gcode_job call executes one step with settling between, and the series can be abandoned anywhere - same safety property, one approval instead of N. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Observed live: a move_z to -95 returned completed while the heartbeat still reported -85 - two identical post-issue beats can both predate the motion because the heartbeat lags ~1s. Direct moves now parse the absolute Z target from the executed gcode (G53 wrap selects the frame) and the settle loop waits until the reported Z matches within 0.15mm; home requires the position to move off its pre-G28 value at least once before accepting stability (25s fallback when starting at home). On timeout the position is returned with position_verified: false and a warning naming query_firmware_position - never silently. XY moves already verified against their target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every motion tool now takes wait_until_moved (default true: block until the returned position is verifiably settled at the target). false returns as soon as the controller accepts the command - useful during the ~15-20s home or long Z descents - always with position_verified: false and a poll-get_position note, never a silently stale number. move_and_capture and goto_work_origin skip the frame too when not waiting (it would not show the commanded position); visual_servo deliberately has no such arg, since its contract is measure-after-step. move_z stages the preference on the job; start_gcode_job can override per call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… keep-out, groups, procedure stop, stock outline probe_program can now survey freshly clamped stock of unknown size from the jig geometry alone, in one approval (work plan: docs/NEW_STOCK_SURVEY_TODO.md): two-operand references (mid/diff/min/max, path-valued plus/minus, any depth), rotary axis + probe geometry stored through the MCP tool set_probe_geometry (never a prerequisite; no stock size stored - rotate_b takes a per-program swept_radius_mm) and seeded as the `axis` namespace, keep-out checks of every hop/column/march at staging and at reference resolution (stored landmarks are CROSSING obstacles - probing inside the rotary landmark is the job, marches and traverse-height hops are exempt; program keep_out boxes are volumes), summary highestAt/lowestAt, a circle expected_profile for cylinders, `group` expansion over B angles, a derived section (thickness, centring, width, yaw, end slope) on the result, and an event-budget refusal. probe_stock_outline (also op kind stock_outline): top at several points with hole rejection, sides marched from an over-extended estimate at top - depth, skipping along each face at a standoff with a dynamic march start, fit to centre (machine + work), size (+ tip), yaw. march.ts: shared march and the stepped touch-probing traverse (retreat on contact, capped); surface scans gain hop_mode stepped. stop_gcode_job now stops PROCEDURES (job 5ad5fcce6b3a: three stops answered ok:false while the scan kept stepping): cooperative stop request checked by every motion primitive, ProcedureStopped at the next step boundary, raise, state `stopped`, partial results (ProcedureAbort.partial) stored on the job record for every abort. probe_sequence marches take on_miss (default continue): a miss records no_contact instead of aborting the sequence. 27 unit checks; tsc and eslint clean; hardware pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e, returns an inspection report Fusion 360 / FreeCAD / any Grbl-Marlin post (or a hand-written file) can now supply the probing toolpath as gcode. The server parses and translates it (probeGcode.ts pure parser/simulator, probeCam.ts planner/runner) instead of sending it raw - the firmware compiles G38 in but on the 3DP probe input, not the CNC touch probe: G38.2/G38.3 become the shared sensor-gated march to the programmed target (the travel limit; retreat to the cycle start; G38.2 without contact aborts per Grbl unless on_miss continue), G38.4/G38.5 a probe-away until release, G0/G1 links follow law 2 (raise to the traverse height + guarded segmented descent, or a stepped touch-probing traverse), a bare G0 B<angle> is a 3+2 station (raise first, then a verified rotate_b), G4 dwells; G90/G91, G53 (absolute even under G91), G20/G21 honoured, feeds ignored, Grbl $ lines skipped; M3/M4, M0/M1, M6, G28, G92, arcs, macros, O-words, B with XYZ, incremental B and A/C refused with the line number. (PROBE ...) comments carry id/name/group/role/feature, nominal, normal, signed tolerances, surface offset and feature size/centre/tolerances; (RESULTS ...) the document/toolpath identity. The inspection report (inspectionReport.ts) is stored as JSON and rendered as Fusion 360 inspection results in the shape Autodesk's own result generator writes (START/RESULTSFILE/DOCUMENTID/MODELVERSION/TIMESTAMP, TOOLPATHID/ TOOLPATH, G331/G330 with the station's B, G800 nominal with O = surface offset and signed L, G801 tip-centre + R, END), as the Renishaw Inspection Plus printout Fusion imports for Probe WCS / Probe Geometry results (features reduced from group/role metadata, SIZE/POSN/OUT OF TOL/OUT OF POS), CSV or Grbl [PRB:] lines; deviations are of the surface (tip centre minus one tip radius along the normal). docs/FUSION_POST_REVIEW.md records the Opus review of the Snapmaker Fusion posts (none supports probing; the 4-axis post is an unmodified Fanuc post); docs/post/snapmaker-probing.cps is a probing-capable post written from scratch to its outline (unverified in Fusion); docs/CAM_TEST_CATALOGUE.md and docs/examples/cam-tests/ carry the survey of existing test material and the fixtures that drive the parser suite. 9 + 6 + 4 unit checks; tsc and eslint clean; hardware pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion prediction on the plan Operator idea 2026-09-07: a low-fidelity game-like scene of the A350 with the rotary and tailstock (operator-supplied CAD -> glTF with articulated nodes) that the MCP fills with verified objects from probe results and landmarks, ghosts for estimates, and that plays every staged procedure's motion list to predict the first collision before approval (verdict + frame on the confirm page). Architecture, coordinates/kinematics, scene object model, collision rules (intended contact vs fault, rotations, coverage), MCP tools, phases, operator inputs, risks. Specification only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Docs: Machine simulator specification - scene with provenance, collision prediction on the plan
Fix: Linux camera capture (v4l2), Ubuntu 24.04 AppArmor launch crash, mac fork builds
… /mcp
Claude Code / Claude Desktop run the MCP authorization flow against an
`http` server before the first JSON-RPC call and refused to connect when
the steps 404'd ("Dynamic Client Registration rejected (HTTP 404)").
oauth.ts answers every step - protected-resource and authorization-server
metadata (both well-known paths, with and without the /mcp suffix, plus
the openid-configuration alias), POST /register (RFC 7591), GET /authorize
(code + PKCE S256, redirects straight back: no login page, the confirm
pages stay the human decision points), POST /token (code and refresh
grants, rotation). It authenticates nobody: /mcp never checks tokens and
never 401s, so clients without the flow and tokens from before a restart
keep working. Redirect URIs must be loopback http(s) or a private scheme.
What the flow buys is attribution: the registered client_name labels
every `tool ...` log line and the initialize line.
Also fixes LAN mode: McpServer.handleRequest hard-coded a loopback
re-check, so a same-subnet client the index.ts gate had admitted still
got `403 loopback only` on /mcp. Both gates now share isTrustedAddress /
isTrustedOrigin with the mcpAllowLan setting. Every OAuth route sits
behind that same gate.
Verified: tsc (services/mcp) and eslint clean; a 41-check harness drives
the real router through discovery, registration, authorize, PKCE
mismatch, code reuse, token, refresh rotation, and /mcp with, without and
with a stale bearer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/TOOLS.md: one line per tool with what it does and when to reach for it, grouped by area (orientation, gcode jobs, direct motion, camera and vision, landmarks, probe feed, tool setter, touch-probe procedures, CAM probing), closing with the standing operator rules the tools assume. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Inspect cycles
First Fusion runs of docs/post/snapmaker-probing.cps (Inspect Surface, program
1001, Fusion CAM 2705.1.11, post engine 5.413.5), fixed from the post log and
the first complete output:
- onCommand swallows COMMAND_PROBE_ON / COMMAND_PROBE_OFF, which Fusion
issues around every Probe / Inspect Surface operation ("Unsupported
probe-on command ... onCommand() for record 400" killed the post). The MCP
arms its probe feed on the operator's approval, so nothing is written.
- Rotations and traverse raises always carry their G0 word: the raise made
G0 modal, so gMotionModal.format(0) emitted a bare "B0." / "G53 Z320.".
- Linking rapids between cycles are no longer echoed: each was turned into a
raise to the traverse height, three raises per point in the output. Every
probe writes its own law-2 approach and the MCP retreats to it itself.
- Inspect Surface targets follow Fusion's probing direction. Fusion's middle
cycle point is already the END of its probing move (nominal plus its own
overtravel) along the direction it chose, which for a chamfer is an axis,
not the surface normal. The post pushed that point a further 15 mm along
-normal, bending the G38.2 path and contacting elsewhere. Now the target
is Fusion's end point, extended along the same direction only until
minOvertravel lies past the nominal.
- Inspect Surface no longer relies on getNumberOfCyclePoints /
isFirstCyclePoint / isLastCyclePoint: points are buffered per cycle and
emitted from onCycleEnd in groups of three (approach, measure, retract),
two, or a lone measure with a synthesised approach.
- getCurrentCyclePointIndex is tested with typeof (bare reference to an
undefined kernel helper is a ReferenceError).
Posted successfully in Fusion after the probe-on fix; the direction and
linking fixes come from reading that output and still need a re-post.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Feature: OAuth/DCR shim so Claude Code connects; LAN gate honoured on /mcp
Docs: Terse per-tool reference for the 47-tool MCP surface
Fix: Fusion probing post - probe on/off commands, G0 on B rotations, Inspect cycles
…C skills New .claude/skills/cnc-motion-rules/SKILL.md is the single home for the seven motion laws, the coordinate doctrine (machine coordinates for planning; the work origin is the operator's; G90/G91 is distance mode, not a frame; every staged job declares its frame), the get_position reliability states, the sanctioned exceptions and a before-any-motion checklist. Written after the 2026-09-12 work-frame G0 Z0 job reached the confirm page with no warning. cnc-probing, cnc-visual-alignment and tool-change now point at it instead of carrying their own copies, and three stale facts found by the memory audit are corrected: heartbeat period is 2 s (not ~1 s), the probe length is read from get_stored_state (71.1 was pre-crash), and work origins do not survive a machine reboot. .claude/skills/README.md indexes the four skills. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ork frame An agent-authored gcode job now has to say which coordinate frame it runs in before it reaches the confirm page. validateGcode tracks G53 (own-line, sticky on this controller) and G54..G59 per line and reports the frame in force at the first motion line; resolveJobFrame settles it against the new submit_gcode_job `frame` argument: G53 in the file = machine; G54+ in the file or frame:"work" = work (a Luban/slicer export selects no workspace and is accepted unmodified); frame:"machine" without a literal G53 is refused; nothing at all is refused with the rule quoted back. Inline "G53 G0 ..." is flagged - the firmware does not honour a one-shot G53. G92, mixed frames, out-of-travel Z (in either frame, resolved through the live offset) and a work-frame absolute Z0 become loud warnings. The confirm page gains a Frame row and machine-resolved Z extents. Every MCP emitter declares too: move_z / move_and_capture work branches send an explicit G54, and the probeCam / probeOutline / probeProgram previews carry G53 like the other planners. The wire pattern G53...G54 is unchanged (operator decision 2026-09-14). First unit tests for the pure MCP modules: `npm run test:mcp` (ts-node, node:assert, no framework) with 15 validator cases including the 2026-09-12 "G90 / G0 Z0" transit job, now refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ent beats One judged machine-frame position replaces every hand-derived `work - originOffset`. New pure module machinePosition.ts judges each distinct heartbeat: a controller echo (position of record) outranks it; a beat whose derived machine value is more than 50 mm outside the travel, or whose raw fields jumped by exactly the offset (frame flip), or that arrived before any offset was reported, is REJECTED - the last accepted position is held with `reliability: awaiting-resync` until a coherent beat rectifies it (operator law 2026-09-14: such a reading is a mistake, never a position; ignore it, do not reinterpret it). Nothing is assumed. getPositionSnapshot now returns the judged `machine` plus `reliability`, `frame`, `reasons` and the rejected beat's `derived` value for diagnostics. assertFreshHeartbeat - already in front of every procedure start, direct move, job start and Z staging - refuses on awaiting-resync/stale, so a Z 555 artefact can no longer pass the traverse-height guard or a landmark clearance check; survey_bed and the tool-setter tools gate explicitly. The state (cached offset, zero-streak, previous raw, last accepted, trusted offset, echo record) is consolidated and forgotten on every (re)connection, because work origins die on a machine reboot. The gcode sequence counter moves into positionOfRecord.ts so tools/machine.ts can read the echo record without importing the channel code. The Workspace console no longer prints its own unguarded subtraction (the ">500" lines): Marlin:state shows the raw report, and a new mcp:position event carries the judged machine position and its reliability once per beat (UI-only broadcast, not recorded on the job). get_mcp_diagnostics reports rejected beats by reason, resyncs and disconnects. 13 unit tests cover the judge, including the recorded incidents (Z 555.7 / Z 656 artefacts, the zero-offset streak, the return from a G53 window). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mcpSafeTraverseZ now defaults to 328 (= home Z on the A350) instead of 320 (operator decision 2026-09-14). The crossing-landmark exemption for segments at or above the traverse height is removed from checkMotion: it existed because the rotary-axis landmark's clearance (328) sat above the old traverse height, and it let a traverse cross the rotary box - tailstock included, height unmeasured - with 8 mm of unverified headroom. At 328 a hop passes every stored clearance on its own merits; a hop below 328 (surface scan, stepped link) is checked like any low segment; marches stay exempt (they stop on contact); volumes refuse at any Z below their clearance. get_stored_state.limits reports safeTraverseZMm. README paragraph on job 34d787bdb2d7 rewritten to record the decision. Six envelopeChecks unit tests pin the behaviour (34 tests total). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README gains a "Coordinate frames and the position of record" section (frames on this controller, machine-coordinates doctrine, the reliability table and the judge's rules), a Safety-model bullet for the staging frame handshake, the missing modules in the file-stack listing (envelopeChecks, positionOfRecord, machinePosition, probeGcode, inspectionReport, programRefs, probeOutline, probeProgram, landmarks, diagnostics, jobTiming, tests/), `npm run test:mcp` in the pre-commit gate and the corrected tool count (47). TOOLS.md documents the `frame` argument on submit_gcode_job, get_position's reliability fields, the rejected-beat counters and the standing rules (machine coordinates, traverse Z328, the work origin is the operator's, canonical guidance in cnc-motion-rules). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s pair apply_tool_length_offset - the one sanctioned work-origin write - now refuses a stored measurement pair that is out of order or predates the machine's last (re)connection (work origins die on a reboot) unless old/new trigger Zs are passed explicitly; requires a reliable position of record; and its confirm page spells out what the touchscreen wizard would: the toolhead machine Z that does not move, work Z before/after, the work-origin Z offset before/after and where work Z0 lands on the machine scale. The generic G92 warning is replaced on this page by the plain statement that this IS the sanctioned path. machinePositionDiagnostics reports resetAt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ged like move_z The 100 mm move_and_capture cap kept forcing transport into hand-written file jobs - which is how a frameless "G0 Z0" got staged on 2026-09-12. The new traverse_xy tool is the transport twin of move_z: an absolute XY target or an ordered series (max 20) at the traverse height, ONE operator approval on the confirm page, one start_gcode_job call per leg on the direct path so the position persists. Refused unless the toolhead is already at or above mcpSafeTraverseZ (328) - raise with move_z first; there is deliberately no override. Every leg is checked against the stored landmarks (checkMotion) and every target against the travel; Z is never written; default frame MACHINE with G53 declared on every step (work-frame steps declare G54). The planner is a pure module (traversePlan.ts, 10 unit tests, 44 total) and the confirm header states frame, current position, Z-unchanged, every leg with its machine coordinates and distance, total travel and the sensor note. The direct-move settle check now verifies whichever of X/Y/Z the executed G1 line names (parseDirectTarget) instead of Z only. TOOLS.md, README (48 tools) and the cnc-motion-rules / cnc-visual-alignment skills point at traverse_xy for transport. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live on the box after the 328 change: home reports machine Z
327.9989959716797, and every "at or above the traverse height" test was
exact, so traverse_xy refused the very first transit from home ("327.999
below 328") and move_and_capture / survey_bed / the tool-setter travel
would have too. TRAVERSE_Z_TOLERANCE_MM = 0.05 (well inside the heartbeat's
resolution, unarguably top gantry height) now applies at all four
comparisons; the traverse planner plans its segments at the traverse
height when within tolerance so the rotary landmark (clearance 328) cannot
refuse a 1 um shortfall either. Regression test pins 327.9989959716797
accepted and 327.9 refused (45 tests).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stead of gating Two things the operator saw on live confirm pages (2026-09-14): - probe_point / probe_vector anchored their plan at the heartbeat's raw Z (327.999 for a 328 home), so the preview and the runner issued "retreat to 327.999" followed by "finish at 328.000" - two commands one micron apart, with heartbeat noise in a commanded position. A start Z within TRAVERSE_Z_TOLERANCE_MM of the traverse height is now the traverse height: one retreat to 328.000, no second line, and the travel limit is computed from the snapped start. - probe_surface_path refused more than 60 stations. A station count is a time and event budget, not a safety line: the path ceiling is now 400 (the grid's), and above 60 stations the confirm preview carries a WARNING with the estimated duration and job-event count. The operator law caps (z_safe_delta 20, hop 60, coarse step 1) are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Live on 2026-09-14 (job 885cb85b5f32): a 60-station surface scan stopped by stop_gcode_job surfaced the raw stop message with result: null - 52 measured stations were only recoverable from the event log. The runner's abort path tested `err instanceof ProcedureAbort`, which is false for an Error subclass once the prototype chain is lost in a down-levelled build, so the partial result it had built was never attached. ProcedureAbort / ProcedureStopped now live in a pure module with a marker property, an explicit prototype and isProcedureAbort / isProcedureStopped helpers, and every runner tests the marker. start_gcode_job stores the partial result on any abort, with the ending beside it. Every job now carries a structured `ending` (kind, reason, when, how many stations/ops were measured): completed, stopped-by-agent, stopped-by-operator, withdrawn, rejected-by-operator, crash-alarm, overtravel-alarm, unexpected-contact, controller-rejected, timeout, operation-failure, machine-stopped, completion-unverified - classified in the pure jobEnding.ts. File jobs log each pause (door interlock or operator pause) and say so in their ending; describe(), get_gcode_job_status and the stop_gcode_job note all report it. 6 new tests (51 total), including the lost-prototype case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n plans) Eight operator-realistic evals (the tailstock scan, the headstock X profile with unknown Z, a transit from home, running a Luban export, a tool change, a rejected heartbeat, stock flatness with unknown height, "don't bother me with confirmations") were planned by fresh Sonnet, Opus and Haiku agents with no memory, reading only the four skills and TOOLS.md, and graded on lawfulness AND operator time. Pass rates: Opus 99 %, Sonnet 96 %, Haiku 85 %, no-skill Sonnet 47 %. The critiques converged; this applies them: cnc-motion-rules: checklist first with "ask once" and a transit fast path; law 1 resolved against law 6 (a named procedure authorises STAGING, the page is the decision point, a program is one decision point); below-328 transits ask for the Z lift; chat-stated landmarks and clearance == 328; law 6 names every motion tool, wait_for_approval_ms, confirm-URL delivery and the "no confirmations" answer; law 7 allows file jobs for programs; work-frame case first in the handshake; operator-stated probe length = which probe, not the value; resync is passive with re-read thresholds; new S7 "Running a program someone else generated" and S8 canonical calls with the real argument schemas (traverse_xy, move_z, submit/start/status, run_tool_setter, the two-op find-then-scan probe_program and the reference grammar); incidents moved to an appendix. cnc-probing: 407 -> 191 lines. Laws are a pointer; leads with find-then- scan; unknown-Z route and its cost; measuring inside an unmeasured region (retiring a keep-out); op-selection table (probe_circle is vertical-axis only); spacing fencepost; hop_mode by spacing x slope; event budget at the point of decision; reading a profile (symmetry centre, not highestAt); bit_length_mm is a protrusion. CAM probing moved to references/, the bed survey moved to cnc-visual-alignment, which gains the viewing-pose arithmetic. TOOLS.md: `home` no longer claims to clear stale position state; argument lists on the job and motion tools. evals/evals.json (8 prompts, assertions) and evals/REVIEW-2026-09-14.md record the method, numbers, findings and the eval-set critique; run outputs live under .claude/skill-evals/ (ignored). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Iteration 2 rerun on Opus / Sonnet / Haiku against commit d9d466b: 99.0 / 96.8 / 83.4 % (iteration 1: 99.0 / 95.9 / 84.6). Schema guessing and estimate-as-start-Z are gone; Sonnet is at the minimum approval count on every eval. Review with per-eval tables and the ranked remaining costs: evals/REVIEW-2026-09-14-iteration-2.md. Skill edits (iteration 3), each tied to a graded failure: - Staging is half a call: §0 item 6 and every §8 canonical call show the staging tool with its start_gcode_job (Haiku omitted the start call in 4 of 7 motion evals). - Probe length: the store is normally set; a measurement is planned only after READING an empty store, never budgeted pre-emptively (Opus added a conditional approval on evals 1, 6, 7). - Law 6 loses the confirm_token aside (hedged by Sonnet in iteration 1, Opus in iteration 2). - §8 gains canonical set_probe_geometry and apply_tool_length_offset calls (guessed by every model) and a surface_grid op with the chuck-jaw keep-out note; tool-change step 5 shows the real call; probing skill names size_x_mm/size_y_mm. - §0: landmark test is against the SEGMENT, not the destination; a question asked is a question waited for. Eval set: [WAIT]-before-consumption and stage-then-start assertions on the motion evals, a jaw keep-out assertion on eval 6, a documented-shape assertion on eval 4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reran the eight evals on Opus / Sonnet / Haiku against the iteration-3 skills (fbeb8c0): 100.0 / 96.9 / 92.1 % (iteration 2: 99.0 / 96.8 / 83.4). Every staging call in all 24 plans is now followed by start_gcode_job; Opus's phantom probe-measurement approval and confirm_token hedge are gone; no model guessed a tool argument shape. Haiku's remaining misses (stages before waiting for its own question; reuses stand-in numbers; dropped expected_profile.circle on a cross-axis scan) and the iteration-4 candidates are in evals/REVIEW-2026-09-14-iteration-3.md. Eval set: the physical-height assertion on the cutting evals (3, 4) now names the object in the spindle (fitted tool after a change, probe during probing) instead of "the probe length", which rewarded the wrong subtraction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MCP: Frame handshake, machine position of record, canonical motion-rules skill
Job fd7fa6cb6396 (2026-09-16): run_tool_setter aborted BEFORE its travel - the deployed build's traverse check read the post-home heartbeat Z 327.999 as below 328 - and the unconditional "retreat to start height" then sent G1 Z205.500 from Z 328 at the home XY (174.5, 340): a 122 mm plunge inside the rotary landmark, with no XY move ever issued. Operator law: an abort retreats STRAIGHT UP to the traverse height, for every MCP procedure, never to a start height. New shared abortRaiseToTop (probing.ts): no motion on an overtravel trip, hold while a probe still reads contact, nothing sent when already at the top (float tolerance), otherwise one Z-only G53 move to mcpSafeTraverseZ. All eight procedures' abort paths use it; the along-axis "back to the start" legs of probe_point / probe_vector are skipped when the start is below the head (mayDescend). Pure decision planAbortRaise + mayDescend in traversePlan.ts with unit tests; confirm-page texts, README law 9 and the cnc-motion-rules skill (law 8, appendix A) updated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MCP: Aborted procedures retreat straight up to the traverse height
A completed run_tool_setter retreated to plan.startZ (trigger + longest-bit delta + clearance, ~235 on the rig) and left the head there, below the traverse height every following XY move must start from (cnc-motion-rules law 2). PR #90 fixed the ABORT path; this makes the SUCCESS path end the same way: a Z-only G53 raise straight up to mcpSafeTraverseZ, nothing sent if the head is already there (float tolerance), never back to the start height. stay_at_trigger (touchscreen manual-swap wizard) is unchanged: the tip is held in contact, no retreat at all. - probing.ts: factor the shared `raiseToTop` (trip = no motion, optional hold-if-triggered, skip at the top, else one Z-only move; returns the outcome and final Z); `abortRaiseToTop` is now a wrapper that uses the abort-* phase names and clears expected contact before the raise, exactly as before. The success-path lift keeps the setter channels expected (it starts in contact), as the old retreat did. - traversePlan.ts: `planAbortRaise` -> `planRaiseToTop` (same decision, both paths); new pure `planToolSetterEnd` (hold / raise / skip) drives the confirm-page end line. - toolSetter.ts: plan.endZ, Phase 6 via raiseToTop with `retreated` / `retreat-skipped` phases, result.finalZ reports where the head was left, header comment and confirm page say so. - tools/toolsetter.ts: description + plan.end_z. - Tests: three #91 cases (below top raises to 328; at top sends nothing; stay_at_trigger holds) - 57 passing. - Docs: README law 9, docs/TOOLS.md, skills cnc-motion-rules (law 8), tool-change, cnc-probing. Closes #91 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MCP: Tool setter ends at the traverse height, not the start height
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.
No warranty expressed or implied. Use at your own risk. Ensure the safety of yourself and others and property at all times, following local laws and safety requirements, use Personal Protective Equipment, and an Enclosure for the CNC.
You must always be next to the machine ready to perform an emergency stop or switch the power off.