Skip to content

editor: Add duct and pipe fittings, routing, and system checks - #769

Merged
wass08 merged 63 commits into
pascalorg:mainfrom
sudhir9297:t3code/update-duct-pipes-ux
Sep 8, 2026
Merged

editor: Add duct and pipe fittings, routing, and system checks#769
wass08 merged 63 commits into
pascalorg:mainfrom
sudhir9297:t3code/update-duct-pipes-ux

Conversation

@sudhir9297

@sudhir9297 sudhir9297 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

  • Surface-aware duct and pipe routing

    • Issue: Runs could fall through walls or ceilings, snap to the wrong level, or stay trapped on a wall after routing moved into free space.
    • Fixed: Duct and DWV drawing now follows walls, floor and roof surfaces, and ceiling undersides before falling back to the active-level grid.
    • Grid, angle, magnetic, and spacing controls remain available while drawing in both views.
  • Expanded fitting and accessory catalogs

    • Issue: The catalog exposed only a small fitting set and grouped unrelated MEP choices together.
    • Fixed: Duct now includes elbows, tees, crosses, reducers, transitions, end caps, dampers, access panels, and couplings; pipe includes elbows, wyes, sanitary tees, crosses, end caps, cleanouts, reducers, and couplings.
    • Each choice has its own thumbnail, and opening MEP starts with no tool selected so only the chosen system's catalog is shown.
  • Shape-aware duct adapters

    • Issue: Connecting different duct profiles could create a same-shape reducer or leave rectangular and round runs joined without a visible adapter.
    • Fixed: Size changes use reducers, while round, rectangular, and oval shape changes create and orient transitions automatically.
  • Compatible and durable connections

    • Issue: Proximity alone could join incompatible systems, multi-floor DWV runs could lose connectivity, and deleting a run could leave damaged fitting topology.
    • Fixed: Connections now compare typed systems in world space, show compatibility feedback, preserve vertical connections across floors, and simplify or remove affected fittings when runs are deleted.
  • Connected-run editing

    • Issue: Continuations, branches, crossings, endpoint moves, and whole-run moves could produce inconsistent previews or many history updates during one gesture.
    • Fixed: Shared continuation and branch planning keeps 2D and 3D behavior aligned, previews edits through live overrides, and commits each placed or edited item as one undo step.
  • DWV slope behavior

    • Issue: Drainage grade was hard to apply, and the full slope editor made the in-canvas routing card too crowded.
    • Fixed: Waste presets and the S shortcut apply the standard grade without showing the large slope form or live readout over the drawing.
    • The side inspector keeps drainage checks for flat, steep, or overlong trap-arm conditions.
  • System checks

    • Issue: Open ends, disconnected branches, incompatible joins, unsupported hangers, and likely intersections were difficult to find from the editor.
    • Fixed: A system-check panel lists these findings and lets the reviewer select and reveal each affected part.
  • Individual hanger control

    • Issue: Automatic hangers could not be adjusted when a generated support was unsuitable or missing.
    • Fixed: Each hanger can be moved, skipped, or assigned to a support host, with a visible warning when no support is available.

How to test

  1. Run bun run checks, bun run lint, and bun run build.

    • Formatting, lint, type checks, package builds, and both editor production builds should pass.
  2. Run bun --cwd packages/nodes test.

    • The node suite should report 2,069 passing tests and one skipped test.
  3. Start the editor, open MEP, and choose Duct or DWV Pipe.

    • No MEP item should be selected initially, unselected tiles should be grey, and the catalog should show only choices for the selected system.
  4. Draw on a wall, move into free space, return to the wall, and repeat on a ceiling underside, floor, roof, and another level.

    • The run should stay on the hovered surface, return to surface snapping correctly, and avoid snapping to a transparent ceiling top face.
  5. Change the active snap mode and snap spacing while drawing in both 2D and 3D.

    • The preview and committed run should immediately use the selected mode and spacing.
  6. Connect rectangular, round, and oval duct runs with matching and different sizes.

    • Same-shape size changes should create reducers; shape changes should create correctly rotated transitions between both profiles.
  7. Draw a vertical DWV stack across floors, add branches, and toggle the standard waste grade with S.

    • Connections should remain part of the same compatible system, and the system checks should report any invalid drainage conditions.
  8. Select a duct or pipe run, move an endpoint or the whole run, then use undo and redo.

    • The live preview should stay connected, and the completed gesture should create one history entry.
  9. Open the system checks and hanger controls for a run.

    • Findings should reveal their affected parts, and individual hangers should support move, skip, host selection, and missing-support warnings.

Screenshots / screen recording

Not added yet.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

Note

Medium Risk
Touches core scene deletion hooks, distribution system graph connectivity, and pointer/grid event plumbing used by many tools; schema version bumps on duct nodes add migration surface.

Overview
MEP build UI now opens the MEP group without auto-arming duct draw; duct and pipe fitting types are picked through ToolOptionsPanel with catalog thumbnails instead of one-off “Add Fitting” buttons.

Routing and placement gain architectural surface raycasting on GridEvent (walls, ceilings, slabs, roofs), ceiling-underside picking, and optional placement lattice anchors so wall-mounted runs and fitting ghosts snap on the hovered host—not the floor grid.

Duct and DWV models expand fitting enums (caps, dampers, access panels, couplings, cleanouts, etc.), add auto hangers / overrides, wall attachment metadata, and richer reducer/transition geometry with inletShape / outletShape. Runs support continue/branch affordances in plan (elbow→tee / tee→cross promotion) and drafting opts into cancelOnHistoryJump so undo/redo while drawing cancels in-flight drafts.

System connectivity joins ports in world space (level elevation + building transform), ignores mismatched system tags at coincident joints, and exports collectSystemPorts / distributionPointToWorld. Node onDelete / onDeleteCascade now receive requestedDeleteIds vs the expanded pending set for smarter fitting cleanup.

Smaller editor fixes: lazy custom inspector panels receive the live node, dev bootstrap re-registers builtin kinds on HMR, and floorplan midpoint handles can use activation: 'action' (click-only).

Reviewed by Cursor Bugbot for commit 1d1bd7f. Bugbot is set up for automated code reviews on this repo. Configure here.

sudhir9297 and others added 30 commits May 19, 2026 02:59
Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add surface-aware drafting, snapping, and run attachments
- Support wall-attached run movement and endpoint updates
- Improve placement grid anchoring and semantic surface events
- Continue routing horizontally after leaving a wall
- Keep quick material actions for pipe fittings only
@pascal

pascal Bot commented Sep 7, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: cc9622c7-8546-4534-9987-a69c31ffcc63

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

<mesh
key={`alt-joint-hit-${index}`}
onPointerDown={(event) => {
if (!event.nativeEvent.altKey || event.nativeEvent.altKey) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alt-joint split never activates

High Severity

The Alt-joint hit mesh always returns before splitting the run. !event.nativeEvent.altKey || event.nativeEvent.altKey is a tautology, so Alt-click never inserts the joint or starts the free-axis drag on ducts or DWV pipes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 721f85d. Configure here.

setTranslationGhost(null)
return
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move keeps stale wall attachment

Medium Severity

When a wall-attached run is dragged off the wall, translateWallRun fails and the tool falls back to free XZ movement, but previewAttachmentRef still holds the original wallAttachment. Commit writes that stale host onto geometry that is no longer on the wall.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 721f85d. Configure here.

gridMesh.position.copy(surfaceNormal).multiplyScalar(planeOffset)
const latticeAnchor = published?.anchor ?? surfacePoint
gridMesh.position.copy(latticeAnchor)
gridMesh.position.addScaledVector(surfaceNormal, -latticeAnchor.dot(surfaceNormal) + planeOffset)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snap lattice follows the ghost

Medium Severity

The oriented snap lattice now defaults to surfacePoint when no anchor is published. Item and opening placement never pass an anchor, so the grid follows the moving ghost instead of staying pinned to the plane foot.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 721f85d. Configure here.

const crossHit = findRunBodyCrossingXZ(start, end, BODY_SNAP_RADIUS_M)
const crossHit = surface
? findRunBodyCrossingSurface(start, end, BODY_SNAP_RADIUS_M, surface)
: null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crossing skipped without a surface

Medium Severity

planDuctDraw and the pipe commit only look for a run crossing when surfaceTarget is set. Directional-arrow routing and free-space continuation set that target to null, so a pass-through no longer mints a cross.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 721f85d. Configure here.

- Add shared snapping for MEP accessories with live setting updates
- Respect surfaces, levels, building transforms, and system boundaries
- Add coverage for snapping and cross-floor port connectivity
- Add duct and DWV fittings, accessories, geometry, placement, and thumbnails
- Unify fitting selection through configurable tool options

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

ed.setStructureLayer('elements')
ed.setCatalogCategory(null)
ed.setMode('build')
ed.setTool(null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEP panel hides after opening

High Severity

Clicking the MEP tile sets mepOpen and mode to build with no tool, but isMepActive only honors mepOpen in select mode. The sub-grid and tile highlight therefore stay off whenever a MEP tool is not already armed, so the catalog the click is meant to open never appears.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 411ac06. Configure here.

const proxy = new Mesh(ceiling.geometry, pickingMaterial)
proxy.matrixWorld.copy(ceiling.matrixWorld)
proxy.layers.mask = ceiling.layers.mask
return raycaster.intersectObject(proxy, false).map((hit) => ({ ...hit, object: ceiling }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ceiling picks ignore grouped meshes

Medium Severity

raycastCeilingUnderside returns no hits unless the registered ceiling root is a Mesh. Drafting replaces the recursive intersectObject(root, true) path with this helper, so grouped ceiling roots are skipped and the cursor falls through to the floor grid again.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 411ac06. Configure here.

- Remove the context-specific DWV Pipe Add Trap button from the Build tab
- Preview pipe and duct edits through live overrides
- Track fitting placement with interaction scopes
- Align surface-aware routing and accessory snapping
- Keep duct and pipe drag, roll, and offset previews out of committed scene state
- Render selection handles from live node overrides during interactions
@sudhir9297 sudhir9297 changed the title editor: Improve duct and DWV routing UX editor: Add duct and pipe fittings, routing, and system checks Sep 8, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

</div>
) : null}
/>
))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pipe trap action was removed

Medium Severity

The DWV panel no longer exposes pipe-trap. The old Add Trap control was replaced by ToolOptionsPanel for pipe-fitting only, so the still-registered trap tool has no activation path in this workflow.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7bc20a. Configure here.

const toUV = (point: Point): [number, number] => [
(point[0] - wall.start[0]) * tx + (point[2] - wall.start[1]) * tz,
point[1],
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wall attachment UVs disagree

Medium Severity

createRunWallAttachment stores UVs in the captured wall frame, while refreshWallRunAttachment rewrites them from wall.start and absolute Y. Movement then mixes those spaces, so attachment coordinates drift after an endpoint edit.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7bc20a. Configure here.

'end-cap',
'damper',
'access-panel',
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duct coupling cannot be chosen

Medium Severity

coupling is on the duct-fitting schema and has geometry and a thumbnail, but both the inspector enum and ductFittingToolOptions omit it, so the new accessory cannot be placed or selected.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7bc20a. Configure here.

Comment thread packages/editor/src/hooks/use-grid-events.ts

@wass08 wass08 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work overall — the Shift-bypass migration in the MEP move/selection tools is exactly the cleanup interaction-scope.md asks for, schema changes are all backward-compatible, and the new hanger/fitting behavior hangs off NodeDefinition the way it should.

One blocker before merge:

Kind-specific branching landed in framework code. This PR introduces the predicate scope.kind === 'drafting' && (scope.tool === 'duct-segment' || scope.tool === 'pipe-segment') in five places across packages/editor — four in hooks/use-grid-events.ts (lines 30, 67, 137, 190) and one in lib/history.ts (isRunDrafting, line 79). Per wiki/architecture/ (layers + node-definitions), new kind-specific dispatch in a framework package is a regression: the behavior belongs on the kind's NodeDefinition, resolved through nodeRegistry.

It's also a real maintenance trap, not just style: lineset and liquid-line draft runs too, but aren't in the list — so they silently don't get semantic surface queries or the undo-cancels-draft behavior, and the next MEP-ish kind has to find and extend a hardcoded list in two files.

Suggested fix (mechanical): declare it on the definition, e.g.

// NodeDefinition
drafting: {
  surfaceQuery: true,        // use-grid-events: architectural surface raycast + level frame
  cancelOnHistoryJump: true, // history: undo/redo cancels the in-flight draft
}

then replace the five predicates with one helper that resolves nodeRegistry.get(scope.tool)?.drafting (or equivalent capability flags). No behavior change for duct/pipe, and the other run kinds can opt in by declaration.

Everything else I looked at checked out clean — happy to approve once this is registry-driven.

interactionScope.kind === 'placing' ||
interactionScope.kind === 'moving' ||
(interactionScope.kind === 'drafting' &&
(interactionScope.tool === 'duct-segment' || interactionScope.tool === 'pipe-segment'))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcoded kind list appears 4× in this file (lines 30, 67, 137, 190). Framework code shouldn't branch on specific node kinds — resolve this from the registry instead, e.g. a drafting.surfaceQuery flag on the NodeDefinition looked up via nodeRegistry.get(scope.tool). As written, lineset / liquid-line drafting silently misses the surface-query path.

Comment thread packages/editor/src/lib/history.ts Outdated
export function isRunDrafting(): boolean {
const scope = useInteractionScope.getState().scope
return (
scope.kind === 'drafting' && (scope.tool === 'duct-segment' || scope.tool === 'pipe-segment')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same kind list as use-grid-events.ts — worth deriving both from one registry-driven helper (e.g. def.drafting.cancelOnHistoryJump) so the next run-drafting kind gets this behavior by declaration instead of by editing framework files.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 11 total unresolved issues (including 9 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1d1bd7f. Configure here.

kind: point.surface.descriptor.kind,
hostId: point.surface.hostId,
face: classifiedFace?.face ?? 'unknown',
levelId: useViewer.getState().selection.levelId ?? undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong level on non-wall surfaces

High Severity

surfaceHit.levelId is always the selected level, not the hit host’s level. Wall targets later recover the wall’s parent, but floor, ceiling, and roof targets keep that selected id. Drawing on another storey’s ceiling or slab parents the run to the active level and stores coordinates in the wrong frame.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d1bd7f. Configure here.

if (match) connections.push(match)
}
return connections
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete cleanup ignores world space

Medium Severity

fittingConnections mates runs to fittings by raw path coordinates. System membership now compares ports in world space, including level elevation and building transform. A vertical or cross-floor joint that the graph treats as connected can be missed on delete, so the fitting is not simplified or removed.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d1bd7f. Configure here.

@sudhir9297
sudhir9297 requested a review from wass08 September 8, 2026 15:09
@wass08
wass08 merged commit 0757ff2 into pascalorg:main Sep 8, 2026
4 checks passed
smokie40 added a commit to smokie40/editor that referenced this pull request Sep 10, 2026
Base the BuildBook carrier on pascalorg/editor c4a7371, retaining the minimal ItemsPanel source-filter seam and the runtime-untraced manifold worker fix from upstream pascalorg#735. This brings pascalorg#769/pascalorg#784 interaction and placement contracts into the exact source graph without carrying old host-specific patches.
Aymericr added a commit that referenced this pull request Sep 12, 2026
* fix(cli): preserve configured Mint host origin

* ci: enable trusted publishing for MCP and CLI releases (#779)

* docs: add verified candidate CLI preview (#780)

* docs: add verified candidate CLI preview

* docs: activate preview runtime during upgrades

* Require measured evidence and target-scoped furniture checks (#781)

* Strengthen furniture fit evidence boundaries

* Record candidate validation status

* Clarify requested geometry scope

* Record furniture evidence gate results

* Document skill validation and safe preview activation (#782)

* Record final skill validation status

* Clarify routing audit result

* Document safe preview activation

* editor: Add duct and pipe fittings, routing, and system checks (#769)

* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* fix: pass nodes to lazy inspector panels

* MEP: unify duct and DWV routing UX

* Point editor dev runtime at local Streetscape plugin

* MEP: add exact lengths and branch affordances

* Make MEP runs surface-aware

* MEP: unify wall-aware duct and pipe run UX

- Add surface-aware drafting, snapping, and run attachments
- Support wall-attached run movement and endpoint updates
- Improve placement grid anchoring and semantic surface events

* MEP: free wall-attached routing and simplify fitting actions

- Continue routing horizontally after leaving a wall
- Keep quick material actions for pipe fittings only

* Fix duct and DWV direction capture from camera rays

* Align MEP snapping with architecture rules

* chore: satisfy repository checks

* test: scope pipe continuation handle assertion

* Add configurable MEP hangers and fix run drawing interactions

* Unify MEP accessory snapping and system connectivity

- Add shared snapping for MEP accessories with live setting updates
- Respect surfaces, levels, building transforms, and system boundaries
- Add coverage for snapping and cross-floor port connectivity

* Improve MEP connection feedback, slope controls, checks and hangers

* MEP: expand fitting catalogs and accessory configuration

- Add duct and DWV fittings, accessories, geometry, placement, and thumbnails
- Unify fitting selection through configurable tool options

* Simplify MEP build tools by removing the Add Trap action

- Remove the context-specific DWV Pipe Add Trap button from the Build tab

* Unify MEP run editing and placement UX

- Preview pipe and duct edits through live overrides
- Track fitting placement with interaction scopes
- Align surface-aware routing and accessory snapping

* Use live overrides for MEP selection previews

- Keep duct and pipe drag, roll, and offset previews out of committed scene state
- Render selection handles from live node overrides during interactions

* Simplify pipe routing status controls

* Make drafting behavior registry-driven

* Fix drafting history test registry setup

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* perf(editor): instance the ceiling corner brackets per level (#784)

* perf(editor): instance ceiling corner brackets per level

Replace the per-corner meshes with two level-wide InstancedMeshes sharing
one unit BoxGeometry. Legs and cubes use the same geometry/material path,
so their individual transforms fit in a single batch per opacity state.
Normal instances use 0.72 opacity and highlighted instances use 0.92;
instanceColor carries the original gray/indigo colors. Three 0.185.1's
StandardNodeLibrary maps MeshBasicMaterial to MeshBasicNodeMaterial, whose
NodeMaterial.setupDiffuseColor multiplies instanceColor into material color.
This requires no custom shader or per-camera sorting.

Keep the per-ceiling drag controllers memoized and non-rendering. Geometry,
height, live overrides and preview changes update that ceiling's matrices;
hover transfers only the affected parts between packed instance arrays.
Capacity doubles with headroom on overflow, count tracks occupied slots,
and React observes the batch store only when meshes are reallocated.
Conservative expanding spheres keep native raycasts valid after writes;
frustumCulled=false prevents stale render bounds from hiding handles.
InstancedMesh.prototype.raycast remains unchanged on the pointer fast path.

Use R3F's per-instance over/out events, with move reconciliation when packed
slots change ownership. Snapshot outgoing hover targets and pointer-down
part identities, allow clicks across highlight-batch transfers at the canvas
root, and retain stable React keys so R3F transfers interaction state on
capacity growth. Preserve the level portal and registry retry, ceiling:click
payload, drag/snap/SFX/override lifecycle, and synchronous capture hiding.

Accepted visual changes from the architect ruling:
- Normal brackets render at 1000 and highlighted brackets at 1001, making
  mixed overlaps deterministically highlighted-on-top.
- Ordering against other transparent objects at 1000 is now per batch,
  using the shared geometry centre at the level origin, rather than per
  bracket. There is no per-camera instance sorting.
No other intentional behavior changes.

Validation:
- Built the local core/viewer package outputs needed for editor validation.
- packages/editor: bun test src -- 848 pass, 0 fail across 119 files.
- Includes 12 new tests for instance indexing, highlights, capacity, old
  leg matrix parity, native raycasts, and mounted R3F hover/click/drag,
  override, capture, and unmount behavior.
- packages/editor: bun run check-types (tsgo --noEmit) -- passed.
- Root: bunx biome check on all four changed files -- clean.
- Runtime draw/frame measurements and pixel comparison remain with the
  architect; no browser or dev server was started.

* fix(editor): stabilize ceiling bracket picking and resource lifetime

Resolve equal-distance bracket hits by ceiling/corner/part identity for
hover, pointer-down and click. Packed instance IDs and opacity batch order
no longer decide the owner at coincident same-height ceiling corners.
Keep native InstancedMesh raycasting and the existing click payload.

Give each batch a clone of the unit box geometry and dispose that geometry
before retiring the mesh/material on growth or teardown. WebGPU owns
instance-attribute cleanup through its geometry disposal listener.

Use StaticDrawUsage for instance matrices and colors. Writes bump versions
and add update ranges for the affected slots. Clear source ranges after
rendering because TSL uploads internal attribute wrappers; their ranges are
consumed by the backend. The attribute scheduler regression verifies that
unchanged resting frames cause no attribute updates.

Poll sceneRegistry.revision and re-resolve the level only when it changes.
Keep a stable portal group attached beneath the current level so replacing
a level object does not remount the same primitives and lose R3F event
registration. Reparenting preserves the mesh, geometry and matrix buffers.
Read the live registry object for every drag-plane query as well. Retain
the initial requestAnimationFrame retry and synchronous capture hiding.
Document that ceiling:click.position remains level-local; do not transform
or otherwise change that payload.

The reviewer's normalView/MRT concern remains uncertain: overlapping faces
with different normals may change AO/ink output when batch order changes.
No normal/MRT changes are made here. The architect will check pixels with
ink and AO enabled. The previously accepted transparency ordering remains.

Validation:
- packages/editor: bun test src -- 852 pass, 0 fail, 119 files.
- New mounted regressions cover 20 repeated moves over coincident corners,
  stable click/drag ownership, and a translated/rotated same-id level
  replacement with unchanged geometry/matrix versions and local payloads.
- Unit regressions verify geometry dispose events on growth/teardown and
  Three's WebGPU attribute scheduler skipping unchanged frames while
  changed slots carry bounded update ranges.
- bunx tsc --noEmit -p packages/editor/tsconfig.json -- exit 0, no output.
- bunx biome check on all four changed files -- clean.

* docs(editor): note accepted small bracket matrix uploads

Accept whole-array uploads on every render for small matrix buffers using Three 0.185.1’s uniform BufferNode path; above the device uniform-buffer limit, the attribute path honors versions and update ranges.

* perf(nodes): skip animation mixers for items without clips (#785)

* feat(capture): add shared clay previews and dollhouse rendering

* docs(capture): document local previews and mesh presentation

* Split canopy regression matrix into independent tests

* feat(skills): fail closed on missing furniture inputs

* perf(nodes): batch ceiling undersides and slab bodies (charter row 16) (#789)

* perf(nodes): batch ceiling undersides and slab bodies

* fix(nodes): close surface batch ownership and rebuild lifecycles

* fix(editor): reconcile paint previews after apply exceptions

* fix(nodes): rebuild slabs and release batches on material cache clear

* fix(nodes): strip the merged wall batch from GLB exports

* fix(editor): include moved node identity in perf receipts

* fix(editor): preserve grid surface hits while batching

* fix: preserve batched surfaces in geometry raycasts

* test(nodes): run source-system probes from a package-local file, not bun -e (#792)

Fix private-editor CI's Lint, Typecheck & Test / Unit tests failure on Bun 1.3.0 Linux: eval probes started at the editor submodule root could not resolve @pascal-app/core from dependencies hoisted to the private root.

Write isolated probes under ignored package-local .turbo directories, resolve source imports and mocks from import.meta.dir, and remove probes in finally. Apply the same fix to the core parser test that imports zod from an eval probe. Preserve all cases and assertions.

Verified both dependency layouts, package and private-root test invocations, eval failure and file success from /tmp with automatic installs disabled, randomized nodes tests (seed 1), core parser tests, Biome, and no-emit typechecks.

* test(nodes): establish probe mocks before any fiber/react import (#793)

* test(nodes): establish probe mocks before any fiber/react import (Bun 1.3.0)

* test(nodes): make source-system probes linker-agnostic (isolated node_modules)

* skills: make furniture follow-ups blocker-aware (#794)

* feat(skills): add verdict-aware furniture follow-ups

* fix(skills): make furniture follow-ups blocker-aware

* fix(skills): enforce furniture action boundaries

* test(skills): pin furniture decision evidence

* docs: record agent skills 0.1.4 release source (#795)

* docs(skills): prepare OpenAI plugin submission

* docs(skills): complete OpenAI review fixtures

* docs(skills): prepare ClawHub publication

* fix(plugin): require MCP for OpenAI submission (#799)

* feat(mcp): add tool execution middleware

* docs(skills): record 0.1.6 as released

* fix(mcp): propagate tool cancellation

* fix(mcp): preserve executor on tool updates

* chore(skills): harden ClawHub bundles

* test(skills): reject ClawHub ignore overrides

* Add official MCP Registry publishing

* ci(mcp): verify live catalog consistency

* feat(skills): bundle local Claude MCP connector

* docs(skills): correct Claude MCP upgrade guidance

* docs(skills): record 0.1.7 release

* fix(skills): hide maintainer workflows from discovery

* fix(mcp): classify all tool side effects

* docs(openai): add tool annotation justifications (#813)

* feat(cli): add hosted agent claim command (#815)

* feat(cli): add hosted agent claim command

* fix(cli): require canonical claim expiry

* fix(ci): authenticate CLI npm publish (#816)

* fix(ci): restore OIDC for CLI publishing (#817)

* feat(cli): prefill hosted agent claim (#818)

* docs(cli): publish verified agent claim preview (#819)

* feat(cli): report hosted agent status (#821)

* docs(cli): publish verified agent status preview (#822)

* feat(skills): add human-openable fit prechecks (#824)

* docs(skills): record agent report release evidence (#825)

* perf: scope undo/redo invalidation to changed geometry and cleared previews (charter row 7) (#805)

* Scope undo invalidation to changed geometry and cleared previews

* Reset editor state before randomized store tests

* Resolve history probe mocks from each consuming package

* Restore discarded preview dependency closures on undo and redo

* Limit rendered slab invalidation to changed boundary bands

* Cover history support transfers and scoped endpoint rebuilds

* Pin endpoint history closure with spatial sync mounted

* Run package tests against core source without rebuilding dist

* test: drop the repo-wide core source preload

* test: verify consecutive undo and redo invalidation

Zundo 2.3.0 appends the just-left snapshot to both destination stacks, so the existing pre-jump length indices are correct. Cover three adjacency-changing moves and each undo/redo with cleared marks and flushed microtasks.

* fix: invalidate old slab covering dependents on reparent

Refresh covering dependents below both parent levels, deduplicating equal resolved levels. Cover reparent from level 2 to level 3 and undo with exact wall/ceiling sets and unrelated levels left clean.

* perf: drain initial wall builds within the time budget (charter row 6) (#800)

* perf: drain initial wall builds within the time budget

* fix(core): invalidate hydration atomically with scene edits

* test: isolate scene fixtures from randomized ordering

* fix(core): complete normalization before publishing hydration

* fix(viewer): preserve and bound initial wall drain lifetime

* docs: clarify hydration lifetime and wall drain counters

* Experience fix pass: placement, selection rotation, roof, stairs, capture, Cmd+S, three 0.186 (#807)

* fix(capture): round armed FOV, add Alt slow modifier for the drone camera

armCaptureFov stored the live camera FOV verbatim, so fractional pose FOVs
printed float tails in the HUD and left the reset button enabled. Both
writers now share clampCaptureFov.

Alt holds the drone at 0.2x speed and look sensitivity for fine framing;
Shift stays the boost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): keep the gable shell base on the wall top

The CSG degeneracy guard enforced its 5 cm minimum by lowering the shell
base, which for wallHeight-0 room roofs put the gable 4 cm inside the
wall and z-fought its faces. Raise the eave instead; mirror the floor in
the opening-placement frame and the shed inset panel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(editor): Cmd/Ctrl+S saves instead of opening the browser dialog

Capture-phase, always-on listener so the page-save dialog never appears.
Hosts can take the chord over via onSaveShortcut; the default flushes the
autosave through the existing executeSave path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(tools): anchor composite presets at their footprint centre, lift previews to the level

Fresh (absolute) placement mapped the cursor to the node origin, so a
cabinet run landed |bounds.center| away from the pointer. Subtract the
rotated centre and keep it under the cursor across R/T. The registry
mover's box/sphere now ride the target level's stacked Y like the other
placement tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): follow the storey height from the elected base

Level-destination stairs returned the full floor-to-floor height even
when a slab lifted their base, so the top overshot the storey plane. The
resolver now subtracts the elected base for both destinations. The panel
exposes Follows storey / Custom rise for level stairs, and the stair tool
and landing toggle seed from the storey instead of a 2.5 m constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(item): drop un-hosted items to the floor, draw the placement box on the right storey

The floor-path Y was frozen at drag start (#638), so an item pulled off a
shelf kept the shelf height after reparenting. Read the live grid Y
instead. The cursor group, grid surface and facing pose now add the
level mesh's stacked Y, which the building-local tool group lacks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(selection): keep member rotation when pressing R/T mid-drag

translateGroupPatches dropped the snapshots' yaw after a mid-gesture
rotation, so the layout orbited while every item kept its old facing and
the commit wrote the same. Carry rotation for vec3/scalar participants,
pivot every session on the shared mesh-box centre the idle shortcut
uses, and engage an armed session before rotating.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* chore(deps): three 0.186.0

No removed export is used and every peer range admits r186. Two
adjustments: Renderer.dispose() is async now, so the capability probe
swallows its rejection; and r186's CommonJS entry re-exports the ES
module, which Bun cannot require() while the same process imports three
as ESM. A bun test preload steers fiber/drei/maath/meshline (no exports
map, CJS main) to their module builds, the way bundlers already resolve
them. Types stay on 0.184.1 (0.185 types OOM tsgo).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: pre-evaluate three in the bun test preload

Bun's plugin onResolve does not run for static imports, so steering the
R3F packages to their module builds never applied in CI (isolated linker)
and the CJS require("three") kept racing the ESM import. Evaluating the
package's own three copy first makes the later require() a cache hit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): lift the inner cutter and deck with the shell eave

The 5 cm CSG floor lifted only the outer shell, so a flat zero-height
roof would have ended up with a solid cap under the deck. Compute the
lift once and apply it to every volume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(editor): fall back to the autosave flush when the host does not handle Cmd+S

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): floor every prism at 5 cm instead of lifting by the shell's eave

A shell-derived lift left overhanging deck cutters with a negative eave.
Clamp each volume's top the way main did, just at 5 cm and without the
base sink, so cutters stay level with the shells they carve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): drop the duplicate geometry Rise control

The rise-mode block already exposes the Rise field in custom mode; the
geometry copy wrote totalRise behind the Follows storey toggle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: import resolveSync explicitly in the three preload

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: skip the three preload where the cwd has no three dependency

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(editor): seed placed stairs from the elected base; keep the gesture when R/T cannot engage

The stair tool seeded the flight from the storey height alone, a slab
thickness too tall until syncStairRises caught up; it now subtracts the
drop point's elected base like the resolver. A failed engage() on R/T no
longer tears down the pointer listeners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): cap the placed rise by the pointed support surface

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix: scale the stair ghost to the placed rise; await renderer.dispose() before the WebGL fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): read the placed rise from the preview scene

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(selection): re-fit alignment bounds from the start footprint after each R/T

Rotating the previous axis-aligned fit inflated the anchors every step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): switching to straight materializes a flight; level labels use the shared display name

A curved stair switched to straight had no stair-segment child and drew
nothing (and vanished on select). The type change now creates a default
flight in the same history step and the viewer falls back to that flight
for already-broken scenes. Stair and elevator panels label levels the way
the level switcher does, and the rise toggle reads Follows level like walls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(roof): wall-footprint roofs follow their source walls' tops

Room roofs computed their elevation once at creation, so a later custom
wall height left the roof at the storey plane. Roofs now remember their
source walls and a core system re-derives position[1] (highest top,
clamped to the level floor) on wall/slab/level edits, history-paused like
the stair rise sync. Moving the roof by hand detaches it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): a flight height edit pins the parent stair to the new total rise

On a follows-level stair the sync handed the edited height straight back,
so the segment slider did nothing. The edit now also writes totalRise
(the stair becomes Custom rise, as editing Rise on its own panel does).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): follow wall tops below the storey plane

Walls shorter than the level (2.5 m in a 3 m storey) left a gap because
the roof elevation was clamped to its level floor. Follow the wall top.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(roof): follow walls by intent, resolved from the footprint

Replace the source-wall id list with support.kind 'walls': room and
conical roofs are created following, the system resolves the enclosure
under the roof centre on the level below and writes the highest wall top
(unclamped), an explicit Y edit or vertical handle drag flips the roof to
custom, and the panel offers Follows walls / Custom like walls do. No
migration; existing roofs stay custom until the user opts in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): pick supporting walls by footprint overlap, not closed-room membership

A room missing a wall, or an L-room whose centre falls outside, left the
roof frozen. Walls whose band overlaps a segment footprint on the level
below now count; segment-less roofs keep the point-in-room lookup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): parent room roofs to the storey above their walls; follow walls on the roof's own level too

Armed on the walls' level, the tool parented the roof to that level and
the follow rule only looked one storey down, so a Floor 1 roof dropped to
the Level 0 wall tops. The roof now goes to the level above the walls
when one exists (top floor keeps it on the walls' level), and the
resolver considers walls on the roof's level and the one below.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* test(editor): resolve history probe modules from packages that depend on them (isolated linker) (#828)

* test(editor): resolve history probe modules from packages that depend on them (isolated linker)

* test(nodes): keep the lean-to canopy angle sweep under the per-test timeout on slow runners

* feat(editor): streamline connected pipe and duct drafting (#827)

* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* fix: pass nodes to lazy inspector panels

* feat: add immersive WebXR editor support

* chore: remove WebXR integration

* chore: remove WebXR support

* chore: checkpoint existing editor work before inline insertion

* docs: track inline insertion implementation steps

* docs: record inline insertion domain contract completion

* feat: add inline pipe fitting insertion and run snapping

- Split pipe runs around inline fittings with preserved connections
- Improve run snapping, marquee selection, and rotation shortcut ownership

* feat: route insertion tools through registry scene context

- Add screen-space projection data for cross-view snapping
- Use registry scene APIs for atomic node changes and selection

* feat: keep run end caps aligned during endpoint moves

- Update mated duct and pipe end caps as endpoints move
- Cache shared handle geometry and materials
- Remove redundant connection and snap labels

* fix: scale run direction feedback geometry

- Preserve ray and arrow dimensions while using unit-sized shared geometry

* fix: resolve architecture review findings

* fix(cli): trim vendored archives from runtime

* fix(nodes): preserve automatic end cap ownership

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>

* skills: portable mcp.json, channel manifests, validator parity (#829)

* chore(skills): portable mcp.json, channel manifests, validator parity

Add the root mcp.json the Agent Plugins spec fixes for Codex and Cursor
(previously only .mcp.json shipped, so those hosts installed the skills
without the MCP server), a Gemini CLI extension manifest, and repository
and icons on server.json. Make plugin.json the single bundle version
source and assert name, version, description and author parity across
all five descriptors, mcp.json/.mcp.json equality, the Claude marketplace
skill set, the documented OpenAI interface fields, byte-identical
.clawhubignore files, and fragment-aware links across skills/README.md
and VALIDATION.md. Fix the broken anchor to the verified GitHub preview,
the 0.1.7 release-notes version, the Cursor snippets to
${env:PASCAL_API_KEY}, add bun run skills:validate for CI and docs,
drop the version literal from mcp-registry.yml, add the skills.sh badge,
the shell-history caveat, and CHANGELOG entries for the distribution work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(skills): add Cursor manifest and plate logo for marketplaces

Directory forms want a 1:1 logo on a background plate, and Cursor's
checklist wants it committed and referenced by relative path. Add the
brand mark on its #171717 plate as assets/pascal-mark-plate.svg and the
byte-identical brand-kit 1024 px PNG, point the OpenAI logo at the plate
SVG (composerIcon keeps the transparent mark), add
.cursor-plugin/plugin.json with Cursor-native fields, list the 1024 icon
on server.json, and assert the Cursor manifest's parity and paths in the
validator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* skills: make skills/ the Claude plugin root (#832)

* fix(skills): make skills/ the Claude plugin root

The Claude marketplace entry sourced the plugin from the repository root,
so every `/plugin install pascal-agent-skills@pascal` copied the whole
monorepo into the plugin cache and, because that root carries package.json
next to bun.lock, ran `bun install --frozen-lockfile --ignore-scripts`
against it on every install and update (60 s timeout, not disableable). A
fresh install produced a 1.2 GB cache, 1.1 GB of it node_modules, to
deliver two markdown skill bundles.

Point the marketplace entry at ./skills and move the Claude plugin manifest
and the bundled local `pascal mcp connect` configuration into that root; a
plugin cannot reference files above its own root, so both have to live
inside skills/. The manifest lists the bundles explicitly because the
default skills/ scan no longer applies once skills/ is itself the root. A
fresh install is now 196 KB with no node_modules, package.json, or
packages/. skills.sh tree URLs, the Codex and Cursor Agent Plugins layout,
Gemini and ClawHub still read the root plugin.json, root mcp.json, and the
same skills/ tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(skills): list the plugin as Pascal in directories

Directory listings show the display name next to product plugins listed
by brand, so use the brand rather than "Pascal agent skills" across the
Claude, Cursor and OpenAI manifests. The identifier stays
pascal-agent-skills.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): link the plugin-root fix to #832

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* skills: bundle the hosted Pascal MCP server with a key prompt (#835)

* feat(plugin): add hosted MCP server to the Claude Code plugin

The plugin only bundled the local `pascal mcp connect` stdio server, so a
Claude Code user with a Pascal account had to leave the plugin and run
`claude mcp add` by hand before touching a hosted project or a Capture scan.
Declaring the key as `userConfig.pascal_api_key` lets Claude Code collect it
in the enable-time prompt and substitute it into the `pascal-hosted` server's
Authorization header, so the hosted tools arrive with the skills.

The option is `sensitive` so Claude Code stores the key in the OS keychain
instead of settings.json, and `required: false` so a local-only install still
works with the field left empty.

`${user_config.*}` is a Claude Code substitution, so the hosted server cannot
live in the portable Agent Plugins `mcp.json` that Codex and Cursor read. The
validators now enforce that split: the `pascal` server must be byte-identical
in both files, `skills/.mcp.json` may add only `pascal-hosted`, and the hosted
Authorization header must stay a `user_config` reference so no literal
credential can ship in the published plugin source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): link the hosted MCP entry to #835

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): graduate prerelease versions on stable bumps (#837)

The stable-bump path split "1.0.0-beta.5" on dots, so major produced
2.0.0, minor 1.1.0, patch failed on "0-beta" arithmetic, and none would
have published a beta version on the latest dist-tag. Any stable bump on
a prerelease now yields its base version, matching npm semver, so the
1.0.0-beta.N line can be released as 1.0.0.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf(cli): prune build-only files from the portable runtime (#838)

`next build` copies its tracing root into `.next/standalone`, so the staged
runtime shipped app sources, repository documentation, build trace metadata and
assets that `server.js` never reads.

Pruned from `dist/runtime`:

- `public/audios/radios` (39.3 MB) — the radio catalogue is played by the hosted
  community app, which serves its own copy; nothing in this repository requests
  `/audios/radios`.
- `next/dist/server/capsize-font-metrics.json` + `font-utils.js` (4.1 MB) —
  `font-utils.js` is the only reader of the metrics and is itself unreachable
  from the standalone server.
- A stray 3.15 MB authoring screenshot and a duplicate `.glb` under
  `public/items` — item assets are addressed by convention, and anything else is
  now dropped and named on stdout.
- `apps/editor/{app,components,lib}` plus dev-only configuration and docs
  (0.5 MB) — TypeScript sources and tests that Node never executes.
- `.nft.json` build trace metadata and source maps under `.next` (0.7 MB).

Before: 107.5 MB tarball, 149.2 MB unpacked, 2956 files.
After:   64.6 MB tarball, 101.7 MB unpacked, 2870 files.

The release budget in the smoke test drops to 75 MB / 115 MB / 3200 files so the
regression cannot come back unnoticed. `stage-runtime` + `smoke-runtime` pass,
and the packed CLI still serves the editor, `/scenes`, a scene page with all 84
of its static chunks, and every sampled public asset.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf(editor): re-encode fitting thumbnails as 256px webp (#842)

`public/icons/fittings/` held 16 PNGs at 1254x1254 RGBA — 11 MB of assets
for thumbnails that render at 56 CSS px in the MEP tool options grid, and
11 MB of the 64.6 MB packed CLI runtime. Every other icon under
`public/icons` is already a small webp.

Each PNG becomes a 256x256 lossy webp with alpha (`cwebp -q 85 -m 6
-alpha_q 100 -resize 256 256`), which is still 2.3x the largest rendered
size — the portable build sets `images.unoptimized`, so the raw file is
what the browser scales. The directory drops from 11 MB to 164 KB.

`build-tab.tsx` derives the path from the fitting type, so the extension
in that template is the only reference to update.

Packed runtime smoke: 54.1 MB compressed, 91.0 MB unpacked, 2870 files
(was 64.6 MB / 101.7 MB / 2870).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* chore(editor): drop unreferenced public assets (#843)

Remove 53 MB of committed assets nothing in this repository reads: the
small-kitchen-cabinet item (10.9 MB; the item catalog resolves every item
from remote storage and no demo references this slug), a stray authoring
screenshot, and the radio catalogue (39 MB) that only the hosted community
app plays from its own copy. The CLI staging script already pruned the
radios and the screenshot; its rm(force) calls tolerate their absence.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): publish through npm trusted publishing only (#839)

The 1.0.0 release failed with EOTP on its first publish: npm no longer
accepts direct publishing with 2FA-bypass granular tokens. Drop
NODE_AUTH_TOKEN from every publish step so npm 11 exchanges the GitHub
Actions OIDC token instead. Requires each @pascal-app package to have
this repository, workflow file and the npm environment configured as a
trusted publisher on npmjs.com.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): ship a small CLI that downloads the web editor runtime (#845)

The npm package carried the whole standalone Next editor: 65 MB compressed,
102 MB unpacked, for 0.1 MB of CLI code. Agents that only speak MCP paid that
cost too, because the MCP bridge started the editor to reach it.

Split the two. `dist/` now holds the CLI plus `services/pascal-mcp.mjs` and a
`runtime-source.json` naming the web runtime archive for this exact version,
its size, and its SHA-256. The web editor runtime ships as a GitHub release
asset and is downloaded once per version, verified, and installed through the
existing atomic install seam.

- MCP is its own managed service (`run/mcp.json`), started on demand by
  `pascal mcp connect` with no editor process and no runtime download.
- Commands that start the editor resolve the runtime from
  `PASCAL_BUNDLED_RUNTIME_DIR`, `--runtime <directory-or-archive>`, the
  installed version, else the release asset; a digest mismatch deletes the
  temporary file and installs nothing.
- Downloads stream over `node:https` with `HTTPS_PROXY`/`NO_PROXY` support and
  no new dependency; concurrent first runs share the install lock.
- `stage-runtime` writes a deterministic `pascal-web-runtime-<version>.tar.gz`
  plus `.sha256`; the release job verifies both before publishing and uploads
  them to the CLI tag right after it is pushed.
- The smoke test now covers MCP-only startup with no runtime present and the
  local-archive install, including a one-byte tamper that must fail closed.

Package: 0.46 MB compressed, 2.46 MB unpacked, 68 files.
Archive: 64.2 MB compressed, 106 MB installed.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* docs(skills): describe the hosted-only capture tools (#846)

Document the hosted-only Capture scan path (list_captures, get_capture,
open_capture_as_project) in the pascal-3d skill and its tool workflows, and
scope the counted 46-tool annotation inventory to the public package so the
hosted server's extra tools do not read as a packet gap.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(plugins): add optional hosted auth to the Cursor plugin (#849)

* feat(plugins): add optional hosted auth to the Cursor plugin

A Cursor install can now reach hosted Pascal — projects, Pascal Capture
scans and shared workspaces — with an optional API key, while the
credential-free local `pascal mcp connect` server keeps working.

`.cursor-plugin/plugin.json` declares an optional `PASCAL_API_KEY`
variable and points `mcpServers` at a new Cursor-dialect
`.cursor-plugin/mcp.json` that adds a `pascal-hosted` server for
https://editor.pascal.app/api/mcp. Cursor substitutes the bare
`${PASCAL_API_KEY}` plugin-variable form from its dashboard, so the
repository holds only the placeholder. The variable is absent from
`required`, so an install with no key still loads and only
`pascal-hosted` fails (401).

The portable `mcp.json` stays credential-free on purpose. Agent Plugins
1.0.0 forbids secrets and placeholder expansion in `headers` (7.2.3,
9.2), its only remote keyword is `streamable-http` rather than Cursor's
`http`, and Codex drops a plugin-supplied `Authorization` as a
client-owned header. Codex users therefore keep using
`codex mcp add --bearer-token-env-var PASCAL_API_KEY`.

ClawHub already declares `PASCAL_API_KEY` optional through
`metadata.openclaw.envVars[].required: false`, so the skills are
unchanged.

`bun run skills:validate` now asserts the Cursor MCP path, a `pascal`
server identical to the portable one, the exact hosted URL and header
template, the optional-and-never-required variable with no unsupported
schema keywords, and that no `${VAR}` in the Cursor config is
undeclared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(plugins): keep the Cursor author block within Cursor's schema

Cursor's plugin.json schema allows only name and email under author
(additionalProperties: false); the url field failed validation on every
install. Compare the Cursor manifest's author on those two fields and
link the changelog entry to #849.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf: per-slab invalidation, candidate-scoped temporal reconciliation and rotation-stable surface planning (row 17) (#850)

* perf(nodes): invalidate slabs by derived polygon changes

* perf(core): scope temporal reconciliation to changed nodes

* fix(nodes): mirror rendered slab context membership and order

* perf(nodes): reuse slab inputs and scope polygon derivation

* test(core): verify structural temporal reconciliation outcomes

* docs: describe temporal candidates and slab dependency tracking

* perf(core): skip disjoint room coverage and rotated surface rewrites

Cache polygon bounds for indexed surface scoping and preserve exact cyclic
outer-ring rotations in the shared slab and ceiling planners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* editor: integrate Environment with generic host and export APIs (#831)

* refactor(packages): fold capture packages into core and viewer (#851)

Avoid npm package sprawl before 1.0.0: `@pascal-app/capture-protocol`
becomes the `@pascal-app/core/capture` subpath and
`@pascal-app/capture-viewer` becomes `@pascal-app/viewer/capture` (plus
`@pascal-app/viewer/capture/preview`), so the release ships seven
packages: core, viewer, editor, nodes, mcp, ifc-converter, cli.

Neither package was ever published to npm, so no npm consumer migrates.
The protocol code is pure zod/TS, so core keeps its no-Three.js layer
rule; the runtime and its reference layers keep viewer's existing peers
and now reach viewer internals through relative imports instead of a
self-referential `@pascal-app/viewer` specifier.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): declare @react-three/test-renderer where the lifecycle test imports it (#852)

The registered tool lifecycle test imports @react-three/test-renderer, but
only the viewer workspace declared it. Hoisting hid the missing dependency;
private-editor CI uses Bun's isolated linker and cannot resolve that import
from the editor workspace. Declare the same ^9.1.0 development dependency
in editor and record it in the workspace lockfile entry.


Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): drop registry-url so npm uses OIDC trusted publishing (#853)

The 1.0.0 run failed publishing core with E404. actions/setup-node with
registry-url writes an .npmrc whose token falls back to the placeholder
XXXXX-XXXXX-XXXXX-XXXXX when NODE_AUTH_TOKEN is unset; npm sent that fake
token instead of exchanging the Actions OIDC token, and the registry
answered 404. Without registry-url no .npmrc is written and npm 11 falls
through to trusted publishing.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): log npm verbosely to surface OIDC exchange errors (#856)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(capture): cache preview artifacts, retry failed downloads, and clarify device-path visibility (#858)

* fix(capture): cache preview data and improve device path visibility

* fix(capture): recover failed JSON preview downloads

* test(viewer): preload one React instance before rendering hooks

* release: @pascal-app/core@1.0.0 @pascal-app/viewer@1.0.0 @pascal-app/editor@1.0.0 @pascal-app/nodes@1.0.0 @pascal-app/mcp@1.0.0 @pascal-app/ifc-converter@1.0.0 @pascal-app/cli@1.0.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: replace the CLI preview instructions with the published npm CLI (#859)

`@pascal-app/cli@1.0.0` is on the npm `latest` tag with `pascal agent claim`,
`pascal agent status`, and the read-only `check_collisions.candidate` input, so
the checksum-verified GitHub prerelease the docs pointed at is obsolete. Delete
the "Verified CLI preview" and "Verified GitHub preview" sections, stop
recommending the `beta` dist-tag (it still resolves to the older
`1.0.0-beta.1`), and drop the inverted claim that the npm package bundles the
web editor runtime — 1.0.0 downloads it from a release asset on first use.

Close the changelog's `Unreleased` heading as `1.0.0 (2026-09-12)` with the
package and contributor sections the earlier releases carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: describe the release workflow and refresh the validation scope (#860)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(editor): keep manifold-3d out of consumer bundler graphs (#735)

manifold-3d's emscripten glue awaits import('node:module') behind a Node
check; the branch never executes in a browser, but webpack refuses to
build any graph that can reach it. export-manager.tsx statically imports
the manifold worker wrapper and ExportManager renders unconditionally
from the editor root, so every external webpack consumer of
@pascal-app/editor failed at build time (#715).

The worker chunk is still built by the consumer's bundler, but it no
longer contains a traceable manifold-3d specifier. The glue is loaded at
runtime through an import() no bundler follows: bare specifier first
(bun tests, dev servers, bundlers that inlined it anyway), then a
version-pinned jsDelivr copy for bundled browser builds — emscripten
locates manifold.wasm relative to the glue's own URL, so the CDN path
self-resolves. configureManifoldRuntime(options) lets offline or
CSP-restricted hosts point both URLs at self-hosted assets.

A failed load no longer poisons later attempts: the cached module
promise resets on rejection.

Fixes #715

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* mcp: make batch-first apply_patch the stated default (#767)

* docs(mcp): make batch-first apply_patch usage the stated default

Tool description, agent guide, from-brief preamble, and README now instruct agents to compose one atomic apply_patch batch per phase instead of looping single-op calls. The tool already validates all ops before applying any; only the guidance was missing.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(mcp): assert batch-first apply_patch guidance surfaces

Lock the tool description, agent guide, from_brief preamble, and README
row that state batch-first as the default without changing apply_patch
runtime behavior.

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(editor): type optional ancestor traversal for downstream consumers (#814)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(viewer): clamp GLB floor animation on slow frames (#820)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(skills): compare bundle paths without a hardcoded separator (#848)

`bun run skills:validate` fails on Windows for every cross-file link
inside a skill bundle and for both OpenAI interface assets, even though
each referenced file exists inside the plugin. `resolve()` returns
backslash-separated paths on Windows, so the `${dir}/` prefix compared
against never matched.

Compare on a normalized separator instead, and cover the predicate with
a focused test so the check stays platform-independent.

---------

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Co-authored-by: Aymeric Rabot <aymeric@pascal.app>
Co-authored-by: Sudhir Yadav <sudhir9297@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>
Co-authored-by: Aymeric Rabot <aymeric.rabot@gmail.com>
Co-authored-by: Adam NAILI <18304870+AxiomeCG@users.noreply.github.com>
Co-authored-by: ActArtech <123718991+ActArtech@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Co-authored-by: Wu Shuwen <mikewushuwen@outlook.com>
Aymericr added a commit that referenced this pull request Sep 12, 2026
* fix(editor): honor millimeter notation across measurement panels

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* feat(skills): add human-openable fit prechecks (#824)

* docs(skills): record agent report release evidence (#825)

* fix(editor): use display precision for level height badges

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* perf: scope undo/redo invalidation to changed geometry and cleared previews (charter row 7) (#805)

* Scope undo invalidation to changed geometry and cleared previews

* Reset editor state before randomized store tests

* Resolve history probe mocks from each consuming package

* Restore discarded preview dependency closures on undo and redo

* Limit rendered slab invalidation to changed boundary bands

* Cover history support transfers and scoped endpoint rebuilds

* Pin endpoint history closure with spatial sync mounted

* Run package tests against core source without rebuilding dist

* test: drop the repo-wide core source preload

* test: verify consecutive undo and redo invalidation

Zundo 2.3.0 appends the just-left snapshot to both destination stacks, so the existing pre-jump length indices are correct. Cover three adjacency-changing moves and each undo/redo with cleared marks and flushed microtasks.

* fix: invalidate old slab covering dependents on reparent

Refresh covering dependents below both parent levels, deduplicating equal resolved levels. Cover reparent from level 2 to level 3 and undo with exact wall/ceiling sets and unrelated levels left clean.

* perf: drain initial wall builds within the time budget (charter row 6) (#800)

* perf: drain initial wall builds within the time budget

* fix(core): invalidate hydration atomically with scene edits

* test: isolate scene fixtures from randomized ordering

* fix(core): complete normalization before publishing hydration

* fix(viewer): preserve and bound initial wall drain lifetime

* docs: clarify hydration lifetime and wall drain counters

* Experience fix pass: placement, selection rotation, roof, stairs, capture, Cmd+S, three 0.186 (#807)

* fix(capture): round armed FOV, add Alt slow modifier for the drone camera

armCaptureFov stored the live camera FOV verbatim, so fractional pose FOVs
printed float tails in the HUD and left the reset button enabled. Both
writers now share clampCaptureFov.

Alt holds the drone at 0.2x speed and look sensitivity for fine framing;
Shift stays the boost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): keep the gable shell base on the wall top

The CSG degeneracy guard enforced its 5 cm minimum by lowering the shell
base, which for wallHeight-0 room roofs put the gable 4 cm inside the
wall and z-fought its faces. Raise the eave instead; mirror the floor in
the opening-placement frame and the shed inset panel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(editor): Cmd/Ctrl+S saves instead of opening the browser dialog

Capture-phase, always-on listener so the page-save dialog never appears.
Hosts can take the chord over via onSaveShortcut; the default flushes the
autosave through the existing executeSave path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(tools): anchor composite presets at their footprint centre, lift previews to the level

Fresh (absolute) placement mapped the cursor to the node origin, so a
cabinet run landed |bounds.center| away from the pointer. Subtract the
rotated centre and keep it under the cursor across R/T. The registry
mover's box/sphere now ride the target level's stacked Y like the other
placement tools.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): follow the storey height from the elected base

Level-destination stairs returned the full floor-to-floor height even
when a slab lifted their base, so the top overshot the storey plane. The
resolver now subtracts the elected base for both destinations. The panel
exposes Follows storey / Custom rise for level stairs, and the stair tool
and landing toggle seed from the storey instead of a 2.5 m constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(item): drop un-hosted items to the floor, draw the placement box on the right storey

The floor-path Y was frozen at drag start (#638), so an item pulled off a
shelf kept the shelf height after reparenting. Read the live grid Y
instead. The cursor group, grid surface and facing pose now add the
level mesh's stacked Y, which the building-local tool group lacks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(selection): keep member rotation when pressing R/T mid-drag

translateGroupPatches dropped the snapshots' yaw after a mid-gesture
rotation, so the layout orbited while every item kept its old facing and
the commit wrote the same. Carry rotation for vec3/scalar participants,
pivot every session on the shared mesh-box centre the idle shortcut
uses, and engage an armed session before rotating.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* chore(deps): three 0.186.0

No removed export is used and every peer range admits r186. Two
adjustments: Renderer.dispose() is async now, so the capability probe
swallows its rejection; and r186's CommonJS entry re-exports the ES
module, which Bun cannot require() while the same process imports three
as ESM. A bun test preload steers fiber/drei/maath/meshline (no exports
map, CJS main) to their module builds, the way bundlers already resolve
them. Types stay on 0.184.1 (0.185 types OOM tsgo).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: pre-evaluate three in the bun test preload

Bun's plugin onResolve does not run for static imports, so steering the
R3F packages to their module builds never applied in CI (isolated linker)
and the CJS require("three") kept racing the ESM import. Evaluating the
package's own three copy first makes the later require() a cache hit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): lift the inner cutter and deck with the shell eave

The 5 cm CSG floor lifted only the outer shell, so a flat zero-height
roof would have ended up with a solid cap under the deck. Compute the
lift once and apply it to every volume.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(editor): fall back to the autosave flush when the host does not handle Cmd+S

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): floor every prism at 5 cm instead of lifting by the shell's eave

A shell-derived lift left overhanging deck cutters with a negative eave.
Clamp each volume's top the way main did, just at 5 cm and without the
base sink, so cutters stay level with the shells they carve.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): drop the duplicate geometry Rise control

The rise-mode block already exposes the Rise field in custom mode; the
geometry copy wrote totalRise behind the Follows storey toggle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: import resolveSync explicitly in the three preload

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* test: skip the three preload where the cwd has no three dependency

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(editor): seed placed stairs from the elected base; keep the gesture when R/T cannot engage

The stair tool seeded the flight from the storey height alone, a slab
thickness too tall until syncStairRises caught up; it now subtracts the
drop point's elected base like the resolver. A failed engage() on R/T no
longer tears down the pointer listeners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): cap the placed rise by the pointed support surface

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix: scale the stair ghost to the placed rise; await renderer.dispose() before the WebGL fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): read the placed rise from the preview scene

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(selection): re-fit alignment bounds from the start footprint after each R/T

Rotating the previous axis-aligned fit inflated the anchors every step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): switching to straight materializes a flight; level labels use the shared display name

A curved stair switched to straight had no stair-segment child and drew
nothing (and vanished on select). The type change now creates a default
flight in the same history step and the viewer falls back to that flight
for already-broken scenes. Stair and elevator panels label levels the way
the level switcher does, and the rise toggle reads Follows level like walls.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(roof): wall-footprint roofs follow their source walls' tops

Room roofs computed their elevation once at creation, so a later custom
wall height left the roof at the storey plane. Roofs now remember their
source walls and a core system re-derives position[1] (highest top,
clamped to the level floor) on wall/slab/level edits, history-paused like
the stair rise sync. Moving the roof by hand detaches it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(stair): a flight height edit pins the parent stair to the new total rise

On a follows-level stair the sync handed the edited height straight back,
so the segment slider did nothing. The edit now also writes totalRise
(the stair becomes Custom rise, as editing Rise on its own panel does).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): follow wall tops below the storey plane

Walls shorter than the level (2.5 m in a 3 m storey) left a gap because
the roof elevation was clamped to its level floor. Follow the wall top.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(roof): follow walls by intent, resolved from the footprint

Replace the source-wall id list with support.kind 'walls': room and
conical roofs are created following, the system resolves the enclosure
under the roof centre on the level below and writes the highest wall top
(unclamped), an explicit Y edit or vertical handle drag flips the roof to
custom, and the panel offers Follows walls / Custom like walls do. No
migration; existing roofs stay custom until the user opts in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): pick supporting walls by footprint overlap, not closed-room membership

A room missing a wall, or an L-room whose centre falls outside, left the
roof frozen. Walls whose band overlaps a segment footprint on the level
below now count; segment-less roofs keep the point-in-room lookup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): parent room roofs to the storey above their walls; follow walls on the roof's own level too

Armed on the walls' level, the tool parented the roof to that level and
the follow rule only looked one storey down, so a Floor 1 roof dropped to
the Level 0 wall tops. The roof now goes to the level above the walls
when one exists (top floor keeps it on the walls' level), and the
resolver considers walls on the roof's level and the one below.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* test(editor): resolve history probe modules from packages that depend on them (isolated linker) (#828)

* test(editor): resolve history probe modules from packages that depend on them (isolated linker)

* test(nodes): keep the lean-to canopy angle sweep under the per-test timeout on slow runners

* feat(editor): streamline connected pipe and duct drafting (#827)

* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* fix: pass nodes to lazy inspector panels

* feat: add immersive WebXR editor support

* chore: remove WebXR integration

* chore: remove WebXR support

* chore: checkpoint existing editor work before inline insertion

* docs: track inline insertion implementation steps

* docs: record inline insertion domain contract completion

* feat: add inline pipe fitting insertion and run snapping

- Split pipe runs around inline fittings with preserved connections
- Improve run snapping, marquee selection, and rotation shortcut ownership

* feat: route insertion tools through registry scene context

- Add screen-space projection data for cross-view snapping
- Use registry scene APIs for atomic node changes and selection

* feat: keep run end caps aligned during endpoint moves

- Update mated duct and pipe end caps as endpoints move
- Cache shared handle geometry and materials
- Remove redundant connection and snap labels

* fix: scale run direction feedback geometry

- Preserve ray and arrow dimensions while using unit-sized shared geometry

* fix: resolve architecture review findings

* fix(cli): trim vendored archives from runtime

* fix(nodes): preserve automatic end cap ownership

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>

* skills: portable mcp.json, channel manifests, validator parity (#829)

* chore(skills): portable mcp.json, channel manifests, validator parity

Add the root mcp.json the Agent Plugins spec fixes for Codex and Cursor
(previously only .mcp.json shipped, so those hosts installed the skills
without the MCP server), a Gemini CLI extension manifest, and repository
and icons on server.json. Make plugin.json the single bundle version
source and assert name, version, description and author parity across
all five descriptors, mcp.json/.mcp.json equality, the Claude marketplace
skill set, the documented OpenAI interface fields, byte-identical
.clawhubignore files, and fragment-aware links across skills/README.md
and VALIDATION.md. Fix the broken anchor to the verified GitHub preview,
the 0.1.7 release-notes version, the Cursor snippets to
${env:PASCAL_API_KEY}, add bun run skills:validate for CI and docs,
drop the version literal from mcp-registry.yml, add the skills.sh badge,
the shell-history caveat, and CHANGELOG entries for the distribution work.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(skills): add Cursor manifest and plate logo for marketplaces

Directory forms want a 1:1 logo on a background plate, and Cursor's
checklist wants it committed and referenced by relative path. Add the
brand mark on its #171717 plate as assets/pascal-mark-plate.svg and the
byte-identical brand-kit 1024 px PNG, point the OpenAI logo at the plate
SVG (composerIcon keeps the transparent mark), add
.cursor-plugin/plugin.json with Cursor-native fields, list the 1024 icon
on server.json, and assert the Cursor manifest's parity and paths in the
validator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* skills: make skills/ the Claude plugin root (#832)

* fix(skills): make skills/ the Claude plugin root

The Claude marketplace entry sourced the plugin from the repository root,
so every `/plugin install pascal-agent-skills@pascal` copied the whole
monorepo into the plugin cache and, because that root carries package.json
next to bun.lock, ran `bun install --frozen-lockfile --ignore-scripts`
against it on every install and update (60 s timeout, not disableable). A
fresh install produced a 1.2 GB cache, 1.1 GB of it node_modules, to
deliver two markdown skill bundles.

Point the marketplace entry at ./skills and move the Claude plugin manifest
and the bundled local `pascal mcp connect` configuration into that root; a
plugin cannot reference files above its own root, so both have to live
inside skills/. The manifest lists the bundles explicitly because the
default skills/ scan no longer applies once skills/ is itself the root. A
fresh install is now 196 KB with no node_modules, package.json, or
packages/. skills.sh tree URLs, the Codex and Cursor Agent Plugins layout,
Gemini and ClawHub still read the root plugin.json, root mcp.json, and the
same skills/ tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(skills): list the plugin as Pascal in directories

Directory listings show the display name next to product plugins listed
by brand, so use the brand rather than "Pascal agent skills" across the
Claude, Cursor and OpenAI manifests. The identifier stays
pascal-agent-skills.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): link the plugin-root fix to #832

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* skills: bundle the hosted Pascal MCP server with a key prompt (#835)

* feat(plugin): add hosted MCP server to the Claude Code plugin

The plugin only bundled the local `pascal mcp connect` stdio server, so a
Claude Code user with a Pascal account had to leave the plugin and run
`claude mcp add` by hand before touching a hosted project or a Capture scan.
Declaring the key as `userConfig.pascal_api_key` lets Claude Code collect it
in the enable-time prompt and substitute it into the `pascal-hosted` server's
Authorization header, so the hosted tools arrive with the skills.

The option is `sensitive` so Claude Code stores the key in the OS keychain
instead of settings.json, and `required: false` so a local-only install still
works with the field left empty.

`${user_config.*}` is a Claude Code substitution, so the hosted server cannot
live in the portable Agent Plugins `mcp.json` that Codex and Cursor read. The
validators now enforce that split: the `pascal` server must be byte-identical
in both files, `skills/.mcp.json` may add only `pascal-hosted`, and the hosted
Authorization header must stay a `user_config` reference so no literal
credential can ship in the published plugin source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(changelog): link the hosted MCP entry to #835

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): graduate prerelease versions on stable bumps (#837)

The stable-bump path split "1.0.0-beta.5" on dots, so major produced
2.0.0, minor 1.1.0, patch failed on "0-beta" arithmetic, and none would
have published a beta version on the latest dist-tag. Any stable bump on
a prerelease now yields its base version, matching npm semver, so the
1.0.0-beta.N line can be released as 1.0.0.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf(cli): prune build-only files from the portable runtime (#838)

`next build` copies its tracing root into `.next/standalone`, so the staged
runtime shipped app sources, repository documentation, build trace metadata and
assets that `server.js` never reads.

Pruned from `dist/runtime`:

- `public/audios/radios` (39.3 MB) — the radio catalogue is played by the hosted
  community app, which serves its own copy; nothing in this repository requests
  `/audios/radios`.
- `next/dist/server/capsize-font-metrics.json` + `font-utils.js` (4.1 MB) —
  `font-utils.js` is the only reader of the metrics and is itself unreachable
  from the standalone server.
- A stray 3.15 MB authoring screenshot and a duplicate `.glb` under
  `public/items` — item assets are addressed by convention, and anything else is
  now dropped and named on stdout.
- `apps/editor/{app,components,lib}` plus dev-only configuration and docs
  (0.5 MB) — TypeScript sources and tests that Node never executes.
- `.nft.json` build trace metadata and source maps under `.next` (0.7 MB).

Before: 107.5 MB tarball, 149.2 MB unpacked, 2956 files.
After:   64.6 MB tarball, 101.7 MB unpacked, 2870 files.

The release budget in the smoke test drops to 75 MB / 115 MB / 3200 files so the
regression cannot come back unnoticed. `stage-runtime` + `smoke-runtime` pass,
and the packed CLI still serves the editor, `/scenes`, a scene page with all 84
of its static chunks, and every sampled public asset.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf(editor): re-encode fitting thumbnails as 256px webp (#842)

`public/icons/fittings/` held 16 PNGs at 1254x1254 RGBA — 11 MB of assets
for thumbnails that render at 56 CSS px in the MEP tool options grid, and
11 MB of the 64.6 MB packed CLI runtime. Every other icon under
`public/icons` is already a small webp.

Each PNG becomes a 256x256 lossy webp with alpha (`cwebp -q 85 -m 6
-alpha_q 100 -resize 256 256`), which is still 2.3x the largest rendered
size — the portable build sets `images.unoptimized`, so the raw file is
what the browser scales. The directory drops from 11 MB to 164 KB.

`build-tab.tsx` derives the path from the fitting type, so the extension
in that template is the only reference to update.

Packed runtime smoke: 54.1 MB compressed, 91.0 MB unpacked, 2870 files
(was 64.6 MB / 101.7 MB / 2870).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* chore(editor): drop unreferenced public assets (#843)

Remove 53 MB of committed assets nothing in this repository reads: the
small-kitchen-cabinet item (10.9 MB; the item catalog resolves every item
from remote storage and no demo references this slug), a stray authoring
screenshot, and the radio catalogue (39 MB) that only the hosted community
app plays from its own copy. The CLI staging script already pruned the
radios and the screenshot; its rm(force) calls tolerate their absence.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): publish through npm trusted publishing only (#839)

The 1.0.0 release failed with EOTP on its first publish: npm no longer
accepts direct publishing with 2FA-bypass granular tokens. Drop
NODE_AUTH_TOKEN from every publish step so npm 11 exchanges the GitHub
Actions OIDC token instead. Requires each @pascal-app package to have
this repository, workflow file and the npm environment configured as a
trusted publisher on npmjs.com.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): ship a small CLI that downloads the web editor runtime (#845)

The npm package carried the whole standalone Next editor: 65 MB compressed,
102 MB unpacked, for 0.1 MB of CLI code. Agents that only speak MCP paid that
cost too, because the MCP bridge started the editor to reach it.

Split the two. `dist/` now holds the CLI plus `services/pascal-mcp.mjs` and a
`runtime-source.json` naming the web runtime archive for this exact version,
its size, and its SHA-256. The web editor runtime ships as a GitHub release
asset and is downloaded once per version, verified, and installed through the
existing atomic install seam.

- MCP is its own managed service (`run/mcp.json`), started on demand by
  `pascal mcp connect` with no editor process and no runtime download.
- Commands that start the editor resolve the runtime from
  `PASCAL_BUNDLED_RUNTIME_DIR`, `--runtime <directory-or-archive>`, the
  installed version, else the release asset; a digest mismatch deletes the
  temporary file and installs nothing.
- Downloads stream over `node:https` with `HTTPS_PROXY`/`NO_PROXY` support and
  no new dependency; concurrent first runs share the install lock.
- `stage-runtime` writes a deterministic `pascal-web-runtime-<version>.tar.gz`
  plus `.sha256`; the release job verifies both before publishing and uploads
  them to the CLI tag right after it is pushed.
- The smoke test now covers MCP-only startup with no runtime present and the
  local-archive install, including a one-byte tamper that must fail closed.

Package: 0.46 MB compressed, 2.46 MB unpacked, 68 files.
Archive: 64.2 MB compressed, 106 MB installed.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* docs(skills): describe the hosted-only capture tools (#846)

Document the hosted-only Capture scan path (list_captures, get_capture,
open_capture_as_project) in the pascal-3d skill and its tool workflows, and
scope the counted 46-tool annotation inventory to the public package so the
hosted server's extra tools do not read as a packet gap.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(plugins): add optional hosted auth to the Cursor plugin (#849)

* feat(plugins): add optional hosted auth to the Cursor plugin

A Cursor install can now reach hosted Pascal — projects, Pascal Capture
scans and shared workspaces — with an optional API key, while the
credential-free local `pascal mcp connect` server keeps working.

`.cursor-plugin/plugin.json` declares an optional `PASCAL_API_KEY`
variable and points `mcpServers` at a new Cursor-dialect
`.cursor-plugin/mcp.json` that adds a `pascal-hosted` server for
https://editor.pascal.app/api/mcp. Cursor substitutes the bare
`${PASCAL_API_KEY}` plugin-variable form from its dashboard, so the
repository holds only the placeholder. The variable is absent from
`required`, so an install with no key still loads and only
`pascal-hosted` fails (401).

The portable `mcp.json` stays credential-free on purpose. Agent Plugins
1.0.0 forbids secrets and placeholder expansion in `headers` (7.2.3,
9.2), its only remote keyword is `streamable-http` rather than Cursor's
`http`, and Codex drops a plugin-supplied `Authorization` as a
client-owned header. Codex users therefore keep using
`codex mcp add --bearer-token-env-var PASCAL_API_KEY`.

ClawHub already declares `PASCAL_API_KEY` optional through
`metadata.openclaw.envVars[].required: false`, so the skills are
unchanged.

`bun run skills:validate` now asserts the Cursor MCP path, a `pascal`
server identical to the portable one, the exact hosted URL and header
template, the optional-and-never-required variable with no unsupported
schema keywords, and that no `${VAR}` in the Cursor config is
undeclared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(plugins): keep the Cursor author block within Cursor's schema

Cursor's plugin.json schema allows only name and email under author
(additionalProperties: false); the url field failed validation on every
install. Compare the Cursor manifest's author on those two fields and
link the changelog entry to #849.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf: per-slab invalidation, candidate-scoped temporal reconciliation and rotation-stable surface planning (row 17) (#850)

* perf(nodes): invalidate slabs by derived polygon changes

* perf(core): scope temporal reconciliation to changed nodes

* fix(nodes): mirror rendered slab context membership and order

* perf(nodes): reuse slab inputs and scope polygon derivation

* test(core): verify structural temporal reconciliation outcomes

* docs: describe temporal candidates and slab dependency tracking

* perf(core): skip disjoint room coverage and rotated surface rewrites

Cache polygon bounds for indexed surface scoping and preserve exact cyclic
outer-ring rotations in the shared slab and ceiling planners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* editor: integrate Environment with generic host and export APIs (#831)

* refactor(packages): fold capture packages into core and viewer (#851)

Avoid npm package sprawl before 1.0.0: `@pascal-app/capture-protocol`
becomes the `@pascal-app/core/capture` subpath and
`@pascal-app/capture-viewer` becomes `@pascal-app/viewer/capture` (plus
`@pascal-app/viewer/capture/preview`), so the release ships seven
packages: core, viewer, editor, nodes, mcp, ifc-converter, cli.

Neither package was ever published to npm, so no npm consumer migrates.
The protocol code is pure zod/TS, so core keeps its no-Three.js layer
rule; the runtime and its reference layers keep viewer's existing peers
and now reach viewer internals through relative imports instead of a
self-referential `@pascal-app/viewer` specifier.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): declare @react-three/test-renderer where the lifecycle test imports it (#852)

The registered tool lifecycle test imports @react-three/test-renderer, but
only the viewer workspace declared it. Hoisting hid the missing dependency;
private-editor CI uses Bun's isolated linker and cannot resolve that import
from the editor workspace. Declare the same ^9.1.0 development dependency
in editor and record it in the workspace lockfile entry.


Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): drop registry-url so npm uses OIDC trusted publishing (#853)

The 1.0.0 run failed publishing core with E404. actions/setup-node with
registry-url writes an .npmrc whose token falls back to the placeholder
XXXXX-XXXXX-XXXXX-XXXXX when NODE_AUTH_TOKEN is unset; npm sent that fake
token instead of exchanging the Actions OIDC token, and the registry
answered 404. Without registry-url no .npmrc is written and npm 11 falls
through to trusted publishing.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): log npm verbosely to surface OIDC exchange errors (#856)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(capture): cache preview artifacts, retry failed downloads, and clarify device-path visibility (#858)

* fix(capture): cache preview data and improve device path visibility

* fix(capture): recover failed JSON preview downloads

* test(viewer): preload one React instance before rendering hooks

* release: @pascal-app/core@1.0.0 @pascal-app/viewer@1.0.0 @pascal-app/editor@1.0.0 @pascal-app/nodes@1.0.0 @pascal-app/mcp@1.0.0 @pascal-app/ifc-converter@1.0.0 @pascal-app/cli@1.0.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: replace the CLI preview instructions with the published npm CLI (#859)

`@pascal-app/cli@1.0.0` is on the npm `latest` tag with `pascal agent claim`,
`pascal agent status`, and the read-only `check_collisions.candidate` input, so
the checksum-verified GitHub prerelease the docs pointed at is obsolete. Delete
the "Verified CLI preview" and "Verified GitHub preview" sections, stop
recommending the `beta` dist-tag (it still resolves to the older
`1.0.0-beta.1`), and drop the inverted claim that the npm package bundles the
web editor runtime — 1.0.0 downloads it from a release asset on first use.

Close the changelog's `Unreleased` heading as `1.0.0 (2026-09-12)` with the
package and contributor sections the earlier releases carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: describe the release workflow and refresh the validation scope (#860)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(editor): keep manifold-3d out of consumer bundler graphs (#735)

manifold-3d's emscripten glue awaits import('node:module') behind a Node
check; the branch never executes in a browser, but webpack refuses to
build any graph that can reach it. export-manager.tsx statically imports
the manifold worker wrapper and ExportManager renders unconditionally
from the editor root, so every external webpack consumer of
@pascal-app/editor failed at build time (#715).

The worker chunk is still built by the consumer's bundler, but it no
longer contains a traceable manifold-3d specifier. The glue is loaded at
runtime through an import() no bundler follows: bare specifier first
(bun tests, dev servers, bundlers that inlined it anyway), then a
version-pinned jsDelivr copy for bundled browser builds — emscripten
locates manifold.wasm relative to the glue's own URL, so the CDN path
self-resolves. configureManifoldRuntime(options) lets offline or
CSP-restricted hosts point both URLs at self-hosted assets.

A failed load no longer poisons later attempts: the cached module
promise resets on rejection.

Fixes #715

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* mcp: make batch-first apply_patch the stated default (#767)

* docs(mcp): make batch-first apply_patch usage the stated default

Tool description, agent guide, from-brief preamble, and README now instruct agents to compose one atomic apply_patch batch per phase instead of looping single-op calls. The tool already validates all ops before applying any; only the guidance was missing.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(mcp): assert batch-first apply_patch guidance surfaces

Lock the tool description, agent guide, from_brief preamble, and README
row that state batch-first as the default without changing apply_patch
runtime behavior.

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(editor): type optional ancestor traversal for downstream consumers (#814)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(viewer): clamp GLB floor animation on slow frames (#820)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(skills): compare bundle paths without a hardcoded separator (#848)

`bun run skills:validate` fails on Windows for every cross-file link
inside a skill bundle and for both OpenAI interface assets, even though
each referenced file exists inside the plugin. `resolve()` returns
backslash-separated paths on Windows, so the `${dir}/` prefix compared
against never matched.

Compare on a normalized separator instead, and cover the predicate with
a focused test so the check stays platform-independent.

* Carry collections through Save Build / Load Build (#783)

Fixes #734

* fix(mcp): declare 2020-12 dialect for tools/list schemas (#787)

The MCP SDK emits tool schemas with a draft-07 dialect, so clients that
enforce JSON Schema 2020-12 reject every tool call. The generated schemas
use no draft-07-only keywords, so re-registering the tools/list handler to
retarget the declared $schema is sufficient.

Fixes #696

* fix(ifc-converter): preserve imported beam geometry (#841)

* fix(editor): skip roof support levels in the floorplan export (#847)

* fix(editor): skip roof support levels in the floorplan export

`resolveExportLevels()` collected every level child of the active
building and filtered on `type === 'level'` only, so a dedicated roof
support level (`metadata.role === 'roof'`) produced an extra page with
just the roof outline. `agent-guide.ts` already states that such a level
is not an occupied story, and the level UI and elevation math honour it;
the export did not.

Filter roof levels out of the export set and cover it with a regression
test, including the case where the roof level is the selected one.

Fixes #618

* ci(release): publish through npm trusted publishing only (#839)

The 1.0.0 release failed with EOTP on its first publish: npm no longer
accepts direct publishing with 2FA-bypass granular tokens. Drop
NODE_AUTH_TOKEN from every publish step so npm 11 exchanges the GitHub
Actions OIDC token instead. Requires each @pascal-app package to have
this repository, workflow file and the npm environment configured as a
trusted publisher on npmjs.com.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(cli): ship a small CLI that downloads the web editor runtime (#845)

The npm package carried the whole standalone Next editor: 65 MB compressed,
102 MB unpacked, for 0.1 MB of CLI code. Agents that only speak MCP paid that
cost too, because the MCP bridge started the editor to reach it.

Split the two. `dist/` now holds the CLI plus `services/pascal-mcp.mjs` and a
`runtime-source.json` naming the web runtime archive for this exact version,
its size, and its SHA-256. The web editor runtime ships as a GitHub release
asset and is downloaded once per version, verified, and installed through the
existing atomic install seam.

- MCP is its own managed service (`run/mcp.json`), started on demand by
  `pascal mcp connect` with no editor process and no runtime download.
- Commands that start the editor resolve the runtime from
  `PASCAL_BUNDLED_RUNTIME_DIR`, `--runtime <directory-or-archive>`, the
  installed version, else the release asset; a digest mismatch deletes the
  temporary file and installs nothing.
- Downloads stream over `node:https` with `HTTPS_PROXY`/`NO_PROXY` support and
  no new dependency; concurrent first runs share the install lock.
- `stage-runtime` writes a deterministic `pascal-web-runtime-<version>.tar.gz`
  plus `.sha256`; the release job verifies both before publishing and uploads
  them to the CLI tag right after it is pushed.
- The smoke test now covers MCP-only startup with no runtime present and the
  local-archive install, including a one-byte tamper that must fail closed.

Package: 0.46 MB compressed, 2.46 MB unpacked, 68 files.
Archive: 64.2 MB compressed, 106 MB installed.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* docs(skills): describe the hosted-only capture tools (#846)

Document the hosted-only Capture scan path (list_captures, get_capture,
open_capture_as_project) in the pascal-3d skill and its tool workflows, and
scope the counted 46-tool annotation inventory to the public package so the
hosted server's extra tools do not read as a packet gap.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* feat(plugins): add optional hosted auth to the Cursor plugin (#849)

* feat(plugins): add optional hosted auth to the Cursor plugin

A Cursor install can now reach hosted Pascal — projects, Pascal Capture
scans and shared workspaces — with an optional API key, while the
credential-free local `pascal mcp connect` server keeps working.

`.cursor-plugin/plugin.json` declares an optional `PASCAL_API_KEY`
variable and points `mcpServers` at a new Cursor-dialect
`.cursor-plugin/mcp.json` that adds a `pascal-hosted` server for
https://editor.pascal.app/api/mcp. Cursor substitutes the bare
`${PASCAL_API_KEY}` plugin-variable form from its dashboard, so the
repository holds only the placeholder. The variable is absent from
`required`, so an install with no key still loads and only
`pascal-hosted` fails (401).

The portable `mcp.json` stays credential-free on purpose. Agent Plugins
1.0.0 forbids secrets and placeholder expansion in `headers` (7.2.3,
9.2), its only remote keyword is `streamable-http` rather than Cursor's
`http`, and Codex drops a plugin-supplied `Authorization` as a
client-owned header. Codex users therefore keep using
`codex mcp add --bearer-token-env-var PASCAL_API_KEY`.

ClawHub already declares `PASCAL_API_KEY` optional through
`metadata.openclaw.envVars[].required: false`, so the skills are
unchanged.

`bun run skills:validate` now asserts the Cursor MCP path, a `pascal`
server identical to the portable one, the exact hosted URL and header
template, the optional-and-never-required variable with no unsupported
schema keywords, and that no `${VAR}` in the Cursor config is
undeclared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(plugins): keep the Cursor author block within Cursor's schema

Cursor's plugin.json schema allows only name and email under author
(additionalProperties: false); the url field failed validation on every
install. Compare the Cursor manifest's author on those two fields and
link the changelog entry to #849.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* perf: per-slab invalidation, candidate-scoped temporal reconciliation and rotation-stable surface planning (row 17) (#850)

* perf(nodes): invalidate slabs by derived polygon changes

* perf(core): scope temporal reconciliation to changed nodes

* fix(nodes): mirror rendered slab context membership and order

* perf(nodes): reuse slab inputs and scope polygon derivation

* test(core): verify structural temporal reconciliation outcomes

* docs: describe temporal candidates and slab dependency tracking

* perf(core): skip disjoint room coverage and rotated surface rewrites

Cache polygon bounds for indexed surface scoping and preserve exact cyclic
outer-ring rotations in the shared slab and ceiling planners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* editor: integrate Environment with generic host and export APIs (#831)

* refactor(packages): fold capture packages into core and viewer (#851)

Avoid npm package sprawl before 1.0.0: `@pascal-app/capture-protocol`
becomes the `@pascal-app/core/capture` subpath and
`@pascal-app/capture-viewer` becomes `@pascal-app/viewer/capture` (plus
`@pascal-app/viewer/capture/preview`), so the release ships seven
packages: core, viewer, editor, nodes, mcp, ifc-converter, cli.

Neither package was ever published to npm, so no npm consumer migrates.
The protocol code is pure zod/TS, so core keeps its no-Three.js layer
rule; the runtime and its reference layers keep viewer's existing peers
and now reach viewer internals through relative imports instead of a
self-referential `@pascal-app/viewer` specifier.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): declare @react-three/test-renderer where the lifecycle test imports it (#852)

The registered tool lifecycle test imports @react-three/test-renderer, but
only the viewer workspace declared it. Hoisting hid the missing dependency;
private-editor CI uses Bun's isolated linker and cannot resolve that import
from the editor workspace. Declare the same ^9.1.0 development dependency
in editor and record it in the workspace lockfile entry.


Claude-Session: https://claude.ai/code/session_01Jmmz2AMwTzcnKsSHHPEMhN

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): drop registry-url so npm uses OIDC trusted publishing (#853)

The 1.0.0 run failed publishing core with E404. actions/setup-node with
registry-url writes an .npmrc whose token falls back to the placeholder
XXXXX-XXXXX-XXXXX-XXXXX when NODE_AUTH_TOKEN is unset; npm sent that fake
token instead of exchanging the Actions OIDC token, and the registry
answered 404. Without registry-url no .npmrc is written and npm 11 falls
through to trusted publishing.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* ci(release): log npm verbosely to surface OIDC exchange errors (#856)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* fix(capture): cache preview artifacts, retry failed downloads, and clarify device-path visibility (#858)

* fix(capture): cache preview data and improve device path visibility

* fix(capture): recover failed JSON preview downloads

* test(viewer): preload one React instance before rendering hooks

* release: @pascal-app/core@1.0.0 @pascal-app/viewer@1.0.0 @pascal-app/editor@1.0.0 @pascal-app/nodes@1.0.0 @pascal-app/mcp@1.0.0 @pascal-app/ifc-converter@1.0.0 @pascal-app/cli@1.0.0

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: replace the CLI preview instructions with the published npm CLI (#859)

`@pascal-app/cli@1.0.0` is on the npm `latest` tag with `pascal agent claim`,
`pascal agent status`, and the read-only `check_collisions.candidate` input, so
the checksum-verified GitHub prerelease the docs pointed at is obsolete. Delete
the "Verified CLI preview" and "Verified GitHub preview" sections, stop
recommending the `beta` dist-tag (it still resolves to the older
`1.0.0-beta.1`), and drop the inverted claim that the npm package bundles the
web editor runtime — 1.0.0 downloads it from a release asset on first use.

Close the changelog's `Unreleased` heading as `1.0.0 (2026-09-12)` with the
package and contributor sections the earlier releases carry.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* docs: describe the release workflow and refresh the validation scope (#860)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(editor): keep manifold-3d out of consumer bundler graphs (#735)

manifold-3d's emscripten glue awaits import('node:module') behind a Node
check; the branch never executes in a browser, but webpack refuses to
build any graph that can reach it. export-manager.tsx statically imports
the manifold worker wrapper and ExportManager renders unconditionally
from the editor root, so every external webpack consumer of
@pascal-app/editor failed at build time (#715).

The worker chunk is still built by the consumer's bundler, but it no
longer contains a traceable manifold-3d specifier. The glue is loaded at
runtime through an import() no bundler follows: bare specifier first
(bun tests, dev servers, bundlers that inlined it anyway), then a
version-pinned jsDelivr copy for bundled browser builds — emscripten
locates manifold.wasm relative to the glue's own URL, so the CDN path
self-resolves. configureManifoldRuntime(options) lets offline or
CSP-restricted hosts point both URLs at self-hosted assets.

A failed load no longer poisons later attempts: the cached module
promise resets on rejection.

Fixes #715

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* mcp: make batch-first apply_patch the stated default (#767)

* docs(mcp): make batch-first apply_patch usage the stated default

Tool description, agent guide, from-brief preamble, and README now instruct agents to compose one atomic apply_patch batch per phase instead of looping single-op calls. The tool already validates all ops before applying any; only the guidance was missing.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(mcp): assert batch-first apply_patch guidance surfaces

Lock the tool description, agent guide, from_brief preamble, and README
row that state batch-first as the default without changing apply_patch
runtime behavior.

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(editor): type optional ancestor traversal for downstream consumers (#814)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(viewer): clamp GLB floor animation on slow frames (#820)

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(skills): compare bundle paths without a hardcoded separator (#848)

`bun run skills:validate` fails on Windows for every cross-file link
inside a skill bundle and for both OpenAI interface assets, even though
each referenced file exists inside the plugin. `resolve()` returns
backslash-separated paths on Windows, so the `${dir}/` prefix compared
against never matched.

Compare on a normalized separator instead, and cover the predicate with
a focused test so the check stays platform-independent.

---------

Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Co-authored-by: Aymeric Rabot <aymeric@pascal.app>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Wassim SAMAD <wass08@gmail.com>
Co-authored-by: Adam NAILI <18304870+AxiomeCG@users.noreply.github.com>
Co-authored-by: ActArtech <123718991+ActArtech@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>

* fix(cli): preserve configured Mint host origin (#778)

* fix(cli): preserve configured Mint host origin

* ci: enable trusted publishing for MCP and CLI releases (#779)

* docs: add verified candidate CLI preview (#780)

* docs: add verified candidate CLI preview

* docs: activate preview runtime during upgrades

* Require measured evidence and target-scoped furniture checks (#781)

* Strengthen furniture fit evidence boundaries

* Record candidate validation status

* Clarify requested geometry scope

* Record furniture evidence gate results

* Document skill validation and safe preview activation (#782)

* Record final skill validation status

* Clarify routing audit result

* Document safe preview activation

* editor: Add duct and pipe fittings, routing, and system checks (#769)

* Add roof surface placement support for items

Items (e.g. solar panels) can now be placed on sloped roof surfaces.
The placement system computes euler rotation from the roof surface
normal so items sit flush on the slope instead of going inside.

- Add roofStrategy to placement-strategies with enter/move/click/leave
- Wire roof:enter/move/click/leave events in the placement coordinator
- Add calculateRoofRotation in placement-math using surface normals
- Support full 3D cursor rotation for sloped surfaces
- Items on roofs are parented to the level with world-space rotation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fixed conflict

* fix: pass nodes to lazy inspector panels

* MEP: unify duct and DWV routing UX

* Point editor dev runtime at local Streetscape plugin

* MEP: add exact lengths and branch affordances

* Make MEP runs surface-aware

* MEP: unify wall-aware duct and pipe run UX

- Add surface-aware drafting, snapping, and run attachments
- Support wall-attached run movement and endpoint updates
- Improve placement grid anchoring and semantic surface events

* MEP: free wall-attached routing and simplify fitting actions

- Continue routing horizontally after leaving a wall
- Keep quick material actions for pipe fittings only

* Fix duct and DWV direction capture from camera rays

* Align MEP snapping with architecture rules

* chore: satisfy repository checks

* test: scope pipe continuation handle assertion

* Add configurable MEP hangers and fix run drawing interactions

* Unify MEP accessory snapping and system connectivity

- Add shared snapping for MEP accessories with live setting updates
- Respect surfaces, levels, building transforms, and system boundaries
- Add coverage for snapping and cross-floor port connectivity

* Improve MEP connection feedback, slope controls, checks and hangers

* MEP: expand fitting catalogs and accessory configuration

- Add duct and DWV fittings, accessories, geometry, placement, and thumbnails
- Unify fitting selection through configurable tool options

* Simplify MEP build tools by removing the Add Trap action

- Remove the context-specific DWV Pipe Add Trap button from the Build tab

* Unify MEP run editing and placement UX

- Preview pipe and duct edits through live overrides
- Track fitting placement with interaction scopes
- Align surface-aware routing and accessory snapping

* Use live overrides for MEP selection previews

- Keep duct and pipe drag, roll, and offset previews out of committed scene state
- Render selection handles from live node overrides during interactions

* Simplify pipe routing status controls

* Make drafting behavior registry-driven

* Fix drafting history test registry setup

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* perf(editor): instance the ceiling corner brackets per level (#784)

* perf(editor): instance ceiling corner brackets per level

Replace the per-corner meshes with two level-wide InstancedMeshes sharing
one unit BoxGeometry. Legs and cubes use the same geometry/material path,
so their individual transforms fit in a single batch per opacity state.
Normal instances use 0.72 opacity and highlighted instances use 0.92;
instanceColor carries the original gray/indigo colors. Three 0.185.1's
StandardNodeLibrary maps MeshBasicMaterial to MeshBasicNodeMaterial, whose
NodeMaterial.setupDiffuseColor multiplies instanceColor into material color.
This requires no custom shader or per-camera sorting.

Keep the per-ceiling drag controllers memoized and non-rendering. Geometry,
height, live overrides and preview changes update that ceiling's matrices;
hover transfers only the affected parts between packed instance arrays.
Capacity doubles with headroom on overflow, count tracks occupied slots,
and React observes the batch store only when meshes are reallocated.
Conservative expanding spheres keep native raycasts valid after writes;
frustumCulled=false prevents stale render bounds from hiding handles.
InstancedMesh.prototype.raycast remains unchanged on the pointer fast path.

Use R3F's per-instance over/out events, with move reconciliation when packed
slots change ownership. Snapshot outgoing hover targets and pointer-down
part identities, allow clicks across highlight-batch transfers at the canvas
root, and retain stable React keys so R3F transfers interaction state on
capacity growth. Preserve the level portal and registry retry, ceiling:click
payload, drag/snap/SFX/override lifecycle, and synchronous capture hiding.

Accepted visual changes from the architect ruling:
- Normal brackets render at 1000 and highlighted brackets at 1001, making
  mixed overlaps deterministically highlighted-on-top.
- Ordering against other transparent objects at 1000 is now per batch,
  using the shared geometry centre at the level origin, rather than per
  bracket. There is no per-camera instance sorting.
No other intentional behavior changes.

Validation:
- Built the local core/viewer package outputs needed for editor validation.
- packages/editor: bun test src -- 848 pass, 0 fail across 119 files.
- Includes 12 new tests for instance indexing, highlights, capacity, old
  leg matrix parity, native raycasts, and mounted R3F hover/click/drag,
  override, capture, and unmount behavior.
- packages/editor: bun run check-types (tsgo --noEmit) -- passed.
- Root: bunx biome check on all four changed files -- clean.
- Runtime draw/frame measurements and pixel comparison remain with the
  architect; no browser or dev server was started.

* fix(editor): stabilize ceiling bracket picking and resource lifetime

Resolve equal-distance bracket hits by ceiling/corner/part identity for
hover, pointer-down and click. Packed instance IDs and opacity batch order
no longer decide the owner at coincident same-height ceiling corners.
Keep native InstancedMesh raycasting and the existing click payload.

Give each batch a clone of the unit box geometry and dispose that geometry
before retiring the mesh/material on growth or teardown. WebGPU owns
instance-attribute cleanup through its geometry disposal listener.

Use StaticDrawUsage for instance matrices and colors. Writes bump versions
and add update ranges for the affected slots. Clear source ranges after
rendering because TSL uploads internal attribute wrappers; their ranges are
consumed by the backend. The attribute scheduler regression verifies that
unchanged resting frames cause no attribute updates.

Poll sceneRegistry.revision and re-resolve the level only when it changes.
Keep a stable portal group attached beneath the current level so replacing
a level object does not remount the same primitives and lose R3F event
registration. Reparenting preserves the mesh, geometry and matrix buffers.
Read the live registry object for every drag-plane query as well. Retain
the initial requestAnimationFrame retry and synchronous capture hiding.
Document that ceiling:click.position remains level-local; do not transform
or otherwise change that payload.

The reviewer's normalView/MRT concern remains uncertain: overlapping faces
with different normals may change AO/ink output when batch order changes.
No normal/MRT changes are made here. The architect will check pixels with
ink and AO enabled. The previously accepted transparency ordering remains.

Validation:
- packages/editor: bun test src -- 852 pass, 0 fail, 119 files.
- New mounted regressions cover 20 repeated moves over coincident corners,
  stable click/drag ownership, and a translated/rotated same-id level
  replacement with unchanged geometry/matrix versions and local payloads.
- Unit regressions verify geometry dispose events on growth/teardown and
  Three's WebGPU attribute scheduler skipping unchanged frames while
  changed slots carry bounded update ranges.
- bunx tsc --noEmit -p packages/editor/tsconfig.json -- exit 0, no output.
- bunx biome check on all four changed files -- clean.

* docs(editor): note accepted small bracket matrix uploads

Accept whole-array uploads on every render for small matrix buffers using Three 0.185.1’s uniform BufferNode path; above the device uniform-buffer limit, the attribute path honors versions and update ranges.

* perf(nodes): skip animation mixers for items without clips (#785)

* feat(capture): add shared clay previews and dollhouse rendering

* docs(capture): document local previews and mesh presentation

* Split canopy regression matrix into independent tests

* feat(skills): fail closed on missing furniture inputs

* perf(nodes): batch ceiling undersides and slab bodies (charter row 16) (#789)

* perf(nodes): batch ceiling undersides and slab bodies

* fix(nodes): close surface batch ownership and rebuild lifecycles

* fix(editor): reconcile paint previews after apply exceptions

* fix(nodes): rebuild slabs and release batches on material cache clear

* fix(nodes): strip the merged wall batch from GLB exports

* fix(editor): include moved node identity in perf receipts

* fix(editor): preserve grid surface hits while batching

* fix: preserve batched surfaces in geometry raycasts

* test(nodes): run source-system probes from a package-local file, not bun -e (#792)

Fix private-editor CI's Lint, Typecheck & Test / Unit tests failure on Bun 1.3.0 Linux: eval probes started at the editor submodule root could not resolve @pascal-app/core from dependencies hoisted to the private root.

Write isolated probes under ignored package-local .turbo directories, resolve source imports and mocks from import.meta.dir, and remove probes in finally. Apply the same fix to the core parser test that imports zod from an eval probe. Preserve all cases and assertions.

Verified both dependency layouts, package and private-root test invocations, eval failure and file success from /tmp with automatic installs disabled, randomized nodes tests (seed 1), core parser tests, Biome, and no-emit typechecks.

* test(nodes): establish probe mocks before any fiber/react import (#793)

* test(nodes): establish probe mocks before any fiber/react import (Bun 1.3.0)

* test(nodes): make source-system probes linker-agnostic (isolated node_modules)

* skills: make furniture follow-ups blocker-aware (#794)

* feat(skills): add verdict-aware furniture follow-ups

* fix(skills): make furniture follow-ups blocker-aware

* fix(skills): enforce furniture action boundaries

* test(skills): pin furniture decision evidence

* docs: record agent skills 0.1.4 release source (#795)

* docs(skills): prepare OpenAI plugin submission

* docs(skills): complete OpenAI review fixtures

* docs(skills): prepare ClawHub publication

* fix(plugin): require MCP for OpenAI submission (#799)

* feat(mcp): add tool execution middleware

* docs(skills): record 0.1.6 as released

* fix(mcp): propagate tool cancellation

* fix(mcp): preserve executor on tool updates

* chore(skills): harden ClawHub bundles

* test(skills): reject ClawHub ignore overrides

* Add official MCP Registry publishing

* ci(mcp): verify live catalog consistency

* feat(skills): bundle local Claude MCP connector

* docs(skills): correct Claude MCP upgrade guidance

* docs(skills): record 0.1.7 release

* fix(skills): hide maintainer workflows from discovery

* fix(mcp): classify all tool side effects

* docs(openai): add tool annotation justifications (#813)

* feat(cli): add hosted agent claim command (#815)

* feat(cli): add hosted agent claim command

* fix(cli): require canonical claim expiry

* fix(ci): authenticate CLI npm publish (#816)

* fix(ci): restore OIDC for CLI publishing (#817)

* feat(cli): prefill hosted agent claim (#818)

* docs(cli): publish verified agent claim preview (#819)

* feat(cli): report hosted agent status (#821)

* docs(cli): publish verified agent status preview (#822)

* feat(skills): add human-openable fit prechecks (#824)

* docs(skills): record agent report release evidence (#825)

* perf: scope undo/redo invalidation to changed geometry and cleared previews (charter row 7) (#805)

* Scope undo invalidation to changed geometry and cleared previews

* Reset editor state before randomized store tests

* Resolve history probe mocks from each consuming package

* Restore discarded preview dependency closures on undo and redo

* Limit rendered slab invalidation to changed boundary bands

* Cover history support transfers and scoped endpoint rebuilds

* Pin endpoint history closure with spatial sync mounted

* Run package tests against core source without rebuilding dist

* test: drop the repo-wide core source preload

* test: verify consecutive undo and redo invalidation

Zundo 2.3.0 appends the just-left snapshot to both destination stacks, so the existing pre-jump length indices are correct. Cover three adjacency-changing moves and each undo/redo with cleared marks and flushed microtasks.

* fix: invalidate old slab covering dependents on reparent

Refresh covering dependents below both parent levels, deduplicating equal resolved levels. Cover reparent from level 2 to level 3 and undo with exact wall/ceiling sets and unrelated levels left clean.

* perf: drain initial wall builds within the time budget (charter row 6) (#800)

* perf: drain initial wall builds within the time budget

* fix(core): invalidate hydration atomically with scene edits

* test: isolate scene fixtures from randomized ordering

* fix(core): complete normalization before publishing hydration

* fix(viewer): preserve and bound initial wall drain lifetime

* docs: clarify hydration lifetime and wall drain counters

* Experience fix pass: placement, selection rotation, roof, stairs, capture, Cmd+S, three 0.186 (#807)

* fix(capture): round armed FOV, add Alt slow modifier for the drone camera

armCaptureFov stored the live camera FOV verbatim, so fractional pose FOVs
printed float tails in the HUD and left the reset button enabled. Both
writers now share clampCaptureFov.

Alt holds the drone at 0.2x speed and look sensitivity for fine framing;
Shift stays the boost.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* fix(roof): keep the gable shell base on the wall top

The CSG degeneracy guard enforced its 5 cm minimum by lowering the shell
base, which for wallHeight-0 room roofs put the gable 4 cm inside the
wall and z-fought its faces. Raise the eave instead; mirror the floor in
the opening-placement frame and the shed inset panel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hQVFZ62S1qJDsBM9ferYc

* feat(editor): Cmd/Ctrl+S saves instead of opening the browser dialog

Capture-phase, always-on listener so the page-save dialog never appears.
Hosts can take the chord over via onSaveShortcut; the default flushes the
autosave through the existing executeSave path.

Co-Authored-By: Claude Fable 5.1 <noreply@…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants