Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
35603a2
feat(sync): conflict defer-gate — busy re-check + idle-drain + saveDa…
knmurphy Jul 13, 2026
fc04324
feat(sync): gate local-first on a build env var, not a per-user toggl…
knmurphy Jul 13, 2026
6553948
docs(sync): local-first rollout + sync architecture / provider seam (…
knmurphy Jul 13, 2026
55800b9
Merge upstream/main: shape command layer, undo/redo, takeoff agent, R…
knmurphy Jul 20, 2026
7b24ac6
fix: .nvmrc 20 → 24 — web/package.json engines requires node >=24
knmurphy Jul 20, 2026
88e2f6c
fix(review): Copilot round — CI node from .nvmrc, mcp/server.json 0.4…
knmurphy Jul 21, 2026
4748074
fix(deploy): drop upstream's netlify.toml CSP block — _headers is the…
knmurphy Jul 21, 2026
c7974f1
fix(sync): agent run + staged proposals join the Slice 5b busy predicate
knmurphy Jul 21, 2026
c4f7a7a
feat(deploy): adopt upstream's CSP pattern — open connect-src, netlif…
knmurphy Jul 21, 2026
b682580
fix(review): Copilot round 2 — DETAIL_STALL_MS honors its documented …
knmurphy Jul 21, 2026
39ec1a0
fix(merge): restore fork-only workflows the upstream merge silently d…
knmurphy Jul 21, 2026
c7a9c05
Merge pull request #150 from knmurphy/sync/upstream-2026-07-20
knmurphy Jul 21, 2026
650af45
fix(auth): persist Google session to sessionStorage across reloads (#…
knmurphy Jul 21, 2026
ccd4b0d
fix(auth): cleanup from #151's post-merge review — gate hydrate, drop…
knmurphy Jul 21, 2026
e85fe27
fix(deploy): make netlify.toml the fork's permanent CSP home, not ups…
knmurphy Jul 21, 2026
48afecd
Merge pull request #154 from knmurphy/fix/csp-permanent-source
knmurphy Jul 21, 2026
b3bfa16
feat(canvas): multi-select mode + bulk label/delete/condition assign …
knmurphy Jul 21, 2026
cf93447
fix(auth): force re-auth on a revoked Google token instead of an opaq…
knmurphy Jul 21, 2026
8c8863f
Add Curved Line tool (Q): smooth spline through clicked points, LF at…
QuisutDeus1 Jul 21, 2026
b96dde4
fix(review): Copilot round — hitShapeC skips the redundant normalize …
knmurphy Jul 21, 2026
25a752a
fix(review): Copilot round 2 — flattenCurve's vertex cap was a soft t…
knmurphy Jul 21, 2026
ba7c9ed
fix(review): Copilot round 3 — marked-set PDF export drew curved line…
knmurphy Jul 21, 2026
bfa5b87
fix(review): Copilot round 4 — hitShapeC caches normalized points not…
knmurphy Jul 21, 2026
eac3148
fix(review): Copilot round 5 — flattenCurve's budget floor made hones…
knmurphy Jul 21, 2026
efe3ca6
fix(review): Copilot round 6 — hitShapeC guards against a zero-dim sh…
knmurphy Jul 21, 2026
82a4187
Merge pull request #156 from knmurphy/feat/curved-line-tool
knmurphy Jul 21, 2026
f5ee65b
docs: sync FEATURES.md and USER_GUIDE.md for the w/Waste rename, Labo…
knmurphy Jul 21, 2026
0fc5634
fix(review): Copilot round — Edit-menu verb list was missing Finish s…
knmurphy Jul 21, 2026
2f299f4
Merge pull request #159 from knmurphy/docs/feature-drift-sync
knmurphy Jul 21, 2026
7175380
chore(deploy): remove this repo's production deploy workflow (#160)
knmurphy Jul 21, 2026
0190564
chore(gitignore): track the example-plans/ ignore in the repo (#162)
knmurphy Jul 21, 2026
1266537
fix(deploy): re-adopt upstream's CSP file shape — _headers is the pol…
knmurphy Jul 21, 2026
b734c17
chore(docs): de-brand company examples — netlify.app for app/project …
knmurphy Jul 21, 2026
b91bef2
Transfer takeoff to a reissued sheet + auto-flag what changed (#149) …
knmurphy Jul 21, 2026
c4b88b8
Merge upstream Kentucky-ai/opentakeoff into the fork (sync 2026-07-21)
knmurphy Jul 21, 2026
3a2f301
Merge origin/main (#161 revision-transfer) into the upstream sync
knmurphy Jul 21, 2026
e8620f5
Merge pull request #165 from knmurphy/sync/upstream-2026-07-21
knmurphy Jul 21, 2026
bb89d92
agentTools: refactor executeAgentTool switch to a handler-map
knmurphy Jul 21, 2026
fbafd51
fix(agentTools): null-proto HANDLERS so prototype-key tool names fall…
knmurphy Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/copilot-review-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Merge gate: a PR to main can't merge until GitHub Copilot has actually posted
# its code review. Copilot reviews automatically on open, but its review lands a
# minute or two AFTER CI goes green — so "green CI" alone let a PR merge before
# Copilot weighed in. That exact gap shipped a ReferenceError to production once
# (it was caught by Copilot's review, which posted just after the merge). This
# job stays RED until a Copilot review exists on the PR, then flips green when the
# review-submitted event re-runs it. Add `copilot-review` to the branch's required
# status checks to enforce it.
#
# Note: this gates that Copilot HAS reviewed, not that every comment is resolved —
# resolving findings stays a human judgment call. It only closes the "merged before
# Copilot ran at all" hole.
name: Copilot Review Gate

on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
# Re-evaluates when Copilot (or anyone) submits/dismisses a review — this is what
# flips the check green once Copilot's review posts. pull_request_review workflows
# run from the base branch, so this must live on main to take effect.
pull_request_review:
types: [submitted, dismissed]

permissions:
pull-requests: read

concurrency:
group: copilot-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
copilot-review:
runs-on: ubuntu-latest
# Drafts aren't ready to merge; don't nag them.
if: github.event.pull_request.draft == false
steps:
- name: Require a Copilot code review
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
// Exact login + Bot type: a prefix match would let any user named
// "copilot-pull-request-reviewer…" satisfy the gate, and a real user
// can't be type "Bot", so together these block impersonation. The
// [bot] suffix is what the reviews API returns; allow the bare form too.
const isCopilot = (u) =>
u && u.type === 'Bot' &&
/^copilot-pull-request-reviewer(\[bot\])?$/i.test(u.login || '');
// A dismissed review no longer counts — dismissing Copilot's review
// (the workflow re-runs on that event) must turn the gate red again.
const reviewed = reviews.some((r) => isCopilot(r.user) && r.state !== 'DISMISSED');
if (!reviewed) {
core.setFailed(
'Waiting for GitHub Copilot code review. It reviews automatically on open; ' +
'this check turns green once its review posts. Do not merge before then.'
);
} else {
core.info('Copilot code review present ✓');
}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ server/.venv/
.env.*
!.env.example

# Local plan corpus (real project PDFs) — never commit; previously only ignored
# via .git/info/exclude, which doesn't travel with clones
example-plans/

# Netlify CLI local state (never commit; the root netlify.toml IS committed)
.netlify/
**/.netlify/
Expand Down
30 changes: 12 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,16 @@ npm run check # typecheck + lint + test + build — exactly what CI runs; gre

## Shipping — the required steps, every change

`main` is protected on GitHub via a ruleset (PR-only, one approving review,
green `web` check, branch up to date — the repo owner has a standing bypass
as the solo maintainer). **Merging to `main` deploys to production**
(<https://opentakeoff.netlify.app>) via `.github/workflows/deploy.yml`, which
re-runs `npm run check` and publishes `web/dist` to Netlify with `--no-build`
— Netlify never builds anything itself.

> **This is the canonical `Kentucky-ai/opentakeoff` repo — production is
> <https://opentakeoff.netlify.app>, nothing else.** A downstream fork
> (`knmurphy/opentakeoff`) tracks this repo as its own upstream and deploys
> separately to `takeoff.345flooring.com` — that URL belongs to *that* fork,
> not this repo. If you're seeing `takeoff.345flooring.com` referenced
> elsewhere in this repo's docs (`docs/DEPLOYMENT.md`,
> `docs/PARENT_FORK_PORTS.md`), it's leftover content from that fork's own
> `AGENTS.md`/docs that rode along in a wholesale history merge (2026-07-13) —
> treat it as describing the *downstream* fork's deployment, not this one's.
`main` is protected on GitHub (PRs only, green `web` check, branch up to date,
no force-pushes — admins included) and a local pre-commit hook rejects commits
made on `main`. **Merging to `main` no longer deploys anything** — the
production deploy workflow has been removed from this repo; it now only
builds and tests every change.

> **The project's live demo is <https://opentakeoff.netlify.app>** — the parent
> repo's (Kentucky-ai/opentakeoff) deployment. This fork tracks that repo and
> exists to contribute back to it, so that's the deployment its README badge and
> links point at; the fork serves none of its own, and merging here does not deploy.

So:

Expand All @@ -44,8 +38,8 @@ So:
(`gh pr merge <n> --squash --delete-branch`), then
`git checkout main && git pull --ff-only` and delete the local branch
(`git branch -D <topic>` — squash merges need `-D`).
5. **Remember a merge is a deploy.** Don't merge work you haven't verified in
the running app.
5. A merge here is not a deploy — still verify your change in the running app
before merging, the same as always.

The tests cover the pure math (`web/test/geometry.test.ts`, `web/test/totals.test.ts`); the canvas itself is verified by hand — **Vite does not flag undefined identifiers in JSX**, so grep for your new identifiers after editing and load the app once before you call it done. The bundled sample plan (`web/public/demo/`, wired to the "Load sample plan" button) is the fastest end-to-end check: load it, press `A`, trace a room, open Report.

Expand Down
30 changes: 6 additions & 24 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,18 @@

All notable changes to OpenTakeoff. Dates are release/merge dates on `main`.

## 2026-07-21 — opentakeoff-mcp 0.5.0
## 2026-07-21

### Added
- **`detect_rooms` — the eleventh tool: batch room detection from the sheet's own labels.** One call reads every room-number label off the sheet's text layer and runs the One-Click flood at each; only cleanly-traced rooms return (a leaking or dense-linework label is silently withheld, matching `one_click`'s precision stance). Same contract as `one_click`: px-only preview without a scale, per-room `area_sf`/`perimeter_lf` with one, `condition` commits every detected room with the standard agent/unreviewed provenance stamp, and a declared `outputSchema` like the other ten.
- **A per-tool conformance suite backs the whole surface** (`mcp/test/conformance.test.ts`, closes #27): every tool's valid replies are schema-validated, every misuse path is a clean `isError` + JSON `{error}`, schema-invalid arguments pin the SDK's `-32602` surface, and the session is proven to survive every failure. Runs in CI on Ubuntu and Windows.
- **Synced upstream (`Kentucky-ai/opentakeoff`), adopting four features we were behind on.** `detect_rooms` — the MCP server's 11th tool: reads every room-number label off a sheet's text layer and runs the One-Click flood at each in one call, returning only cleanly-traced rooms (pure core in `web/src/lib/detectRooms.ts`). A **per-tool MCP conformance suite** (`mcp/test/conformance.test.ts`) that schema-validates every tool's replies and pins its misuse/`-32602` surfaces, on Ubuntu and Windows CI. **opentakeoff-mcp 0.5.0.** And a fix for **jagged rendering of very large ingested images** — `autoRenderScale`'s floor no longer overrides the physical panel-budget cap, so an oversized image-ingest page renders below the ×2 baseline instead of minting a canvas past Chrome's GPU budget (`web/src/lib/canvasUtil.js`, regression-tested in `web/test/renderBudget.test.ts`). Our fork-specific divergences were preserved through the merge: no deploy workflow, `web/public/_headers` as the CSP home, marquee multi-select, the refined `flattenCurve`/`hitShapeC`, and the de-branded docs.

## 2026-07-20

### Fixed
- **Jagged, blocky linework on very large ingested images.** An ingested image becomes a 1 px = 1 pt PDF page, so a 7920×5280 scan minted a 7920-point-wide page — and the base raster's fixed ×2 scale then built a 15840×10560 canvas (669 MB backing store). The pixels in it were actually crisp, but a canvas that size blows past Chrome's GPU budget, so the compositor silently displays a degraded low-res proxy: giant nearest-neighbor blocks at any zoom, on that sheet only. `autoRenderScale`'s floor no longer overrides the physical caps — oversized pages now render below the ×2 baseline, inside the 28 MP panel budget (the existing `factorFor` scale plumbing already handles non-baseline rasters), and deep zoom stays sharp through the detail-view re-render as usual. Regression-tested in `web/test/renderBudget.test.ts`.
### Changed
- **Security headers re-adopt upstream's file shape: `web/public/_headers` is the CSP home again, `netlify.toml` carries no `[[headers]]` block.** Both files are now byte-identical to upstream (Kentucky-ai/opentakeoff), which has since adopted every policy line this fork's netlify.toml block carried — Google sign-in origins, the `@import`'d webfonts, HSTS, the FLoC opt-out — so this is a pure mechanism swap with **zero change to the served policy**. It reverses 2026-07-20's "netlify.toml is the permanent CSP home" call: now that this repo no longer deploys production and exists to track upstream with minimal friction, matching upstream's file layout is what keeps every future sync conflict-free. Still true: never let both files carry a CSP at once — two CSP headers on one response enforce as their intersection.

### Tests
- **MCP tool-conformance suite — every tool, both directions** (`mcp/test/conformance.test.ts`, closes #27). Each of the eleven tools now has: a canonical valid call whose `structuredContent` is validated against the tool's declared output schema and byte-checked against the back-compat text item; semantic-misuse cases proving a clean `isError` + JSON `{error}` reply (no-plan gates, unknown sheets, degenerate calibrations, unenclosed one-click seeds, non-PDF documents, unwritable export paths); and schema-invalid-argument cases pinning the SDK's `-32602` input-validation surface — with follow-up calls proving the session survives every failure. Also covers title-block sheet addressing, exact scale math on the demo plan, deduct-role subtraction, provenance receipts on export, and `read_sheet_text` region windows.
## 2026-07-20

### Added
- **Curved Line tool (`Q`)** — like Linear, but the line bends smoothly through your clicks: radius walls, curved transitions, winding corridors. A centripetal Catmull-Rom spline (`web/src/lib/curve.js`) passes through every clicked point; LF is priced at the **true curved length**, not the chords, and a condition thickness yields border SF exactly like Linear. The shape stores only the clicked control points — drag one later and the curve re-smooths — while rendering, hit-testing, thickness re-flow, and re-scale re-flow all measure the flattened spline (vertex-capped, render-invariant). Curved shapes ride the normal linear schema plus a `curved: true` flag (declared in `docs/CONTRIBUTION_SPEC.md`); older readers degrade to the straight polyline through the points.
- **`detect_rooms` — the MCP server's 11th tool: batch room detection from the sheet's own labels.** Reads every room-number label off a sheet's text layer (`134`, `139A`, `OFFICE 101`) and runs the existing One-Click flood at each one in a single call — one round-trip instead of `read_sheet_text` + reasoning + N `one_click` calls. Only cleanly-traced rooms come back; a label that leaks or lands in dense linework is silently withheld, matching `one_click`'s own precision stance. Same contract: no scale set returns px-only quantities per room; pass `condition` to commit every detected room under that finish tag with the standard agent/unreviewed provenance stamp. Pure core lives in `web/src/lib/detectRooms.ts`.
- **Local-first sync, continued (Slices 5b–7).** A conflict defer-gate (`web/src/lib/canvasBusy.ts`) holds a remote sync adopt until the canvas is genuinely idle — covering trace/calibrate/check in progress, One-Click review, an active drag, the open text editor, an in-flight OCR scan, and (extended in review) the in-canvas agent mid-run or its staged proposals, so a background sync can never wipe unsaved or unreviewed work. The sync layer itself is gated behind a build-time flag (`VITE_CLOUD_SYNC=1`), never a per-user toggle. `docs/SYNC_ARCHITECTURE.md` documents the design: IndexedDB canonical, revision-precondition conflict handling (remote wins on real divergence, local is snapshotted first — nothing lost silently), and a provider seam so Drive can be swapped for OneDrive/SharePoint later.
- **A no-waste "Labor view" for the Report's Columns picker.** One click hides the waste-baked columns (Waste %, SF w/Waste, SY w/Waste) and surfaces the raw **Total SF** column (previously opt-in only) — a standalone quantity view tied directly to conditions, for anyone attaching their own labor rates externally. **Reset** returns to the default view.
- **Flip Horizontal / Flip Vertical**, in the Edit menu. Mirrors the selected shape about its own bbox center — an isometry, so SF/LF never change. Routes through the existing shape-command layer, so undo/redo and provenance stamping come for free. No keyboard shortcut (the single-letter tool hotkeys don't guard Shift). The Edit menu also gained a **Redo** label — `⇧⌘Z` already worked, it just wasn't listed.

### Changed
- **Report quantities that carry waste now read "w/Waste," not "SF ordered."** On-screen table, CSV, and Excel: `SF w/Waste` / `SY w/Waste` / `Total SF w/Waste` / `LF w/Waste`. Only display strings moved — the underlying `total_sf_net`/`sy_net`/`lf_net` keys and the versioned JSON export are unchanged.

### Fixed
- **Zoomed-out pan flicker + laggy toolbars.** Panning while zoomed out was re-rendering the entire canvas component (including the full unmemoized shape/markup SVG overlay) on every ~90ms sync tick, even though nothing visible depended on the pan position — only scale-dependent stroke widths did. The tick now skips the state write when scale hasn't changed and the high-zoom detail view isn't engaged, so a pure pan at overview zoom is a true no-op on the React side.
- **Two conflicting `Content-Security-Policy` headers were shipping to production.** `netlify.toml` and `web/public/_headers` both set a CSP on `/*`; multiple CSP headers are each enforced by the browser as a per-directive intersection, and `netlify.toml`'s block predated the Google-sign-in work, so it would have silently stripped `accounts.google.com`/the webfonts origins back out — breaking sign-in and fonts with no visible error — the moment team cloud mode's env vars are set in production. `_headers` is now the single source of truth; its `connect-src` stays deliberately open (`* data: blob:`, not narrowed to Google-only) so the BYO-AI agent endpoint and the optional `/ai/*` contribute path — both user-configured to arbitrary origins by design — keep working. That trade-off (an XSS could exfiltrate the Google token to any origin) is documented explicitly in `_headers`.
- **`.nvmrc` had drifted to `20`** while `web/package.json`'s `engines` already required `>=24`, and CI hardcoded `node-version: 20` in two places rather than reading the pin. Both now read `24` / `node-version-file: .nvmrc`.
- **`mcp/server.json` still read `0.3.0`** while `mcp/package.json` was already at `0.4.0`. Version numbers now match.
- **`AGENTS.md` / `docs/DEPLOYMENT.md` misattributed this repo's production URL** as `takeoff.345flooring.com` — that's real, but it's a downstream fork's (`knmurphy/opentakeoff`) own deployment; its fork-specific docs rode along into this repo during the 2026-07-13 wholesale history merge and were never re-scoped. Both now correctly point at `opentakeoff.netlify.app`.
- **Curved Line tool (`Q`)** — like Linear, but the line bends smoothly through your clicks: radius walls, curved transitions, winding corridors. A centripetal Catmull-Rom spline (`web/src/lib/curve.js`) passes through every clicked point; LF is priced at the **true curved length**, not the chords, and a condition thickness yields border SF exactly like Linear. The shape stores only the clicked control points — drag one later and the curve re-smooths — while rendering, hit-testing, thickness re-flow, and re-scale re-flow all measure the flattened spline (vertex-capped, render-invariant). Curved shapes ride the normal linear schema plus a `curved: true` flag (declared in `docs/CONTRIBUTION_SPEC.md`); older readers degrade to the straight polyline through the points. (Cherry-picked from upstream's Kentucky-ai/opentakeoff#76 — not this repo's own #76.)

## 2026-07-19

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ Drive**. To set it up, see [`docs/GOOGLE_SETUP.md`](docs/GOOGLE_SETUP.md) and
also opt into **local-first sync** (`VITE_CLOUD_SYNC=1`): annotations stay canonical
in the browser and sync to Drive in the background, so the canvas is instant and
survives a flaky network — see [`docs/SYNC_ARCHITECTURE.md`](docs/SYNC_ARCHITECTURE.md).

## What's in the box

| Area | What you get |
Expand Down
Loading
Loading