Methodology

Every drawing is one Claude Code session running Fable 5.2. It gets your picture, a short prompt and a handful of robot tools. It writes its own code to turn the picture into pen strokes, checks them, and drives a Franka FR3 to draw them. We wrote no drawing code, no font and no layout logic.

The harness

The harness is built on ARC, our platform for operating robots over the internet. An ARC eval normally runs a policy model on the robot. Here the policy is an MCP client: the eval's endpoint is mcp:fable-5.2, and the robot waits for tool calls instead of model actions. The run is recorded like any other eval, which is where the replays come from.

  1. Your uploadThe picture goes to private Blob storage and a row joins the queue in SpacetimeDB.
  2. RunnerA small Node program claims the next job and starts one recorded eval run on ARC for it.
  3. Claude Code, headlessclaude -p with Fable 5.2, the picture, the prompt below and one MCP server. One session per drawing.
  4. NorthstarARCHosts the MCP endpoint, checks the token and finds the robot's running eval. Each tool call goes to the robot over the same connection teleoperators use, and the result and camera frames come back.
  5. Robot PCARC's robot runtime turns tool calls into checked joint commands for the Franka FR3 at 30 Hz.
  6. Pen on paperA black pen taped to the closed gripper. Two cameras watch: one over the page and one on the wrist.

The tools

The robot exposes the physical primitive and nothing more. What to draw, where on the page and how to letter the name is Fable's job.

get_taskThe task text and how much time is left.
observeJoint positions plus the latest frame from each camera.
get_pageThe sheet size in mm, and the box of every drawing already on it.
draw_strokesPolylines in page millimetres. dry_run plans the whole call (IK for every point, timing) without moving.
get_end_effector_pose, move_end_effectorSmall Cartesian moves, at most 10 cm and 30° per call.
move_joints, homeJoint targets inside a safe box, and the FR3 ready pose.
finish_taskEnds the run. The pen lifts and the arm goes home.

Drawing without measuring the pen

The pen is taped on, so where its tip sits is never measured. Instead we guide the arm by hand to three corners of the sheet with the tip on the paper and record the hand's pose at each. Every drawing move then holds the hand at the average of those orientations. With the orientation fixed, the tip is a constant offset from the hand, so the plane through the three recorded points is the paper. A point in page millimetres maps straight to that plane.

What the robot side enforces

  • Every point of a draw_strokes call goes through FR3 inverse kinematics before anything moves. A point it cannot reach accurately, or a jump in a joint, refuses the whole call with the reason.
  • Joints stay inside a safe box, speed is capped at 1 rad/s, commands go out at 30 Hz, and one call moves for at most 90 s.
  • The gripper stays closed while a page is calibrated, so the pen cannot be dropped.
  • Before going home the pen lifts off the page, so it never drags across someone's drawing.
  • Each finished call is logged on the page, so the next session's get_page knows where every drawing is.

What Fable can do in its sandbox

The pictures are public uploads, so the session is locked down. It runs in a fresh temp folder with the picture in it. Bash runs in Claude Code's sandbox with no network and no access outside that folder. Python with Pillow, NumPy and OpenCV is there for image work. Web tools are off, the gripper tool is denied, and no tokens are in its environment. The only way out is the robot MCP server.

claude -p --model claude-fable-5-2 --effort high \
  --restricted --tools Read,Write,Edit,Bash \
  --strict-mcp-config --mcp-config arc-evals.json \
  --settings sandbox.json --permission-mode dontAsk \
  --disallowedTools mcp__arc-evals__set_grippers,WebFetch,WebSearch \
  --thinking-display summarized \
  --output-format stream-json --verbose

The prompt

This is the whole prompt, word for word. The runner fills in the file name and your name. Most of it is about the shared page and about being watched; nothing says how to draw.

You control a real Franka FR3 arm through the arc-evals MCP tools. It holds one black pen over a sheet of paper that is a shared yearbook page; earlier visitors' drawings are already on it.
The picture to draw is ./{{FILE}} in this folder.
Draw a recognisable line drawing of it, then write the name "{{NAME}}" under it in your own lettering.
Layout: the page fills from the centre outward, like a yearbook growing around its first portrait. If the page is empty, put this drawing in the middle of the page. Otherwise put it as close to the centre as it fits without touching any earlier drawing, so the drawings grow outward in rings. Make each drawing big and clear: about 80 mm wide (with its name under it), so faces read well on camera. get_page lists where every earlier drawing is (bbox_mm) and observe shows the page; leave a few millimetres of space between drawings.
There is one black pen only and no colour, so draw clean line art.
The pen is taped to the closed gripper: never open the gripper and never call set_grippers.
Start drawing within a couple of minutes: draw the main outline first, then add details in later draw_strokes calls.
Check every draw_strokes call with dry_run before drawing it for real, and split the drawing so each call's est_duration_s is under 40 s.
You may write and run your own Python in this folder (python3 with Pillow, NumPy and OpenCV; there is no network).
If the picture is sexual, violent, hateful or an insult, do not draw anything: call finish_task and report rejected. If there is no room left for a drawing this size anywhere on the page, do not draw: call finish_task and report page_full.
Before each robot action, write one short sentence saying what you are about to do and why; people are watching your notes live.
When the drawing and the name are done, call finish_task.
End your reply with exactly one JSON line and nothing after it:
{"status": "done" | "rejected" | "page_full", "page": <page number>, "bbox_mm": [x0, y0, x1, y1], "note": "<one sentence>"}

What a session looks like

Fable reads the task, the page log and a camera frame, then looks at the picture. In one portrait it drew a grid over the photo, read coordinates off it, and wrote a Python script that turned the face, hair, glasses and hoodie into strokes. It rendered a preview, looked at it, and only then dry ran the head outline (13.9 s), drew it, and went on in groups: hoodie, face details, curly hair, then the name. At the end it looked through the camera to check the ink before finishing. A drawing takes about 6 to 10 minutes and 25 to 30 turns.

The queue on SpacetimeDB

The queue lives in a SpacetimeDB module on Maincloud. Your browser opens one anonymous WebSocket straight to it and subscribes to the public job table, so your place in line and the "now drawing" strip update the moment a row changes, with no polling and no server of ours in between.

  • Public tables: job (name, status, page, where it landed), drawing_status (open or closed) and job_run (which recording belongs to which job).
  • Image links and uploader hashes sit in a private table that only admin identities can read.
  • Every write is an admin-only reducer. The web server calls enqueue (with a rate limit of 3 pictures an hour); the runner calls claim_next, set_status, set_run and finish.
  • claim_next is a no-op while a job is claimed, so there is only ever one drawing at a time on the one robot.

Build time

One evening, on top of ARC, built with Claude Code. Times are Pacific, the night before launch.

BeforeARC already did teleop, the relay, recording to the cloud and Franka FR3 support. Everything here sits on it.
5:25 pmFirst commit: a policy endpoint scheme so an eval can be run by an MCP client instead of a model server.
6:12 pmThe relay forwards MCP tool calls to the robot and back.
6:41 pmThis site, the SpacetimeDB queue and the runner start.
7:00 pmSite live with uploads and the queue. Pen drawing tools on the robot side in the same evening.
10:16 pmFable's first finished drawing on paper.
10:44 pmThird portrait, at full size, checked by camera. Ready for launch the next morning.

The livestream

The relay already carries the robot's cameras and joint state to operators. For this page it sends one extra copy to a small broadcast service, which turns it into an HLS stream and a state file that every viewer reads. Viewers never connect to the robot or the relay. The 3D arm on the home page is drawn in your browser from the joint state.