Repository navigation
Conversation
…ed file Two of the six operator approvals in the 2026-09-19 session were spent re-deriving G-code from refusal prose: "put G53 on its own line" and "declare the distance mode". The validator knew exactly what the file should say and made the agent write it out again anyway. suggestGcode() returns a corrected draft for the three edits that need no decision: an inline G53 split onto its own line, a missing G90 added first, and a trailing G54 when the file would otherwise leave the controller in the machine workspace. Anything needing a judgement - which frame an undeclared file meant, whether a G92 was intended - returns null; a suggestion is only offered when it is certain, and it is re-validated before being handed over. validate_gcode reports it as suggested_gcode / suggested_changes, and a staging refusal appends it under "Re-submit this instead". The submitted file is still never edited: this is the program the agent should have written, not a rewrite of theirs. The epilogue is judged on the SPLIT program, because an inline G53 selects nothing - which is the bug being fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ools traverse_xy and move_z exist because a hand-written transit keeps getting the same things wrong: on 2026-09-12 one reached the confirm page with no frame at all, and on 2026-09-19 one was rejected for an undeclared distance mode, restaged, and then left the controller in the machine workspace for the rest of the session. Both were staged through submit_gcode_job, which had no opinion about what the file was for. A cnc file that is nothing but transport is now refused at staging, naming the tool that emits the declared, frame-restoring, landmark-checked version. "Transport" is drawn tightly enough that real work is never caught by it: the file must move in XY only or in Z only - exactly what the two tools can express between them - with no spindle, no probing, no arcs, no B rotation, no G92, no relative motion, and no more than eight motion lines. A toolpath, a slicer export or anything that moves in XY and Z together is left alone. The validator gains motionAxes and usesProbing to say this precisely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clearance_z has always meant "minimum safe toolhead Z, tool length included by whoever set it". Conflating the obstacle's height with the fitted tool has two costs: the number has to be re-stated on every tool change, which nobody does, so it ends up set for the longest thing ever fitted - and it therefore sits at the machine ceiling. The rotary-axis landmark declares 328, the homing height, because it had to cover a ~73 mm touch probe on top of the hardware, and the README records what that cost: job 34d787bdb2d7 lost its last op when a hop at 320 OUTSIDE the rotary footprint was refused against it. Landmarks now carry clearanceBasis. 'physical' states the top of the obstacle itself, so the tool can be added at check time and the number stays true. set_landmark takes obstacle_top_z for that, and keeps clearance_z as the legacy form; giving both is refused, since they are the same number measured to different things. Nothing is re-interpreted: every stored record loads as 'toolhead' and is enforced exactly as before. Reading 328 as a physical height would demand a toolhead Z of 400, so the migration has to be the operator's word, not ours. The check itself is unchanged in this commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stating an obstacle's height physically only works if the server can say what is above it. Nothing reports which tool is in the collet: the tool setter measures one when asked, and the operator declares the longest bit in use and the touch probe's effective length once each. Any of the three could be fitted right now. resolveToolProtrusion takes the longest of them, with provenance. A measurement therefore only ever LENGTHENS the requirement - shortening a clearance on the strength of a measurement that may predate a tool change nobody mentioned is exactly the risky reading. When nothing at all is known the answer is null, never a guess, and the note names the tools that would fix it. run_tool_setter now records derivedBitLengthMm - what the tool actually protrudes, as opposed to the length declared to plan the descent - so the measured candidate exists at all. Measurements written before this carry no derived length and are ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A physically stated clearance now demands obstacle top + how far the fitted tool hangs below the toolhead + a 5 mm margin, resolved through the one place that reads the stored tool state (clearanceContext). With the touch probe fitted a 250 mm obstacle reproduces exactly the 328 that used to be hard-coded into the landmark; with a 2 mm engraving bit it asks for 257, which is where the machine gets its working room back. A physical obstacle whose requirement CANNOT be computed - nothing known about the tool - is impassable, not passable, and the refusal names the two tools that would fix it. A legacy 'toolhead' clearance is enforced exactly as before and is never inflated by the tool a second time. The direct-move guard had its own copy of the comparison, which is the one that actually fired on 2026-09-19 (move_and_capture -> obstaclesOnPath): it now shares the same requirement and the same float-noise epsilon, so the 327.999994-against-328 refusal is fixed on that path too, not just in the procedure planners. Every checkMotion caller passes the live tool length; traversePlan stays pure and takes it from its caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o restate An agent reading get_stored_state saw a landmark with "clearance_z: 328" and no way to know that 328 is a toolhead height with a touch probe baked into it rather than anything about the rotary. Every landmark now reports requiredToolheadZ - what its clearance actually demands of the toolhead with the tool currently resolved - and says in words where that number came from. Records still on the legacy basis carry the restatement they want, and the summary lists them together with the resolved tool protrusion, its source and the margin, so the migration is visible in one read instead of having to be inferred landmark by landmark. set_landmark echoes the same advice when it writes a legacy clearance, at the moment someone is thinking about that obstacle. Nothing changes in what is enforced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One number did both jobs: where a procedure hops, retreats and ends, AND the lowest Z any XY move could happen at. It sits at the ceiling because of the second job - the rotary landmark declares clearance 328 since that number had to cover a fitted touch probe, so the floor had to rise to meet it (README, job 34d787bdb2d7, where a hop at 320 OUTSIDE the rotary footprint was refused). With clearances now stating obstacle heights and the tool added at check time, the two separate. safeTraverseZ stays the park height at 328; motionFloorZ is new, defaults to 320, is clamped to never exceed the park height, and is what law 2 is enforced against - traverse_xy, move_and_capture, survey_bed and the tool setter's travel to the setter. Hops, retreats, probe start heights and every procedure's ending are untouched. The traverse-height exemption from crossing checks stays REMOVED: a hop at the floor is checked against every stored box exactly like any low segment, which is what makes a floor below the park height safe at all. planTraverseXy also stops planning its segments at the park height and plans them at the height the toolhead is actually at (snapped up only by the heartbeat's float noise). That was harmless while the floor WAS the park height and wrong the moment transport is allowed lower - it would have checked a corridor the toolhead was not in. Residual risk, stated plainly: 8 mm less blind protection for anything on the bed with no landmark. mcpMotionFloorZ is the one setting that reverts it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_stored_state.limits reports motionFloorZMm alongside safeTraverseZMm, so a fresh session reads both numbers rather than inferring one from the other. README law 2 becomes "at or above the motion floor", says plainly that the floor is not the park height, records why it could only drop once clearances stopped carrying tool length, and states what it costs: 8 mm less blind protection for anything on the bed with no landmark. Law 4 now describes obstacle_top_z, how the tool protrusion and margin are added, that an unknown tool makes a physical obstacle impassable rather than passable, and that legacy records are enforced unchanged until re-stated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A camera bootstrap has to solve against features whose machine coordinates are already known, and it needs POINTS, not just a line: the rotary axis fixes X and Z, and naming an end fixes Y as well. set_probe_geometry gains rotary_tailstock_y and rotary_chuck_face_y, which also settle which end is the chuck - "the non-chuck end" was a guess in the 2026-09-19 session and cost several poses. rotaryAxisPoints() returns whichever of them is stated as a full 3D point. set_tool_setter_config gains disc_diameter_mm: nothing in the tool setter needs it, but a circle of known size in a frame is an absolute scale constraint that does not depend on the pose solution being right. No landmark topZ field is needed for this - a landmark stated with obstacle_top_z already carries the physical height of its own top face. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator law, 2026-09-19: the camera is not a rig constant. It can sit differently after every power cycle, be knocked, be re-aimed, or be a different camera entirely. So camera geometry is session state. CameraModel carries where the camera is (offset and rotation relative to the toolhead), what it sees (intrinsics, with k1 only when the targets constrain it), the Z band its poses actually covered, its residuals, and a state that says whether any of it may still be used. judgeCameraModel decides that on evidence rather than age: a different camera or a different resolution needs a full bootstrap; a reconnect needs a verification, because nothing about a camera survives a power cycle on trust; and a model that has only ever agreed with the data it was fitted to has demonstrated nothing, so it stays unverified until it passes a pose that was not in its own fit. An unnamed device is unknown, not different - an MJPEG URL names none. A new solve never overwrites the old one: the previous model is kept superseded with its residuals, so "was the camera moved between these two jobs?" stays answerable afterwards. Nothing consumes the model yet; this is the type, the store and the judgement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The question the 2026-09-19 session could not ask was "where must the toolhead go to see this?". It guessed from a remembered "the camera looks -X, 90-150 mm", moved 30 mm to find out, and spent three operator approvals establishing a sign. cameraGeometry answers it from the model: viewPose returns the toolhead position that puts a machine point dead centre, whatever direction the camera looks and whatever the tilt, with the standoff it implies. machineToPixel and pixelToMachine are inverses of each other on a STATED Z plane - the plane is always an argument, never a guess, because a single frame cannot say how far away what it sees is, and assuming one plane for a feature on another is what read ~4x wrong on the board. fovAt measures the real footprint across the frame's own corners, which is what makes an overlapping survey computable at all. jacobianAt regenerates the legacy 2x2 so visual_servo is untouched - that matrix was always this model linearised at one Y and one Z, it just had no model to come from. Everything works in machine coordinates, which is why one rigid transform is enough: the gantry carries X and Z and the platform carries Y, but the machine frame is the tool relative to the work, and in it the camera centre is the toolhead position plus a fixed offset. Nothing converts through a model that is not verified, a pixel outside the region the fit constrains is refused rather than silently converted, a point behind the camera is reported as such, and a toolhead Z outside the band the poses covered is flagged as extrapolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tools around the model. get_camera_model reports it with its state and, when it is not usable, why and which tool fixes it. set_camera_model stores a solve from the bootstrap script, fingerprinting the camera from a frame taken right then, and refuses a rotation whose columns are not an orthonormal frame - a rotation that is not a rotation skews every conversion silently. A stored model is always UNVERIFIED: agreeing with the data it was fitted to demonstrates nothing. verify_camera_model is what makes it usable - it predicts where a target of known machine coordinates should appear at the current toolhead position, compares that with where it actually appears, and records the residual in pixels and millimetres. Beyond tolerance the model stays unverified and the note says plainly that the camera has probably been moved and that no pixel should be converted through it meanwhile. It is also the first camera call of any session, which is the point: a model is bound to a connection epoch, so a reconnect demands this check before anything trusts the geometry again. No motion - position the toolhead first. requireCameraModel is the gate the pixel-consuming tools will use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two staged procedures, one approval each, because the second cannot be planned until someone has looked at the first. "search" is the step that needs no calibration to mean anything, which is why it comes first: a serpentine grid at the park height across the X band the camera could be looking from, bracketing the tool setter, whose machine XY is known exactly. Which frames contain the setter, compared with the toolhead XY of those frames, gives the camera offset INCLUDING ITS SIGN with no prior assumption whatever. The 2026-09-19 session reached the same grid only after forty minutes of single poses and an assumed offset that pointed the wrong way. "poses" visits the poses that coarse offset implies and sweeps Z from the park height to the motion floor with XY STATIONARY - a vertical baseline, not a diagonal through unknown space - capturing at every stop. Targets at different heights over that baseline are what turn a flat pixels-per-millimetre figure into perspective. The planning is pure and tested. Transport is always at the park height and the head returns there before the next XY move; a descent is segmented and crash-guarded like any other. The camera looks into keep-outs on purpose, but the toolhead does not enter one: a pose it cannot reach is dropped with a reason, a sweep that would descend into a box keeps the stops above it, and nothing is ever quietly adjusted. For a descent every obstacle is judged as a volume - the 'crossing' exemption is for probing inside a footprint, not for looking at it. A bootstrap with no target of known machine coordinates measures nothing, so that is a refusal naming the tools that would fix it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…odel scripts/camera_bootstrap.py turns a bootstrap frame set into the payload set_camera_model takes. Ten unknowns - the camera's offset from the toolhead, its orientation, and a pinhole's fx/fy/cx/cy - fitted against sightings of points whose machine coordinates the machine already knows: the tool setter's centre and plate top, the rotary axis line and its named ends. It refuses to call a fit a solve when it is not one. Fewer than six observations, or fewer than three distinct poses, is a refusal that says why: the offset and the focal length trade off against each other from a single standoff, which is exactly what the Z sweep exists to separate. Detection is optional and deliberately conservative - a wrong detection is worse than none, because the fit believes it - so hand marks are a first-class input and always win. k1 is never fitted here: a distortion term from a handful of points would be noise wearing a physical name, and the cost is carried honestly by central_region, computed from how much of the frame the targets actually covered. --self-test recovers a known model from synthetic sightings, then does it again with the marks jittered by 1.5 px, because a solver that only works on exact input is not a solver for this. The half-turn case in the Rodrigues inverse is handled explicitly: a camera looking straight down at a bed IS a half-turn from the identity, so the naive form divides by zero on the most ordinary mounting there is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
plan_view_pose answers "where must the toolhead go to see this machine point?" from the verified model, so the direction the camera looks and the sign of its offset are measured facts rather than something remembered. It returns the toolhead XY, the standoff, the field of view on the plane named, and whether the toolhead Z asked for is outside the band the model was solved over. On 2026-09-19 an assumed "the camera looks -X, 90-150 mm" pointed the wrong way and cost three operator approvals to find out. visual_servo takes an optional plane_z. Given one, and a verified model, the 2x2 is derived from the model at this exact pose instead of read from a stored calibration - the stored matrix IS that model linearised at one Y and one Z, and unlike the model it cannot say whether it is still about the camera that is plugged in. It is derived as an ordinary entry, so the sign check, the depth-plane cross-check and the series memory are all untouched, and the warnings say which was used and on what plane. With neither, the refusal now names both routes rather than only the stored matrix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Seamless" is a relationship between the grid pitch and the field of view, and until the camera model existed nothing knew the field of view - so the pitch was a number somebody chose and the frames overlapped, or did not, by luck. survey_bed takes overlap_fraction and plane_z. Given an overlap, the pitch is computed from the model's real footprint on that plane: pitchForOverlap is pure and tested, including that a 30% pitch really does leave about 30% shared. An absurd overlap is clamped rather than producing a pitch of nothing, and pitch_mm still caps the result. It needs a VERIFIED model and says so - the field of view is the whole basis of the number, and guessing it is exactly what this replaces. plane_z is stated, never inferred: a frame cannot tell how far away what it sees is. The envelope the operator approves carries the pitch and where it came from, including a warning when the Z is outside the band the model was solved over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-09-19 session wanted camera shots at several heights. It had to stage that as a separate job, which then could not be withdrawn, and the agent ended up telling its operator in prose not to approve it. survey_bed takes z_levels: up to six full passes of the grid, highest first, one approval for the series. Each level is entered with XY STATIONARY, so the survey is a stack of flat passes and never a diagonal through unknown space, and every level must be at or above the motion floor unless the operator has explicitly said otherwise - the refusal names the levels that are too low rather than silently clamping them. Frames carry their own Z and the index lists the levels, so a mosaic can be built per plane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The point is not prettiness. Once every frame is placed where the model says it belongs, "the hole is at mosaic pixel (u, v) on plane Z" becomes a lookup through the index's own affine - instead of what the 2026-09-19 session was reduced to, which was inferring a position from one frame and a remembered scale that turned out to have the wrong sign. One mosaic per Z pass, written beside the frames, with its machine bounding box, its millimetres per pixel, the pixel-to-machine affine and the fraction of the canvas no frame covered. Machine Y runs UP the picture and the affine says so. Each frame is drawn by inverse mapping over its own footprint: for every mosaic pixel it could cover, the model is asked which of its pixels that machine point projects to. Hole-free where a forward splat would leave gaps, and it costs the mosaic's area rather than frames times pixels. A whole-bed mosaic is coarsened rather than refused - the scale gives way, not the coverage. Seams are not blended. A hard edge at the right coordinates says more than an average of two disagreeing views, and where two frames disagree that is evidence about the camera rather than something to smooth over. Without a verified model there is no mosaic and the index says so, naming camera_bootstrap; the frames themselves are still all there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Where a second frame claims a mosaic pixel the first already drew, the two saw the same ground from different poses. Comparing them costs nothing during the render and is the only free evidence available about whether the model still describes the camera. A pure brightness difference is not disagreement - auto-exposure does that between any two frames - so it is subtracted before judging. What is left is structural: the same ground landing in different places, which means the camera was knocked, re-aimed or swapped since the model was solved. Past the threshold the model is marked unverified there and then, with a note naming verify_camera_model and camera_bootstrap. Too little shared ground is inconclusive, never a pass and never a condemnation: the note says so and points at overlap_fraction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Law 2 becomes a floor: XY transport at or above machine Z320, with the park height (Z328) named separately as where procedures hop, retreat on abort and end. The skill says plainly that the floor could only drop once clearances stopped carrying tool length, that there is still no exemption for being high, and what it costs - 8 mm less blind protection for anything on the bed with no landmark. Law 4 becomes the obstacle's own height: state obstacle_top_z, the server adds the fitted tool and a margin, a measurement only ever lengthens the requirement, and an unknown tool makes a physically stated obstacle impassable rather than passable. With the probe fitted a 250 mm obstacle still demands 328; with a 2 mm bit it demands 257. The position-of-record section gains the case that cost the 2026-09-19 session: a controller left in the machine workspace, which no amount of waiting fixes, is recognised after three beats and cured by restore_work_frame - not by a re-home, which only works by accident because it emits a G54 on the way. And the hygiene that stops it: every machine-frame job hands the frame back, which is the real reason to use traverse_xy and move_z rather than writing the file yourself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skill used to state the camera's offset as fact: "the camera looks -X, seeing roughly 90-150 mm to the toolhead's -X side", with arithmetic to match. On 2026-09-19 an agent followed it, went to toolhead X 290 for a feature at X~170, moved +30 mm to check, watched the workpiece slide further out of frame, and was corrected to "260 is about the max" - three operator approvals to establish a sign that one measurement settles. That section is gone. In its place: the camera is session state, so verify_camera_model is the first camera call of every session, with a table of what each outcome means and which tool fixes it; camera_bootstrap solves the geometry from nothing in two stages, the first of which needs no calibration to mean anything; and plan_view_pose answers "where must the toolhead go to see this" from the measured model. The old number survives only as the cautionary tale, quoted as the thing that was wrong. The survey section leads with "survey first, single poses second" - the same session found what it wanted in the first grid it ran, after forty minutes of single poses - and documents overlap_fraction, z_levels, the machine-indexed mosaic and the seams as the drift check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two new dry-run scenarios. "view-unfamiliar-workpiece-and-map-its-top" checks that a model solved two days ago is verified before any pose arithmetic, that a survey beats a chain of single poses, and that a position is read off the mosaic rather than inferred from one frame and a remembered scale. "camera-knocked-and-controller-stuck-in-g53" puts both of the 2026-09-19 faults in one prompt and grades diagnosing them separately, curing the position with restore_work_frame rather than a re-home, and not computing a pose until the camera model is verified again. Every eval gains two assertions: transport at or above the motion floor with the park height kept distinct, and no assumed camera offset, field of view or viewing direction. Eval 0 loses the hint it used to carry - "the toolhead camera looks -X and sees ~90-150 mm to its -X side". Handing an agent that number is exactly the mistake being fixed, and grading it on following the hint would have rewarded the behaviour that cost the session. The visual-alignment description no longer advertises Y-keyed calibration as the mechanism. These are the scenarios, not a rerun: the fresh-agent evals across Opus, Sonnet and Haiku still need running against the new skills. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six tools were added across this stack and none of them was in TOOLS.md, which is the file an agent reads to find out what exists. Discoverability is not a footnote here: the 2026-09-19 session hand-wrote transit gcode while traverse_xy sat unused, and reached a camera grid only after forty minutes of single poses. Adds restore_work_frame beside the direct-motion tools, a "the camera model" section that leads with the camera being session state rather than a rig constant, and the new survey_bed arguments - overlap-derived pitch, z_levels, the machine-indexed mosaic and the seams as a drift check. The plan doc records what was built and, as plainly, what was not: the fresh-agent eval rerun, and any exercise against the live machine, which was not reachable from this session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live 2026-09-19 on the box: the KB2040 U2IF bridge enumerated on USB, Blinka resolved its board id, and the first digitalio.DigitalInOut() never returned - the bridge was wedged. The transport reported "GPIO monitor produced no ready line within 30000 ms" and retried quietly for eighteen minutes. Nobody saw anything: the machine was not connected, so the Workspace pills were not on screen, and the only other signal was connected:false among forty fields. Probing was never unsafe - assertChannelReady already refuses without a live feed - but finding out when you try to probe is not good enough for a sensor whose whole job is to be watching. Two changes, both about saying what is true: The monitor now emits progress as it comes up (imports done, each pin configured), so a ready timeout can be classified. "Blinka loaded and the bridge answered as KB2040_U2IF, but configuring the first pin never returned - the board is enumerated on USB but not servicing requests; UNPLUG AND REPLUG it, nothing in software can reset it" is a different fault from "the interpreter produced nothing, check the venv and adafruit-blinka", and the old message covered both. And the status carries a sentence: how long it has been down, how many attempts, what it blocks (every probing procedure and tool-setter run, because the overtravel tripwire cannot be armed without it) and that nothing needs restarting once the physical cause is fixed. It rides on get_stored_state, which is the call every session is told to make first. The judgement is pure and tested, including that a wedged bridge is never told to reinstall Blinka and a failed import is never told to replug the board. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…g a human The previous commit called this a wedged bridge and said to unplug and replug it, "nothing in software can reset it". That was wrong, and the operator said so: it is a previous instance not releasing its reference. Confirmed on the box. `lsusb -t` showed the KB2040's HID interface as Driver=[none] instead of Driver=usbhid - a libusb claim that a monitor killed by the 30 s ready timeout never reattached. The board still enumerated and `import board` still resolved KB2040_U2IF in about a second, which is why this looked like dead hardware; only the first digitalio.DigitalInOut() blocked. USBDEVFS_RESET on the device node fixed it outright: before If 2 Class=Human Interface Device, Driver=[none] after If 2 Class=Human Interface Device, Driver=usbhid pin configuration 40 s+ hang -> 1.17 s, D2 idle as documented No root needed - the node carries a plugdev ACL - so the transport now does it itself. On a ready timeout that carries the leaked-claim signature (board loaded, no pin configured) it resets the bridge and retries, and the error says what it did. Node has no ioctl and this transport already requires a Python interpreter, so the reset runs there. The scan is deliberately narrow: only known U2IF vendors, and only a device whose HID interface actually has no driver bound. Resetting USB devices at large is not this module's business. Non-Linux hosts skip it rather than failing - the retry is still what recovers. Verified against the real device in its healthy state: the helper finds the KB2040 and correctly declines to reset it, "HID interface still bound to its kernel driver". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Probe feed: say what a dead sensor bridge means, and recover it automatically
Fix workspace jog steps using hidden saved values
The .zip files under .claude/skills are the packages uploaded to another agent. They were built on 2026-09-15 and never rebuilt, so while this branch carries the corrected camera doctrine in SKILL.md, the packaged copies still told an agent "the camera looks -X, seeing roughly 90-150 mm to the toolhead's -X side" - the exact folklore that cost the 2026-09-19 session three operator approvals and sent the toolhead the wrong way. Rebuilt from the sources in this branch. The visual-alignment package also gains scripts/camera_bootstrap.py, which was never in it, and the motion-rules package picks up the two new eval scenarios. Built with a sorted walk so the archives are deterministic: a diff between two builds means a real content change rather than a reordered directory listing. Checked by reading back out of the archives - the old arithmetic appears zero times as an instruction and survives only inside the paragraph that quotes it as the thing that was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… links Operator rulings of 2026-10-17 (issues #241 and #242). #241 - "if we've got to top z there's no approval for move at top z": - safeZPolicy.ts (pure, tested): traverse_xy, the XY step of goto_tool_change_position (its no-op Z step skipped) and an UP-only move_z to no higher than the traverse height run on the call when the position of record is verified/heartbeat, the machine is homed, the head is already at or above mcpSafeTraverseZ and the route passed the landmark/travel checks. - The job record is kept and marked approval.source "policy" (dashboard, log, timing, stop_gcode_job unchanged); the result says executed_without_approval: true, or staged_because when it staged. - Setting mcpTraverseWithoutApprovalAtSafeZ (default on; env LUBAN_MCP_TRAVERSE_WITHOUT_APPROVAL_AT_SAFE_Z) on the MCP Server pane. - start_gcode_job's direct-step runner is extracted (executeDirectStep) and reused, so the policy path executes exactly what an approved job would. #242 - probe_sequence retreated to Z328 between every station (~59 s of every 98 s station on the FR4 re-registration): - Opt-in local_links {clearance_z, max_hop_mm?} (sequenceLinks.ts, pure, tested): hops between stations at a stated toolhead machine Z, each <= max_hop_mm (cap 60, the surface scans' bound), checked against every landmark/keep-out at that height; after a march the probe retreats to its start and the next hop raises only to max(clearance, current Z). First hop and final exit stay at the traverse height. clearance_z may be a bounded program reference to an earlier top touch. - cnc-probing gains a re-registration recipe (one top touch when the plane is known flat, inside probe_circle on a known hole, one edge pair + one perpendicular touch; opposite pairs only for pre-travel calibration) and cnc-motion-rules / planning.md / TOOLS.md / README describe both changes; skill zips rebuilt. Closes #241 Closes #242 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e-local-hops Feature: Run top-Z transport on the call and add probe_sequence local links
Fix: Add adaptive probing and explicit setter datum evidence
tools/toolsetter.ts now imports clearanceContext, landmarks, safeZPolicy, traversePlan and tools/gcode for the #241 top-Z policy leg of goto_tool_change_position; the isolated toolDatum fixture refused them as unstubbed (9 failures on the startup/base run 39075606273). The pure modules load as themselves, the server-bound ones are inert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Test: Stub the tool-change park policy dependencies in toolDatum.test
The operator asked for "F400 to 10 mm above the copper, then F60 for the
last 10 mm" as one staged job. move_z took one feed for the whole series
and the hand-written file was refused as pure transport, so the standard
fast-then-creep approach had no sanctioned path.
- z_targets entries are a number or {z, feed_rate?} (plain numbers keep
working); a step without its own feed runs at the call's feed_rate.
Resolution lives in the new pure moveZPlan.ts (resolveZTargets): every
feed is clamped to MOVE_Z_FEED and every clip is REPORTED - on the
confirm page (`; CLAMPED:` header lines) and in the result
(feed_clamps), via the new procedureLimits.clampReported.
- The confirm-page header shows each step's feed and each step's gcode
carries its own F word (the controller's modal F survives between
files). decideSafeZRaise is unaffected: it judges machine targets only.
- The pure-transport refusal (validator.TRANSPORT_REFUSAL) was what
refused the issue's file (Z only, three rapids, no spindle); it now
points at the per-step move_z form.
- Tool description, docs/TOOLS.md and the cnc-motion-rules canonical
calls (zip rebuilt) describe the new form.
Tests: tests/moveZPlan.test.ts (registered in run.ts); validator.test
proves the issue's exact file is pure transport and the refusal names
the new form.
Closes #240
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes #142. set_enclosure_light / set_enclosure_fan {percent 0-100 | state on/off} and get_enclosure_status are first-class MCP calls: no motion, no confirm page, and never a hand-typed M-code. ConnectionManager gains readEnclosureReport / setEnclosureLevel, which make the same channel calls the Workspace enclosure widget makes (SSTP GET/POST /api/v1/enclosure, SACP getEnclosreInfo / setEnclosureLight / setEnclosureFan, the serial M1010 S3/S4) and return the outcome instead of emitting it; SstpHttpChannel's enclosure poll and setters now share two promise helpers so both paths use one endpoint and one parser. mcp/enclosure.ts reads every channel's raw report into one status shape (present, light/fan percent, door detection, door open, reason) and parses the requested level; the set tools read the status first and refuse when the machine reports no enclosure, then read it back and report whether the readback matches. Tests cover the argument and status mapping; TOOLS.md and the cnc-motion-rules confirm-page table (a no-motion row) are updated and the skill zip rebuilt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Issue #187: an agent staged a floor probe of an internal pocket deeper than the probe's exposed stylus, and only the march's abort at its no-contact limit kept the probe body off the rim. The probe's tool length (the setter contact offset) says nothing about how far the tip reaches below the body. - stylusReach.ts (pure): checkStylusReach judges the deepest commanded toolhead Z against a stated rim (rim_z_machine / alias surface_z_machine, the toolhead Z at which the tip touches the highest surface under the body) using the stored probe_stylus_exposed_mm (tip bottom to the first body shoulder) minus the ball minus reach_margin_mm (named limit STYLUS_REACH_MARGIN_MM, default 2, 0-10). A refusal carries the numbers and the toolhead Z below which the body lands; no stored reach yields "stylus reach unknown: body collision not checked" on the result and the confirm page; a stored reach with no rim is reported as rim-unknown. - rotaryGeometry: stylusGeometry() / stylusReachForPlan(); get_stored_state geometry.stylus exposes reachMm, bodyDiameterMm, tipDiameterMm. - Planners checked at staging and reporting result.stylusReach: probe_point, probe_vector, probe_sequence (deepest descend / march limit), surface path/grid (floor), probe_circle (side-contact Z), probe_corner, probe_wall_follow and probe_trace_perimeter (z_machine); the three rim arguments are on every schema (RIM_ARG_SCHEMA) and resolve as references inside probe_program. - tests/stylusReach.test.ts registered in run.ts. - Skill text: cnc-probing jig facts row, the "probe tool length != tip length" rule and planning guidance; cnc-probing.zip rebuilt; TOOLS.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
submit_gcode_job and validate_gcode only took an inline `gcode` string, so a real CAM file (295 KB on 2026-10-15, 870 KB / 46 720 lines on 2026-10-17) could not be staged through the MCP at all, and the one attempt that was made submitted a truncated copy that reached the confirm page with nothing but an "M3 without M5" warning (#237). Both tools now take the program from exactly one of three intakes, and every route is validated, frame-resolved and staged identically: - `gcode_path`: a file the Luban server reads from an allow-listed directory - `<data dir>/mcp-jobs-inbox/` (created on demand, reported by get_stored_state.jobIntake and on every intake result/refusal) plus one optional extra directory (`mcpJobInboxDir` / LUBAN_MCP_JOB_INBOX_DIR, Settings -> MCP Server -> Job files). Paths outside the roots, any `..` segment, and symlinks/junctions resolving out of a root are refused after realpath; only regular files pass. - `upload_id`: begin_gcode_upload {name, total_bytes, sha256} / append_gcode_chunk {upload_id, seq, text} (ordered, idempotent on an identical repeat, refused on a different one or a gap) / finish_gcode_upload (byte count and SHA-256 verified; a mismatch discards the upload). validate_gcode peeks, submit_gcode_job consumes on success. Untouched uploads expire after 30 min; at most 8 open. - `gcode`: inline, for short programs. Limits are named in procedureLimits.ts with their reasons: the 4 MiB program cap is McpServer's request-body cap, so no intake exceeds what one call could already carry; chunks are 256 KiB. Every job now carries a program identity (bytes, SHA-256, lines, motion lines, first/last motion line, last line) on jobManager.describe and in a "Program identity" table on the confirm page, so the operator can tell a staged program is the complete file. The validator reports spindle.onAtEnd and spindleRefusal() turns a program whose spindle is on and never turned off, or still running at the end, into a refusal at both tools, with what was received. Tests: tests/gcodeIntake.test.ts (path allow-listing incl. an injected symlink escape, chunk assembly and hashing, expiry, truncation refusal, identity lines, and synthetic 300 KB / 870 KB work-frame CAM files staged by path and by upload giving the same report and identity), registered in tests/run.ts. Docs: TOOLS.md and cnc-motion-rules §7/§8 (zip rebuilt). Closes #237 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The geometry store modelled the rotary axis as one rotary_axis_x / rotary_axis_z_physical for every Y, so probe_program's seeded axis.x handed every op the same X whatever its Y station - on a rotary yawed ~1.2 deg off machine Y that is 5 mm of X over 250 mm (issue #155). - rotaryAxisLine.ts (pure): the axis as a line through measured points {x, y, z_physical, source, measured_at} at two or more Y stations; derived yaw_deg / pitch_deg, the x / z at any Y, the seeded axis namespace and its per-Y re-evaluation. Two stations fix the line, more are least-squares fitted with the worst residual reported; stations at the same Y refuse. - rotaryGeometry.ts: rotary_axis_points stored (config / env LUBAN_MCP_ROTARY_AXIS_POINTS); the legacy scalars become the line's values at the reference Y (derived once points exist, refused by hand); a store with only the scalars loads unchanged as yaw 0 flagged skew unmeasured. get_stored_state -> geometry.rotary carries referenceY / yawDeg / pitchDeg / skew / points and geometry.rotaryAxisPoints the stations with provenance. - set_probe_geometry: rotary_axis_point (one station, add or restate at its Y) and rotary_axis_points (the list; null clears), each with source and measured_at - the rotary-axis check's side-pair X and opposite-face Z at the Y it measured them, fed to the store where it is measured. - probe_program: axis.x / axis.z_physical / axis.z_contact are evaluated at each op's own Y station (programOps.opStationY reads it from the op's arguments) before that op's references resolve; the reference grammar is unchanged (dotted paths, no axis.x_at(y) call - the choice is documented in probeProgram.ts). The job record phase op-<id>-axis says which Y and what the axis came to; the confirm page states the line and its skew. - rotate_b: insideSweptCylinder measures the tip's perpendicular distance from the yawed, pitched line at the toolhead's live Y (envelopeChecks.ts SweptAxis / distanceFromSweptAxis); the scalar form is the zero-slope case. - procedureLimits: MAX_ROTARY_AXIS_POINTS (8) with its reason. - Tests: tests/rotaryAxisLine.test.ts (two points -> yaw / pitch, x and z at y, three-point residual, refusals, unmeasured fallback, namespace per Y, cylinder check straight vs yawed, point validation) registered in run.ts; opStationY cases in programOps.test.ts. - Skills: cnc-probing references/rotary-axis.md (record each station, axis.* per op Y, the cylinder check on the line), cnc-probing SKILL.md store table and geometry paragraph, cnc-motion-rules section 2 (the axis is a measured line) and the reference grammar; both zips rebuilt. MCP README / TOOLS.md / CAMERA_SURVEY_PLAN.md updated to match. Refs #155 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…177) Frames from the traverse height (machine Z328) cannot resolve a contact point or a pocket wall. The program `capture` op now takes an optional `z` - a MEASURED or operator-stated toolhead machine Z, in a program a bounded reference to an earlier top touch, never a guess. - programOps.captureOpArgs accepts z (finite, 0..MAX_STATED_MACHINE_Z_MM); the new pure captureMotion lists the op's moves (viewing hop + descent column, or the column alone where the head stands) for envelopeChecks, so the staging check covers the column against landmarks and the program's keep-out volumes; the runner re-checks from the live position. - probeProgram: z may be a bounded reference (validated, previewed at the mid-point, REFUSED outside its bounds at run time like any other op number). After the viewing hop the runner does the guarded descent as a sequence descend step does - probing.guardedDescent, new: from the verified position, descendInSegments to DESCENT_GUARD_MM above, then 1 mm sensor-checked steps at the coarse feed, any contact aborts - waits for the machine to settle, frames (result.statedZ, result.descent), then raiseToTop STRAIGHT back to the traverse height (a held probe stops the program), so the op still ends raised. Needs the probe channel. - programEnvelope.describeCaptureOp enumerates the column (the fast segments, every guarded 1 mm step at F100) and the raise as the moves they are; the program header counts captures that descend. - DESCENT_GUARD_MM moves to procedureLimits (pure, for the describer); probeSequence re-exports it for its importers. - Tool description, docs/TOOLS.md, cnc-probing and cnc-camera-operations skill text (zips rebuilt). Tests: programOps.test (z validation, captureMotion, a column into a keep-out volume refused), programEnvelope.test (the rendered column, Z extents 215..328, header count). Closes #177 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Feature: Accept large G-code jobs from an inbox path or chunked upload
Feature: Store the rotary axis as a measured line with yaw and pitch
Closes #218. The MCP HTTPS listener runs from mkcert files, but a browser that has never seen the local CA refuses the origin (Chrome offers no exception) and system notifications on the confirm and dashboard pages need a trusted HTTPS origin. Two plain-HTTP routes on the MCP server fix that: GET /ca.crt serves the public CA certificate (application/x-x509-ca-cert, attachment luban-mcp-rootCA.crt) and GET /trust is an HTML page with the download link, the CA fingerprint and install steps for Windows, macOS, Linux, iOS and Android browsers; a request for either over HTTPS redirects to the HTTP listener. The CA file is the new optional setting mcpHttpsCaFile (env LUBAN_MCP_HTTPS_CA; api-mcp GET/POST, the Settings -> MCP Server pane with i18n) or, when unset, rootCA.pem under `mkcert -CAROOT` (one execFile, cached, warmed at service start). The key is never served: a file that parses as a private key - or carries one beside the certificate - is refused, as is a non-CA leaf certificate, and the page warns when the HTTPS server certificate was not issued by the CA it serves. The pane links to /trust once HTTPS is configured and shows which CA file would be served; the job dashboard shows the link from status.json. caTrust.test.ts covers the setting precedence, the mkcert cache, PEM classification, the refusals, the redirect and the page; README (settings table and HTTPS section) and TOOLS.md document the routes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ca-trust Feature: Add enclosure light/fan tools and serve the HTTPS CA trust page
…166) "1 mm x/y traverse but 5 mm lunge": the operator found the 5 mm descent segments risky. The law is not changed silently - 5 mm stays the default and the cap - but the segment length becomes a named, bounded limit a procedure can LOWER per call where the column is not proven clear. - procedureLimits.DESCENT_SEGMENT_MM: Bounded {default 5, min 1 (the march segment), max 5}, with the reasons written next to it: a descent waits on the controller's ok per segment, the crash guard is asynchronous and latches within the segment in flight, so 5 mm is the most the head travels after a hit; 1 mm everywhere would make a 300 mm descent from home ~300 settle-verified moves for no gain on a proven column. probing.ts re-exports it; TRACE_COARSE_STEP_MM shares the cap. - probing.descendInSegments takes options.segmentMm (clamped into the limit) and reports the length it used; guardedDescent passes it on. - descent_segment_mm on probe_sequence / probe_program `descend` steps (per step), on the surface scans (the approach to station 1) and on a capture at a stated Z. Every clip is REPORTED: `; CLAMPED:` on the page and the plan carries the requested value. - Every confirm page states the segment length it will use: sequence and surface pages the step's / scan's own, the other describers (circle, corner, outline, trace, wall follow, tool setter, probe cam) the named default instead of a literal "5 mm"; the program header says "<= 5 mm segments, or the shorter descent_segment_mm an op states". - cnc-motion-rules law 5 gains the descent-segment paragraph (why 5 is the default, when to lower it); cnc-probing names the argument; both zips rebuilt. docs/TOOLS.md and the tool schemas describe it. Tests: procedureLimits.test (the bound, clip reporting, segment counts), programOps.test (capture descent_segment_mm, needs a z), programEnvelope .test (the page states 2 mm / reports a clipped 12); probeInspection fixtures carry the new plan fields. Closes #166 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ndoff Issue #188: during the 2026-09-23/24 datum setups the operator needed the touch probe brought to the stock, slowed near the surface, stopped on contact or at a measured datum and LEFT THERE for the touchscreen work-origin confirmation; the existing tools did not compose into one approved, sensor-gated approach that holds, and a continuous descent was proposed instead. This is the stock-side counterpart of run_tool_setter stay_at_trigger, built on the proven engine. - approachHold.ts (pure): the plan (raise if below the traverse height, hop there to x/y, guarded descent to start_z_machine, sensor-gated march along dx/dy/dz - default -Z - bounded by max_travel_mm, the toolhead travel, machine Z 0 and an optional stated datum_z_machine), the confirm-page text that enumerates every move plus the hold and its release, judgeApproachEnd (contact IS the datum with on_contact "hold"; UNEXPECTED CONTACT with "abort" - never continued to the target; the stated datum reached released is the positioning success; the bare limit is an abort), planHoldRelease (trip -> nothing; operator moved the head -> Z-only raise, no back-off, held if still triggered; angled triggered hold -> back off along the reverse vector, verify release, raise; straight -Z -> the raise is the release) and the handoff text. - probeApproachHold.ts: the runner. Refuses at staging without an armed probe channel; checks landmarks/keep-outs (law 4) and the stylus reach (#187); contact at the median is confirmed with the release / fine / confirm cycles and the HELD position is driven to and reported separately (hold_at contact | released). The hold is a real pause: the job stays started, job.hold / the "holding" event carry the held machine XYZ, the measured contact, the sensor state and the handoff; the loop watches the stop request, the trip latch, the sensor and the heartbeat (touchscreen jogs are reported as operatorMoved and drop the position of record). stop_gcode_job is the sanctioned raise: release along the vector, then straight up; a probe still triggered after the back-off is held, never dragged; the measurement and the hold leave on the stopped job's result. - jobs.ts: McpJob.hold on the record and in describe(); stop_gcode_job description says it ends a hold with the raise. - tests/approachHold.test.ts registered in run.ts (plan defaults and refusals, datum bounding, travel/bed clamps, page enumeration, end judgement incl. contact at the last increment, release planning, handoff). - Skill text: cnc-motion-rules law 8 and a S4 sanctioned-exception entry; cnc-probing "Origin-setup handoff" section with the canonical calls, the op table row and planning.md; both zips rebuilt; TOOLS.md and README. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stimate T8c (job 96cf38d16b74, alpha.5) closed the QuadEink pocket lap but left four residuals (#185). Every number below is from that job's result (timing.byKind, trace.perimeter) or the T8b fixture. 1. Corner fits read low off a narrow slice. The greedy wall extension gave every wall-to-arc transition point (within the tolerance of the line AND the circle) to the wall, so the chuck -X circle was fitted to 61 of 90 deg (r 4.06 for a ~4.4 corner). perimeterAnalysis now extends an arc's fit set over the ends of both adjacent walls by the split that minimises the two models' total squared residual - a point goes to the model that explains it better, judged over the whole transition - capped at the arc a tangent circle can subtend between the walls (180 - interior angle + the tolerance band's width). The corner reports arcSpanDeg, extendedPoints and lowConfidence (span under CORNER_ARC_SPAN_CONFIDENT_DEG = 70: the radius rides on a short sagitta and reads to a tenth, not 0.1 mm). On the real laps: T8b chuck +X (the corner with a verified centre) r 4.76 -> 4.63 vs the known 4.68, centre 0.15 -> 0.11 mm off, span 74 deg; T8c chuck -X 4.06 -> 4.17 over 80 deg; the two exits that are real facets (0.13 -> 0.005 mm off the wall line, on the circle) move the other two fits 0.1-0.17 the other way; the spread of the four fits tightens from 0.70 to 0.46 mm and every span is on the record. The result note says "r over N deg [LOW CONFIDENCE]". 2. Two fixed 3.5 s waits: the confirm cycle's own march retreated one 0.1 mm step from its coarse contact and waited the full release timeout while the switch's hysteresis held, then the next step released at once (2 of 9 cycles, 7.0 s). marchToContact's release retreat now waits the standoff window (standoffReleaseWindowFor, 500 ms at GPIO 50 ms) on its FIRST step - it lands on ground the march sensed free - and the full timeout on every later step and on the step whose answer is the fault (back at the start or at the retreat cap): marchCore.releaseRetreatPatience. 3. No net speed-up: the profile is 4250 commands at ~270 ms of transport + controller overhead each (1169 s of 1489), motion 172 s, sensor windows 131 s - the runner has no redundant settle or sense left; every command is one sensed <= 0.1 mm move into unproven ground or a retreat to a proven point, and batching coarse steps would breach the 1 mm sensed-move law. What the step kinds do show: 315 of the 645 fine contacts are the 6 mm straightness window walked at fine steps after the first wall and after every corner (50-61 each). straight_points already governs that window (3 halves it; the smallest radius that passes as straight falls from ~45 to ~11 mm, chord^2 / 8 tol); the tool description, confirm page and skill now say so instead of a code change the lap cannot verify. 4. The estimate said 5 confirm cycles and 7..61 min; T8c ran 9 in 24.8. expectedConfirmCycles follows the confirm_at cadence: the first wall, then per corner its first block ('unexpected') plus one per major_turn_deg turned inside it ('turns') - 2 per 90 deg corner at the defaults, 9 for four corners; 'turns' alone spreads over the lap's 360 deg; the corners share that turn whatever the bounds' aspect ratio. TRACE_CYCLE_SECONDS 0.9 -> 1.5 (876 contacts: advance + ~1.7 bumps + ~2 standoff retreats), TRACE_CONFIRM_SECONDS 10 -> 4, march 0.33 -> 1.0 s per mm, and the best case counts the straightness windows at fine steps: 330 mm is 16..84 min. probeProgram's eventBudget uses the corrected worstCycles / confirmCycles unchanged. Tests: perimeterAnalysis (T8b corner spans, extension bounds, a synthetic R3 corner whose 60 deg slice is flagged), perimeterTrace (confirm-cycle cadence, calibrated estimate, release retreat patience). Tool description, README and the cnc-probing skill (+ zip) updated. Closes #185 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Feature: Per-step move_z feeds, capture at a stated Z, bounded descent segments
The isolated probeInspection surface plan predates the stylus-reach check; describeProbeSurfacePlanAsGcode now prints the verdict's note, so the fixture carries the reach-unknown verdict the planner would have produced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stylus-reach Feature: Add probe_approach_and_hold and refuse probes beyond the stylus reach
…er-followups Fix: Perimeter tracer T8c follow-ups - corner spans, confirm lifts, estimate
jobs.ts now computes every job's program identity through ./gcodeIntake (#237); the isolated jobModalRecovery fixture refused it as unstubbed (4 failures on the startup/base run 39080339896). The module is pure, so the fixture loads the real one, as jobDashboard.test already does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stub Test: Supply gcodeIntake to the jobModalRecovery job fixture
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.