# OpenTakeoff: an agent can trace the floor, only a person can ink it

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/opentakeoff
> date: 2026-10-13
> tags: mcp, agents, tool-calling, developer-tools, licensing

A post from [@RoundtableSpace](https://x.com/RoundtableSpace/status/2109452742725132486) made four claims about a repository I had not heard of. OpenTakeoff *"measures rooms, counts symbols and calculates materials and waste, mainly for flooring estimates"*. It *"runs in the browser without an account or file uploads, and exports to Excel and CSV"*. Its *"MCP server exposes 53 tools for opening plans, setting scale, tracing rooms and exporting"*. And *"agent results remain proposals until a person approves them."*

That last line is why I opened it. Most tools that let a model touch numbers somebody will sign treat review as a UI nicety. A construction bid is a number somebody signs, and the error costs money in a way a chatbot transcript does not. One reply under the post put it well: *"the error cost shows up in the bid, not the demo."* The repository owner replied too, saying they use it every day for commercial estimating.

So I shallow-cloned [`Kentucky-ai/opentakeoff`](https://github.com/Kentucky-ai/opentakeoff) at `9624be8` (2026-10-10, MCP package `0.9.98`) and read it: the scale code, the geometry, the totals, the symbol matcher, every tool registration in the MCP server, the review model on both sides, the export writers and the security notes. I did not run it. Everything below comes from the source, the repository's own docs and evaluation records, and arithmetic I did on those.

The short version: all four claims hold, and the repository is more careful than the post. The interesting parts are in the edges. The count of 53 is a count after two tools were pulled. "Proposals until approved" is a review state, not a quarantine, and one verb walks around it. "No uploads" is true of plans, not quite of page views.

<RepoCard repo="Kentucky-ai/opentakeoff" />

<Figure
  src="https://ai.thesatyajit.com/articles/opentakeoff/fig1.jpg"
  alt="A crop of a VA medical centre finish plan in OpenTakeoff. Three patient rooms are filled with a brick-red hatch for condition WD-1 and a clean linen room with a blue grid for VCT-1. A panel at top right reads WD-1 TOTAL (3), 743.4 SF, 82.6 SY."
  caption="A takeoff on a federal finish plan: three rooms under one finish condition, totalled live at 743.4 SF. This is the project's own social card (OpenTakeoff README)."
/>

## What a takeoff is, in one paragraph

A takeoff is the step before pricing a construction job: someone measures quantities off the drawings. For flooring, that means square feet of each finish (carpet, vinyl, tile) per room, linear feet of wall base and transition strips, a count of thresholds and drains, and an order quantity that adds cutting waste and rounds consumables up to whole buckets. The drawings arrive as PDFs, usually CAD exports with real vector linework, sometimes scans. Commercial tools for this exist and are seat-licensed. The README says that until OpenTakeoff there was no open-source takeoff engine at all. I can't prove a negative, but I didn't find one either.

OpenTakeoff is two front ends over one engine. The browser canvas in `web/` is a React app that renders PDFs with pdf.js and keeps everything in IndexedDB. The MCP server in `mcp/` is a stdio process that imports the same TypeScript modules from `web/src/lib`, so a ring an agent commits and a ring an estimator draws go through the same `computeShapeMetrics` and the same `conditionTotals`. That shared code is what makes the agent half more than a wrapper. The two halves still differ in one place the README admits: room detection paths are not identical, which is part of why One-Click is switched off right now.

## Paper inches, image pixels, feet

Every coordinate in the system lives in one frame, and every tool description repeats it: *"image px at render scale 2.0: PDF pt × 2, origin top-left, y down."* A PDF point is 1/72 of a paper inch, the engine rasterises at 2×, so one paper inch is 144 image pixels. That number is the whole scale table. In `web/src/lib/sheets.ts`:

```ts
const PX_PER_IN = 72 * RENDER_SCALE;                     // 144
const arch = (inPerFt: number): number => (1 / inPerFt) / PX_PER_IN;
// '1/8" = 1\'-0"'  →  upp = 8 / 144 ≈ 0.0556 real feet per image px
```

`upp`, units per pixel, is the only number a sheet's scale carries. Architectural scales (`1/16"` through `3"` to the foot), engineering scales (`1" = 10'` through `1" = 60'`) and metric ratios from 1:20 to 1:500 are all one line each. Metric is a display toggle; the stored quantity is always feet.

There are four ways to set it, and `set_scale` refuses unless you give exactly one: a standard `label`, a raw `upp`, `use_detected` to adopt the scale note read off the sheet, or `calibrate`, two points and the real distance between them. Calibration is one division:

```ts
// mcp/src/session.ts, setScale
const px = Math.hypot(p2[0] - p1[0], p2[1] - p1[1]);
upp = feet / px;
```

Area is where it gets unforgiving. `closedMetrics` in `geometry.js` is the textbook shoelace sum over the ring in pixels, and `computeShapeMetrics` multiplies that by `upp * upp`:

$$
A_{\text{SF}} = \frac{1}{2}\Big|\sum_i (x_i y_{i+1} - x_{i+1} y_i)\Big| \cdot \text{upp}^2,
\qquad P_{\text{LF}} = \sum_i \lVert v_{i+1} - v_i \rVert \cdot \text{upp}
$$

A scale error is a length error, and the area squares it. Calibrate on a dimension string that reads 24'-0" but type 24'-6", and every length on that sheet is about 2.1% long and every area about 4.2% large. Pick `1/4"` on a `1/8"` sheet and every area comes out at a quarter of the truth. The README says it bluntly: *"Pixels × a wrong scale² is every number wrong at once."*

The widget below runs those formulas on a made-up L-shaped room drawn at `1/8" = 1'-0"`, 336 SF as drawn. Leave the scale unset and `measure_polygon` refuses with the server's own message. Pick a standard scale, or calibrate on the 24'-0" dimension and mistype it, and watch the area move by the square of the mistake. The right-hand panel is the totals step, which the next section is about.

<ScaleTakeoff />

## The scale is a gate, and the agent's scale is pencil

The README's list of rules an agent can rely on starts with this one: *"Scale is a gate, not a default."* It holds in the code. Every measure verb opens with the same check:

```ts
// mcp/src/session.ts, measurePolygon (and measureLine, measureSurface, …)
if (s.upp == null) throw new UserError(this.scaleGate(s));
// → "Set the scale for plan.pdf first — use set_scale (detected: 1/8" = 1'-0")."
```

Two details in `setScale` are better than the README makes them sound. A scale the agent sets always lands with `confirmed: false`; quantities still flow, but `takeoff_summary` names every sheet whose scale nobody has confirmed, and only a person at the canvas clears the flag. And an agent cannot rescale a sheet that already holds measurements a person accepted. It gets *"This sheet contains human-reviewed measurements. Recalibrate it in the canvas…"* instead. Rescaling silently re-prices every shape on the sheet, so this is the right place for a hard stop.

The detected scale is a suggestion read off the page text, never applied. `detectScale` canonicalises every text item (curly quotes to straight, whitespace stripped) and matches it against the scale table with boundary checks, so `11/8"` does not read as `1/8"` and `1:500` does not read as `1:50`. A hit in the title-block region (the right 45% of the sheet, the bottom half) wins. A single page-wide note is accepted. Several distinct scales and no title-block note returns nothing at all, because details on a plan sheet are usually drawn larger, and guessing between them is the error the gate exists to prevent. When a sheet carries several notes, `set_scale` says so, and a ring that sits inside a viewport whose note disagrees comes back with a warning.

The canvas adds the estimator's own check. *Check a dimension* (`K`) takes a printed dimension and grades the current scale against it; the README gives the bands as green within 1%, amber within 5%, red past that. Its working order for a real bid spends one line on it: *"Ten seconds a sheet, and it's the only mistake that gets every number at once."*

## Waste lives in the order, not the measurement

The totals step is `conditionTotals` in `web/src/lib/totals.js`, and it is short enough to state completely. A *condition* is one finish, say `CPT-1`, carrying a waste percentage, a ×N multiplier for identical floors, a wall height and a list of supporting materials. For each condition the function sums its shapes by role (floor area adds, deducts subtract, wall traces add wall SF, linear runs add LF and, with a thickness, border SF, counts add EA), multiplies everything by N, and only then applies waste:

```js
// web/src/lib/totals.js, conditionTotals
floor *= mult; wall *= mult; border *= mult; lf *= mult; ea *= mult; seam *= mult;
const total = floor + wall + border;
const w = 1 + waste / 100;
// ...
total_sf_net: round2(total * w),
sy_net: round2((total * w) / 9),
```

Waste never touches the measured number. The canvas shows the measured square footage, the report shows both, and the order columns carry the allowance. A comment in the file owns up to a naming wart: the `_net` suffix means the waste-adjusted order quantity, the opposite of what an estimator calls net, and it stays because the CSV headers and the versioned `report.v1` keys are public.

Supporting materials are where a careless implementation would double-count, and this one does not. A materials row is a coverage rate (square feet per gallon of adhesive, say) and a basis (area, linear, count or figured seam length), and the order is `Math.ceil(basis / per)`. The basis is `total`, the multiplied but un-wasted area. Cut-off carpet does not get glued down, so dividing the wasted figure would buy adhesive for the trash bin. Whether every estimator agrees is a pricing question; the choice is consistent and documented.

The README's own report screenshot checks out against those formulas. WD-1 measures 743.4 SF at 10% waste: 743.4 × 1.10 is 817.74, shown as 817.7, and 817.74 ÷ 9 is 90.86 SY, shown as 90.9. VCT-1 is 103.5 SF at 5%, which gives 108.7.

<Figure
  src="https://ai.thesatyajit.com/articles/opentakeoff/fig3.png"
  alt="An OpenTakeoff report. Rows for WD-1 (3 shapes, 743.4 floor SF, 10% waste, 817.7 SF ordered, 90.9 SY) and VCT-1 (1 shape, 103.5 floor SF, 5% waste, 108.7 SF ordered, 12.1 SY), a by-sheet table with measured quantities only, and a supporting-materials buy list in gallons."
  caption="The report: measured floor SF, the per-condition waste, the ordered SF and SY, a by-sheet table that never applies waste, and the buy list (OpenTakeoff README, report screenshot)."
/>

Roll goods are the one place a percentage is the wrong model, and the engine knows it. Opt a condition into broadloom or sheet vinyl and `rollgoods.js` lays out lanes and seams per room. The README's example is the clearest argument I have seen for doing this properly: a 20-ft-wide room off a 12-ft roll seams once down its length, while the same square footage as two 10-ft rooms seams not at all, and *"no factor on area can tell those apart."* Seam tape and weld rod are bought by that figured seam length, which is why `seam_lf` is a materials basis of its own. One reply under the post made the same point.

## Counting without a vision model

The post says it counts symbols. I expected an image model. It is geometry. `symbolsweep.ts` takes one marqueed example of a drain or a fixture and finds every other placement of the same cluster of vector segments:

1. The fingerprint is every segment fully inside the seed rectangle, relative to its length-weighted centroid. Fully inside, so a marquee around a drain doesn't drag in the wall behind it.
2. Candidates come from up to three anchor segments of distinct lengths, rarest on the sheet first. Every sheet segment of matching length votes for where the symbol's centre would be, under each of the eight transforms of a square (four rotations, with and without a mirror).
3. Each candidate is scored as the length-weighted fraction of seed segments that find a sheet segment with both endpoints within 2 px. At `1/8"` that tolerance is about 1.3 inches of real distance.
4. A score of at least 0.92 counts. Between 0.75 and 0.92 the placement comes back in `withheld`, with its location, as a question.

That second band is the design decision I like most in the repository. A near-miss is neither silently counted nor silently dropped, and the server's initialize instructions spend a whole paragraph on it: *"WITHHELD IS NOT A FAILURE — IT IS THE ANSWER."* The same doctrine runs through the derivations. `derive_transitions` will not commit a carpet-to-tile transition between two rooms separated by a wall, because the real transition is a threshold in a doorway and nothing in a trace says where the doorway is. It reports the run, its length and the gap in inches, and leaves it to a person or a follow-up `place_count`.

Two more choices show the same temperament. The matcher never searches over scale. A symbol seeded on a detail drawn at `1-1/2"` to the foot is 12 times the size of the same mark on a `1/8"` plan, and the sweep resizes by that exact ratio only when both sheets have a scale set; otherwise it runs at 1:1 and says `scale_assumed`, *"because an unknown ratio plus a zero count is not evidence of absence."* And the candidate budget has a hard ceiling of 250,000 scored placements, with `complete: false` in the reply when the ceiling cut scoring short, so a truncated count is reported as a floor and not a total.

Two siblings cover the cases where geometry alone is the wrong tool. `count_marks` counts value-annotated device tags (an `S1` with `200` CFM under it) from the text layer in one deterministic call. `sweep_schedule_row` starts from a schedule row's key, fingerprints the marker that tag is drawn in, and counts a match only when the row's own tag sits inside the marker, because drafting reuses one bubble shape for many marks.

## Fifty-three, counted

The post's 53 is the README's number, and the README generates it. A script, `check-tool-count.mjs`, rewrites every `<!--tool-count-->` marker in the docs from the code and fails CI when prose drifts. So I went to the code.

`mcp/src/tools.ts` has 55 `server.registerTool` calls. `staging.ts` sorts the same 55 into four stages and a test enforces that the stages partition them exactly:

| Stage | Tools | What they do |
|---|---|---|
| setup | 11 | load a plan, sheet info, set the scale, the sheet graph, resolve a room tag, find schedules, read text, sheet structure, raw vectors, render a view |
| measure | 13 | propose a batch, two One-Click flood tools, polygon, line, wall surface, cut-out, counts, three sweeps or censuses, two derivations |
| revise | 24 | list, edit, delete, undo, proposal and condition-edit revisions, scope collisions, condition twins, annotations, verdict marks, four RFI verbs |
| handoff | 7 | summary, three exports and a DXF, import, re-apply rules |

`gate.ts` then withholds `one_click` and `detect_rooms`. The flood-fill engine behind them, the README's headline feature, is *"being re-validated against a wider plan corpus"*, and on a default build the two verbs are not registered at all, so an agent never sees a tool it cannot use. 55 minus 2 is the 53. Set `OPENTAKEOFF_ONE_CLICK=1` and you get 55; set `OPENTAKEOFF_MCP_STAGED_TOOLS=1` and the server starts with the 11 setup tools plus an `open_tool_stage` verb that enables the rest stage by stage.

The README's own tool table lists 46 names, the two gated ones included. The nine it leaves out are `propose_takeoff`, `revise_proposal`, `withdraw_proposal`, `propose_condition_edit`, `withdraw_condition_edit`, `scope_duplicates`, `scope_merge`, `count_marks` and `edit_annotation`. Five of those are the proposal machinery the post's last claim rests on. `docs/MCP_TOOL_INDEX.md` and `mcp/README.md` name all 55; the front-page table is stale.

Staging exists because the descriptions are long. I extracted them from `tools.ts`: the 53 default tools carry a little over 62,000 characters of description before any parameter docs, and `symbol_sweep` alone is about 7,800. That text is where the rules live (trace on the innermost wall face, cross doors on the wall centreline, look at the overlay before trusting a total), and the repository has evidence that it works, which I come back to below. The agent manual is honest about the cost: staging is *"context economy, not a permission boundary."*

## Pencil and ink

The review model is the reason to read this codebase. It has two actors and two kinds of mark. Agent work is *pencil*: every commit from the MCP server goes through one private method, `Session.commit`, which stamps the origin and forces the review flag:

```ts
// mcp/src/session.ts, commit()
if (origin?.actor === "agent") origin = { ...origin, reviewed: false };
if (origin) origin = this.stampProposal(origin);   // attach the open batch
if (origin?.actor === "agent" && !origin.assignment)
  origin = { ...origin, assignment: { source: "asserted" } };
```

`propose_takeoff` opens a named batch with a required label and rationale ("Level 2 rooms per finish schedule A-601"), and every agent commit after it attaches to that batch. The agent hands the batch over by writing the `takeoff_canvas.v1` JSON with `export_takeoff`; the estimator imports it in the browser, where agent shapes draw dashed, and a single *Accept* pill turns the whole batch into ink. Forty rooms become one decision. The estimator can still correct a vertex first, and when they do, `stampEdit` freezes the machine's original ring in `origin.proposed_verts_norm` beside the correction. The README is explicit that this pairing, the machine's guess next to the expert's fix, is the training signal its authors want to collect.

The project's own recording shows the whole loop. In a terminal on the left, Claude Code loads a VA medical centre finish plan, sets the scale the sheet declares, and one-clicks carpet in three patient rooms. Each export lands in the browser on the right as dashed pencil, and the operator accepts it. The recording leans on `one_click`, one of the two flood-fill tools the current build withholds, so today's agent would trace those rooms by hand instead.

<Video
  src="https://ai.thesatyajit.com/articles/opentakeoff/mcp-live-demo"
  poster="https://ai.thesatyajit.com/articles/opentakeoff/mcp-live-demo-poster.jpg"
  alt="Screen recording. Left, a terminal where Claude Code drives the OpenTakeoff MCP server: it loads va-bldg28-finish-plan.pdf, sets AF101 to the sheet's declared 1/8 inch = 1 foot scale, then traces carpet in patient rooms 161, 162 and 163, exporting after each. Right, the OpenTakeoff web app showing the finish plan, where the imported rooms appear as dashed proposals and the operator accepts them."
  caption="A real run at 3× speed, from the repository: an agent one-clicks three rooms over MCP, each export arrives as pencil, and a person inks it; the README gives the result as 743.64 SF (OpenTakeoff README, mcp-live-demo.gif, re-encoded)."
/>

<Figure
  src="https://ai.thesatyajit.com/articles/opentakeoff/fig2.jpg"
  alt="The OpenTakeoff canvas on VA finish plan AF101. A dashed blue pill at the top reads: Accept AF101 floor takeoff: rooms 134, 134A, 136, 137, 137A per finish schedule AF600, with a count of 6. A small cluster of rooms is outlined in dashed pencil. The status bar reads: Imported 6 shapes, 6 dashed pending your review, Accept turns pencil to ink."
  caption="An agent's takeoff imported into the canvas: six shapes in one proposal, dashed until the estimator clicks the Accept pill (OpenTakeoff README, agent review walkthrough)."
/>

<Figure
  src="https://ai.thesatyajit.com/articles/opentakeoff/fig4.png"
  alt="Two dashed Accept pills: Accept Level 1 offices per finish schedule, 3 shapes; and Accept Base from the office rooms, 3 shapes. Each has a red cross to reject."
  caption="One Accept pill per proposal batch, labelled with the agent's own title for it (OpenTakeoff docs, proposals screenshot)."
/>

Approval is a second, separate mark. The estimator's green `APPROVED` seal is minted only by the canvas's Approve tool. `mark_verdict` lets an agent sign its own work, but it takes no actor argument, so the only thing it can mint is a grey `AGENT` diamond. The marked-set PDF tallies the two on its cover: *N estimator-approved · N agent-marked*. A project manager can see at a glance how much of a set a person actually looked at.

Then I went looking for where accepted work is protected, because that is where a design like this usually leaks. It mostly holds. `edit_shape` refuses a shape with `reviewed: true`. So does `cut_out`, and so does `scope_merge` when the reviewed shape would be the one trimmed. `withdraw_proposal` removes only the pending shapes of a batch and reports how many accepted ones it left alone. `delete_verdict` refuses a human seal.

`delete_shape` has no such check. `Session.deleteShape` looks up the id and splices the shape out; there is no `reviewed` test anywhere in it, and none in the tool handler. That contradicts the `import_takeoff` description, which promises that reviewed shapes *"arrive untouchable by agent verbs."* The blast radius is smaller than it sounds: the MCP session is a separate process with its own copy, and the canvas import is additive, so deleting an accepted shape in the session cannot reach the estimator's canvas. It does reach anything the session exports afterwards, including the report and the marked set. I'd call it a one-line fix the project will want, not a hole in the model.

The other thing to understand is what *proposal* does not mean. `conditionTotals` sums shapes by role and never looks at the review flag. Pending pencil counts in `takeoff_summary`, in `export_report` and in the canvas's live counter from the moment it is committed; the review screenshot above shows a CPT-1 total that includes shapes still waiting for the Accept pill. The marked set does disclose it: when it is written from an MCP session, a credit line reads *"Machine-traced via OpenTakeoff MCP"* and counts the shapes still pending human review. And condition knobs are softer than geometry. `propose_condition_edit` exists and holds a waste or tag change pending, but `edit_condition` changes waste and multiplier directly with *"No review gate — quantity config, not traced geometry."* A careful agent will use the proposal; nothing forces it to.

So the post's claim is right about the state of the work and loose about the numbers. Agent results stay *unreviewed* until a person accepts them. They are not *excluded* until then.

The widget walks the same rules on three made-up rooms in one batch. Try the agent verbs on a room before and after the Accept pill, and watch which ones refuse.

<PencilInk />

One more boundary is worth stating, because the README almost states it. The review flag and the seal's actor are fields in a JSON file. The canvas trusts the `actor` field of an imported approval (*"the actor field is the authority"*), and the provenance module calls its own trust model *"initials on a paper markup."* Against the 53 MCP verbs, the pencil-and-ink line is structural. Against an agent that also has a shell and can write a JSON file by hand, as Claude Code does, it is a convention the estimator's eyes enforce. For a tool with no accounts I think that is a fair model; just know which of the two you are relying on.

## What "nothing uploads" covers

The web app is client-only, and I found nothing that contradicts that for plans. `ingest.js` unpacks zips and wraps images into PDFs in the browser, `store.js` keeps PDF bytes and the takeoff in IndexedDB, and the Excel export is a hand-rolled 262-line SpreadsheetML writer (`xlsx.js`) that writes every cell as an inline string, so a finish tag that starts with `=` stays inert text instead of becoming a formula. The MCP server is a stdio subprocess that opens no socket; `SECURITY.md` is unusually clear about where its trust boundary sits and why a `path` argument writing where you asked is not a vulnerability.

Three things do talk to the network, and the repo discloses all of them. The bring-your-own-model agent panel sends your goal, the tool results and crops of the sheet (up to 1,024 px a side) to an endpoint you configure, and nothing if you configure none. The optional team mode syncs to your own Google Drive. The *Contribute* button posts a whitelisted, derived-only payload to a capture server you run. Because those endpoints are user-chosen, the CSP leaves `connect-src` open, and `_headers` explains the trade-off it accepts in a comment.

The fourth is not optional. `web/index.html` loads the Cloudflare Web Analytics beacon with the project's token, and the CSP allows its origin. It is a cookie-less page-view counter; it sees that a page loaded, not what plan you dragged in. But the README's *"nothing phones home"* is a statement about your plans, and anyone self-hosting a fork will want to delete that line.

## How well an agent actually traces

With One-Click gated, an agent measures a room by reading wall strokes with `get_sheet_vectors` and drawing the ring itself with `measure_polygon`. Whether a model can do that well is the real question, and the repository has the most honest answer I've seen a project publish about itself, in `evals/mcp-workflow-bench/plan-set/`.

The set is three public plans: a VA healthcare finish plan, a VA clinic plan and a city ADU plan, with reference rings prepared from the PDF linework. The pass gate is strict on purpose, an IoU of at least 0.985 and every vertex within 2.5 px of the other ring, so a skipped one-foot door notch fails a room. Blind runs of one agent (Claude Fable 5.1, flat tool mode, One-Click off) passed 1 of 6 rooms on St. Cloud, 3 of 4 on Roseburg and 0 of 6 on Porterville against the corrected references, 4 of 16 by my sum. Read with the gate's strictness in mind, most of the misses are small: all six St. Cloud rings were within 0.4% of the reference area. The big ones are genuine estimator questions, a washer-dryer alcove on Porterville that was 17.47% off and a sink alcove at 6.39%, both of which the project lists as open questions rather than agent errors.

<Figure
  src="https://ai.thesatyajit.com/articles/opentakeoff/fig5.png"
  alt="A crop of the St. Cloud VA finish plan with rooms 134, 134A, 136, 137 and 137A. Thin green reference rings and red agent rings nearly coincide along every wall; small red and green dots mark vertices at door jambs."
  caption="One blind run on St. Cloud AF101: the agent's rings in red over the reference rings in green. They agree closely except at door jambs, where 3 to 6 px readings sit just outside the 2.5 px gate (OpenTakeoff plan-set evaluation, blind run of 2026-09-12)."
/>

Two things in those records raised my opinion of the project. First, the blind runs found errors in the *references* before they found errors in the agent: the first version had traced a door leaf as a wall face and missed door notches, and the references were re-authored from the linework, with the old ones kept beside them. Second, a controlled trial isolates what the long tool descriptions buy. Same model, same stripped task with no tracing conventions in it, one change: the server build with the tracing rules packaged into the descriptions and the wiki. Rooms with every door crossed on the wall centreline went from 0 of 6 to 6 of 6, and median area error from 1.45% to 0.05%. Strict passes stayed at 1 of 6. One run per condition, one model, one plan, as the write-up itself says; it is evidence that the descriptions change behaviour, not a benchmark.

## AGPL as of last Saturday

The repository was Apache-2.0 through commit `84c7f48e` (tag `apache-2.0-final`) and is AGPL-3.0-only from 2026-10-10, three days before I write this. `NOTICE` is clear that the Apache code stays Apache for everyone who has it. New contributions go through a CLA that grants Kentucky AI the right to license them *"under terms other than AGPL-3.0, including commercial terms."* It is the usual open-core arrangement, stated in the open, and the README draws the same line around the data, where the engine is open and the models trained on the company's own estimating archive stay proprietary.

For most readers this changes little. Running the canvas or the MCP server for yourself is unaffected. Hosting a modified copy for other people means publishing your changes, and wrapping it in a product means reading the licence first. The reply under the post that flagged the relicensing was right to.

## Would I hand it to an agent?

For flooring takeoffs, yes, with three habits. Confirm every sheet's scale in the canvas myself, because one wrong scale is every number wrong. Read the `withheld` arrays, because that is where the engine puts the questions it refuses to answer for you. And treat `takeoff_summary` as pencil totals until the batch is accepted.

The parts I would lift for other domains are the contract, not the flooring. Refuse instead of defaulting when a unit is missing. Return near-misses as questions with coordinates. Keep the machine's original guess frozen next to the human's correction. Make the human's mark a code path the agent cannot reach. If you build an MCP server that lets a model touch numbers a person signs, read `mcp/src/session.ts` and `docs/AGENT_GUIDE.md` before you design your own. The same question, what an agent may do and who it has to ask, comes up in [OpenShell](/articles/openshell) at the kernel boundary and in [the argument about which constraints survive better models](/articles/scaffolding-gets-eaten). The construction-data side of this site is [KOLC+ and Gaussian splats in BIM](/articles/kolc-3dgs-bim); the browser-only, IndexedDB-backed architecture is the same one [OpenCut](/articles/opencut) uses for video.

To try it, the canvas is at `opentakeoff.kentucky-ai.com` with a sample plan, and the agent side is one line:

```bash
claude mcp add opentakeoff -- npx -y opentakeoff-mcp
```

## How I checked

I cloned `Kentucky-ai/opentakeoff` with `--depth 1` at `9624be8` (2026-10-10) and did not execute any of its code. I counted tools by listing every `server.registerTool` call in `mcp/src/tools.ts` (55) and diffing that list against the names in the README's table (46); the stage split is `TOOL_STAGES` in `mcp/src/staging.ts` and the gate is `mcp/src/gate.ts`. The description length is my own extraction of each tool's `description` string from `tools.ts`, with the shared coordinate sentence substituted and template expressions dropped, so treat 62,000 characters as approximate. Scale, area and totals formulas are from `web/src/lib/sheets.ts`, `geometry.js`, `shapeMetrics.js`, `totals.js` and `Session.setScale`; the report arithmetic is mine on the README's screenshot. The symbol matcher constants (2 px, 0.92, 0.75, three anchors, a 250,000 ceiling) are from `web/src/lib/symbolsweep.ts`. The review guards are from reading `editShape`, `cutOut`, `scopeMerge`, `withdrawProposal`, `deleteShape` and `commit` in `mcp/src/session.ts`, plus `approvals.js`, `proposals.js` and `importTakeoff.js` in `web/src/lib`; the `delete_shape` finding is from reading, not from a run. Network behaviour is from `web/index.html`, `web/public/_headers`, `SECURITY.md` and a search for `fetch(` across `web/src`. The evaluation numbers are the repository's own (`evals/mcp-workflow-bench/plan-set/`), not reproduced. The licence history is from `NOTICE`, `CLA.md` and the changelog. The source post has no thread and no video; the replies I quote are from its first page of replies.
