diff --git a/artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json b/artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json
new file mode 100644
index 000000000..32a32f224
--- /dev/null
+++ b/artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json
@@ -0,0 +1,155 @@
+{
+ "title": "kernelCAD robot hand design loop",
+ "goal": "Produce a robot hand artifact, not a flat grasp fixture, with visible palm, thumb, fingers, supported joints, connected distal fingertips, and deterministic validation evidence.",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "attempts": [
+ {
+ "id": "01",
+ "title": "Planar three-finger grasp fixture",
+ "source": "tests/fixtures/robot-hand/rejected-function-first-three-finger-hand.kcad.ts",
+ "status": "rejected",
+ "evidence": {
+ "iso": "/tmp/function-first-hand-preview/iso.png"
+ },
+ "visualFindings": [
+ "Reads as a flat test fixture with a target cylinder, not as a hand.",
+ "Palm is slab-like and the fingers are side-mounted bars rather than recognizable digits.",
+ "Functional aperture evidence was not enough to satisfy the visual and physical hand brief."
+ ],
+ "decision": "Reject as the user-facing robot hand artifact. Keep only as a narrow grasp-test fixture if needed."
+ },
+ {
+ "id": "02",
+ "title": "Five-finger kinematic hand before fingertip repair",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "rejected",
+ "evidence": {
+ "front": "/tmp/five-finger-hand-preview/front.png",
+ "iso": "/tmp/five-finger-hand-preview/iso.png"
+ },
+ "visualFindings": [
+ "Reads as a robot hand from the front: palm, four fingers, angled thumb, visible clevis joints, and wrist block are present.",
+ "Black fingertip pads visibly float above the distal links in front and iso views.",
+ "Floating fingertip pads violate the no-stray-or-floating-geometry and attachment-plausibility checks."
+ ],
+ "decision": "Repair the distal phalanx construction before accepting."
+ },
+ {
+ "id": "03",
+ "title": "Five-finger kinematic hand with connected distal fingertips",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "rejected-as-working-hand",
+ "evidence": {
+ "front": "/tmp/five-finger-hand-preview-fixed/front.png",
+ "top": "/tmp/five-finger-hand-preview-fixed/top.png",
+ "iso": "/tmp/five-finger-hand-preview-fixed/iso.png"
+ },
+ "visualFindings": [
+ "Front and iso views now show fingertip ends attached to continuous distal links instead of floating black blocks.",
+ "The model reads as a robot hand: palm plate, wrist block, four vertical fingers, angled thumb, visible clevis joints, screws, tendon rods, and distal fingertip pads.",
+ "Canonical views still show a constructed mechanism rather than a single visual mesh or disconnected decorative fragments.",
+ "The model still lacks declared grasp-task evidence: no physicalUseCase contacts, loads, stable parts, or actuator limits prove that it can hold an object."
+ ],
+ "deterministicChecks": [
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t \"not accepted as a working hand\" --reporter=dot"
+ ],
+ "remainingCaveats": [
+ "The render preview used no_mechanism_check:true for speed, so the preview images themselves are visual evidence only.",
+ "The example now explicitly fails the working-hand acceptance gate with assembly.physical-use-case.missing until grasp-task evidence is added.",
+ "Next slices should replace decorative/unexplained hardware with load-bearing grasp components, then add physicalUseCase contacts and force/torque limits before any acceptance claim."
+ ]
+ },
+ {
+ "id": "04",
+ "title": "Five-finger hand with declared cylinder grasp task",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "rejected-for-unreached-grasp-and-unsupported-actuation",
+ "evidence": {
+ "front": "/tmp/five-finger-hand-grasp-thumb-opposed/front.png",
+ "top": "/tmp/five-finger-hand-grasp-thumb-opposed/top.png",
+ "iso": "/tmp/five-finger-hand-grasp-thumb-opposed/iso.png",
+ "maxCloseFront": "/tmp/five-finger-hand-grasp-max-close/front.png"
+ },
+ "visualFindings": [
+ "The model now declares a grasp-cylinder part and a power-cylinder-grasp physicalUseCase with target load, contacts, actuator limits, and palm reaction path.",
+ "The default and max-close rendered views still show the blue cylinder near the hand rather than actually captured between thumb and opposing fingers.",
+ "The thumb does not visually participate in the grasp, so the declared contact task is not satisfied by the current kinematic geometry.",
+ "The physical-use-case gate now rejects every thumb/index/middle actuatorLimit because those revolute mates have no mechanicalJoint support contract."
+ ],
+ "deterministicChecks": [
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t \"declares a cylinder-grasp\" --reporter=dot",
+ "npx tsx -e \"evaluateAndBuildScript(...)\""
+ ],
+ "remainingCaveats": [
+ "Fast physicalUseCase declaration review now fails with assembly.physical-use-case.actuator-support-missing until supported mechanicalJoint contracts exist for driven grasp joints.",
+ "The correct pose-envelope reachability review is currently too slow on this 15-mate model for the interactive loop and was interrupted after several minutes.",
+ "The next implementation should either redesign the thumb/index/middle kinematics around the cylinder workspace or add a fast targeted reachability gate for declared physicalUseCase contacts."
+ ]
+ },
+ {
+ "id": "05",
+ "title": "Five-finger hand with supported MCP grasp-drive slice",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "partial-pass-supported-mcp-slice-not-complete-hand",
+ "evidence": {
+ "studioScreenshot": "/tmp/five-finger-supported-mcp-hand-v2.png",
+ "studioUrl": "http://127.0.0.1:5175/studio?script=tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts"
+ },
+ "visualFindings": [
+ "The model still reads as the five-finger robot hand with palm, four fingers, angled thumb, clevis joints, wrist block, and blue grasp cylinder.",
+ "Three black MCP servo blocks for middle, index, and thumb are now seated against explicit palm-root mounting pads rather than floating in front of the hand.",
+ "The physical use case no longer claims PIP/DIP actuation. Only the three supported MCP mates are listed in actuatorLimits.",
+ "The cylinder is still not visually captured by a convincing thumb/opposing-finger grasp, so this is not accepted as a complete working hand."
+ ],
+ "deterministicChecks": [
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t \"supported base knuckle\" --reporter=dot",
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot",
+ "npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts --reporter=dot",
+ "npm run typecheck"
+ ],
+ "remainingCaveats": [
+ "Studio initially reported interferences: 16 because the footer counted raw contact-noise pairs. Attempt 06 classifies and filters those pairs against the shared mechanism cap.",
+ "The supported-drive evidence is limited to middle/index/thumb MCP joints. PIP/DIP joints remain visual kinematic joints until real transmission/support paths are modeled.",
+ "Contact reachability against the declared cylinder is still not proven by a fast gate."
+ ]
+ },
+ {
+ "id": "06",
+ "title": "Interference footer classification for supported MCP hand",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "partial-pass-actionable-interference-count-clean",
+ "evidence": {
+ "studioScreenshot": "/tmp/five-finger-supported-mcp-hand-interference-filter.png",
+ "studioUrl": "http://127.0.0.1:5176/studio?script=tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts"
+ },
+ "visualFindings": [
+ "The rendered hand remains visually the same supported-MCP slice: palm, five fingers, seated middle/index/thumb MCP servo blocks, and blue grasp cylinder.",
+ "Studio footer now reports interferences: 0 because the footer counts only pairs above the shared 20 mm3 actionable interference cap.",
+ "The raw default-pose detector still sees 16 tiny overlap pairs, all below the cap. They are documented as joint/contact-noise inventory rather than hidden."
+ ],
+ "deterministicChecks": [
+ "npx vitest run src/studio/__tests__/StudioShell.status.test.tsx --reporter=dot",
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot",
+ "npm run typecheck"
+ ],
+ "remainingCaveats": [
+ "This slice fixes false-positive actionable interference reporting; it does not prove the grasp captures the cylinder.",
+ "The raw 16 below-cap pairs should shrink in future geometry cleanup, but they are no longer blocking under the existing mechanism-truth threshold.",
+ "PIP/DIP transmission and fast contact reachability remain unresolved."
+ ]
+ },
+ {
+ "id": "07",
+ "title": "Tooling gate: targeted grasp reachability",
+ "source": "tests/fixtures/robot-hand/rejected-five-finger-kinematic-hand.kcad.ts",
+ "status": "rejected-by-physical-use-case-contact-reachability",
+ "deterministicChecks": [
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t \"physical use case reachability\" --reporter=dot"
+ ],
+ "remainingCaveats": [
+ "The model remains visually inspectable but is rejected because declared contacts do not reach the cylinder within maxSlipMm.",
+ "Next geometry slice must redesign thumb/index/middle placement or actuator travel using this diagnostic, not visual guesswork."
+ ]
+ }
+ ]
+}
diff --git a/artifacts/robot-hand-workflow-comparison/index.html b/artifacts/robot-hand-workflow-comparison/index.html
new file mode 100644
index 000000000..a2057cad8
--- /dev/null
+++ b/artifacts/robot-hand-workflow-comparison/index.html
@@ -0,0 +1,186 @@
+
Robot Hand Workflow Benchmark
+
Thin prototypes scored against the same target: a reference-matched, physically valid parametric robot hand.
Builds: CAD primitives fitted to visible mesh features
+
Failure caught: bad primitive fit, missing shaft axes, repeated-module mismatch
+
Caveat: Promising for automation, but bad segmentation can create false confidence.
+
+ Physics
+ Reference fit
+ Stability
+ Automation
+ Validation
+
+
+
diff --git a/docs/superpowers/plans/2026-07-08-mesh-conditioned-robot-hand.md b/docs/superpowers/plans/2026-07-08-mesh-conditioned-robot-hand.md
new file mode 100644
index 000000000..6a5904080
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-08-mesh-conditioned-robot-hand.md
@@ -0,0 +1,78 @@
+# Mesh-Conditioned Robot Hand Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Convert the robot hand example from manually scattered dimensions into a reference-landmark-driven parametric assembly with mechanical completion.
+
+**Architecture:** Keep the prototype local to the hand example. Add a `referenceLandmarks` evidence object, generate the visible palm/finger/thumb/wrist details from it, and keep the existing clevis/mate/load validation path as the mechanical completion layer.
+
+**Tech Stack:** KernelCAD `.kcad.ts` example script, Vitest integration tests, `evaluateAndBuildScript`.
+
+---
+
+### Task 1: Add Reference-Landmark Contract Test
+
+**Files:**
+- Modify: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [ ] **Step 1: Write the failing test**
+
+Add assertions to the existing front-facing silhouette test:
+
+```ts
+expect(source).toContain('const referenceLandmarks =');
+expect(source).toContain('referenceLandmarks.fingers.forEach(addFinger)');
+expect(source).toContain('referenceLandmarks.actuatorWindows');
+expect(source).toContain('referenceLandmarks.tendons');
+expect(source).toContain('referenceLandmarks.screws');
+expect(source).toContain('angleDeg: 38');
+expect(source).not.toMatch(/\[\s*\{ name: 'little'/);
+```
+
+- [ ] **Step 2: Run test to verify it fails**
+
+Run: `npm test -- tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot`
+
+Expected: FAIL because `referenceLandmarks` does not exist yet.
+
+### Task 2: Move Visible Evidence Into `referenceLandmarks`
+
+**Files:**
+- Modify: `examples/robot-hand/five-finger-kinematic-hand.kcad.ts`
+
+- [ ] **Step 1: Add the landmark object**
+
+Create a top-level `referenceLandmarks` object holding palm dimensions, actuator windows, screws, tendons, and finger specs.
+
+- [ ] **Step 2: Generate palm details from landmarks**
+
+Replace hard-coded actuator-window, screw, and tendon loops with loops over
+`referenceLandmarks.actuatorWindows`, `referenceLandmarks.screws`, and
+`referenceLandmarks.tendons`.
+
+- [ ] **Step 3: Generate fingers from landmarks**
+
+Replace the inline finger spec array with `referenceLandmarks.fingers.forEach(addFinger)`.
+
+- [ ] **Step 4: Run test to verify it passes**
+
+Run: `npm test -- tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot`
+
+Expected: PASS, or a real KernelCAD evaluation failure to fix before continuing.
+
+### Task 3: Verify Open/Closed Evaluation Directly
+
+**Files:**
+- No file changes.
+
+- [ ] **Step 1: Run the integration test**
+
+Run: `npm test -- tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot`
+
+Expected: both open and closed pose evaluation assertions pass with zero error diagnostics.
+
+- [ ] **Step 2: Inspect worktree diff**
+
+Run: `git status --short` and `git diff -- examples/robot-hand/five-finger-kinematic-hand.kcad.ts tests/integration/examples/fiveFingerKinematicHand.test.ts docs/superpowers/specs/2026-07-08-mesh-conditioned-robot-hand-design.md docs/superpowers/plans/2026-07-08-mesh-conditioned-robot-hand.md`
+
+Expected: only the prototype files changed.
diff --git a/docs/superpowers/plans/2026-07-09-physical-plausibility-tooling.md b/docs/superpowers/plans/2026-07-09-physical-plausibility-tooling.md
new file mode 100644
index 000000000..8700b3c29
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-09-physical-plausibility-tooling.md
@@ -0,0 +1,1092 @@
+# Physical Plausibility Tooling Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build tooling that rejects or explains physically implausible CAD assemblies before agents continue visual/model iterations.
+
+**Architecture:** Add small deterministic review units that sit under `src/modeling/` and are surfaced through `review_cad`, Studio, and `design_loop`. The hand model becomes a consumer of these gates, not the place where we encode one-off discipline. Each task is test-first and independently shippable.
+
+**Tech Stack:** TypeScript, Vitest, KernelCAD assembly/mate APIs, existing `review_cad`, `design_loop`, Studio React UI, Playwright only for final browser evidence.
+
+---
+
+## File Structure
+
+- Create `src/modeling/runtime/interferenceClassification.ts`
+ - Pure helper that classifies raw `InterferencePair[]` into `contact-noise` and `actionable` using `jointContactCapMm3()`.
+- Create `tests/unit/runtime/interferenceClassification.test.ts`
+ - Unit tests for classification, summary counts, and boundary behavior at the cap.
+- Modify `src/agent/mcp/tools/reviewCad.ts`
+ - Add `interferenceSummary` to `review_cad` output while keeping `rawInterferencePairs` for low-level consumers.
+- Modify `src/agent/mcp/toolOutputSchemas.ts`
+ - Expose `interferenceSummary` in MCP output schemas.
+- Modify `src/studio/StudioShell.tsx`
+ - Read `interferenceSummary.actionableCount` when available, falling back to classified raw pairs.
+- Modify `src/studio/components/Layout/StatusBar.tsx`
+ - Show actionable count and concise tooltip text that mentions raw/contact-noise/actionable counts.
+- Modify `src/studio/types.ts` and `src/studio/hooks/useRecomputeResult.ts`
+ - Add `interferenceSummary` to the Studio recompute contract.
+- Modify `src/studio/__tests__/StudioShell.status.test.tsx`
+ - Cover actionable footer count behavior.
+- Modify `src/studio/components/Layout/StatusBar.test.tsx`
+ - Cover footer tooltip/status rendering.
+- Create `src/modeling/mates/physicalUseCaseReachability.ts`
+ - Targeted use-case contact reachability sampler that samples only actuator mates named in `physicalUseCase.actuatorLimits`.
+- Modify `src/modeling/mates/physicalUseCase.ts`
+ - Call the targeted reachability review when enabled and emit concrete `contact-unreachable` diagnostics with closest distance.
+- Modify `tests/integration/mcp/physicalUseCaseGate.test.ts`
+ - Add fast fixtures for reachable and unreachable declared contacts.
+- Modify `src/agent/mcp/tools/reviewCad.ts`
+ - Add input option `includePhysicalUseCaseReachability?: boolean`, defaulting to true when `requirePhysicalUseCase` is true and `includePoseEnvelope` is false.
+- Modify `src/agent/mcp/toolRegistry.ts`
+ - Document the new reachability option and output behavior.
+- Create `src/modeling/joints/supportedServoRevolute.ts`
+ - Helper builder for a supported servo revolute drive intent.
+- Modify `src/modeling/joints/index.ts` and `src/modeling/api.ts`
+ - Expose the helper under `joint.supportedServoRevolute(...)`.
+- Create `src/modeling/joints/supportedServoRevolute.test.ts`
+ - Unit tests for generated geometry/connectors/intents.
+- Modify `tests/integration/mcp/designLoop.test.ts`
+ - Add a tooling acceptance report test: an accepted attempt must have no actionable interference, supported actuators, reachable declared contacts, and screenshot review.
+
+---
+
+### Task 1: Shared Interference Classification
+
+**Files:**
+- Create: `src/modeling/runtime/interferenceClassification.ts`
+- Create: `tests/unit/runtime/interferenceClassification.test.ts`
+
+- [x] **Step 1: Write failing tests**
+
+Create `tests/unit/runtime/interferenceClassification.test.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { classifyInterferencePairs, summarizeInterferencePairs } from '../../../src/modeling/runtime/interferenceClassification';
+import { jointContactCapMm3 } from '../../../src/modeling/runtime/jointContactCap';
+
+describe('interference classification', () => {
+ it('classifies raw pairs below or equal to the cap as contact noise', () => {
+ const cap = jointContactCapMm3();
+
+ const result = classifyInterferencePairs([
+ { a: 'palm-root', b: 'index-proximal', volumeMm3: 0.5 },
+ { a: 'index-proximal', b: 'index-middle', volumeMm3: cap },
+ ]);
+
+ expect(result.map((pair) => pair.classification)).toEqual(['contact-noise', 'contact-noise']);
+ expect(result.map((pair) => pair.actionable)).toEqual([false, false]);
+ });
+
+ it('classifies raw pairs above the cap as actionable', () => {
+ const cap = jointContactCapMm3();
+
+ const result = classifyInterferencePairs([
+ { a: 'servo', b: 'palm-root', volumeMm3: cap + 0.01 },
+ ]);
+
+ expect(result).toEqual([
+ {
+ a: 'servo',
+ b: 'palm-root',
+ volumeMm3: cap + 0.01,
+ capMm3: cap,
+ classification: 'actionable',
+ actionable: true,
+ },
+ ]);
+ });
+
+ it('summarizes raw, contact-noise, and actionable counts', () => {
+ const cap = jointContactCapMm3();
+
+ expect(summarizeInterferencePairs([
+ { a: 'a', b: 'b', volumeMm3: 1 },
+ { a: 'c', b: 'd', volumeMm3: cap + 1 },
+ ])).toMatchObject({
+ rawCount: 2,
+ contactNoiseCount: 1,
+ actionableCount: 1,
+ capMm3: cap,
+ });
+ });
+});
+```
+
+- [x] **Step 2: Run test and verify red**
+
+Run:
+
+```bash
+npx vitest run tests/unit/runtime/interferenceClassification.test.ts --reporter=dot
+```
+
+Expected: FAIL because `src/modeling/runtime/interferenceClassification.ts` does not exist.
+
+- [x] **Step 3: Implement minimal classifier**
+
+Create `src/modeling/runtime/interferenceClassification.ts`:
+
+```ts
+import type { InterferencePair } from './detectInterferences';
+import { jointContactCapMm3 } from './jointContactCap';
+
+export type InterferenceClassification = 'contact-noise' | 'actionable';
+
+export interface ClassifiedInterferencePair extends InterferencePair {
+ readonly capMm3: number;
+ readonly classification: InterferenceClassification;
+ readonly actionable: boolean;
+}
+
+export interface InterferenceSummary {
+ readonly rawCount: number;
+ readonly contactNoiseCount: number;
+ readonly actionableCount: number;
+ readonly capMm3: number;
+ readonly pairs: readonly ClassifiedInterferencePair[];
+}
+
+export function classifyInterferencePairs(
+ pairs: readonly InterferencePair[],
+ capMm3 = jointContactCapMm3(),
+): ClassifiedInterferencePair[] {
+ return pairs.map((pair) => {
+ const actionable = pair.volumeMm3 > capMm3;
+ return {
+ ...pair,
+ capMm3,
+ classification: actionable ? 'actionable' : 'contact-noise',
+ actionable,
+ };
+ });
+}
+
+export function summarizeInterferencePairs(
+ pairs: readonly InterferencePair[],
+ capMm3 = jointContactCapMm3(),
+): InterferenceSummary {
+ const classified = classifyInterferencePairs(pairs, capMm3);
+ const actionableCount = classified.filter((pair) => pair.actionable).length;
+ return {
+ rawCount: classified.length,
+ contactNoiseCount: classified.length - actionableCount,
+ actionableCount,
+ capMm3,
+ pairs: classified,
+ };
+}
+```
+
+- [x] **Step 4: Run test and verify green**
+
+Run:
+
+```bash
+npx vitest run tests/unit/runtime/interferenceClassification.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add src/modeling/runtime/interferenceClassification.ts tests/unit/runtime/interferenceClassification.test.ts
+git commit -m "feat: classify interference actionability"
+```
+
+---
+
+### Task 2: Wire Interference Summary Through `review_cad` And Studio
+
+**Files:**
+- Modify: `src/agent/mcp/tools/reviewCad.ts`
+- Modify: `src/agent/mcp/toolOutputSchemas.ts`
+- Modify: `src/studio/types.ts`
+- Modify: `src/studio/hooks/useRecomputeResult.ts`
+- Modify: `src/studio/StudioShell.tsx`
+- Modify: `src/studio/components/Layout/StatusBar.tsx`
+- Modify: `src/studio/__tests__/StudioShell.status.test.tsx`
+- Modify: `src/studio/components/Layout/StatusBar.test.tsx`
+
+- [x] **Step 1: Write failing Studio/status tests**
+
+In `src/studio/__tests__/StudioShell.status.test.tsx`, keep raw pairs in the mock and add `interferenceSummary`:
+
+```ts
+let recomputeInterferenceSummary: {
+ rawCount: number;
+ contactNoiseCount: number;
+ actionableCount: number;
+ capMm3: number;
+} | null = null;
+```
+
+Extend the mocked `useRecomputeResult` return:
+
+```ts
+interferenceSummary: recomputeInterferenceSummary,
+```
+
+Add test:
+
+```ts
+it('uses actionable interference summary for footer count', () => {
+ recomputeRawPairs = [
+ { a: 'raw-a', b: 'raw-b', volumeMm3: 1 },
+ { a: 'real-a', b: 'real-b', volumeMm3: 30 },
+ ];
+ recomputeInterferenceSummary = {
+ rawCount: 2,
+ contactNoiseCount: 1,
+ actionableCount: 1,
+ capMm3: 20,
+ };
+
+ render();
+
+ expect(screen.getByTestId('status-interferences').textContent).toBe('1');
+});
+```
+
+In `src/studio/components/Layout/StatusBar.test.tsx`, add:
+
+```ts
+it('renders actionable interference count with summary title', () => {
+ render(
+ ,
+ );
+
+ const text = screen.getByText(/interferences: 1/);
+ expect(text).toHaveAttribute('title', expect.stringContaining('raw: 16'));
+ expect(text).toHaveAttribute('title', expect.stringContaining('contact-noise: 15'));
+});
+```
+
+- [x] **Step 2: Run tests and verify red**
+
+Run:
+
+```bash
+npx vitest run src/studio/__tests__/StudioShell.status.test.tsx src/studio/components/Layout/StatusBar.test.tsx --reporter=dot
+```
+
+Expected: FAIL because `interferenceSummary` is not yet part of the Studio contract and StatusBar props.
+
+- [x] **Step 3: Add `interferenceSummary` to `review_cad` output**
+
+In `src/agent/mcp/tools/reviewCad.ts`, import:
+
+```ts
+import { summarizeInterferencePairs, type InterferenceSummary } from '../../../modeling/runtime/interferenceClassification';
+```
+
+Add `interferenceSummary: InterferenceSummary;` beside `rawInterferencePairs` in both output variants.
+
+After `rawInterferencePairs` is computed:
+
+```ts
+const interferenceSummary = summarizeInterferencePairs(rawInterferencePairs);
+```
+
+Return `interferenceSummary` in both `ok: true` and `ok: false` branches that already return `rawInterferencePairs`.
+
+- [x] **Step 4: Update output schema**
+
+In `src/agent/mcp/toolOutputSchemas.ts`, add `interferenceSummary` next to `rawInterferencePairs`:
+
+```ts
+interferenceSummary: {
+ type: 'object',
+ additionalProperties: true,
+ description: 'Classified interference counts and pairs: raw, contact-noise, actionable, and capMm3.',
+},
+```
+
+- [x] **Step 5: Update Studio types and hook**
+
+In `src/studio/types.ts`, add:
+
+```ts
+readonly interferenceSummary: {
+ readonly rawCount: number;
+ readonly contactNoiseCount: number;
+ readonly actionableCount: number;
+ readonly capMm3: number;
+} | null;
+```
+
+In `src/studio/hooks/useRecomputeResult.ts`, add:
+
+```ts
+const interferenceSummary = useMemo(
+ () => workbench.scriptReview?.interferenceSummary ?? null,
+ [workbench.scriptReview],
+);
+```
+
+Return it and add it to the dependency list.
+
+- [x] **Step 6: Update StudioShell and StatusBar**
+
+In `src/studio/StudioShell.tsx`, replace direct raw-pair filtering with:
+
+```ts
+const interferenceCount = recompute.interferenceSummary?.actionableCount
+ ?? (recompute.rawInterferencePairs ?? []).filter((pair) => pair.volumeMm3 > jointContactCapMm3()).length;
+```
+
+Pass summary:
+
+```tsx
+
+```
+
+In `src/studio/components/Layout/StatusBar.tsx`, extend props:
+
+```ts
+interferenceSummary?: {
+ rawCount: number;
+ contactNoiseCount: number;
+ actionableCount: number;
+ capMm3: number;
+} | null;
+```
+
+Render title:
+
+```tsx
+const interferenceTitle = interferenceSummary
+ ? `actionable: ${interferenceSummary.actionableCount}, contact-noise: ${interferenceSummary.contactNoiseCount}, raw: ${interferenceSummary.rawCount}, cap: ${interferenceSummary.capMm3} mm3`
+ : undefined;
+```
+
+Attach `title={interferenceTitle}` to the `interferences: N` element.
+
+- [x] **Step 7: Run tests and verify green**
+
+Run:
+
+```bash
+npx vitest run src/studio/__tests__/StudioShell.status.test.tsx src/studio/components/Layout/StatusBar.test.tsx --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 8: Run review_cad interference tests**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/reviewCad-interference.test.ts --reporter=dot
+```
+
+Expected: PASS and no output schema failures.
+
+- [x] **Step 9: Commit**
+
+```bash
+git add src/agent/mcp/tools/reviewCad.ts src/agent/mcp/toolOutputSchemas.ts src/studio/types.ts src/studio/hooks/useRecomputeResult.ts src/studio/StudioShell.tsx src/studio/components/Layout/StatusBar.tsx src/studio/__tests__/StudioShell.status.test.tsx src/studio/components/Layout/StatusBar.test.tsx
+git commit -m "feat: surface actionable interference summary"
+```
+
+---
+
+### Task 3: Targeted Physical-Use-Case Reachability Gate
+
+**Files:**
+- Create: `src/modeling/mates/physicalUseCaseReachability.ts`
+- Modify: `src/modeling/mates/physicalUseCase.ts`
+- Modify: `src/agent/mcp/tools/reviewCad.ts`
+- Modify: `src/agent/mcp/toolRegistry.ts`
+- Modify: `tests/integration/mcp/physicalUseCaseGate.test.ts`
+
+- [x] **Step 1: Write failing unreachable-contact test**
+
+In `tests/integration/mcp/physicalUseCaseGate.test.ts`, add:
+
+```ts
+it('blocks declared physical-use-case contacts that targeted actuator sampling cannot reach', async () => {
+ const result = await reviewCadTool({
+ includePoseEnvelope: false,
+ includeInterference: false,
+ includePhysicalUseCaseReachability: true,
+ requirePhysicalUseCase: true,
+ code: `
+ const arm = assembly('unreachable use case contact');
+ arm.part('base', box(30, 30, 8, true))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 4] }, axis: [0, 0, 1] })
+ .connector('target', { type: 'frame', origin: { kind: 'vec3', value: [120, 0, 4] } })
+ .connector('support', { type: 'frame', origin: { kind: 'vec3', value: [0, 0, 4] } });
+ arm.part('finger', box(40, 6, 6, true).translate(20, 0, 0))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [0, 0, 1] })
+ .connector('tip', { type: 'frame', origin: { kind: 'vec3', value: [40, 0, 0] } });
+ arm.part('servo', box(8, 8, 8, true))
+ .connector('mount', { type: 'frame', origin: { kind: 'vec3', value: [0, 0, 0] } });
+ arm.mate('servo-fix', 'base.support', 'servo.mount', 'fastened');
+ arm.mate('curl', 'base.axis', 'finger.axis', 'revolute', { limitsDeg: [0, 30] });
+ arm.mechanicalJoint('curl-drive', {
+ mate: 'curl',
+ actuator: 'servo',
+ shaft: 'base',
+ supports: ['base'],
+ output: 'finger',
+ });
+ arm.physicalUseCase('touch-target', {
+ stableParts: ['base'],
+ loads: [{ part: 'finger', force: [0, 0, -1] }],
+ contacts: [{ a: 'finger.tip', b: 'base.target', normal: [1, 0, 0], friction: 0.5, normalForceN: 2 }],
+ actuatorLimits: [{ mate: 'curl', maxTorqueNmm: 500 }],
+ criteria: { maxSlipMm: 2 },
+ });
+ return arm.model();
+ `,
+ });
+
+ expect(result.ok).toBe(false);
+ if (!result.ok) {
+ const unreachable = result.diagnostics.find((diagnostic) =>
+ diagnostic.code === 'assembly.physical-use-case.contact-unreachable'
+ );
+ expect(unreachable).toMatchObject({
+ contactA: 'finger.tip',
+ contactB: 'base.target',
+ toleranceMm: 2,
+ });
+ }
+});
+```
+
+- [x] **Step 2: Run test and verify red**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts -t "targeted actuator sampling cannot reach" --reporter=dot
+```
+
+Expected: FAIL because `includePhysicalUseCaseReachability` is not wired and no targeted reachability diagnostic is emitted.
+
+- [x] **Step 3: Implement targeted sampler**
+
+Create `src/modeling/mates/physicalUseCaseReachability.ts`:
+
+```ts
+import type { Assembly } from '../capture/assembly';
+import type { PhysicalUseCaseContact, PhysicalUseCaseRecord } from './physicalUseCase';
+import { parseConnectorRef } from './mate';
+
+export interface PhysicalUseCaseReachabilityIssue {
+ readonly useCaseName: string;
+ readonly contact: PhysicalUseCaseContact;
+ readonly minDistanceMm?: number;
+ readonly toleranceMm: number;
+}
+
+export async function reviewPhysicalUseCaseReachability(
+ arm: Assembly,
+ useCase: PhysicalUseCaseRecord,
+ opts: { samplesPerMate?: number } = {},
+): Promise {
+ const samplesPerMate = opts.samplesPerMate ?? 3;
+ const samples = buildTargetedPoseSamples(arm, useCase, samplesPerMate);
+ const best = new Map();
+
+ for (const poses of samples) {
+ const solved = await arm.solve(poses);
+ for (const contact of useCase.contacts) {
+ const a = connectorWorldPoint(arm, solved.transforms, contact.a);
+ const b = connectorWorldPoint(arm, solved.transforms, contact.b);
+ if (a === undefined || b === undefined) continue;
+ const distance = Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
+ const key = `${contact.a}\t${contact.b}`;
+ best.set(key, Math.min(best.get(key) ?? Infinity, distance));
+ }
+ }
+
+ return useCase.contacts.flatMap((contact) => {
+ const toleranceMm = useCase.criteria?.maxSlipMm ?? 0;
+ const key = `${contact.a}\t${contact.b}`;
+ const minDistanceMm = best.get(key);
+ if (minDistanceMm !== undefined && minDistanceMm <= toleranceMm) return [];
+ return [{ useCaseName: useCase.name, contact, minDistanceMm, toleranceMm }];
+ });
+}
+
+function buildTargetedPoseSamples(
+ arm: Assembly,
+ useCase: PhysicalUseCaseRecord,
+ samplesPerMate: number,
+): Array> {
+ const matesByName = new Map(arm.__mates().map((mate) => [mate.name, mate]));
+ const actuatorMates = useCase.actuatorLimits
+ .map((limit) => matesByName.get(limit.mate))
+ .filter((mate): mate is NonNullable => mate !== undefined && mate.type === 'revolute');
+
+ if (actuatorMates.length === 0) return [{}];
+ const samples: Array> = [{}];
+ for (const mate of actuatorMates) {
+ const [min, max] = mate.limitsDeg ?? [0, 0];
+ const values = samplesPerMate <= 1
+ ? [min]
+ : Array.from({ length: samplesPerMate }, (_, i) => min + ((max - min) * i) / (samplesPerMate - 1));
+ const next: Array> = [];
+ for (const base of samples) {
+ for (const value of values) next.push({ ...base, [mate.name]: value });
+ }
+ samples.splice(0, samples.length, ...next.slice(0, 64));
+ }
+ return samples;
+}
+
+function connectorWorldPoint(
+ arm: Assembly,
+ transforms: ReadonlyMap,
+ ref: string,
+): [number, number, number] | undefined {
+ const parsed = parseConnectorRef(ref);
+ const part = arm.__parts().find((candidate) => candidate.name === parsed.partName);
+ const connector = part?.mateConnectors.find((candidate) => candidate.name === parsed.connectorName);
+ if (connector === undefined || connector.origin.kind !== 'vec3') return undefined;
+ return transforms.get(parsed.partName)?.point(connector.origin.value);
+}
+```
+
+If `arm.solve(...)` returns a different property name than `transforms`, inspect `src/modeling/capture/assembly.ts` and adjust the one accessor only. Do not change the sampler API.
+
+- [x] **Step 4: Wire diagnostics into physical use-case review**
+
+Change `reviewPhysicalUseCases(...)` to async only if current callers can handle it. If not, add a new async wrapper:
+
+```ts
+export async function reviewPhysicalUseCasesWithReachability(
+ arm: Assembly,
+ opts: { requirePhysicalUseCase?: boolean; includeReachability?: boolean; samplesPerMate?: number } = {},
+): Promise {
+ const base = reviewPhysicalUseCases(arm, opts);
+ if (opts.includeReachability !== true) return base;
+ const diagnostics = [...base.diagnostics];
+ for (const useCase of arm.__physicalUseCases()) {
+ const issues = await reviewPhysicalUseCaseReachability(arm, useCase, { samplesPerMate: opts.samplesPerMate });
+ for (const issue of issues) {
+ diagnostics.push({
+ code: 'assembly.physical-use-case.contact-unreachable',
+ severity: 'error',
+ useCaseName: issue.useCaseName,
+ contactA: issue.contact.a,
+ contactB: issue.contact.b,
+ ...(issue.minDistanceMm === undefined ? {} : { minDistanceMm: issue.minDistanceMm }),
+ toleranceMm: issue.toleranceMm,
+ message: issue.minDistanceMm === undefined
+ ? `Physical use case '${issue.useCaseName}' contact '${issue.contact.a}' to '${issue.contact.b}' could not be checked in targeted actuator samples.`
+ : `Physical use case '${issue.useCaseName}' contact '${issue.contact.a}' to '${issue.contact.b}' never gets within ${issue.toleranceMm.toFixed(2)} mm; closest targeted sample is ${issue.minDistanceMm.toFixed(2)} mm.`,
+ hint: `physical-use-case.contact-unreachable — revise the target, connector placement, or actuator mate limits so '${issue.contact.a}' reaches '${issue.contact.b}' within maxSlipMm ${issue.toleranceMm.toFixed(2)}.`,
+ });
+ }
+ }
+ return { ...base, diagnostics };
+}
+```
+
+- [x] **Step 5: Wire review_cad option**
+
+In `ReviewCadInput`, add:
+
+```ts
+includePhysicalUseCaseReachability?: boolean;
+physicalUseCaseReachabilitySamplesPerMate?: number;
+```
+
+In `reviewCadTool`, replace the physical use-case review call with:
+
+```ts
+const physicalUseCases = await reviewPhysicalUseCasesWithReachability(arm, {
+ requirePhysicalUseCase: input.requirePhysicalUseCase,
+ poseEnvelope,
+ includeReachability: input.includePhysicalUseCaseReachability ?? input.requirePhysicalUseCase === true,
+ samplesPerMate: input.physicalUseCaseReachabilitySamplesPerMate,
+});
+```
+
+- [x] **Step 6: Update registry docs**
+
+In `src/agent/mcp/toolRegistry.ts`, add schema entries:
+
+```ts
+includePhysicalUseCaseReachability: {
+ type: 'boolean',
+ description: 'When true, declared physicalUseCase contacts are checked by sampling only actuatorLimits mates, faster than full pose envelope.',
+},
+physicalUseCaseReachabilitySamplesPerMate: {
+ type: 'number',
+ description: 'Samples per actuator mate for targeted physicalUseCase contact reachability. Default 3.',
+},
+```
+
+- [x] **Step 7: Run focused tests**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts -t "targeted actuator sampling cannot reach" --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 8: Run physical use-case suite**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 9: Commit**
+
+```bash
+git add src/modeling/mates/physicalUseCaseReachability.ts src/modeling/mates/physicalUseCase.ts src/agent/mcp/tools/reviewCad.ts src/agent/mcp/toolRegistry.ts tests/integration/mcp/physicalUseCaseGate.test.ts
+git commit -m "feat: check physical use case contact reachability"
+```
+
+---
+
+### Task 4: Supported Servo Revolute Helper
+
+**Files:**
+- Create: `src/modeling/joints/supportedServoRevolute.ts`
+- Create: `src/modeling/joints/supportedServoRevolute.test.ts`
+- Modify: `src/modeling/joints/index.ts`
+- Modify: `src/modeling/api.ts`
+- Modify: `src/agent/mcp/tools/listApi.ts`
+
+- [x] **Step 1: Write failing API test**
+
+Create `src/modeling/joints/supportedServoRevolute.test.ts`:
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { CaptureSession } from '../capture/captureSession';
+import { createApi } from '../api';
+
+describe('joint.supportedServoRevolute', () => {
+ it('creates seated actuator geometry and mechanicalJoint intent for a revolute mate', () => {
+ const session = new CaptureSession();
+ const kcad = createApi({ session });
+ const arm = kcad.assembly('supported drive');
+
+ arm.part('base', kcad.box(30, 30, 8, true))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 4] }, axis: [0, 0, 1] })
+ .connector('servo-mount', { type: 'frame', origin: { kind: 'vec3', value: [0, -15, 4] } });
+ arm.part('link', kcad.box(40, 6, 6, true).translate(20, 0, 0))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [0, 0, 1] });
+ arm.mate('curl', 'base.axis', 'link.axis', 'revolute', { limitsDeg: [0, 45] });
+
+ const drive = kcad.joint.supportedServoRevolute(arm, {
+ name: 'curl-drive',
+ mate: 'curl',
+ support: 'base',
+ supportMount: 'base.servo-mount',
+ output: 'link',
+ axis: 'base.axis',
+ });
+
+ expect(drive.actuatorPartName).toBe('curl-drive-servo');
+ expect(arm.__mechanicalJointIntents()).toEqual([
+ expect.objectContaining({
+ name: 'curl-drive',
+ mate: 'curl',
+ actuator: 'curl-drive-servo',
+ shaft: 'base',
+ supports: ['base'],
+ output: 'link',
+ }),
+ ]);
+ expect(arm.__mates().some((mate) => mate.name === 'curl-drive-servo-fix' && mate.type === 'fastened')).toBe(true);
+ });
+});
+```
+
+- [x] **Step 2: Run test and verify red**
+
+Run:
+
+```bash
+npx vitest run src/modeling/joints/supportedServoRevolute.test.ts --reporter=dot
+```
+
+Expected: FAIL because `joint.supportedServoRevolute` does not exist.
+
+- [x] **Step 3: Implement helper**
+
+Create `src/modeling/joints/supportedServoRevolute.ts`:
+
+```ts
+import type { Assembly } from '../capture/assembly';
+import type { Shape } from '../capture/proxy';
+import type { KernelCadApi } from '../api';
+
+export interface SupportedServoRevoluteOptions {
+ readonly name: string;
+ readonly mate: string;
+ readonly support: string;
+ readonly supportMount: string;
+ readonly output: string;
+ readonly axis: string;
+ readonly servoBody?: Shape;
+ readonly minBearingLengthMm?: number;
+}
+
+export interface SupportedServoRevoluteResult {
+ readonly actuatorPartName: string;
+}
+
+export function supportedServoRevolute(
+ kc: KernelCadApi,
+ arm: Assembly,
+ opts: SupportedServoRevoluteOptions,
+): SupportedServoRevoluteResult {
+ const actuatorPartName = `${opts.name}-servo`;
+ const servoBody = opts.servoBody
+ ?? kc.box(20, 13, 16, true)
+ .union(kc.cylinder(5, 4.5, 24).alongAxis([1, 0, 0]).translate(0, -7, 0));
+
+ arm.part(actuatorPartName, servoBody)
+ .connector('mount', { type: 'frame', origin: { kind: 'vec3', value: [0, 0, 0] } });
+ arm.mate(`${opts.name}-servo-fix`, opts.supportMount, `${actuatorPartName}.mount`, 'fastened');
+ arm.mechanicalJoint(opts.name, {
+ mate: opts.mate,
+ actuator: actuatorPartName,
+ shaft: opts.support,
+ supports: [opts.support],
+ output: opts.output,
+ requiredSupport: {
+ kind: 'hinge-bracket',
+ around: opts.axis,
+ supports: [opts.support],
+ minBearingLengthMm: opts.minBearingLengthMm ?? 8,
+ },
+ });
+
+ return { actuatorPartName };
+}
+```
+
+- [x] **Step 4: Expose helper**
+
+In `src/modeling/joints/index.ts`, export the helper type/function.
+
+In `src/modeling/api.ts`, add method under `joint` namespace:
+
+```ts
+supportedServoRevolute: (arm, opts) => supportedServoRevolute(api, arm, opts),
+```
+
+Adjust exact names to match the existing `makeJointNamespace` pattern.
+
+- [x] **Step 5: Update API listing**
+
+In `src/agent/mcp/tools/listApi.ts`, extend the `joint` API description with:
+
+```ts
+`joint.supportedServoRevolute(assembly, { name, mate, support, supportMount, output, axis, servoBody?, minBearingLengthMm? })` creates a seated servo part, fastened mount, and mechanicalJoint support contract for a driven revolute mate.
+```
+
+- [x] **Step 6: Run helper test**
+
+Run:
+
+```bash
+npx vitest run src/modeling/joints/supportedServoRevolute.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 7: Commit**
+
+```bash
+git add src/modeling/joints/supportedServoRevolute.ts src/modeling/joints/supportedServoRevolute.test.ts src/modeling/joints/index.ts src/modeling/api.ts src/agent/mcp/tools/listApi.ts
+git commit -m "feat: add supported servo revolute helper"
+```
+
+---
+
+### Task 5: Design-Loop Physical Acceptance Report
+
+**Files:**
+- Modify: `src/agent/mcp/tools/designLoop.ts`
+- Modify: `tests/integration/mcp/designLoop.test.ts`
+- Modify: `tests/unit/mcp/designLoopNextActionPrompt.test.ts`
+
+- [x] **Step 1: Write failing design-loop test**
+
+In `tests/integration/mcp/designLoop.test.ts`, add:
+
+```ts
+it('rejects visual acceptance when physical use case contacts are unreachable', async () => {
+ const result = await designLoopTool({
+ goal: 'Reject a visually reviewed hand if the declared grasp contact cannot be reached.',
+ requireVisualReview: true,
+ includePoseEnvelope: false,
+ includeInterference: false,
+ attempts: [{
+ id: 'bad-grasp',
+ title: 'Visually accepted but physically unreachable',
+ code: `
+ const arm = assembly('bad grasp');
+ arm.part('base', box(30, 30, 8, true))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 4] }, axis: [0, 0, 1] })
+ .connector('target', { type: 'frame', origin: { kind: 'vec3', value: [120, 0, 4] } })
+ .connector('support', { type: 'frame', origin: { kind: 'vec3', value: [0, 0, 4] } });
+ arm.part('finger', box(40, 6, 6, true).translate(20, 0, 0))
+ .connector('axis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [0, 0, 1] })
+ .connector('tip', { type: 'frame', origin: { kind: 'vec3', value: [40, 0, 0] } });
+ arm.part('servo', box(8, 8, 8, true)).connector('mount', { type: 'frame', origin: { kind: 'vec3', value: [0, 0, 0] } });
+ arm.mate('servo-fix', 'base.support', 'servo.mount', 'fastened');
+ arm.mate('curl', 'base.axis', 'finger.axis', 'revolute', { limitsDeg: [0, 30] });
+ arm.mechanicalJoint('curl-drive', { mate: 'curl', actuator: 'servo', shaft: 'base', supports: ['base'], output: 'finger' });
+ arm.physicalUseCase('touch-target', {
+ stableParts: ['base'],
+ loads: [{ part: 'finger', force: [0, 0, -1] }],
+ contacts: [{ a: 'finger.tip', b: 'base.target', normal: [1, 0, 0], friction: 0.5, normalForceN: 2 }],
+ actuatorLimits: [{ mate: 'curl', maxTorqueNmm: 500 }],
+ criteria: { maxSlipMm: 2 },
+ });
+ return arm.model();
+ `,
+ visualReview: {
+ accepted: true,
+ screenshotPath: '/tmp/bad-grasp.png',
+ findings: ['The fixture is visible in the screenshot.'],
+ checks: requiredPassingVisualChecks('Visible fixture with connector labels and seated servo mount.'),
+ },
+ }],
+ });
+
+ expect(result.attempts[0].accepted).toBe(false);
+ expect(result.attempts[0].reviewFacts.some((fact) =>
+ fact.code === 'assembly.physical-use-case.contact-unreachable'
+ )).toBe(true);
+});
+```
+
+Use the existing helper pattern for `requiredPassingVisualChecks(...)` in that test file. If the helper is named differently, reuse the local helper that creates complete visual-review checks.
+
+- [x] **Step 2: Run test and verify red**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/designLoop.test.ts -t "physically unreachable" --reporter=dot
+```
+
+Expected: FAIL because design_loop is not forwarding targeted physical-use-case reachability.
+
+- [x] **Step 3: Forward reachability through design_loop**
+
+In `src/agent/mcp/tools/designLoop.ts`, where `reviewCadTool` is called, add:
+
+```ts
+includePhysicalUseCaseReachability: true,
+requirePhysicalUseCase: true,
+```
+
+only when the attempt contains `physicalUseCase(` or when the tool input explicitly asks for physical acceptance. If adding an input flag is cleaner, add:
+
+```ts
+requirePhysicalAcceptance?: boolean;
+```
+
+Default it to `true` for mechanism/robot-hand goals only if current design_loop conventions already infer goal-dependent gates. Otherwise default false and set true in this test.
+
+- [x] **Step 4: Make next-action prompt explicit**
+
+In `src/agent/mcp/tools/designLoop.ts`, ensure contact-unreachable facts are included in `nextActionPrompt` by preserving them in `reviewFacts`.
+
+In `tests/unit/mcp/designLoopNextActionPrompt.test.ts`, add:
+
+```ts
+expect(prompt).toContain('assembly.physical-use-case.contact-unreachable');
+expect(prompt).toContain('closest');
+expect(prompt).toContain('maxSlipMm');
+```
+
+- [x] **Step 5: Run design-loop tests**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add src/agent/mcp/tools/designLoop.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts
+git commit -m "feat: reject unreachable physical use cases in design loop"
+```
+
+---
+
+### Task 6: Apply Tooling To Robot Hand Without Geometry Changes
+
+**Files:**
+- Modify: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+- Modify: `artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json`
+
+- [x] **Step 1: Add hand reachability assertion**
+
+In `tests/integration/examples/fiveFingerKinematicHand.test.ts`, add a test that runs `reviewCadTool` with:
+
+```ts
+const result = await reviewCadTool({
+ file: EXAMPLE_PATH,
+ includePoseEnvelope: false,
+ includeInterference: false,
+ requirePhysicalUseCase: true,
+ includePhysicalUseCaseReachability: true,
+ physicalUseCaseReachabilitySamplesPerMate: 3,
+});
+```
+
+Expected for current hand:
+
+```ts
+expect(result.ok).toBe(false);
+expect(result.diagnostics.some((diagnostic) =>
+ diagnostic.code === 'assembly.physical-use-case.contact-unreachable'
+)).toBe(true);
+```
+
+This locks the current hand as rejected for a concrete, tool-generated reason.
+
+Implementation note: the final test evaluates the same fixture and calls `reviewPhysicalUseCasesWithReachability(...)` directly because the full `reviewCadTool` path was too slow/hung for this fixture. The spec and quality review accepted this as satisfying the task intent.
+
+- [x] **Step 2: Run test and verify expected failure/pass**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t "physical use case reachability" --reporter=dot
+```
+
+Expected: PASS because the hand is intentionally rejected by the new gate.
+
+- [x] **Step 3: Update design-loop artifact**
+
+Append an attempt entry:
+
+```json
+{
+ "id": "07",
+ "title": "Tooling gate: targeted grasp reachability",
+ "status": "rejected-by-physical-use-case-contact-reachability",
+ "deterministicChecks": [
+ "npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t \"physical use case reachability\" --reporter=dot"
+ ],
+ "remainingCaveats": [
+ "The model remains visually inspectable but is rejected because declared contacts do not reach the cylinder within maxSlipMm.",
+ "Next geometry slice must redesign thumb/index/middle placement or actuator travel using this diagnostic, not visual guesswork."
+ ]
+}
+```
+
+Use valid JSON and include the actual screenshot path from the latest browser run if one was captured during execution.
+
+- [x] **Step 4: Validate JSON**
+
+Run:
+
+```bash
+node -e "JSON.parse(require('fs').readFileSync('artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json','utf8')); console.log('json ok')"
+```
+
+Expected: `json ok`.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add tests/integration/examples/fiveFingerKinematicHand.test.ts artifacts/robot-hand-design-loop/2026-07-09-five-finger-hand-loop.json
+git commit -m "test: reject hand by targeted grasp reachability"
+```
+
+---
+
+## Final Verification
+
+- [x] Run fast unit coverage:
+
+```bash
+npx vitest run tests/unit/runtime/interferenceClassification.test.ts src/modeling/joints/supportedServoRevolute.test.ts src/studio/__tests__/StudioShell.status.test.tsx src/studio/components/Layout/StatusBar.test.tsx --reporter=dot
+```
+
+Expected: all pass.
+
+- [x] Run physical-use-case and hand integration coverage:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+Expected: all pass; the hand-specific reachability test passes by asserting rejection.
+
+- [x] Run design-loop coverage:
+
+```bash
+npx vitest run tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts --reporter=dot
+```
+
+Expected: all pass.
+
+- [x] Run typecheck:
+
+```bash
+npm run typecheck
+```
+
+Expected: `tsc -b --noEmit` exits 0. Existing TanStack route warnings may appear and are not part of this plan.
+
+- [x] Browser evidence:
+
+```bash
+npm run dev -- --host 127.0.0.1 --port 5173
+```
+
+Open:
+
+```text
+http://127.0.0.1:/studio?script=examples/robot-hand/five-finger-kinematic-hand.kcad.ts
+```
+
+Expected:
+- Status footer shows actionable interference count, not raw contact-noise count.
+- Validity/design-loop output reports `assembly.physical-use-case.contact-unreachable` for the current hand until geometry is redesigned.
+
+---
+
+## Self-Review
+
+- Spec coverage: interference classification, visible reporting, targeted reachability, mechanism-drive helper, and design-loop acceptance are all mapped to tasks.
+- Placeholder scan: no placeholder markers, no open-ended “add tests” steps, and every behavior change has an explicit test command.
+- Type consistency: `InterferenceSummary`, `ClassifiedInterferencePair`, `includePhysicalUseCaseReachability`, and `supportedServoRevolute` are introduced once and reused with the same names.
+- Scope discipline: this plan explicitly avoids hand geometry changes until tooling can produce a concrete reachability rejection.
diff --git a/docs/superpowers/plans/2026-07-10-five-finger-hand-topology-repair.md b/docs/superpowers/plans/2026-07-10-five-finger-hand-topology-repair.md
new file mode 100644
index 000000000..e6c6e510a
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-10-five-finger-hand-topology-repair.md
@@ -0,0 +1,473 @@
+# Five-Finger Hand Topology Repair Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make the current five-finger hand pass `reviewJointTopology(...)` by adding real passive joint-support evidence and treating the grasp cylinder as a contact target rather than hand structure.
+
+**Architecture:** Add an assembly-level passive `jointSupport(...)` intent that reuses the support-side fastened-path rule already used by driven `mechanicalJoint(...)`. Add a narrow part role for contact targets so topology ignores load-path checks on objects that are explicitly not part of the hand structure. Apply both to the existing hand without visual redesign.
+
+**Tech Stack:** TypeScript, Vitest, KernelCAD Assembly capture API, existing mate topology reviewer and five-finger hand example.
+
+---
+
+## File Structure
+
+- Modify `src/modeling/capture/assembly.ts`
+ - Add `JointSupportIntentOpts`, `JointSupportIntentRecord`, `AssemblyPartRole`, `AssemblyPartOpts.role`, stored part role, `jointSupport(...)`, and `__jointSupportIntents()`.
+- Modify `tests/unit/assemblies/assemblyCapture.test.ts`
+ - Add capture/validation tests for passive joint support and contact-target role storage.
+- Modify `src/modeling/mates/jointTopology.ts`
+ - Accept either driven `mechanicalJoint(...)` or passive `jointSupport(...)` as revolute support evidence.
+ - Skip `assembly.connectivity.no-load-path` for physical-use-case load parts whose stored role is `contact-target`.
+- Modify `src/modeling/mates/jointTopology.test.ts`
+ - Add passive-supported hinge pass/fail tests.
+ - Add contact-target load-path skip test.
+- Modify `examples/robot-hand/five-finger-kinematic-hand.kcad.ts`
+ - Add passive support declarations for every unsupported revolute.
+ - Mark `grasp-cylinder` as `role: 'contact-target'`.
+- Modify `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+ - Change the topology regression from expected blockers to expected empty diagnostics.
+
+---
+
+### Task 1: Capture Passive Joint Support And Contact Target Role
+
+**Files:**
+- Modify: `src/modeling/capture/assembly.ts`
+- Modify: `tests/unit/assemblies/assemblyCapture.test.ts`
+
+- [x] **Step 1: Write failing capture tests**
+
+Add tests near the existing `mechanicalJoint(...)` capture tests:
+
+```ts
+it('captures passive joint support intent records', () => {
+ const session = new CaptureSession();
+ const kcad = createApi({ session });
+ const arm = kcad.assembly('passive support');
+
+ const returned = arm.jointSupport('pip-bearing', {
+ mate: 'index-pip',
+ shaft: 'index-proximal',
+ supports: ['index-proximal'],
+ output: 'index-middle',
+ requiredSupport: {
+ kind: 'hinge-bracket',
+ around: 'index-proximal.pip',
+ supports: ['index-proximal'],
+ minBearingLengthMm: 6,
+ },
+ });
+
+ expect(returned).toBe(arm);
+ expect(arm.__jointSupportIntents()).toEqual([
+ {
+ name: 'pip-bearing',
+ mate: 'index-pip',
+ shaft: 'index-proximal',
+ supports: ['index-proximal'],
+ output: 'index-middle',
+ requiredSupport: {
+ kind: 'hinge-bracket',
+ around: 'index-proximal.pip',
+ supports: ['index-proximal'],
+ minBearingLengthMm: 6,
+ },
+ },
+ ]);
+});
+
+it('stores contact target part roles', () => {
+ const session = new CaptureSession();
+ const kcad = createApi({ session });
+ const arm = kcad.assembly('contact target');
+
+ arm.part('grasp-cylinder', kcad.cylinder(20, 10), { role: 'contact-target' });
+
+ expect(arm.__parts().find((part) => part.name === 'grasp-cylinder')?.role).toBe('contact-target');
+});
+```
+
+- [x] **Step 2: Verify red**
+
+Run:
+
+```bash
+npx vitest run tests/unit/assemblies/assemblyCapture.test.ts -t "passive joint support|contact target" --reporter=dot
+```
+
+Expected: FAIL because `jointSupport`, `__jointSupportIntents`, and `role` do not exist.
+
+- [x] **Step 3: Implement capture API**
+
+In `src/modeling/capture/assembly.ts`:
+
+```ts
+export type AssemblyPartRole = 'structure' | 'contact-target';
+
+export interface JointSupportIntentOpts {
+ readonly mate: string;
+ readonly shaft: string;
+ readonly supports: readonly string[];
+ readonly output: string;
+ readonly requiredSupport?: MechanicalJointSupportRequirement;
+}
+
+export interface JointSupportIntentRecord extends JointSupportIntentOpts {
+ readonly name: string;
+}
+```
+
+Add `role?: AssemblyPartRole` to `AssemblyPartOpts` and `AssemblyPartStored`. Store it in `part(...)` only when defined.
+
+Add a private array beside `mechanicalJointIntents`:
+
+```ts
+private readonly jointSupportIntents: JointSupportIntentRecord[] = [];
+```
+
+Add `jointSupport(name, opts)` mirroring `mechanicalJoint(...)` validation, without `actuator`:
+
+```ts
+jointSupport(name: string, opts: JointSupportIntentOpts): this {
+ validateMechanicalIntentName('name', name);
+ if (this.jointSupportIntents.some((intent) => intent.name === name)) {
+ throw new KernelError(
+ 'feature.invalid-args',
+ `assembly.jointSupport.duplicate-name: joint support intent '${name}' is already declared.`,
+ undefined,
+ `invalid-args.assembly.joint-support-duplicate-name — use a unique jointSupport name.`,
+ );
+ }
+ validateMechanicalIntentName('mate', opts.mate);
+ validateMechanicalIntentName('shaft', opts.shaft);
+ validateMechanicalIntentName('output', opts.output);
+ if (!Array.isArray(opts.supports) || opts.supports.length === 0) {
+ throw new KernelError(
+ 'feature.invalid-args',
+ `assembly.jointSupport.invalid-ref: joint support intent '${name}' requires at least one support part.`,
+ undefined,
+ `invalid-args.assembly.joint-support-invalid-ref — pass supports: ['support-part-name', ...].`,
+ );
+ }
+ for (const support of opts.supports) {
+ validateMechanicalIntentName('supports[]', support);
+ }
+ if (opts.requiredSupport !== undefined) {
+ validateMechanicalIntentName('requiredSupport.kind', opts.requiredSupport.kind);
+ validateMechanicalIntentName('requiredSupport.around', opts.requiredSupport.around);
+ for (const support of opts.requiredSupport.supports ?? []) {
+ validateMechanicalIntentName('requiredSupport.supports[]', support);
+ }
+ if (
+ opts.requiredSupport.minBearingLengthMm !== undefined &&
+ (!Number.isFinite(opts.requiredSupport.minBearingLengthMm) || opts.requiredSupport.minBearingLengthMm <= 0)
+ ) {
+ throw new KernelError('feature.invalid-args', `assembly.jointSupport.invalid-required-support: minBearingLengthMm must be a positive finite number.`);
+ }
+ if (
+ opts.requiredSupport.clearanceMm !== undefined &&
+ (!Number.isFinite(opts.requiredSupport.clearanceMm) || opts.requiredSupport.clearanceMm < 0)
+ ) {
+ throw new KernelError('feature.invalid-args', `assembly.jointSupport.invalid-required-support: clearanceMm must be a non-negative finite number.`);
+ }
+ }
+
+ this.jointSupportIntents.push({
+ name,
+ mate: opts.mate,
+ shaft: opts.shaft,
+ supports: [...opts.supports],
+ output: opts.output,
+ ...(opts.requiredSupport !== undefined ? {
+ requiredSupport: {
+ ...opts.requiredSupport,
+ ...(opts.requiredSupport.supports !== undefined ? { supports: [...opts.requiredSupport.supports] } : {}),
+ },
+ } : {}),
+ });
+ return this;
+}
+```
+
+Add:
+
+```ts
+__jointSupportIntents(): readonly JointSupportIntentRecord[] {
+ return this.jointSupportIntents;
+}
+```
+
+- [x] **Step 4: Verify green**
+
+Run:
+
+```bash
+npx vitest run tests/unit/assemblies/assemblyCapture.test.ts -t "passive joint support|contact target" --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add src/modeling/capture/assembly.ts tests/unit/assemblies/assemblyCapture.test.ts
+git commit -m "feat: capture passive joint support"
+```
+
+---
+
+### Task 2: Teach Topology Gate Passive Support And Contact Targets
+
+**Files:**
+- Modify: `src/modeling/mates/jointTopology.ts`
+- Modify: `src/modeling/mates/jointTopology.test.ts`
+
+- [x] **Step 1: Write failing topology tests**
+
+Add tests to `src/modeling/mates/jointTopology.test.ts`:
+
+```ts
+it('accepts passive support intents for supported revolute hinges', () => {
+ const arm = armLike({
+ parts: [
+ { name: 'proximal', role: 'structure', mateConnectors: [{ name: 'pip', type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }] },
+ { name: 'middle', role: 'structure', mateConnectors: [{ name: 'pip', type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }] },
+ ],
+ mates: [{ name: 'pip', a: 'proximal.pip', b: 'middle.pip', type: 'revolute', limitsDeg: [0, 40] }],
+ jointSupportIntents: [{ mate: 'pip', shaft: 'proximal', supports: ['proximal'], output: 'middle' }],
+ });
+
+ expect(codesOf(arm)).not.toContain('assembly.joint-topology.unsupported-axis');
+});
+
+it('rejects passive support intents disconnected from the hinge support side', () => {
+ const arm = armLike({
+ parts: [
+ { name: 'proximal', role: 'structure', mateConnectors: [{ name: 'pip', type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }] },
+ { name: 'middle', role: 'structure', mateConnectors: [{ name: 'pip', type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }] },
+ { name: 'fake-shaft', role: 'structure', mateConnectors: [] },
+ { name: 'fake-support', role: 'structure', mateConnectors: [] },
+ ],
+ mates: [{ name: 'pip', a: 'proximal.pip', b: 'middle.pip', type: 'revolute', limitsDeg: [0, 40] }],
+ jointSupportIntents: [{ mate: 'pip', shaft: 'fake-shaft', supports: ['fake-support'], output: 'middle' }],
+ });
+
+ expect(codesOf(arm)).toContain('assembly.joint-topology.unsupported-axis');
+});
+
+it('does not require load paths for contact target load parts', () => {
+ const arm = armLike({
+ parts: [
+ { name: 'palm-root', role: 'structure', mateConnectors: [] },
+ { name: 'grasp-cylinder', role: 'contact-target', mateConnectors: [] },
+ ],
+ physicalUseCases: [{
+ name: 'grasp',
+ stableParts: ['palm-root'],
+ loads: [{ part: 'grasp-cylinder', force: [0, 0, -3] }],
+ }],
+ });
+
+ expect(codesOf(arm)).not.toContain('assembly.connectivity.no-load-path');
+});
+```
+
+Update `armLike(...)` to include:
+
+```ts
+jointSupportIntents?: unknown[];
+__jointSupportIntents: () => overrides.jointSupportIntents ?? [],
+```
+
+- [x] **Step 2: Verify red**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+Expected: FAIL because passive support intents are ignored and contact-target role is not skipped.
+
+- [x] **Step 3: Implement topology support**
+
+In `jointTopology.ts`, import `JointSupportIntentRecord`.
+
+Generalize `isCompleteMechanicalIntent(...)` into a shared support check for records shaped like:
+
+```ts
+interface JointSupportLikeIntent {
+ readonly mate: string;
+ readonly shaft: string;
+ readonly supports: readonly string[];
+ readonly output: string;
+}
+```
+
+Then `collectSupportedRevoluteMates(...)` must iterate both:
+
+```ts
+for (const intent of arm.__mechanicalJointIntents()) {
+ if (!isCompleteDrivenMechanicalIntent(intent, matesByName, partsByName, fastenedGraph)) continue;
+ supported.add(intent.mate);
+}
+
+for (const intent of arm.__jointSupportIntents()) {
+ if (!isCompleteJointSupportIntent(intent, matesByName, partsByName, fastenedGraph)) continue;
+ supported.add(intent.mate);
+}
+```
+
+Use the same support-side fastened reachability for both; only driven `mechanicalJoint(...)` also checks `actuator`.
+
+In the physical-use-case load-path loop, skip contact targets:
+
+```ts
+const loadPart = partsByName.get(load.part);
+if (loadPart === undefined) continue;
+if (loadPart.role === 'contact-target') continue;
+```
+
+- [x] **Step 4: Verify green**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add src/modeling/mates/jointTopology.ts src/modeling/mates/jointTopology.test.ts
+git commit -m "feat: review passive joint support"
+```
+
+---
+
+### Task 3: Repair Five-Finger Hand Topology
+
+**Files:**
+- Modify: `examples/robot-hand/five-finger-kinematic-hand.kcad.ts`
+- Modify: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [x] **Step 1: Write failing example expectation**
+
+Change the topology regression to require no topology diagnostics:
+
+```ts
+const topologyReview = reviewJointTopology(assembly);
+
+expect(topologyReview.diagnostics).toEqual([]);
+```
+
+- [x] **Step 2: Verify red**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t "topology" --reporter=dot
+```
+
+Expected: FAIL with current unsupported-axis and grasp-cylinder no-load-path diagnostics.
+
+- [x] **Step 3: Add passive supports and contact target role**
+
+In `examples/robot-hand/five-finger-kinematic-hand.kcad.ts`, add a helper inside `assemblyTasks.push(...)` after mates are declared:
+
+```ts
+function supportRevolute(mate, shaft, output, around) {
+ hand.jointSupport(`${mate}-support`, {
+ mate,
+ shaft,
+ supports: [shaft],
+ output,
+ requiredSupport: {
+ kind: 'hinge-bracket',
+ around,
+ supports: [shaft],
+ minBearingLengthMm: 6,
+ },
+ });
+}
+```
+
+For every finger, call:
+
+```ts
+if (!supportedGraspMcpNames.includes(spec.name)) {
+ supportRevolute(`${spec.name}-mcp`, 'palm-root', `${spec.name}-proximal`, `palm-root.${spec.name}Mcp`);
+}
+supportRevolute(`${spec.name}-pip`, `${spec.name}-proximal`, `${spec.name}-middle`, `${spec.name}-proximal.pip`);
+supportRevolute(`${spec.name}-dip`, `${spec.name}-middle`, `${spec.name}-distal`, `${spec.name}-middle.dip`);
+```
+
+Mark the object:
+
+```ts
+const graspCylinder = hand.part('grasp-cylinder', ..., { role: 'contact-target' });
+```
+
+- [x] **Step 4: Verify green**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t "topology" --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Run full hand integration**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 6: Commit**
+
+```bash
+git add examples/robot-hand/five-finger-kinematic-hand.kcad.ts tests/integration/examples/fiveFingerKinematicHand.test.ts
+git commit -m "fix: satisfy hand topology gate"
+```
+
+---
+
+## Final Verification
+
+- [x] Capture tests:
+
+```bash
+npx vitest run tests/unit/assemblies/assemblyCapture.test.ts -t "passive joint support|contact target" --reporter=dot
+```
+
+- [x] Topology unit tests:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+- [x] Review/design-loop integration:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts --reporter=dot
+```
+
+- [x] Hand integration:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+- [x] Typecheck:
+
+```bash
+npm run typecheck
+```
diff --git a/docs/superpowers/plans/2026-07-10-five-finger-topology-gate.md b/docs/superpowers/plans/2026-07-10-five-finger-topology-gate.md
new file mode 100644
index 000000000..0411523e7
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-10-five-finger-topology-gate.md
@@ -0,0 +1,203 @@
+# Five-Finger Topology Gate Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a deterministic topology/connectivity gate that rejects robot hands with disconnected moving links, unsupported revolute axes, missing limits, or invalid connector contracts before any visual iteration is accepted.
+
+**Architecture:** Add a pure `reviewJointTopology(assembly)` reviewer under `src/modeling/mates/`, wire its diagnostics into `review_cad`, then preserve them through `design_loop`. The gate uses existing assembly surfaces: `__parts()`, `__mates()`, `__mechanicalJointIntents()`, and `__physicalUseCases()`.
+
+**Tech Stack:** TypeScript, Vitest, KernelCAD Assembly/Mate APIs, existing `review_cad` and `design_loop`.
+
+---
+
+## File Structure
+
+- Create `src/modeling/mates/jointTopology.ts`
+ - Pure deterministic reviewer for part graph connectivity and non-fastened mate contracts.
+- Create `src/modeling/mates/jointTopology.test.ts`
+ - Fast unit tests using small in-memory assemblies.
+- Modify `src/agent/mcp/tools/reviewCad.ts`
+ - Include topology diagnostics in review diagnostics and fitness summary inputs.
+- Modify `src/modeling/mates/mechanismFitness.ts`
+ - Accept topology diagnostics as blocking reasons.
+- Modify `tests/integration/mcp/physicalUseCaseGate.test.ts`
+ - Add `review_cad` integration for topology failures and passing clean fixture.
+- Modify `tests/integration/mcp/designLoop.test.ts`
+ - Add design-loop prompt preservation for topology diagnostics.
+- Modify `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+ - Add regression proving current five-finger hand fails topology/connectivity before geometry redesign is accepted.
+
+---
+
+### Task 1: Pure Topology Reviewer
+
+**Files:**
+- Create: `src/modeling/mates/jointTopology.ts`
+- Create: `src/modeling/mates/jointTopology.test.ts`
+
+- [x] **Step 1: Write failing tests**
+
+Create tests for:
+
+- `assembly.connectivity.floating-moving-part` when a moving link is isolated from physical-use-case stable parts.
+- `assembly.joint-topology.missing-limit` when a revolute mate has no `limitsDeg`.
+- `assembly.joint-topology.unsupported-axis` when a revolute mate has no mechanical support intent.
+- clean supported hinge passes with stable root, finite limits, and `mechanicalJoint(...)`.
+
+- [x] **Step 2: Verify red**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+Expected: FAIL because `jointTopology.ts` does not exist.
+
+- [x] **Step 3: Implement reviewer**
+
+Create `reviewJointTopology(arm)` with:
+
+- graph nodes from `arm.__parts()`;
+- graph edges from every mate whose refs parse and whose parts exist;
+- stable roots from `arm.__physicalUseCases().flatMap(useCase => useCase.stableParts)`, plus explicit `palm-root`, `palm`, `base`, `root` fallback when present;
+- moving parts from all non-fastened mate endpoints;
+- non-fastened mate contract checks:
+ - connector exists;
+ - connector origin must be numeric `vec3`;
+ - revolute/cylindrical/pin_slot connector axes must be finite non-zero vectors;
+ - revolute/cylindrical/pin_slot require finite `limitsDeg`;
+ - prismatic requires finite `limitsMm`;
+ - revolute requires a `mechanicalJoint` intent whose `mate` equals the mate name.
+
+- [x] **Step 4: Verify green**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add src/modeling/mates/jointTopology.ts src/modeling/mates/jointTopology.test.ts
+git commit -m "feat: review joint topology"
+```
+
+---
+
+### Task 2: Wire Topology Into Review And Design Loop
+
+**Files:**
+- Modify: `src/agent/mcp/tools/reviewCad.ts`
+- Modify: `src/modeling/mates/mechanismFitness.ts`
+- Modify: `tests/integration/mcp/physicalUseCaseGate.test.ts`
+- Modify: `tests/integration/mcp/designLoop.test.ts`
+
+- [x] **Step 1: Write integration tests**
+
+Add tests proving:
+
+- `review_cad` returns `assembly.joint-topology.unsupported-axis` for a revolute mate with finite limits but no support intent.
+- `review_cad` returns `assembly.connectivity.floating-moving-part` for an articulated load part with no stable-root path.
+- `design_loop` includes topology diagnostics in `reviewFacts` and `nextActionPrompt`.
+
+- [x] **Step 2: Verify red**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts --reporter=dot
+```
+
+Expected: FAIL on missing topology diagnostics in review output.
+
+- [x] **Step 3: Wire reviewer**
+
+- Import `reviewJointTopology` and its diagnostic type in `reviewCad.ts`.
+- Add topology diagnostics to the review diagnostic list before physical-use-case reachability diagnostics.
+- Include topology diagnostics in `summarizeMechanismFitness(...)`.
+- Extend `MechanismFitnessResult` inputs to treat topology diagnostics as blocking reasons.
+- Ensure repair prompt generation includes topology diagnostics through the existing diagnostic list path.
+
+- [x] **Step 4: Verify green**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 5: Commit**
+
+```bash
+git add src/agent/mcp/tools/reviewCad.ts src/modeling/mates/mechanismFitness.ts tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts
+git commit -m "feat: gate reviews on joint topology"
+```
+
+---
+
+### Task 3: Five-Finger Hand Regression
+
+**Files:**
+- Modify: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [x] **Step 1: Write current-hand regression**
+
+Add a test that evaluates `examples/robot-hand/five-finger-kinematic-hand.kcad.ts`, extracts `front-facing-five-finger-robot-hand`, runs `reviewJointTopology(assembly)`, and asserts at least one blocking topology diagnostic is present.
+
+- [x] **Step 2: Verify red or diagnostic behavior**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts -t "topology" --reporter=dot
+```
+
+Expected: PASS if current hand has topology failures; FAIL if the current hand is topologically clean, in which case add a `review_cad` assertion that the hand still fails reachability and record that topology is not the current first blocker.
+
+- [x] **Step 3: Tighten only the gate, not geometry**
+
+Do not edit `examples/robot-hand/five-finger-kinematic-hand.kcad.ts` in this task. If the current hand passes topology, leave it passing topology and preserve reachability as the current blocker.
+
+- [x] **Step 4: Commit**
+
+```bash
+git add tests/integration/examples/fiveFingerKinematicHand.test.ts
+git commit -m "test: check hand topology gate"
+```
+
+---
+
+## Final Verification
+
+- [ ] Run topology unit tests:
+
+```bash
+npx vitest run src/modeling/mates/jointTopology.test.ts --reporter=dot
+```
+
+- [ ] Run review/design-loop integration:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/designLoopNextActionPrompt.test.ts --reporter=dot
+```
+
+- [ ] Run hand integration:
+
+```bash
+npx vitest run tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+- [ ] Run typecheck:
+
+```bash
+npm run typecheck
+```
+
+- [ ] Run final review.
diff --git a/docs/superpowers/plans/2026-07-11-joint-reaction-and-clevis-structure.md b/docs/superpowers/plans/2026-07-11-joint-reaction-and-clevis-structure.md
new file mode 100644
index 000000000..86bdd15d3
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-11-joint-reaction-and-clevis-structure.md
@@ -0,0 +1,607 @@
+# Joint Reaction and Clevis Structure Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
+
+**Goal:** Add pose-bound joint reaction, declared envelope, and geometry/material-derived clevis strength certificates to the physical-use-case gate.
+
+**Architecture:** Enrich the existing static contact certificate, derive reaction wrenches through uniquely rooted articulated trees, then evaluate those reactions in two separate layers: declared resultant envelopes and a narrow `joint.clevis` closed-form structural model. `review_cad` and `design_loop` orchestrate the layers and keep ambiguous or unsupported physics blocking.
+
+**Tech Stack:** TypeScript, Vitest, existing Assembly/mate solver, kernelCAD MCP tool schemas.
+
+---
+
+## File Map
+
+- `src/modeling/mates/physicalUseCaseStatics.ts`: make contact force point, sign, and mechanism ownership explicit.
+- `src/modeling/mates/physicalUseCaseJointReactions.ts`: exact-pose topology validation and subtree wrench propagation.
+- `src/modeling/mates/physicalUseCaseJointReactions.test.ts`: hand-calculated reaction solver tests.
+- `src/modeling/mates/mate.ts`: public capacity types and legacy adapter type.
+- `src/modeling/capture/assembly.ts`: capture-time capacity validation and preservation.
+- `src/modeling/mates/physicalUseCaseJointCapacity.ts`: declared envelope comparison.
+- `src/modeling/mates/physicalUseCaseJointCapacity.test.ts`: capacity capture, conversion, and threshold tests.
+- `src/modeling/joints/types.ts`: structural material/model types and clevis engineering options.
+- `src/modeling/joints/clevis.ts`: emit structural dimensions from resolved build geometry.
+- `src/modeling/joints/index.ts`: export new public structural types.
+- `src/modeling/mates/clevisJointStructure.ts`: pure closed-form clevis checks.
+- `src/modeling/mates/clevisJointStructure.test.ts`: equation, safety-factor, and unsupported-load tests.
+- `src/modeling/mates/physicalUseCase.ts`: orchestrate reaction/capacity/structure reviews and map diagnostics.
+- `src/agent/mcp/tools/reviewCad.ts`: request and return new certificates.
+- `src/agent/mcp/tools/designLoop.ts`: enable both checks for physical-use-case attempts.
+- `src/agent/mcp/toolRegistry.ts`: publish new input fields.
+- `src/agent/mcp/toolOutputSchemas.ts`: publish new output evidence.
+- Existing physical-use-case, MCP, bar-grasp, and five-finger tests: integration/regression coverage.
+
+### Task 1: Exact-Pose Joint Reaction Certificate
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCaseStatics.ts`
+- Create: `src/modeling/mates/physicalUseCaseJointReactions.ts`
+- Create: `src/modeling/mates/physicalUseCaseJointReactions.test.ts`
+- Modify: `src/modeling/mates/physicalUseCaseStatics.test.ts`
+
+- [x] **Step 1: Write failing tests for explicit static contact evidence**
+
+Update the passing static-certificate assertion to require the actual world
+contact point, mechanism part, and unambiguous held-object force:
+
+```ts
+expect(certificate.contactForces[0]).toMatchObject({
+ pointWorldMm: [50, 0, 0],
+ mechanismPart: 'finger',
+ forceOnHeldWorldN: expect.any(Array),
+});
+```
+
+Replace every existing test read of `contact.force` with
+`contact.forceOnHeldWorldN` so the ambiguous field can be removed.
+
+- [x] **Step 2: Run the static test and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseStatics.test.ts
+```
+
+Expected: FAIL because `pointWorldMm`, `mechanismPart`, and
+`forceOnHeldWorldN` do not exist.
+
+- [x] **Step 3: Add explicit contact fields to static evidence**
+
+Change the public evidence type and `evaluateCandidate()` construction:
+
+```ts
+export interface PhysicalUseCaseStaticContactForce {
+ readonly contactA: string;
+ readonly contactB: string;
+ readonly pointWorldMm: Vec3;
+ readonly mechanismPart: string;
+ readonly forceOnHeldWorldN: Vec3;
+ readonly normalForceN: number;
+ readonly tangentialForceN: number;
+ readonly normalCapacityN: number;
+ readonly friction: number;
+}
+```
+
+Use `safePartName(contact.mechanismRef)` and copy `contact.point`/the solved
+held force. Update internal consumers to read `forceOnHeldWorldN`; do not keep
+an ambiguous public `force` alias after test migration.
+
+- [x] **Step 4: Run the static test and verify GREEN**
+
+Run the command from Step 2. Expected: all existing statics tests pass with the
+new explicit evidence.
+
+- [x] **Step 5: Write failing reaction tests**
+
+Create fixtures around the public reaction review entry point:
+
+```ts
+const result = await reviewPhysicalUseCaseJointReactions(
+ arm,
+ useCase,
+ staticCertificate,
+);
+expect(result.issues).toEqual([]);
+expect(result.certificates[0].reactions).toEqual(expect.arrayContaining([
+ expect.objectContaining({ mateName: 'distal', resultantMomentNmm: 500 }),
+ expect.objectContaining({ mateName: 'proximal', resultantMomentNmm: 1500 }),
+]));
+```
+
+Add separate tests proving exact-pose moment-arm changes, branch-force vector
+cancellation, force negation exactly once, fastened-group collapse, rejection
+of an articulated loop, and rejection of two stable roots.
+
+- [x] **Step 6: Run the reaction test and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseJointReactions.test.ts
+```
+
+Expected: FAIL because the module and review function do not exist.
+
+- [x] **Step 7: Implement the minimal reaction solver**
+
+Export these contracts:
+
+```ts
+export interface PhysicalUseCaseJointReactionEvidence {
+ readonly mateName: string;
+ readonly parentPart: string;
+ readonly childPart: string;
+ readonly pointWorldMm: Vec3;
+ readonly axisWorld: Vec3;
+ readonly forceWorldN: Vec3;
+ readonly momentWorldNmm: Vec3;
+ readonly resultantForceN: number;
+ readonly resultantMomentNmm: number;
+ readonly axialForceN: number;
+ readonly radialForceN: number;
+ readonly axisMomentNmm: number;
+ readonly bendingMomentNmm: number;
+}
+
+export type PhysicalUseCaseJointReactionIssue =
+ | { readonly kind: 'joint-reaction-input-incomplete'; readonly useCaseName: string; readonly message: string }
+ | { readonly kind: 'joint-reaction-indeterminate'; readonly useCaseName: string; readonly message: string };
+
+export interface PhysicalUseCaseJointReactionCertificate {
+ readonly useCaseName: string;
+ readonly poses: NumericPoses;
+ readonly reactions: readonly PhysicalUseCaseJointReactionEvidence[];
+}
+```
+
+Use union-find to collapse fastened mates, reject any loaded articulated
+component that is not a single-root tree, orient it from the stable group, and
+accumulate subtree wrenches bottom-up. Resolve joint origins and axes from the
+exact solved transforms. Apply `forceOnMechanism = -forceOnHeldWorldN` once.
+
+- [x] **Step 8: Run reaction and statics tests and verify GREEN**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseJointReactions.test.ts src/modeling/mates/physicalUseCaseStatics.test.ts
+```
+
+Expected: both files pass.
+
+### Task 2: Unit-Bearing Mate Envelope
+
+**Files:**
+- Modify: `src/modeling/mates/mate.ts`
+- Modify: `src/modeling/capture/assembly.ts`
+- Create: `src/modeling/mates/physicalUseCaseJointCapacity.ts`
+- Create: `src/modeling/mates/physicalUseCaseJointCapacity.test.ts`
+- Modify: `src/modeling/mates/jointLoadCapacity.ts`
+
+- [x] **Step 1: Write failing public capture tests**
+
+Add tests that author and inspect:
+
+```ts
+arm.mate('hinge', 'base.axis', 'link.axis', 'revolute', {
+ capacity: {
+ envelope: {
+ maxResultantForceN: 120,
+ maxResultantMomentNmm: 800,
+ },
+ },
+});
+expect(arm.__mates()[0].capacity?.envelope).toEqual({
+ maxResultantForceN: 120,
+ maxResultantMomentNmm: 800,
+});
+```
+
+Also assert that zero, negative, NaN, or infinite limits throw; supplying both
+`capacity` and `maxLoad` throws; and legacy `{ force: 120, torque: 0.8 }`
+normalizes to 120 N and 800 Nmm for the new review.
+
+- [x] **Step 2: Run the capacity test and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseJointCapacity.test.ts
+```
+
+Expected: FAIL because `capacity` is not accepted or preserved.
+
+- [x] **Step 3: Add capture types and validation**
+
+Add:
+
+```ts
+export interface MateCapacityEnvelope {
+ readonly maxResultantForceN: number;
+ readonly maxResultantMomentNmm: number;
+}
+
+export interface MateCapacity {
+ readonly envelope?: MateCapacityEnvelope;
+}
+```
+
+Extend `MateRecord` and `Assembly.mate()` options with `capacity` and the legacy
+`maxLoad`. Validate positive finite envelope fields, reject `capacity` plus
+`maxLoad`, and copy nested objects so later caller mutation cannot alter
+captured evidence. Keep the old `maxLoad` record only for compatibility with
+the old external-load adapter. Task 3 extends `MateCapacity` with structural
+evidence after the clevis type exists.
+
+- [x] **Step 4: Write failing envelope comparison tests**
+
+Use hand-built `PhysicalUseCaseJointReactionEvidence` and assert:
+
+```ts
+expect(reviewJointReactionCapacity(mate, reaction)).toMatchObject({ status: 'pass' });
+expect(reviewJointReactionCapacity(overloadedMate, reaction)).toMatchObject({
+ status: 'exceeded',
+ forceExceeded: true,
+});
+expect(reviewJointReactionCapacity(mateWithoutCapacity, reaction)).toMatchObject({
+ status: 'undeclared',
+});
+```
+
+Cover exact threshold equality and one-time legacy Nm-to-Nmm conversion.
+
+- [x] **Step 5: Run the comparison test and verify RED**
+
+Run the command from Step 2. Expected: capture tests pass after Step 3 but the
+comparison tests fail because the function is missing.
+
+- [x] **Step 6: Implement the pure envelope comparison**
+
+Return unit-bearing evidence:
+
+```ts
+export interface JointReactionCapacityEvidence {
+ readonly mateName: string;
+ readonly status: 'pass' | 'exceeded' | 'undeclared';
+ readonly resultantForceN: number;
+ readonly resultantMomentNmm: number;
+ readonly maxResultantForceN?: number;
+ readonly maxResultantMomentNmm?: number;
+ readonly forceExceeded: boolean;
+ readonly momentExceeded: boolean;
+}
+```
+
+Normalize the legacy adapter in one helper. A partial legacy declaration is
+`undeclared`; it must never synthesize an infinite counterpart. Mark the old
+manual external-load checker as deprecated in its JSDoc and remove any message
+that says a load was exceeded when no comparison ran.
+
+- [x] **Step 7: Run capacity tests and verify GREEN**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseJointCapacity.test.ts src/modeling/mates/jointLoadCapacity.test.ts
+```
+
+Expected: both files pass.
+
+### Task 3: Geometry-Derived Clevis Strength
+
+**Files:**
+- Modify: `src/modeling/joints/types.ts`
+- Modify: `src/modeling/joints/clevis.ts`
+- Modify: `src/modeling/joints/index.ts`
+- Modify: `src/modeling/joints/clevis.test.ts`
+- Modify: `src/modeling/mates/mate.ts`
+- Modify: `src/modeling/capture/assembly.ts`
+- Create: `src/modeling/mates/clevisJointStructure.ts`
+- Create: `src/modeling/mates/clevisJointStructure.test.ts`
+
+- [x] **Step 1: Write failing clevis descriptor tests**
+
+Build a clevis with explicit style and engineering material:
+
+```ts
+const steel = {
+ name: 'test steel',
+ model: 'isotropic-ductile' as const,
+ yieldStrengthMPa: 250,
+ bearingStrengthMPa: 400,
+};
+const result = joint.clevis({
+ ...bodies,
+ axis: 'Y',
+ pivotParent: [0, 0, 0],
+ style: { pinR: 3, holeClearance: 0.2, plateT: 4, tongueY: 5, forkGapY: 6, knuckleR: 10 },
+ engineering: { pin: steel, fork: steel, tongue: steel },
+});
+expect(result.structural).toMatchObject({
+ kind: 'clevis-double-shear-v1',
+ pinDiameterMm: 6,
+ boreDiameterMm: 6.4,
+ forkPlateThicknessMm: 4,
+ tongueThicknessMm: 5,
+ forkGapMm: 6,
+ supportSpanMm: 10,
+ edgeDistanceMm: 10,
+});
+```
+
+Assert that invalid material strengths throw at construction and that omitting
+`engineering` still emits geometry with no materials.
+
+- [x] **Step 2: Run clevis tests and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/joints/clevis.test.ts
+```
+
+Expected: FAIL because engineering options and `structural` output do not
+exist.
+
+- [x] **Step 3: Add structural public types and clevis emission**
+
+Add the exact `StructuralMaterial` and `ClevisStructuralModel` contracts from
+the design spec, plus `engineering?: { pin; fork; tongue }` on
+`ClevisJointOptions` and `structural` on `ClevisJoint`. Validate every declared
+strength as positive finite. Build all structural dimensions only from the
+resolved `style` used by geometry.
+
+Extend `MateCapacity` with
+`readonly structure?: ClevisStructuralModel`, preserve a defensive copy in
+`Assembly.mate()`, and reject structural evidence on non-revolute mates.
+
+- [x] **Step 4: Run clevis tests and verify GREEN**
+
+Run the command from Step 2. Expected: all clevis tests pass.
+
+- [x] **Step 5: Write failing pure equation tests**
+
+Create a radial-load fixture and independently calculate expected values:
+
+```ts
+const result = reviewClevisJointStructure({
+ reaction,
+ model,
+ minSafetyFactor: 2,
+});
+expect(result.status).toBe('pass');
+expect(result.checks.pinDoubleShear.stressMPa).toBeCloseTo(
+ radialForceN / (2 * Math.PI * pinDiameterMm ** 2 / 4),
+ 10,
+);
+expect(result.checks.pinBending.stressMPa).toBeCloseTo(
+ 32 * (radialForceN * supportSpanMm / 4) / (Math.PI * pinDiameterMm ** 3),
+ 10,
+);
+```
+
+Add tests where only pin diameter flips pass to failed, only material strength
+flips pass to failed, invalid ligament is input-incomplete, missing materials
+is input-incomplete, axial force is unsupported, and perpendicular moment is
+unsupported. Verify the implicit shear allowable records the
+`yield/sqrt(3)` assumption.
+
+- [x] **Step 6: Run equation tests and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/clevisJointStructure.test.ts
+```
+
+Expected: FAIL because the structural review module does not exist.
+
+- [x] **Step 7: Implement the pure structural review**
+
+Implement the equations and statuses exactly as specified:
+
+```ts
+export type ClevisJointStructureStatus =
+ | 'pass'
+ | 'failed'
+ | 'input-incomplete'
+ | 'unsupported-load-case';
+
+export interface ClevisJointStructureReview {
+ readonly mateName: string;
+ readonly status: ClevisJointStructureStatus;
+ readonly minSafetyFactor: number;
+ readonly checks: Readonly>;
+ readonly assumptions: readonly string[];
+ readonly message?: string;
+}
+```
+
+Use N/mm2 = MPa directly. Reject `minSafetyFactor < 2`, non-positive geometry,
+axial force above 0.01 N, and perpendicular bending moment above 0.1 Nmm. Do
+not consume PBR material, density, or generic BREP measurements.
+
+- [x] **Step 8: Run clevis and equation tests and verify GREEN**
+
+Run:
+
+```bash
+npx vitest run src/modeling/joints/clevis.test.ts src/modeling/mates/clevisJointStructure.test.ts
+```
+
+Expected: both files pass.
+
+### Task 4: Physical Gate and MCP Integration
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCase.ts`
+- Modify: `src/modeling/mates/physicalUseCase.test.ts`
+- Modify: `src/agent/mcp/tools/reviewCad.ts`
+- Modify: `src/agent/mcp/tools/designLoop.ts`
+- Modify: `src/agent/mcp/toolRegistry.ts`
+- Modify: `src/agent/mcp/toolOutputSchemas.ts`
+- Modify: `tests/integration/mcp/physicalUseCaseGate.test.ts`
+- Modify: `tests/integration/mcp/designLoop.test.ts`
+- Modify: `tests/unit/mcp/reviewCadOutputSchema.test.ts`
+- Modify: `tests/integration/examples/functionFirstBarGraspSkeleton.test.ts`
+- Modify: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [x] **Step 1: Write failing orchestration tests**
+
+Add an in-memory complete clevis mechanism and request structure review:
+
+```ts
+const result = await reviewPhysicalUseCasesWithReachability(arm, {
+ includeReachability: true,
+ includeStatics: true,
+ includeJointReactions: true,
+ includeJointStructure: true,
+});
+expect(result.jointReactionCertificates).toHaveLength(1);
+expect(result.jointStructuralCertificates[0].joints[0]).toMatchObject({
+ envelope: { status: 'pass' },
+ structure: { status: 'pass' },
+});
+```
+
+Add diagnostic assertions for indeterminate topology, undeclared envelope,
+envelope exceeded, missing structural model/materials, unsupported axial or
+perpendicular moment, and insufficient safety factor. Verify
+`minJointSafetyFactor` defaults to 2 and values below 2 are rejected.
+
+- [x] **Step 2: Run physical-use-case tests and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCase.test.ts
+```
+
+Expected: FAIL because options, outputs, and diagnostics do not exist.
+
+- [x] **Step 3: Implement physical-use-case orchestration**
+
+Extend `PhysicalUseCaseCriteria` with `minJointSafetyFactor`, validating it as
+finite and at least 2. Extend options/results with:
+
+```ts
+readonly includeJointReactions?: boolean;
+readonly includeJointStructure?: boolean;
+readonly jointReactionCertificates: readonly PhysicalUseCaseJointReactionCertificate[];
+readonly jointStructuralCertificates: readonly PhysicalUseCaseJointStructuralCertificate[];
+```
+
+Define the aggregate certificate explicitly:
+
+```ts
+export interface PhysicalUseCaseJointStructuralCertificate {
+ readonly useCaseName: string;
+ readonly poses: NumericPoses;
+ readonly joints: readonly {
+ readonly mateName: string;
+ readonly envelope: JointReactionCapacityEvidence;
+ readonly structure?: ClevisJointStructureReview;
+ }[];
+}
+```
+
+`includeJointStructure` implies reactions; reactions imply statics. For every
+passing static certificate, run reactions, then envelope and structure reviews.
+Map every issue/status to the exact diagnostic codes in the design spec. Only
+emit a structural certificate when the review actually ran; preserve failed
+evidence alongside its blocking diagnostic.
+
+- [x] **Step 4: Run physical-use-case tests and verify GREEN**
+
+Run the command from Step 2. Expected: all tests pass.
+
+- [x] **Step 5: Write failing MCP/tool integration tests**
+
+Assert `review_cad` accepts:
+
+```ts
+{
+ includePhysicalUseCaseJointReactions: true,
+ includePhysicalUseCaseJointStructure: true,
+}
+```
+
+and returns `physicalUseCaseJointReactionCertificates` and
+`physicalUseCaseJointStructuralCertificates`. Assert that `design_loop`
+forwards both flags automatically for source containing `physicalUseCase(`.
+Update schema tests to require arrays with unit-bearing reaction and stress
+properties.
+
+- [x] **Step 6: Run MCP tests and verify RED**
+
+Run:
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/reviewCadOutputSchema.test.ts
+```
+
+Expected: FAIL because the registry, tools, and output schemas lack the fields.
+
+- [x] **Step 7: Wire review_cad, design_loop, registry, and schemas**
+
+Add the two booleans to the review input and registry. Forward them to the
+physical-use-case review, include both certificate arrays in success and
+failure output, and describe that structure implies reaction/statics. In
+`designLoop`, set both true beside existing reachability/statics flags whenever
+the attempt declares a physical use case. Extend JSON output schemas without
+loosening existing required fields.
+
+- [x] **Step 8: Make example expectations honest**
+
+Keep the direct bar test green only when structure review is not requested.
+Add a structure-enabled assertion requiring one of:
+
+```ts
+expect(codes).toContain(
+ 'assembly.physical-use-case.joint-structure-input-incomplete',
+);
+expect(codes).toContain(
+ 'assembly.physical-use-case.joint-structure-unsupported-load-case',
+);
+```
+
+Keep the five-finger test's reachability rejection unchanged; do not add a
+structural pass assertion for it.
+
+- [x] **Step 9: Run all focused tests**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseStatics.test.ts src/modeling/mates/physicalUseCaseJointReactions.test.ts src/modeling/mates/physicalUseCaseJointCapacity.test.ts src/modeling/joints/clevis.test.ts src/modeling/mates/clevisJointStructure.test.ts src/modeling/mates/physicalUseCase.test.ts tests/integration/mcp/physicalUseCaseGate.test.ts tests/integration/mcp/designLoop.test.ts tests/unit/mcp/reviewCadOutputSchema.test.ts tests/integration/examples/functionFirstBarGraspSkeleton.test.ts tests/integration/examples/fiveFingerKinematicHand.test.ts
+```
+
+Expected: all focused tests pass; the bar and five-finger rejection assertions
+remain explicit.
+
+- [x] **Step 10: Run static verification**
+
+Run:
+
+```bash
+npm run typecheck
+npm run lint
+```
+
+Expected: both commands exit zero. Existing generated TanStack route warnings
+may be reported by typecheck but are not failures.
+
+- [x] **Step 11: Inspect the final diff**
+
+Run:
+
+```bash
+git diff --check
+git status --short
+```
+
+Expected: no whitespace errors and only the intended current-session files
+plus the pre-existing dirty hand/tooling files. Do not revert or commit
+unrelated existing changes.
diff --git a/docs/superpowers/plans/2026-07-11-pose-bound-static-equilibrium.md b/docs/superpowers/plans/2026-07-11-pose-bound-static-equilibrium.md
new file mode 100644
index 000000000..48256bf6a
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-11-pose-bound-static-equilibrium.md
@@ -0,0 +1,280 @@
+# Pose-Bound Static Equilibrium Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Certify that one sampled common-contact pose can balance the declared held-object wrench inside contact friction/capacity and actuator torque limits.
+
+**Architecture:** Refactor targeted reachability to retain reusable solved pose witnesses. A new statics module converts each witness into a conservative eight-edge friction-pyramid feasibility problem, searches contact forces with projected gradient, independently verifies candidate certificates, and computes actuator torque through finite-difference relative-contact Jacobians with coupling expansion.
+
+**Tech Stack:** TypeScript, Vitest, KernelCAD mate solver and SE(3) transforms, pure-TypeScript numerical helpers.
+
+---
+
+### Task 1: Add explicit statics evidence fields
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCase.ts`
+- Modify: `src/modeling/mates/physicalUseCase.test.ts`
+
+- [ ] **Step 1: Write failing deep-copy and validation tests**
+
+Extend the record test with:
+
+```ts
+loads: [{ part: 'object', at: 'object.com', force: [0, 0, -4] }],
+contacts: [{
+ a: 'finger.tip',
+ b: 'object.contact',
+ normal: [0, -1, 0],
+ normalFrame: 'b',
+ friction: 0.6,
+ normalForceN: 8,
+}],
+criteria: {
+ maxSlipMm: 1,
+ maxForceResidualN: 0.01,
+ maxTorqueResidualNmm: 0.1,
+},
+```
+
+Add invalid-value cases for unsupported normal frames, non-positive residual tolerances, and attempts to loosen the numerical defaults.
+
+- [ ] **Step 2: Run RED**
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCase.test.ts --reporter=dot
+```
+
+Expected: FAIL because the fields are not represented or validated.
+
+- [ ] **Step 3: Implement schema and copy behavior**
+
+Add optional `load.at`, `contact.normalFrame: 'world' | 'a' | 'b'`, and the two positive residual criteria. Omitted normal frames remain semantically world-space without materializing a new property. Residual criteria may tighten but never exceed the built-in defaults.
+
+- [ ] **Step 4: Run GREEN**
+
+Run the Step 2 command. Expected: PASS.
+
+---
+
+### Task 2: Retain shared solved pose witnesses
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCaseReachability.ts`
+- Modify: `src/modeling/mates/physicalUseCaseReachability.test.ts`
+
+- [ ] **Step 1: Write a failing witness test**
+
+For the existing split-pose fixture, call a wished-for `assessPhysicalUseCaseReachability(...)`. Assert two solved witnesses with expanded poses and world endpoint distances while findings still contain the simultaneous-contact failure. For the valid common-pose fixture, assert `commonPoseSamples` contains the sample satisfying every contact.
+
+- [ ] **Step 2: Run RED**
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseReachability.test.ts --reporter=dot
+```
+
+Expected: FAIL because the assessment API does not exist.
+
+- [ ] **Step 3: Implement collection and compatibility wrapper**
+
+Add:
+
+```ts
+export interface PhysicalUseCaseSolvedContact {
+ readonly contactA: string;
+ readonly contactB: string;
+ readonly pointA: Vec3;
+ readonly pointB: Vec3;
+ readonly distanceMm: number;
+}
+
+export interface PhysicalUseCasePoseWitness {
+ readonly poses: NumericPoses;
+ readonly transforms: ReadonlyMap;
+ readonly contacts: readonly PhysicalUseCaseSolvedContact[];
+ readonly complete: boolean;
+ readonly maxDistanceMm?: number;
+}
+```
+
+`assessPhysicalUseCaseReachability(...)` builds samples once, solves each once, computes findings, and returns complete in-tolerance samples as `commonPoseSamples`. Keep `reviewPhysicalUseCaseReachability(...)` as a wrapper returning only findings.
+
+- [ ] **Step 4: Run GREEN**
+
+Run the Step 2 command. Expected: PASS with existing reachability behavior unchanged.
+
+---
+
+### Task 3: Add held-object wrench feasibility
+
+**Files:**
+- Create: `src/modeling/mates/physicalUseCaseStatics.ts`
+- Create: `src/modeling/mates/physicalUseCaseStatics.test.ts`
+
+- [ ] **Step 1: Write failing statics tests**
+
+Use real assemblies and pose witnesses for:
+
+```text
+1. common pose + missing load.at -> static-input-incomplete
+2. force balance possible but contact offset leaves an unbalanced moment -> static-equilibrium-unmet
+3. two symmetric contacts balance a vertical held-object load -> certificate
+4. normalFrame on a rotated endpoint resolves to the expected world normal -> certificate
+```
+
+The symmetric fixture has one held part, contacts at x=-10/+10 mm, normal caps 8 N, friction 0.5, and a 6 N downward load at its center.
+
+- [ ] **Step 2: Run RED**
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseStatics.test.ts --reporter=dot
+```
+
+Expected: FAIL because the module does not exist.
+
+- [ ] **Step 3: Implement input resolution**
+
+Require exactly one held load part, load application connectors on that part, one held endpoint per contact, numeric connector origins, finite transforms, `normalForceN`, supported normal frames, and finite positive tolerances. Unsupported evidence returns input-incomplete.
+
+- [ ] **Step 4: Implement friction search**
+
+Construct eight conservative generators per contact. Assemble six held-part wrench rows about the first load point. Minimize normalized residual while projecting each contact group onto:
+
+```text
+lambda[k] >= 0
+sum(lambda[k]) <= normalForceN
+```
+
+Use deterministic ordering, a bounded iteration count, and a step bounded by the matrix Frobenius norm.
+
+- [ ] **Step 5: Independently verify candidates**
+
+Reconstruct forces and verify the true circular friction inequality, normal cap, force residual, and moment residual. Only verified data becomes a `PhysicalUseCaseStaticCertificate`; otherwise emit static-equilibrium-unmet with best residual evidence.
+
+- [ ] **Step 6: Run GREEN**
+
+Run the Step 2 command. Expected: PASS.
+
+---
+
+### Task 4: Add finite-difference actuator torque constraints
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCaseStatics.ts`
+- Modify: `src/modeling/mates/physicalUseCaseStatics.test.ts`
+
+- [ ] **Step 1: Write failing low/high torque tests**
+
+Create a one-axis finger contacting a held target at a 10 mm lever arm. The held-object equilibrium requires 1 N. Assert a 5 Nmm actuator limit fails with `static-actuator-torque-insufficient`, while 20 Nmm produces a certificate.
+
+- [ ] **Step 2: Run RED**
+
+Run the Task 3 test command. Expected: low-torque fixture incorrectly passes or lacks the actuator diagnostic.
+
+- [ ] **Step 3: Implement relative-contact Jacobians**
+
+For each revolute actuator:
+
+1. Perturb the source pose by `1e-4 rad` in degrees.
+2. Stay inside limits using central or inward one-sided differences.
+3. Re-expand couplings and solve.
+4. Differentiate mechanism-endpoint minus held-endpoint world displacement.
+5. Compute ideal generalized torque with `J^T f`.
+
+Missing limits, unsupported mates, failed perturbations, independent contact-path mates without actuator limits, coupled mates without transmission intent, or contradictory coupling/transmission ratios return input-incomplete.
+
+- [ ] **Step 4: Add actuator constraints and post-check**
+
+Run a contact-only search first. If it certifies equilibrium, rerun with squared actuator-limit violation penalties. Independently verify every absolute torque limit before issuing a certificate.
+
+- [ ] **Step 5: Run GREEN**
+
+Run the Task 3 test command. Expected: both low/high torque fixtures pass their assertions.
+
+---
+
+### Task 5: Wire diagnostics, review tooling, and design loop
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCase.ts`
+- Modify: `src/agent/mcp/tools/reviewCad.ts`
+- Modify: `src/agent/mcp/toolRegistry.ts`
+- Modify: `src/agent/mcp/tools/designLoop.ts`
+- Modify: `tests/integration/mcp/physicalUseCaseGate.test.ts`
+- Modify: `tests/integration/mcp/designLoop.test.ts`
+
+- [ ] **Step 1: Write failing integration tests**
+
+Add input-incomplete, unbalanced-wrench, low-torque, and high-torque cases using `includePhysicalUseCaseStatics: true`. Assert diagnostics, fitness blockers, repair prompts, and certificate evidence.
+
+- [ ] **Step 2: Run RED**
+
+```bash
+npx vitest run tests/integration/mcp/physicalUseCaseGate.test.ts -t "static equilibrium|static actuator" --reporter=dot
+```
+
+Expected: FAIL because the option and diagnostics are not wired.
+
+- [ ] **Step 3: Extend physical-use-case review**
+
+Add typed diagnostics:
+
+```text
+assembly.physical-use-case.static-input-incomplete
+assembly.physical-use-case.static-equilibrium-unmet
+assembly.physical-use-case.static-actuator-torque-insufficient
+```
+
+When reachability has no findings, pass its exact common-pose samples to statics and return successful certificates.
+
+- [ ] **Step 4: Wire review_cad and registry**
+
+Add `includePhysicalUseCaseStatics?: boolean` and expose certificates on output. Document sampled, linearized, opt-in static certification.
+
+- [ ] **Step 5: Preserve failures in design_loop**
+
+Physical-acceptance attempts enable statics. Preserve all statics codes in review facts and repair prompts. Add a focused regression.
+
+- [ ] **Step 6: Run GREEN**
+
+Run Task 5 focused coverage and the full `designLoop.test.ts`. Expected: PASS.
+
+---
+
+### Task 6: Exercise the function-first hand and verify
+
+**Files:**
+- Modify: `examples/robot-hand/function-first-bar-grasp-skeleton.kcad.ts`
+- Modify: `tests/integration/examples/functionFirstBarGraspSkeleton.test.ts`
+- Test: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [ ] **Step 1: Add explicit bar load evidence**
+
+Add `target-bar.load-point` at the bar center, set the load's `at`, request statics in the regression, and assert a certificate for `bar-grasp`.
+
+- [ ] **Step 2: Run the bar regression**
+
+```bash
+npx vitest run tests/integration/examples/functionFirstBarGraspSkeleton.test.ts --reporter=dot
+```
+
+If it fails equilibrium, retain the failure and inspect the contact layout/forces. Do not change tolerances or capacities solely to obtain green.
+
+- [ ] **Step 3: Run hand regressions**
+
+```bash
+npx vitest run tests/integration/examples/functionFirstBarGraspSkeleton.test.ts tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+Expected: the bar skeleton provides a certificate or exposes a concrete blocker; the old five-finger hand remains rejected.
+
+- [ ] **Step 4: Run focused and static verification**
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseReachability.test.ts src/modeling/mates/physicalUseCaseStatics.test.ts src/modeling/mates/physicalUseCase.test.ts tests/integration/mcp/designLoop.test.ts --reporter=dot
+npm run typecheck
+npx eslint src/modeling/mates/physicalUseCaseReachability.ts src/modeling/mates/physicalUseCaseStatics.ts src/modeling/mates/physicalUseCaseStatics.test.ts src/modeling/mates/physicalUseCase.ts src/agent/mcp/tools/reviewCad.ts src/agent/mcp/tools/designLoop.ts src/agent/mcp/toolRegistry.ts
+git diff --check
+```
+
+Expected: all commands exit 0. Existing route-generator warnings may remain during typecheck, but no TypeScript errors are allowed.
diff --git a/docs/superpowers/plans/2026-07-11-simultaneous-grasp-reachability.md b/docs/superpowers/plans/2026-07-11-simultaneous-grasp-reachability.md
new file mode 100644
index 000000000..e94d9e975
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-11-simultaneous-grasp-reachability.md
@@ -0,0 +1,132 @@
+# Simultaneous Grasp Reachability Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Reject a multi-contact physical use case unless one targeted actuator sample satisfies every declared contact at once.
+
+**Architecture:** Extend the existing reachability issue type with a use-case-level simultaneous-contact variant. Evaluate a complete contact-distance vector for each solved pose, preserve existing per-contact minima, and emit the simultaneous variant only when all contacts are individually reachable but no common pose passes.
+
+**Tech Stack:** TypeScript, Vitest, KernelCAD assembly mate solver.
+
+---
+
+### Task 1: Add the common-pose reachability gate
+
+**Files:**
+- Modify: `src/modeling/mates/physicalUseCaseReachability.test.ts`
+- Modify: `src/modeling/mates/physicalUseCaseReachability.ts`
+- Modify: `src/modeling/mates/physicalUseCase.ts`
+- Modify: `src/agent/mcp/tools/designLoop.ts`
+- Modify: `src/agent/mcp/toolRegistry.ts`
+- Test: `tests/integration/mcp/designLoop.test.ts`
+- Test: `tests/integration/examples/functionFirstBarGraspSkeleton.test.ts`
+- Test: `tests/integration/examples/fiveFingerKinematicHand.test.ts`
+
+- [x] **Step 1: Write the failing counterexample test**
+
+Add a base with a revolute `yaw` mate, two colocated moving contact connectors at `[10, 0, 0]`, and fixed targets at `[10, 0, 0]` and `[0, 10, 0]`. Sample `[0, 90]` degrees. Assert that `reviewPhysicalUseCaseReachability(...)` returns one issue with:
+
+```ts
+{
+ kind: 'simultaneous-contacts-unreachable',
+ useCaseName: 'split-pose-grasp',
+ toleranceMm: 0.1,
+ bestMaxDistanceMm: expect.any(Number),
+ contactDistances: expect.arrayContaining([
+ expect.objectContaining({ contactA: 'finger.a', contactB: 'base.target-a' }),
+ expect.objectContaining({ contactA: 'finger.b', contactB: 'base.target-b' }),
+ ]),
+}
+```
+
+- [x] **Step 2: Run the new test and verify RED**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseReachability.test.ts -t "rejects contacts that are reachable only at different actuator poses" --reporter=dot
+```
+
+Expected: FAIL because the current implementation returns `[]` after independently minimizing both contacts.
+
+- [x] **Step 3: Add a discriminated simultaneous-contact issue**
+
+In `physicalUseCaseReachability.ts`, preserve the current contact issue fields and add:
+
+```ts
+export interface PhysicalUseCaseSimultaneousContactsReachabilityIssue {
+ readonly kind: 'simultaneous-contacts-unreachable';
+ readonly useCaseName: string;
+ readonly toleranceMm: number;
+ readonly bestMaxDistanceMm?: number;
+ readonly contactDistances: readonly {
+ readonly contactA: string;
+ readonly contactB: string;
+ readonly distanceMm?: number;
+ }[];
+}
+```
+
+Preserve the existing exported `PhysicalUseCaseReachabilityIssue` interface and introduce `PhysicalUseCaseReachabilityFinding` as the union of that interface and the new shape.
+
+- [x] **Step 4: Implement common-pose evaluation**
+
+For each solved sample, calculate every contact distance in one array. Update independent minima exactly as today. Only complete arrays are common-pose candidates. Track the candidate with the smallest maximum contact distance and whether any complete candidate has every distance `<= toleranceMm`.
+
+After sampling:
+
+```ts
+const contactIssues = /* existing unreachable/uncheckable contacts */;
+if (contactIssues.length > 0 || useCase.contacts.length < 2 || hasPassingCommonPose) {
+ return contactIssues;
+}
+return [{
+ kind: 'simultaneous-contacts-unreachable',
+ useCaseName: useCase.name,
+ toleranceMm,
+ ...(bestCommonPose === undefined ? {} : { bestMaxDistanceMm: bestCommonPose.maxDistanceMm }),
+ contactDistances: useCase.contacts.map(/* distances from best common pose when available */),
+}];
+```
+
+- [x] **Step 5: Verify the focused test is GREEN**
+
+Run the Step 2 command. Expected: PASS.
+
+- [x] **Step 6: Wire the blocking diagnostic**
+
+Add `PhysicalUseCaseSimultaneousContactsUnreachableDiagnostic` to the `PhysicalUseCaseDiagnostic` union in `physicalUseCase.ts` with code `assembly.physical-use-case.simultaneous-contacts-unreachable`. In `reviewPhysicalUseCasesWithReachability(...)`, discriminate the new issue and emit a use-case-level error. Keep the existing per-contact mapping unchanged.
+
+- [x] **Step 7: Add diagnostic and design-loop integration coverage**
+
+Extend an adjacent `physicalUseCase` test to call `reviewPhysicalUseCasesWithReachability(...)` and assert the new code, use-case name, tolerance, and contact-distance evidence are surfaced as a blocking error. Add a `designLoopTool(...)` regression that requires the code and repair hint in `reviewFacts` and `nextActionPrompt`. Update the `review_cad` tool description to state that all contacts must pass in the same sampled actuator pose.
+
+- [x] **Step 8: Run focused reachability coverage**
+
+Run:
+
+```bash
+npx vitest run src/modeling/mates/physicalUseCaseReachability.test.ts src/modeling/mates/physicalUseCase.test.ts --reporter=dot
+```
+
+Expected: PASS.
+
+- [x] **Step 9: Run hand regressions**
+
+Run:
+
+```bash
+npx vitest run tests/integration/examples/functionFirstBarGraspSkeleton.test.ts tests/integration/examples/fiveFingerKinematicHand.test.ts --reporter=dot
+```
+
+Expected: the bar-grasp skeleton passes and the five-finger regression remains intentionally rejected by its asserted diagnostics.
+
+- [x] **Step 10: Check types and whitespace**
+
+Run the repository TypeScript check if defined in `package.json`, then run:
+
+```bash
+git diff --check
+```
+
+Expected: exit 0.
diff --git a/docs/superpowers/specs/2026-07-08-function-first-robot-hand-design.md b/docs/superpowers/specs/2026-07-08-function-first-robot-hand-design.md
new file mode 100644
index 000000000..1e70c53d7
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-08-function-first-robot-hand-design.md
@@ -0,0 +1,134 @@
+# Function-First Robot Hand Design
+
+## Purpose
+
+Build robot hands from grasp requirements instead of visual appearance. A model
+is accepted only when it can satisfy explicit object-holding tasks with a
+physically connected mechanism. Visual references and mesh fitting are secondary
+inputs used after functional gates pass.
+
+## Problem
+
+The previous hand attempts started from a front-facing hand image or from visual
+workflow variants. That produced repeated failures:
+
+- parts looked disconnected or unsupported;
+- fingers were placed to satisfy a silhouette instead of contact geometry;
+- visual rods and panels did not prove load paths;
+- mesh/reference fitting could not recover hidden joints, bearings, pins, or
+ actuation anchors.
+
+The correct starting point is: what must the hand hold, where must it contact,
+and what loads must travel through the mechanism?
+
+## Core Rule
+
+Function before form.
+
+For a robot hand, tests are the design brief. The CAD model is generated from
+grasp tasks, contact targets, joint limits, and load paths. A hand that looks
+good but cannot satisfy the grasp tests is rejected.
+
+## Required Grasp Tasks
+
+The initial task set lives in `scripts/robotHandFunctionalRequirements.ts`.
+
+1. **Pinch thin plate**
+ Hold a 2-5 mm plate or card edge between thumb and fingertip.
+
+2. **Power grasp cylinder**
+ Wrap around a 30-55 mm cylinder such as a bottle neck, tool handle, or pipe.
+
+3. **Spherical grasp**
+ Enclose a 35-65 mm sphere with at least three useful contact regions.
+
+4. **Box grasp**
+ Hold a rectangular block using opposed faces, not fingertip-only cheating.
+
+5. **Hook or handle pull**
+ Curl around a handle or ring section and carry pull load through the palm.
+
+6. **Wide object aperture**
+ Open far enough to accept an object wider than the relaxed palm contact span.
+
+## Acceptance Gates
+
+Each candidate hand must prove:
+
+- target contact points are reachable;
+- contact normals oppose object escape;
+- joint limits are respected;
+- pose envelope has no breaking self-collisions;
+- object clearance exists in open and closed poses;
+- loaded parts are in the mate graph;
+- joint axes pass through supported material;
+- actuation/transmission has anchored load paths;
+- visual reference styling is applied only after functional gates pass.
+
+## Build Sequence
+
+```text
+grasp tasks
+ -> object envelopes and contact targets
+ -> joint skeleton and thumb placement
+ -> finger count and phalanx lengths
+ -> palm and bearing support geometry
+ -> actuator/transmission anchors
+ -> pose/load/interference validation
+ -> visual styling and reference fit
+```
+
+## First Artifact
+
+The first real artifact should be a three-finger functional hand, not a
+five-finger visual hand.
+
+Minimum structure:
+
+- one opposed thumb;
+- two fingers that can cooperate or oppose the thumb;
+- palm with physically supported hinge blocks;
+- named parts and mates for every loaded body;
+- fingertip/contact connectors;
+- declared grasp targets for plate, cylinder, sphere, box, handle, and wide
+ object aperture.
+
+Why three fingers first:
+
+- covers the grasp test set with fewer joints;
+- exposes disconnected parts faster;
+- makes reachability and load-path failures easier to debug;
+- avoids wasting effort on decorative non-contact fingers.
+
+## Relation To Meshes And References
+
+Meshes and images remain useful, but only after function is defined:
+
+- reference images provide styling and rough proportions;
+- meshes can suggest object envelopes or visible surfaces;
+- neither source is allowed to satisfy a mechanical gate by itself.
+
+If mesh fitting cannot produce joint centers, supported axes, contact targets,
+and load paths, it is evidence only, not a design.
+
+## Tooling Implications
+
+KernelCAD should support a function-first hand workflow:
+
+- declare grasp tasks and target objects;
+- generate contact targets and workspace requirements;
+- synthesize a skeleton from those targets;
+- generate physical joints and transmission anchors;
+- run pose-envelope, aperture, collision, and load-path reviews;
+- only then apply visual/reference fitting.
+
+The current two-finger aperture review and workspace tools are the nearest
+existing primitives. The next implementation should extend them from gripper
+aperture into multi-contact grasp tests.
+
+## Success Criteria
+
+- `scripts/robotHandFunctionalRequirements.ts` defines the required grasp tasks.
+- tests enforce that hand requirements start from tasks, contacts, and gates.
+- future hand examples can be rejected before visual review if they do not pass
+ the functional grasp brief.
diff --git a/docs/superpowers/specs/2026-07-08-generic-physical-plausibility-gate-design.md b/docs/superpowers/specs/2026-07-08-generic-physical-plausibility-gate-design.md
new file mode 100644
index 000000000..a1104569e
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-08-generic-physical-plausibility-gate-design.md
@@ -0,0 +1,59 @@
+# Generic Physical Plausibility Gate — High-Level Design
+
+## Problem
+
+KernelCAD can currently reject many bad mechanisms: floating parts, disconnected bodies, unsupported joints, pose-envelope collisions, and some MuJoCo gravity/drop failures. That still allows designs that are visually connected and kinematically moving but have no declared task physics: no load, no contact contract, no actuator limit, and no stability criterion.
+
+The goal is a generic gate that asks: "what physical situation is this assembly supposed to survive or perform?" The gate must not be hand-specific. A gripper, hinge, latch, arm, bracket, watch lug, or lamp should all use the same evidence pattern.
+
+## Approach
+
+Add an assembly-level declaration:
+
+```ts
+arm.physicalUseCase('hold-cylinder', {
+ stableParts: ['target-cylinder'],
+ loads: [{ part: 'target-cylinder', force: [0, 0, -8] }],
+ contacts: [
+ { a: 'thumb-finger.tip', b: 'target-cylinder.mount', normal: [-1, 0, 0], friction: 0.6 },
+ ],
+ actuatorLimits: [{ mate: 'grip', maxTorqueNmm: 180 }],
+ criteria: { maxSlipMm: 2, settleTimeMs: 500 },
+});
+```
+
+First slice is structural, not full force simulation. It checks that the assembly declares enough physics evidence to be reviewable:
+
+- loads reference real parts and have non-zero force or torque
+- contacts reference real connectors, have normals, and positive friction
+- actuator limits reference real driven mates and positive torque
+- stable parts reference real parts
+- mechanisms can be reviewed with `requirePhysicalUseCase: true`
+
+Later slices can consume the same record in deterministic statics and MuJoCo dynamic tasks.
+
+## Alternatives
+
+| Option | Description | Tradeoff |
+|---|---|---|
+| A recommended | Add generic `physicalUseCase(...)` records and opt-in review gate first. | Small, testable, does not break the existing corpus immediately. |
+| B | Immediately make every mechanism fail without a physical use case. | Stronger product stance, but will break many current examples before the contract is mature. |
+| C | Add hand/grasp-specific simulation first. | Faster for the robot hand, but repeats the mistake of building special fixtures instead of reusable tooling. |
+
+## First Slice Acceptance
+
+1. `review_cad({ requirePhysicalUseCase: true })` fails a mechanism with no physical use case.
+2. A malformed use case fails with actionable diagnostics.
+3. A minimally declared generic use case passes the use-case gate.
+4. Existing reviews are unchanged unless `requirePhysicalUseCase` is set.
+
+## Later Slices
+
+1. Quasi-static load path and torque margin checks.
+2. Contact friction/slip margin checks.
+3. MuJoCo task runner: settle, gravity, impulse, disturbance, held-object pose drift.
+4. Material/stiffness budget checks for bending and bearing pressure.
+
+## Concern
+
+This first slice proves declaration quality, not real physics. That is intentional: a reusable record must exist before statics or dynamics can consume it.
diff --git a/docs/superpowers/specs/2026-07-08-mesh-conditioned-robot-hand-design.md b/docs/superpowers/specs/2026-07-08-mesh-conditioned-robot-hand-design.md
new file mode 100644
index 000000000..3a7180cdf
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-08-mesh-conditioned-robot-hand-design.md
@@ -0,0 +1,103 @@
+# Mesh-Conditioned Robot Hand Prototype Design
+
+## Purpose
+
+Build a narrow prototype that keeps the original robot hand reference in the
+modeling loop without treating it as a complete physical design. The reference
+drives visible fit. A hand-specific mechanical completion template adds the
+hidden structure needed for joints, load paths, clearances, and validation.
+
+## Problem
+
+The current robot hand workflow fails in two ways:
+
+- Manual primitive edits drift away from the original reference render.
+- Reference-derived shape alone is physically incomplete and cannot prove a
+ working mechanism.
+
+The prototype must avoid both failures. It should not become a generic mesh
+importer yet.
+
+## Scope
+
+In scope:
+
+- Add a structured `referenceLandmarks` block to the five-finger robot hand
+ example.
+- Generate visible hand proportions from those landmarks: palm, wrist, finger
+ roots, finger lengths, thumb angle, actuator windows, tendon rods, and screws.
+- Generate missing physical structure from a hand mechanism template: clevises,
+ pins, connector axes, revolute mates, load limits, clearances, and external
+ load checks.
+- Add integration tests that make the reference-conditioned workflow explicit
+ and reject render-budget loose-body patterns.
+
+Out of scope:
+
+- Real GLB/STL mesh import.
+- Automatic mesh segmentation.
+- Generic `fit.box()` / `fit.hinge()` APIs.
+- Full visual similarity scoring.
+
+## Architecture
+
+The example script owns two layers:
+
+1. `referenceLandmarks`: an evidence layer with visible dimensions and feature
+ positions from the original robot hand render.
+2. Mechanical completion functions: deterministic generators that convert
+ landmarks into an articulated, validated KernelCAD assembly.
+
+The assembly remains the source of truth. The reference layer is kept beside
+the parameters so future agents cannot silently replace it with guessed
+coordinates.
+
+## Data Model
+
+`referenceLandmarks` contains:
+
+- `palm`: width, depth, height, center, and shoulder geometry.
+- `wrist`: block dimensions, position, and tendon anchor row.
+- `actuatorWindows`: visible black inserts on the palm face.
+- `screws`: visible screw-head positions.
+- `tendons`: visible rod paths.
+- `fingers`: one entry per finger with root x/z, phalanx lengths, width, visual
+ angle, curl limits, and load limits.
+
+This is intentionally plain JavaScript data inside the `.kcad.ts` example so it
+is easy to inspect and mutate without adding public API surface.
+
+## Mechanical Completion
+
+The generator must add what the reference cannot prove:
+
+- clevis fork/tongue geometry at MCP, PIP, and DIP joints;
+- pin caps and hole clearances;
+- connector frames on both sides of each joint;
+- revolute mates with limits;
+- load limits and external loads;
+- unioned non-articulated visible details on the palm root so they are not
+ separate floating bodies.
+
+## Acceptance Criteria
+
+- The example evaluates at open pose and closed pose with no error diagnostics.
+- The script has a top-level `referenceLandmarks` object.
+- All five fingers are generated from `referenceLandmarks.fingers`.
+- The thumb angle and root position come from the reference landmark data.
+- No render-budget loose-body patterns remain: no `parts.push`, no `return parts`,
+ no `addPart(`.
+- The assembly exposes exactly one palm root plus three parts per finger.
+- The assembly exposes fifteen revolute mates.
+- Visible palm details are unioned into the palm root or articulated parts, not
+ left as separate loose solids.
+
+## Non-Goals And Risks
+
+This prototype does not prove that arbitrary meshes can become parametric CAD.
+It proves a smaller workflow: reference evidence can stay in the source while a
+mechanical template completes missing physical structure.
+
+The main risk is pretending the landmark object is automatic mesh fitting. It is
+not. It is a hand-authored evidence layer that should later be produced by
+mesh/image landmark extraction.
diff --git a/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-gate-design.md b/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-gate-design.md
new file mode 100644
index 000000000..303e9d757
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-gate-design.md
@@ -0,0 +1,93 @@
+# Five-Finger Hand Topology Gate Design
+
+## Problem
+
+The current robot hand can look like a hand while still being mechanically invalid. The next slice must stop that failure mode before geometry iteration continues.
+
+The target remains a five-finger anthropomorphic hand, but the immediate deliverable is not a prettier hand. The deliverable is a deterministic gate that rejects a hand whose fingers, joints, or moving links are not structurally connected and mechanically meaningful.
+
+## First-Principles Check
+
+A robot hand is a load-bearing articulated mechanism, not a visual arrangement of finger-like solids.
+
+The minimum physical truths for this slice are:
+
+- Every moving finger link must transfer load back to the palm/root through mates or declared structural links.
+- Every non-fastened joint must connect two real parts through real connector references.
+- A revolute finger joint without a supported axis or finite travel limit is not a joint.
+- A moving segment that is disconnected, only decorative, or connected through impossible geometry is not part of a working hand.
+
+This gate belongs before reachability, collision sweep, and load simulation. Kinematic and dynamic checks are only meaningful after the mechanism graph is structurally coherent.
+
+## Architecture
+
+Add a deterministic topology review layer under the existing CAD review path:
+
+1. Build a part graph from assembly parts, mates, and declared structural relationships.
+2. Identify stable/root parts from physical-use-case declarations, falling back to explicit palm/root naming only in tests or fixtures that intentionally model a hand.
+3. Classify moving parts as any part participating in a non-fastened mate.
+4. For every moving part, verify a load path to a stable/root part.
+5. For every non-fastened mate, verify a joint contract:
+ - both parts exist,
+ - both connector refs resolve,
+ - connector origins and axes are finite,
+ - revolute-like joints have finite travel limits,
+ - revolute-like joints have support evidence through an existing supported-joint intent or equivalent declared support metadata.
+6. Return structured diagnostics through `review_cad` and `design_loop`.
+
+The review should be conservative. If a mechanism omits support metadata, the gate should fail with a repair hint instead of guessing that visual contact is sufficient.
+
+## Diagnostics
+
+Initial diagnostic codes:
+
+| Code | Meaning |
+| --- | --- |
+| `assembly.connectivity.floating-moving-part` | A moving part has no load path back to a stable/root part. |
+| `assembly.connectivity.no-load-path` | A declared load-bearing part cannot transfer force to any stable/root part. |
+| `assembly.joint-topology.connector-missing` | A mate references a missing part connector. |
+| `assembly.joint-topology.axis-invalid` | A joint connector axis is missing, zero-length, non-finite, or inconsistent. |
+| `assembly.joint-topology.missing-limit` | A moving joint lacks finite travel limits. |
+| `assembly.joint-topology.unsupported-axis` | A revolute-like joint has no support/bearing intent. |
+
+Diagnostics must include the part or mate name, a plain-language message, and a repair hint that tells the agent what structural evidence to add.
+
+## Integration
+
+- `review_cad` should include topology diagnostics in the same blocking diagnostic stream as physical-use-case reachability.
+- `design_loop` should preserve these diagnostics in `reviewFacts` and `nextActionPrompt`.
+- The current five-finger hand fixture should fail this gate if it has disconnected links, fake joints, missing limits, or unsupported axes.
+- A small clean fixture should pass: palm, two or three articulated links, supported revolute joints, finite limits, and a declared stable root/load path.
+
+## Test Strategy
+
+Write tests before implementation:
+
+1. Unit tests for graph reachability over small synthetic assemblies.
+2. Unit tests for joint-contract diagnostics on missing connectors, missing limits, invalid axes, and unsupported revolute joints.
+3. Integration test proving `review_cad` emits topology diagnostics.
+4. Design-loop test proving topology failures are carried into the next action prompt.
+5. Current five-finger hand regression proving the hand is rejected by topology/connectivity before any geometry redesign is accepted.
+
+## Accepted Limitations
+
+- This gate does not prove the hand can grasp an object.
+- This gate does not prove collision-free motion.
+- This gate does not compute torque or structural stress.
+- This gate may reject a visually plausible joint until the model declares support metadata. That is intentional; hidden assumptions are not physical evidence.
+
+## Alternatives Considered
+
+| Option | Description | Decision |
+| --- | --- | --- |
+| Graph + joint-contract gate | Check load paths, connector validity, limits, and support metadata before kinematics. | Recommended because it tests the minimum physical structure. |
+| Geometry contact-only gate | Check touching/floating solids using mesh contact. | Rejected as insufficient; visual contact can still hide fake hinges and unsupported axes. |
+| Dynamics simulation first | Use physics simulation before topology checks. | Rejected for this slice; simulation on an incoherent mechanism produces misleading failures. |
+
+## Decisions
+
+| # | Decision | Rationale |
+| --- | --- | --- |
+| 1 | Build topology/connectivity before more hand geometry work. | The current failure is architectural: the model can be visually hand-like while mechanically disconnected. |
+| 2 | Treat missing joint support metadata as blocking. | A revolute axis without support evidence is a fantasy joint. |
+| 3 | Preserve first-principles checks in the spec and future plan. | This keeps the agent from optimizing for appearance before physical coherence. |
diff --git a/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-repair-design.md b/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-repair-design.md
new file mode 100644
index 000000000..655052ce5
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-10-five-finger-hand-topology-repair-design.md
@@ -0,0 +1,63 @@
+# Five-Finger Hand Topology Repair Design
+
+## Problem
+
+The five-finger hand now fails the topology gate for the right reason: it is not mechanically complete. The current blockers are unsupported revolute axes on finger joints and a grasp-cylinder load with no structural path to the palm/root.
+
+The next pass must make the current hand satisfy the topology gate without hiding the failure behind visual edits or fake drive declarations.
+
+## First-Principles Check
+
+A finger joint can be driven or passive, but it cannot be unsupported. For a revolute joint to be physically meaningful, the support side needs bearing or bracket evidence tied into the mate endpoint that carries the shaft. A passive PIP/DIP hinge should not have to pretend it has its own actuator; it should declare support as support.
+
+A grasp object is different from a structural hand part. It is useful as a contact/load target, but the topology graph should not require the object itself to be mated into the hand assembly unless the scenario is explicitly modeling a grasped object as a held payload with structural contact closure.
+
+## Architecture
+
+Add a small passive joint-support intent alongside the existing driven `mechanicalJoint(...)` intent.
+
+1. Capture `assembly.jointSupport(name, opts)` records with:
+ - `mate`: revolute mate being supported,
+ - `shaft`: support-side part or shaft part,
+ - `supports`: one or more support-side parts,
+ - `output`: moving side of the mate,
+ - optional `requiredSupport` metadata matching the existing support contract shape.
+2. Extend `reviewJointTopology(...)` so a revolute mate is supported by either:
+ - a complete driven `mechanicalJoint(...)`, or
+ - a complete passive `jointSupport(...)`.
+3. Apply the new support intent to the five-finger hand:
+ - all PIP/DIP hinges,
+ - little/ring MCP hinges,
+ - any remaining revolute not already backed by existing MCP drive support.
+4. Treat the grasp cylinder as a non-structural contact target for topology, not a moving hand link with a required mate path. It should still remain available to physical-use-case and reachability checks.
+
+## Diagnostics And Behavior
+
+- The current hand should produce no `reviewJointTopology(...)` diagnostics.
+- `review_cad` should stop reporting `assembly.joint-topology.unsupported-axis` for the hand.
+- The topology repair must not silence physical-use-case reachability, load, collision, or visual gates.
+- Existing fake-support tests must still fail when support parts are unrelated to the support-side endpoint.
+
+## Test Strategy
+
+1. Unit test the passive support intent capture and stored records.
+2. Unit test `reviewJointTopology(...)` accepts a passive supported revolute hinge.
+3. Unit test unrelated passive supports do not satisfy the topology gate.
+4. Update the five-finger hand regression so topology diagnostics are empty.
+5. Keep existing review/design-loop integration tests green.
+
+## Accepted Limitations
+
+- This pass does not prove the hand can grasp the cylinder.
+- This pass does not solve dynamic simulation.
+- This pass does not redesign geometry or add transmissions for every passive finger joint.
+- A contact target marked non-structural is still only a modeling target; grasp plausibility remains the job of the physical-use-case/reachability gates.
+
+## Decisions
+
+| # | Decision | Rationale |
+| --- | --- | --- |
+| 1 | Add passive joint-support intent instead of abusing `mechanicalJoint(...)`. | Passive hinges need support evidence but not fake actuators. |
+| 2 | Reuse the existing support-side fastened-path rule. | Support evidence must be grounded in the mate endpoint, not a matching string. |
+| 3 | Keep grasp-cylinder out of topology moving-part/load-path requirements. | It is a contacted object, not part of the hand structure. |
+| 4 | Stop at topology clean, not full grasp success. | The next gate should fail honestly if reachability/load/dynamics are still invalid. |
diff --git a/docs/superpowers/specs/2026-07-11-joint-reaction-and-clevis-structure-design.md b/docs/superpowers/specs/2026-07-11-joint-reaction-and-clevis-structure-design.md
new file mode 100644
index 000000000..477a8778d
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-11-joint-reaction-and-clevis-structure-design.md
@@ -0,0 +1,314 @@
+# Joint Reaction and Clevis Structure Design
+
+## Status
+
+Approved for implementation on 2026-07-11.
+
+## Problem
+
+The physical-use-case gate can currently prove that a held object is in sampled
+quasi-static equilibrium and that declared actuator torque limits are not
+exceeded. It cannot prove how the resulting contact wrench travels through the
+mechanism or whether modeled joint hardware can carry it.
+
+The older `jointLoadCapacity` path is not a suitable base for this work. It
+uses manually supplied per-part loads, does not propagate loads across joints,
+uses inconsistent torque units, and cannot consume the exact pose/contact
+evidence in a static certificate. `MateRecord.maxLoad` is also not captured by
+the public `Assembly.mate()` API, so source declarations are currently dropped.
+
+## Goals
+
+1. Derive a deterministic reaction wrench at every articulated mate on a
+ supported load path from an exact passing static certificate.
+2. Compare each reaction with an explicit, unit-bearing declared envelope
+ without presenting that declaration as structural proof.
+3. Derive a narrow clevis pin/bearing strength certificate from the same
+ dimensions used to construct `joint.clevis()` geometry and from explicit
+ engineering material properties.
+4. Block on ambiguous topology, missing evidence, unsupported load cases, and
+ insufficient safety factor.
+5. Integrate the new evidence into `review_cad` and `design_loop` so physical
+ acceptance cannot bypass it when the new checks are requested.
+
+## Non-Goals
+
+- Finite-element analysis, contact-pressure simulation, fatigue, creep,
+ printed-material anisotropy, cap pullout, or fork-root bending.
+- Stiffness-based reaction sharing across closed loops or multiple supports.
+- Structural certification of custom hand-built hinge geometry from BREP/AABB
+ heuristics.
+- Repairing or accepting the existing five-finger hand.
+- Making the current bar-grasp model pass the structural gate.
+
+## Evidence Pipeline
+
+The pipeline has three independent certificates. A later certificate may only
+run from a passing earlier certificate.
+
+1. `PhysicalUseCaseStaticCertificate`: exact common-contact pose, held-object
+ force/moment residuals, contact forces, and actuator torque evidence.
+2. `PhysicalUseCaseJointReactionCertificate`: reaction wrench at each
+ articulated mate on each loaded tree.
+3. `PhysicalUseCaseJointStructuralCertificate`: declared-envelope result and,
+ when present and applicable, geometry/material-derived clevis checks.
+
+An envelope pass is not a structural pass. The final result reports these
+statuses separately.
+
+## Static Contact Evidence
+
+Each `PhysicalUseCaseStaticContactForce` gains explicit semantics:
+
+```ts
+interface PhysicalUseCaseStaticContactForce {
+ contactA: string;
+ contactB: string;
+ pointWorldMm: Vec3;
+ mechanismPart: string;
+ forceOnHeldWorldN: Vec3;
+ normalForceN: number;
+ tangentialForceN: number;
+ normalCapacityN: number;
+ friction: number;
+}
+```
+
+The old ambiguous `force` field is removed from new evidence. The force applied
+to the mechanism is exactly `-forceOnHeldWorldN`; it must be negated once and
+must not be combined with the held-object load a second time.
+
+## Reaction Solver
+
+### Supported topology
+
+For the exact certificate pose:
+
+1. Solve mates using the certificate's expanded coupled poses.
+2. Collapse parts joined by `fastened` mates into rigid groups.
+3. Build a graph whose remaining edges are articulated mates.
+4. For every group receiving a certified contact force, find its connected
+ component.
+5. Require exactly one stable rigid group in that loaded component.
+6. Require the component to be a tree (`edgeCount === vertexCount - 1`).
+
+Closed loops, parallel articulated paths, zero stable roots, and multiple
+stable roots produce `joint-reaction-indeterminate`. The solver must never pick
+an arbitrary spanning tree or invent load sharing.
+
+### Wrench propagation
+
+Orient each supported tree away from its stable root. Attach each mechanism
+contact force at `pointWorldMm` to the rigid group containing
+`mechanismPart`. Traverse child subtrees from leaves to root.
+
+For a subtree wrench expressed about point `q1`, shift it to joint point `q2`
+with:
+
+```text
+M(q2) = M(q1) + (q1 - q2) x F
+```
+
+The reaction reported at an edge is the wrench exerted by the parent side on
+the child-side free body, equal to the negative of the child's accumulated
+external subtree wrench about the mate origin.
+
+The mate origin is the solved world position of its connector pair. The axis
+is the solved, normalized world direction of the parent-side axis connector.
+Connector origins must coincide within the existing solver tolerance.
+
+```ts
+interface PhysicalUseCaseJointReactionEvidence {
+ mateName: string;
+ parentPart: string;
+ childPart: string;
+ pointWorldMm: Vec3;
+ axisWorld: Vec3;
+ forceWorldN: Vec3;
+ momentWorldNmm: Vec3;
+ resultantForceN: number;
+ resultantMomentNmm: number;
+ axialForceN: number;
+ radialForceN: number;
+ axisMomentNmm: number;
+ bendingMomentNmm: number;
+}
+```
+
+All internal mechanics use N, mm, Nmm, and MPa.
+
+## Declared Envelope
+
+The public mate API gains a unit-explicit capacity namespace:
+
+```ts
+interface MateCapacityEnvelope {
+ maxResultantForceN: number;
+ maxResultantMomentNmm: number;
+}
+
+interface MateCapacity {
+ envelope?: MateCapacityEnvelope;
+ structure?: ClevisStructuralModel;
+}
+
+arm.mate(name, a, b, type, {
+ capacity: {
+ envelope: {
+ maxResultantForceN: 120,
+ maxResultantMomentNmm: 800,
+ },
+ structure: clevis.structural,
+ },
+});
+```
+
+Envelope values must be positive and finite. A supplied structural model is
+only valid on a revolute mate. Missing either envelope limit is `undeclared`,
+not pass.
+
+Legacy `maxLoad: { force, torque }` is accepted only as a deprecated adapter.
+`force` maps to N and `torque` maps once from Nm to Nmm. Supplying both
+`capacity` and `maxLoad` is invalid. A partial legacy declaration remains
+undeclared for the new resultant-envelope gate.
+
+Envelope status is `pass | exceeded | undeclared`. Exceeded and undeclared are
+blocking when reaction-capacity review is requested.
+
+## Clevis Structural Descriptor
+
+`joint.clevis()` emits a descriptor from its resolved build dimensions. It is
+not reconstructed later from rendered material or BREP heuristics.
+
+```ts
+interface StructuralMaterial {
+ name: string;
+ model: 'isotropic-ductile';
+ yieldStrengthMPa: number;
+ bearingStrengthMPa: number;
+ shearStrengthMPa?: number;
+}
+
+interface ClevisStructuralModel {
+ kind: 'clevis-double-shear-v1';
+ source: 'joint.clevis';
+ pinDiameterMm: number;
+ boreDiameterMm: number;
+ forkPlateThicknessMm: number;
+ forkPlateCount: 2;
+ tongueThicknessMm: number;
+ forkGapMm: number;
+ supportSpanMm: number;
+ edgeDistanceMm: number;
+ materials?: {
+ pin: StructuralMaterial;
+ fork: StructuralMaterial;
+ tongue: StructuralMaterial;
+ };
+}
+```
+
+For resolved clevis style:
+
+- `pinDiameterMm = 2 * pinR`
+- `boreDiameterMm = 2 * (pinR + holeClearance)`
+- `forkPlateThicknessMm = plateT`
+- `tongueThicknessMm = tongueY`
+- `forkGapMm = forkGapY`
+- `supportSpanMm = forkGapY + plateT`
+- `edgeDistanceMm = knuckleR`
+
+PBR material and part density are never treated as strength evidence.
+
+## Clevis Strength Model
+
+The first model applies only when axial force is at most 0.01 N and
+perpendicular reaction moment is at most 0.1 Nmm, matching the non-weakenable
+default statics residual tolerances. A revolute-axis moment is delegated to
+the already-required actuator/transmission evidence and is not attributed to
+pin friction.
+
+Given radial reaction `V`, pin diameter `d`, bore diameter `db`, fork plate
+thickness `tf`, tongue thickness `tt`, support span `L`, and edge distance `e`:
+
+```text
+pin area A = pi * d^2 / 4
+double-shear pin stress = V / (2 * A)
+center-load pin moment = V * L / 4
+pin bending stress = 32 * M / (pi * d^3)
+pin von Mises stress = sqrt(bending^2 + 3 * shear^2)
+tongue bearing stress = V / (d * tt)
+fork bearing stress = V / (2 * d * tf)
+ligament = e - db / 2
+tongue tear-out stress = V / (2 * ligament * tt)
+fork tear-out stress = V / (4 * ligament * tf)
+tongue net-section stress = V / ((2 * e - db) * tt)
+fork net-section stress = V / (2 * (2 * e - db) * tf)
+```
+
+Geometry with non-positive area, ligament, or net section is invalid.
+`shearStrengthMPa` is used when explicitly declared; otherwise the solver may
+derive `yieldStrengthMPa / sqrt(3)` only for the declared
+`isotropic-ductile` model and records that assumption. Bearing strength is
+always explicit.
+
+Every check reports stress, allowable, and factor of safety (`null` only for
+zero stress, meaning unbounded rather than unknown). The use-case
+criterion `minJointSafetyFactor` defaults to 2.0 and may only be increased.
+The structural status is `pass | failed | input-incomplete |
+unsupported-load-case`.
+
+Axial force above 0.01 N, perpendicular reaction moment above 0.1 Nmm, missing materials, and
+custom geometry without a `joint.clevis` descriptor are blockers. They are not
+silently omitted.
+
+## Diagnostics
+
+New error diagnostics:
+
+- `assembly.physical-use-case.joint-reaction-input-incomplete`
+- `assembly.physical-use-case.joint-reaction-indeterminate`
+- `assembly.physical-use-case.joint-capacity-undeclared`
+- `assembly.physical-use-case.joint-capacity-exceeded`
+- `assembly.physical-use-case.joint-structure-input-incomplete`
+- `assembly.physical-use-case.joint-structure-unsupported-load-case`
+- `assembly.physical-use-case.joint-structure-insufficient`
+
+Diagnostic messages name the use case and mate and include the measured value,
+limit, or missing/unsupported evidence. No diagnostic may claim a comparison
+was performed when it was not.
+
+## Tool Integration
+
+`review_cad` adds:
+
+- `includePhysicalUseCaseJointReactions`
+- `includePhysicalUseCaseJointStructure`
+- `physicalUseCaseJointReactionCertificates`
+- `physicalUseCaseJointStructuralCertificates`
+
+Structure review implies statics and reactions. Reaction review implies
+statics. `design_loop` enables both checks whenever an attempt declares a
+physical use case, alongside the existing reachability and statics checks.
+
+## Acceptance Tests
+
+1. A 10 N serial-chain load with 50 mm and 150 mm arms produces 500 Nmm and
+ 1500 Nmm joint moments.
+2. Changing the certified articulated pose changes the derived moment arm.
+3. Branch forces combine vectorially and can cancel at an upstream joint.
+4. The contact force is negated exactly once and applied at its certified
+ midpoint.
+5. A fastened group is collapsed; a closed articulated loop and a two-root
+ tree are rejected as indeterminate.
+6. Public `mate({ capacity })` capture validates and preserves unit-bearing
+ values; the legacy Nm adapter converts once.
+7. Hand-calculated clevis equation tests cover pass/fail boundaries and invalid
+ geometry.
+8. `joint.clevis()` emits dimensions identical to its resolved style.
+9. A complete clevis fixture passes, then fails when only pin diameter or
+ material strength is reduced.
+10. Missing descriptor, axial load, and perpendicular moment are blocking.
+11. The current bar-grasp example is structurally incomplete/unsupported, not
+ green.
+12. The current five-finger example retains its reachability rejection.
diff --git a/docs/superpowers/specs/2026-07-11-pose-bound-static-equilibrium-design.md b/docs/superpowers/specs/2026-07-11-pose-bound-static-equilibrium-design.md
new file mode 100644
index 000000000..526bf8c99
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-11-pose-bound-static-equilibrium-design.md
@@ -0,0 +1,141 @@
+# Pose-Bound Static Equilibrium Design
+
+## Problem
+
+KernelCAD now rejects a multi-contact grasp unless all declared contacts are reachable in one sampled actuator pose. The remaining physical-use-case force and torque checks are not tied to that pose: they use declared vectors and part-local connector coordinates independently, sum directional capacities, and estimate actuator torque without solving contact-force balance. Such checks can accept a grasp whose forces balance translation but not moment, or whose contact forces require more actuator torque than declared.
+
+## First-Principles Requirement
+
+A sampled grasp state is statically feasible only when one contact-force allocation simultaneously satisfies all of the following:
+
+1. Every declared contact is within `criteria.maxSlipMm` at the same successfully solved pose.
+2. Contact forces are compressive and stay inside a conservative Coulomb friction pyramid.
+3. The net world-space force and moment on every loaded part are within explicit residual tolerances.
+4. Generalized holding torque at every declared actuator stays within `maxTorqueNmm`, including declared coupled/transmitted joints.
+
+This is a quasi-static load-case check. It does not prove arbitrary force closure, dynamic stability, impact survival, structural stiffness, or feasibility between sampled poses.
+
+## Explicit Evidence Contract
+
+Extend physical-use-case declarations without changing existing vector defaults:
+
+```ts
+arm.physicalUseCase('hold-object', {
+ loads: [{
+ part: 'object',
+ at: 'object.center-of-mass',
+ force: [0, 0, -8],
+ }],
+ contacts: [{
+ a: 'finger.tip',
+ b: 'object.contact',
+ normal: [0, -1, 0],
+ normalFrame: 'world',
+ friction: 0.6,
+ normalForceN: 12,
+ }],
+ actuatorLimits: [{ mate: 'grip', maxTorqueNmm: 180 }],
+ criteria: {
+ maxSlipMm: 1,
+ maxForceResidualN: 0.01,
+ maxTorqueResidualNmm: 0.1,
+ },
+});
+```
+
+- `load.at` is a connector on `load.part` and names the force application point. It is required by the statics gate when a force is declared, and at least one load must provide it as the wrench reference even for a pure-torque case. Load force and free torque vectors remain world-space; torque units are Nmm.
+- `contact.normalFrame` is `'world' | 'a' | 'b'` and defaults to `'world'` for compatibility. Local normals are rotated to world space with the selected part transform.
+- A contact normal points from side `b` toward side `a`. A compressive contact force acts along `+normal` on `a` and `-normal` on `b`.
+- `normalForceN` is the maximum compressive normal force available at that contact and is required by the statics gate.
+- A connector pair may appear only once per use case, regardless of endpoint order. Distinct physical contact patches require distinct connector evidence; duplicate declarations cannot multiply capacity.
+- `maxForceResidualN` and `maxTorqueResidualNmm` default to conservative numerical tolerances. Callers may tighten them, but cannot increase them above the defaults because residual tolerance is solver hygiene, not a physical requirement that an agent may weaken.
+
+Missing or unusable required evidence does not silently skip physics. Once a use case has a common contact pose and statics is enabled, it emits an input-incomplete blocker.
+
+V1 deliberately supports one rigid held part per use case. Every declared load acts on that part, every contact has exactly one endpoint on it, and the held part is not a stable part or a structurally mated member of the mechanism. Multi-body held systems and contacts between two loaded bodies are uncheckable in this slice.
+
+## Shared Pose Sampling
+
+Refactor targeted reachability into an internal assessment that returns both findings and successfully solved contact samples. Each sample contains requested mate poses, part transforms, world-space contact points, and maximum contact distance. The existing reachability API remains as a compatibility wrapper returning only findings.
+
+The statics evaluator consumes only complete samples whose every contact distance is within `maxSlipMm`. It never resamples or resolves the assembly independently, so reachability and statics cannot accidentally validate different states.
+
+## Contact Model
+
+For each contact, construct a deterministic eight-edge friction pyramid. If `n` is the unit normal and `t1`, `t2` are an orthonormal tangent basis, its generators are evenly spaced around:
+
+```text
+n + friction * (cos(theta) * t1 + sin(theta) * t2)
+theta = 0, 45, 90, ... 315 degrees
+```
+
+Non-negative generator weights are constrained so their sum is at most `normalForceN`. This pyramid is inscribed in the circular Coulomb cone and is therefore conservative.
+
+For each loaded part, assemble a six-dimensional wrench equation about an explicit reference point. External forces contribute at their `load.at` points; free torques contribute directly. Contact forces contribute at the midpoint of the two sampled connector points so the allowed slip gap does not create a fictitious couple.
+
+## Feasibility Solver
+
+Use a small deterministic projected-gradient feasibility search over the contact generator weights:
+
+- Objective: normalized squared residual of all loaded-part force and moment equations plus squared actuator-limit violations.
+- Constraint projection: independently project each contact's eight weights onto the non-negative capped simplex whose sum is at most `normalForceN`.
+- Run a contact-only solve first. If no sampled pose balances the declared wrenches, report static equilibrium failure.
+- Run a second solve including actuator-limit penalties. If equilibrium is possible but no sampled solution also satisfies torque limits, report actuator torque failure.
+- A sample passes only after independently reconstructing its forces and verifying force balance, moment balance, the true circular Coulomb inequality, normal-force caps, and actuator limits against explicit tolerances. Solver convergence by itself is never a pass, and iteration exhaustion never counts as a pass.
+
+The optimization is convex under the linearized friction pyramid. A passing post-check is a concrete force-allocation certificate. Failure wording says that no certificate was found in the sampled linearized model; it does not claim analytical impossibility.
+
+## Actuator Torque
+
+For each scalar revolute actuator limit, numerically differentiate every mechanism-to-held contact point displacement with respect to that actuator's source coordinate. Perturbations are expressed in radians, stay inside the mate limits, re-expand declared couplings, and re-solve the assembly. Central differences are used away from a limit and an inward one-sided difference at a limit.
+
+Apply virtual work directly to the relative contact Jacobian:
+
+```text
+actuator generalized torque = sum(relative contact Jacobian dot mechanism-side contact force)
+```
+
+Because every perturbed sample expands the existing coupling records, source-to-driven ratios are included kinematically. Coupled motion must also have the already validated transmission intent; missing limits, failed perturbation solves, unsupported mate types, or missing transmission evidence makes statics input incomplete rather than assuming a zero torque path. V1 treats the declared transmission as ideal and lossless.
+
+Every independent articulated mate on a mate-graph path from a mechanism-side contact to a declared stable part must resolve to an `actuatorLimits` source. Driven coupled mates resolve transitively to their independent source. A declared transmission ratio, when present, must match the corresponding kinematic coupling ratio. This prevents omitted hinges or contradictory transmission evidence from silently contributing unbounded holding torque.
+
+## Diagnostics
+
+Add blocking physical-use-case diagnostics:
+
+- `assembly.physical-use-case.static-input-incomplete`
+- `assembly.physical-use-case.static-equilibrium-unmet`
+- `assembly.physical-use-case.static-actuator-torque-insufficient`
+
+Diagnostics include the use-case name, best sampled poses when available, residual force/moment, actuator torque evidence, and an actionable hint. `review_cad`, mechanism fitness, and `design_loop` surface and preserve these errors like the existing contact-reachability failures.
+
+## Integration Defaults
+
+Add `includePhysicalUseCaseStatics` to `review_cad`. It is opt-in during this compatibility slice; the design loop enables it for physical-acceptance attempts, and the function-first bar-grasp regression requests it explicitly. Cheap reviews and existing direct `requirePhysicalUseCase` callers remain unchanged until the contract has broader corpus coverage.
+
+Successful reviews expose a compact static-equilibrium certificate with the sampled actuator poses, residual wrench, contact forces and utilization, and required/allowed actuator torque. Agents should not have to infer success only from diagnostic silence.
+
+## Alternatives
+
+### Directional Capacity Sums
+
+Rejected. Summing force magnitudes ignores moment balance and can combine mutually incompatible contact directions.
+
+### Full MuJoCo Contact Simulation
+
+Deferred. The existing MuJoCo probe checks articulated mechanism gravity/drop behavior, but abstract physical-use-case contacts are not collision constraints or actuator models. Building those correctly is a later dynamic-validation slice.
+
+### External Linear-Programming Dependency
+
+Deferred. The problem sizes are small and the repository already contains pure-TypeScript numerical helpers. A focused projected convex search plus independent certificate verification avoids adding a runtime dependency. A bounded Phase-I simplex remains a future replacement if the projected search produces unacceptable false negatives; it must preserve the same evidence and post-check contract.
+
+## Test Strategy
+
+1. RED: contacts are geometrically reachable and have enough summed force, but their wrench cannot balance the load moment.
+2. RED: object equilibrium is feasible, but every feasible allocation exceeds a direct actuator torque limit.
+3. GREEN: the same fixture passes after increasing actuator torque capacity.
+4. GREEN: local contact normals are rotated into world space at the winning pose.
+5. RED: common contact pose exists but `load.at` or `normalForceN` is missing.
+6. Regression: unreachable contacts still produce reachability diagnostics without extra statics noise.
+7. Regression: the function-first bar-grasp skeleton is evaluated by the new gate; if it fails, preserve the failure and repair the model or contract rather than weakening validation.
+8. Regression: the rejected five-finger hand remains rejected.
diff --git a/docs/superpowers/specs/2026-07-11-simultaneous-grasp-reachability-design.md b/docs/superpowers/specs/2026-07-11-simultaneous-grasp-reachability-design.md
new file mode 100644
index 000000000..1aab4d8c5
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-11-simultaneous-grasp-reachability-design.md
@@ -0,0 +1,42 @@
+# Simultaneous Grasp Reachability Design
+
+## Problem
+
+The targeted physical-use-case reachability gate currently minimizes every declared contact independently across all sampled actuator poses. That can accept an impossible grasp: one finger may reach its target only while open and another only while closed, even though no single mechanism state satisfies both contacts.
+
+## First-Principles Requirement
+
+A grasp is one physical state. Every contact declared by a physical use case must therefore be evaluated against the same solved actuator pose. Independent best poses are useful diagnostics, but they are not evidence that the grasp exists.
+
+## Chosen Approach
+
+Retain the deterministic targeted sampler and coupling expansion. For every successfully solved sample, calculate all declared contact distances together. Continue tracking each contact's independent minimum so a specifically unreachable or uncheckable contact keeps the existing diagnostic. When every contact is individually reachable but no complete sample places all contacts within `maxSlipMm`, emit one use-case-level `assembly.physical-use-case.simultaneous-contacts-unreachable` error.
+
+The diagnostic records the contact distances from the best common sample, chosen by the smallest worst contact distance. This makes the failure actionable without pretending that a discrete sampler is a continuous dynamics solver.
+
+## Alternatives
+
+- Keep independent per-contact minima: rejected because it proves several different configurations, not one grasp.
+- Run continuous optimization or dynamics simulation now: deferred because topology, limits, couplings, and discrete common-pose feasibility must be coherent before a more expensive solver is meaningful.
+
+## Behavior
+
+- A contact that is never resolved or never enters tolerance produces the existing `contact-unreachable` diagnostic.
+- A use case with two or more individually reachable contacts but no common passing sample produces one `simultaneous-contacts-unreachable` diagnostic.
+- A use case with one contact keeps the existing behavior and never gets a redundant simultaneous-contact diagnostic.
+- Failed mate solves are ignored as candidate states.
+- A sample with an unresolved declared contact is not a complete common-pose candidate.
+- Pose-envelope and targeted per-contact diagnostics remain deduplicated as they are today.
+- `design_loop` preserves the simultaneous-contact error as a physical acceptance fact and includes it in the next repair prompt.
+
+## Scope
+
+This slice changes validation only. It does not alter hand geometry, add a continuous optimizer, claim force closure, or claim dynamic stability.
+
+## Verification
+
+1. A regression model has two contacts on one rotating link: contact A reaches at 0 degrees and contact B reaches at 90 degrees. The old implementation returns no issue; the new implementation must emit the simultaneous-contact issue.
+2. Existing coupling-expansion and unusable-solve tests continue to pass.
+3. The function-first bar-grasp skeleton still passes because all three contacts are aligned at one common actuator sample.
+4. The rejected five-finger hand remains rejected; this slice must not weaken existing gates.
+5. The agent design loop carries the exact simultaneous-contact code and repair hint into the next attempt.
diff --git a/examples/robot-hand/workflow-candidates-comparison.kcad.ts b/examples/robot-hand/workflow-candidates-comparison.kcad.ts
new file mode 100644
index 000000000..770688dd7
--- /dev/null
+++ b/examples/robot-hand/workflow-candidates-comparison.kcad.ts
@@ -0,0 +1,191 @@
+// Five actual robot-hand workflow candidate models on one comparison board.
+//
+// A: mechanism-template first
+// B: reference-conditioned visible fit + physical completion
+// C: mesh-feature fitting
+// D: master skeleton
+// E: validation-loop view
+
+setCameraTarget(0, 0, 35);
+setCameraDistance(620);
+
+const beige = '#d8d3c9';
+const tan = '#b9b3a8';
+const dark = '#111827';
+const metal = '#d9dee5';
+const blue = '#2563eb';
+const red = '#dc2626';
+const green = '#16a34a';
+const orange = '#f59e0b';
+const ghost = '#cbd5e1';
+const graphite = '#475569';
+
+function solid(w, d, h, x, y, z, color) {
+ return box(w, d, h, true).translate(x, y, z).color(color);
+}
+
+function rodXZ(x1, z1, x2, z2, y, thickness, color) {
+ const dx = x2 - x1;
+ const dz = z2 - z1;
+ const len = Math.sqrt(dx * dx + dz * dz);
+ const angle = Math.atan2(dx, dz) * 180 / Math.PI;
+ return box(thickness, 4, len, true)
+ .rotate([0, 1, 0], angle)
+ .translate((x1 + x2) / 2, y, (z1 + z2) / 2)
+ .color(color);
+}
+
+function pin(x, z, y = -11, r = 4) {
+ return cylinder(5, r, 20).alongAxis([0, 1, 0]).translate(x, y, z).color(metal);
+}
+
+function basePanel(cx, label, color) {
+ return solid(92, 8, 12, cx, 7, -76, color)
+ .union(blockLetter(label, cx - 33, -9, -80, dark));
+}
+
+function stroke(w, h, x, y, z, color) {
+ return solid(w, 3, h, x, y, z, color);
+}
+
+function blockLetter(label, x, y, z, color) {
+ if (label === 'A') {
+ return rodXZ(x - 8, z - 8, x, z + 10, y, 3.2, color)
+ .union(rodXZ(x + 8, z - 8, x, z + 10, y, 3.2, color))
+ .union(stroke(12, 3, x, y, z, color));
+ }
+ if (label === 'B') {
+ return stroke(3, 22, x - 7, y, z, color)
+ .union(stroke(12, 3, x, y, z + 10, color))
+ .union(stroke(12, 3, x, y, z, color))
+ .union(stroke(12, 3, x, y, z - 10, color))
+ .union(stroke(3, 9, x + 7, y, z + 5, color))
+ .union(stroke(3, 9, x + 7, y, z - 5, color));
+ }
+ if (label === 'C') {
+ return stroke(3, 22, x - 7, y, z, color)
+ .union(stroke(14, 3, x, y, z + 10, color))
+ .union(stroke(14, 3, x, y, z - 10, color));
+ }
+ if (label === 'D') {
+ return stroke(3, 22, x - 7, y, z, color)
+ .union(stroke(12, 3, x, y, z + 10, color))
+ .union(stroke(12, 3, x, y, z - 10, color))
+ .union(stroke(3, 18, x + 7, y, z, color));
+ }
+ return stroke(3, 22, x - 7, y, z, color)
+ .union(stroke(14, 3, x, y, z + 10, color))
+ .union(stroke(12, 3, x - 1, y, z, color))
+ .union(stroke(14, 3, x, y, z - 10, color));
+}
+
+function simplePalm(cx, color = tan, y = 0) {
+ return solid(72, 16, 70, cx, y, 6, color)
+ .union(solid(56, 18, 16, cx, y - 1, -42, dark))
+ .union(solid(42, 18, 22, cx, y - 1, -60, graphite));
+}
+
+function simpleFinger(rootX, rootZ, lengths, width, angleDeg, color = beige, y = 0) {
+ const [a, b, c] = lengths;
+ const root = solid(width, 10, a, 0, y, a / 2, color);
+ const mid = solid(width * 0.82, 9, b, 0, y, a + b / 2 + 5, color);
+ const tip = solid(width * 0.70, 8, c, 0, y, a + b + c / 2 + 10, dark);
+ return root.union(mid).union(tip)
+ .rotate([0, 1, 0], angleDeg)
+ .translate(rootX, 0, rootZ);
+}
+
+function basicHand(cx, opts = {}) {
+ const y = opts.y ?? 0;
+ const palmColor = opts.palmColor ?? tan;
+ const linkColor = opts.linkColor ?? beige;
+ let model = simplePalm(cx, palmColor, y)
+ .union(simpleFinger(cx - 36, 42, [34, 24, 16], 10, -4, linkColor, y))
+ .union(simpleFinger(cx - 12, 44, [42, 29, 20], 11, -1, linkColor, y))
+ .union(simpleFinger(cx + 12, 45, [46, 32, 22], 11, 0, linkColor, y))
+ .union(simpleFinger(cx + 36, 42, [38, 27, 18], 10, 4, linkColor, y))
+ .union(simpleFinger(cx + 52, -4, [30, 22, 16], 10, 38, linkColor, y));
+ for (const x of [cx - 36, cx - 12, cx + 12, cx + 36]) {
+ model = model.union(pin(x, 42, y - 11, 3.8));
+ }
+ model = model.union(pin(cx + 52, -4, y - 11, 3.8));
+ return model;
+}
+
+function mechanismTemplate(cx) {
+ let model = basePanel(cx, 'A', '#e0e7ff').union(basicHand(cx));
+ for (const x of [cx - 36, cx - 12, cx + 12, cx + 36, cx + 52]) {
+ model = model
+ .union(solid(16, 6, 9, x, -14, 39, graphite))
+ .union(solid(10, 6, 7, x, -17, 31, metal));
+ }
+ return model.union(rodXZ(cx - 34, -40, cx - 36, 42, -16, 2, metal))
+ .union(rodXZ(cx - 10, -42, cx - 12, 44, -16, 2, metal))
+ .union(rodXZ(cx + 12, -42, cx + 12, 45, -16, 2, metal));
+}
+
+function referenceConditioned(cx) {
+ let model = basePanel(cx, 'B', '#cffafe')
+ .union(solid(82, 4, 78, cx, 8, 8, ghost))
+ .union(solid(18, 4, 82, cx - 38, 8, 78, ghost))
+ .union(solid(18, 4, 94, cx - 12, 8, 83, ghost))
+ .union(solid(18, 4, 98, cx + 12, 8, 85, ghost))
+ .union(solid(18, 4, 84, cx + 38, 8, 78, ghost))
+ .union(rodXZ(cx + 50, -2, cx + 92, 54, 8, 9, ghost))
+ .union(basicHand(cx, { y: -2 }));
+ for (const x of [cx - 26, cx, cx + 26]) {
+ model = model.union(solid(10, 3, 24, x, -13, 12, dark));
+ }
+ return model;
+}
+
+function meshFeatureFitting(cx) {
+ let model = basePanel(cx, 'C', '#fef3c7')
+ .union(solid(82, 14, 58, cx, 5, 8, ghost))
+ .union(solid(22, 14, 72, cx - 38, 5, 72, ghost))
+ .union(solid(24, 14, 86, cx - 10, 5, 82, ghost))
+ .union(solid(24, 14, 90, cx + 16, 5, 84, ghost))
+ .union(solid(22, 14, 76, cx + 42, 5, 74, ghost))
+ .union(solid(72, 6, 48, cx, -10, 8, orange));
+ for (const x of [cx - 38, cx - 10, cx + 16, cx + 42]) {
+ model = model
+ .union(solid(14, 6, 66, x, -10, 66, orange))
+ .union(pin(x, 39, -14, 3.5));
+ }
+ return model.union(rodXZ(cx + 46, -4, cx + 88, 48, -10, 8, orange));
+}
+
+function masterSkeleton(cx) {
+ let model = basePanel(cx, 'D', '#dcfce7')
+ .union(basicHand(cx, { y: 0, palmColor: '#e6dfd2', linkColor: '#e9e2d5' }))
+ .union(rodXZ(cx, -58, cx, 122, -18, 2.5, blue))
+ .union(solid(118, 3, 2, cx, -18, 42, red));
+ for (const x of [cx - 36, cx - 12, cx + 12, cx + 36]) {
+ model = model
+ .union(rodXZ(x, 42, x - 6, 112, -18, 2.2, blue))
+ .union(pin(x, 42, -20, 3.2));
+ }
+ model = model.union(rodXZ(cx + 52, -4, cx + 92, 55, -18, 2.2, blue));
+ return model;
+}
+
+function validationLoop(cx) {
+ const model = basePanel(cx, 'E', '#fee2e2')
+ .union(basicHand(cx))
+ .union(solid(20, 5, 20, cx - 46, -18, -40, green))
+ .union(rodXZ(cx - 52, -40, cx - 46, -32, -21, 3, green))
+ .union(rodXZ(cx - 46, -32, cx - 35, -50, -21, 3, green))
+ .union(solid(22, 5, 22, cx + 64, -18, 82, red))
+ .union(rodXZ(cx + 56, 74, cx + 72, 90, -21, 4, red))
+ .union(rodXZ(cx + 72, 74, cx + 56, 90, -21, 4, red));
+ return model;
+}
+
+const centers = [-250, -125, 0, 125, 250];
+const comparison = mechanismTemplate(centers[0])
+ .union(referenceConditioned(centers[1]))
+ .union(meshFeatureFitting(centers[2]))
+ .union(masterSkeleton(centers[3]))
+ .union(validationLoop(centers[4]));
+
+return comparison;
diff --git a/scripts/robotHandFunctionalRequirements.ts b/scripts/robotHandFunctionalRequirements.ts
new file mode 100644
index 000000000..13604c7ea
--- /dev/null
+++ b/scripts/robotHandFunctionalRequirements.ts
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors
+export type RobotHandCheck =
+ | 'reachable-contact-points'
+ | 'joint-limits-respected'
+ | 'no-self-collision'
+ | 'load-path-to-palm'
+ | 'opposing-contact-normals'
+ | 'object-clearance'
+ | 'actuation-anchored';
+
+export interface RobotHandGraspTask {
+ id: string;
+ name: string;
+ object: string;
+ purpose: string;
+ contacts: string[];
+ requiredChecks: RobotHandCheck[];
+}
+
+export const FUNCTION_FIRST_ROBOT_HAND_PRINCIPLES = [
+ 'Function before form: define grasp tasks and object contacts before visual styling.',
+ 'Contacts before fingers: finger count, thumb placement, and palm shape come from required contact geometry.',
+ 'Skeleton before solids: joint centers, axes, envelopes, and limits are authored before decorative bodies.',
+ 'Validation before polish: a hand that cannot hold target objects is rejected even if it looks plausible.',
+ 'Reference after function: visual references tune proportions only after the grasp tasks pass.',
+] as const;
+
+const BASE_CHECKS: RobotHandCheck[] = [
+ 'reachable-contact-points',
+ 'joint-limits-respected',
+ 'no-self-collision',
+ 'load-path-to-palm',
+];
+
+export const ROBOT_HAND_GRASP_TASKS: RobotHandGraspTask[] = [
+ {
+ id: 'pinch-thin-plate',
+ name: 'Pinch thin plate',
+ object: '2-5 mm plate or card edge',
+ purpose: 'Prove fingertip opposition and fine-object aperture without cheating through interpenetration.',
+ contacts: ['thumb pad', 'index fingertip'],
+ requiredChecks: [...BASE_CHECKS, 'opposing-contact-normals', 'object-clearance'],
+ },
+ {
+ id: 'power-cylinder',
+ name: 'Power grasp cylinder',
+ object: '30-55 mm diameter cylinder such as a bottle neck or handle',
+ purpose: 'Prove wraparound grasp and palm/finger load path under torque.',
+ contacts: ['thumb side', 'index phalanx', 'middle phalanx', 'palm saddle'],
+ requiredChecks: [...BASE_CHECKS, 'opposing-contact-normals', 'actuation-anchored'],
+ },
+ {
+ id: 'spherical-object',
+ name: 'Spherical grasp',
+ object: '35-65 mm sphere',
+ purpose: 'Prove multi-point enclosure rather than one flat clamp line.',
+ contacts: ['thumb pad', 'index fingertip', 'middle fingertip'],
+ requiredChecks: [...BASE_CHECKS, 'opposing-contact-normals', 'object-clearance'],
+ },
+ {
+ id: 'box-grasp',
+ name: 'Box grasp',
+ object: '45 x 30 x 25 mm rectangular block',
+ purpose: 'Prove stable grasp on flat-sided objects without relying only on fingertip points.',
+ contacts: ['thumb pad', 'index inner face', 'middle inner face', 'palm face'],
+ requiredChecks: [...BASE_CHECKS, 'opposing-contact-normals', 'object-clearance'],
+ },
+ {
+ id: 'hook-handle',
+ name: 'Hook or handle pull',
+ object: '8-16 mm handle or ring section',
+ purpose: 'Prove load-bearing hook geometry and pin/load path through the palm.',
+ contacts: ['curled finger inner surface', 'palm reaction support'],
+ requiredChecks: [...BASE_CHECKS, 'actuation-anchored'],
+ },
+ {
+ id: 'wide-object',
+ name: 'Wide object aperture',
+ object: 'object wider than relaxed palm contact span',
+ purpose: 'Prove the hand opens far enough before closing around the target.',
+ contacts: ['thumb outer reach', 'opposing finger outer reach'],
+ requiredChecks: [...BASE_CHECKS, 'object-clearance'],
+ },
+];
+
+export const ROBOT_HAND_ACCEPTANCE_GATES = [
+ 'grasp-aperture-covers-target-object',
+ 'opposing-contact-normals-resist-escape',
+ 'pose-envelope-has-no-breaking-collisions',
+ 'all-loaded-parts-are-in-mate-graph',
+ 'actuation-path-has-anchored-transmission',
+ 'all-contacting-fingers-have-physically-realized-joints',
+ 'visual-reference-is-applied-only-after-functional-gates-pass',
+] as const;
+
+export function summarizeRobotHandFunctionalBrief() {
+ return {
+ firstArtifact: 'three-finger functional hand',
+ deferred: [
+ 'five-finger visual styling',
+ 'mesh feature fitting',
+ 'cosmetic palm shell',
+ 'extra non-contact fingers',
+ ],
+ why: 'A three-finger hand can cover the grasp tests with fewer joints, making disconnected parts, invalid axes, and fake load paths easier to detect before adding visual complexity.',
+ };
+}
diff --git a/scripts/robotHandWorkflowCompare.ts b/scripts/robotHandWorkflowCompare.ts
new file mode 100644
index 000000000..bf2e676d1
--- /dev/null
+++ b/scripts/robotHandWorkflowCompare.ts
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors
+import { mkdirSync, writeFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+export const ROBOT_HAND_WORKFLOW_WEIGHTS = {
+ physicalCompleteness: 0.30,
+ referenceFit: 0.22,
+ parametricStability: 0.20,
+ automationPotential: 0.13,
+ validationCoverage: 0.15,
+} as const;
+
+export type RobotHandWorkflowId =
+ | 'mechanism-templates'
+ | 'reference-conditioned'
+ | 'mesh-feature-fitting'
+ | 'master-skeleton'
+ | 'validation-loop';
+
+export interface WorkflowScore {
+ physicalCompleteness: number;
+ referenceFit: number;
+ parametricStability: number;
+ automationPotential: number;
+ validationCoverage: number;
+}
+
+export interface WorkflowCandidate {
+ id: RobotHandWorkflowId;
+ label: string;
+ role: 'generator' | 'evidence-to-generator' | 'skeleton' | 'validator';
+ inputs: string;
+ builds: string;
+ failureCaught: string;
+ caveat: string;
+ score: WorkflowScore;
+}
+
+export interface ScoredWorkflowCandidate extends WorkflowCandidate {
+ weightedScore: number;
+}
+
+export interface WorkflowComparisonResult {
+ weights: typeof ROBOT_HAND_WORKFLOW_WEIGHTS;
+ candidates: ScoredWorkflowCandidate[];
+ bestIndividual: ScoredWorkflowCandidate;
+ recommendedCombination: {
+ ids: RobotHandWorkflowId[];
+ score: number;
+ reason: string;
+ };
+}
+
+const CANDIDATES: WorkflowCandidate[] = [
+ {
+ id: 'mechanism-templates',
+ label: 'Mechanism Templates',
+ role: 'generator',
+ inputs: 'mechanism family, target DOF, rough envelope',
+ builds: 'known-good palm, clevis, pin, tendon, and finger modules',
+ failureCaught: 'missing joints, unsupported pins, floating visual parts',
+ caveat: 'Reliable mechanically, but can drift visually when the reference has strong style cues.',
+ score: {
+ physicalCompleteness: 92,
+ referenceFit: 58,
+ parametricStability: 82,
+ automationPotential: 72,
+ validationCoverage: 68,
+ },
+ },
+ {
+ id: 'reference-conditioned',
+ label: 'Reference-Conditioned CAD',
+ role: 'evidence-to-generator',
+ inputs: 'reference image or mesh landmarks plus mechanism family',
+ builds: 'landmark-driven visible proportions with mechanical completion',
+ failureCaught: 'visual drift, wrong thumb angle, lost palm/wrist language',
+ caveat: 'Landmarks are manual until mesh or image extraction is added.',
+ score: {
+ physicalCompleteness: 88,
+ referenceFit: 91,
+ parametricStability: 76,
+ automationPotential: 66,
+ validationCoverage: 72,
+ },
+ },
+ {
+ id: 'mesh-feature-fitting',
+ label: 'Mesh Feature Fitting',
+ role: 'evidence-to-generator',
+ inputs: 'segmented mesh regions, fitted planes, cylinders, boxes, axes',
+ builds: 'CAD primitives fitted to visible mesh features',
+ failureCaught: 'bad primitive fit, missing shaft axes, repeated-module mismatch',
+ caveat: 'Promising for automation, but bad segmentation can create false confidence.',
+ score: {
+ physicalCompleteness: 58,
+ referenceFit: 86,
+ parametricStability: 54,
+ automationPotential: 88,
+ validationCoverage: 50,
+ },
+ },
+ {
+ id: 'master-skeleton',
+ label: 'Master Skeleton',
+ role: 'skeleton',
+ inputs: 'datums, joint centers, axes, envelopes, motion arcs',
+ builds: 'stable parametric skeleton that downstream solids follow',
+ failureCaught: 'sideways hands, broken axes, unstable edits, impossible motion',
+ caveat: 'Best as a control layer; still needs either templates or reference evidence for solids.',
+ score: {
+ physicalCompleteness: 84,
+ referenceFit: 64,
+ parametricStability: 94,
+ automationPotential: 62,
+ validationCoverage: 74,
+ },
+ },
+ {
+ id: 'validation-loop',
+ label: 'Validation Loop',
+ role: 'validator',
+ inputs: 'candidate assembly, reference evidence, physical requirements',
+ builds: 'acceptance gates, scoring, repair hints, reject/pass decision',
+ failureCaught: 'floating parts, invalid mates, collisions, weak loads, visual drift',
+ caveat: 'This is not a generator; it decides whether a generated model is acceptable.',
+ score: {
+ physicalCompleteness: 78,
+ referenceFit: 72,
+ parametricStability: 70,
+ automationPotential: 76,
+ validationCoverage: 96,
+ },
+ },
+];
+
+function weightedScore(score: WorkflowScore): number {
+ const weights = ROBOT_HAND_WORKFLOW_WEIGHTS;
+ return Math.round(
+ score.physicalCompleteness * weights.physicalCompleteness
+ + score.referenceFit * weights.referenceFit
+ + score.parametricStability * weights.parametricStability
+ + score.automationPotential * weights.automationPotential
+ + score.validationCoverage * weights.validationCoverage,
+ );
+}
+
+export function compareRobotHandWorkflows(): WorkflowComparisonResult {
+ const candidates = CANDIDATES
+ .map((candidate) => ({ ...candidate, weightedScore: weightedScore(candidate.score) }))
+ .sort((a, b) => b.weightedScore - a.weightedScore);
+
+ const byId = new Map(candidates.map((candidate) => [candidate.id, candidate]));
+ const comboIds: RobotHandWorkflowId[] = ['reference-conditioned', 'master-skeleton', 'validation-loop'];
+ const comboScore = Math.round(comboIds.reduce((sum, id) => sum + (byId.get(id)?.weightedScore ?? 0), 0) / comboIds.length);
+
+ return {
+ weights: ROBOT_HAND_WORKFLOW_WEIGHTS,
+ candidates,
+ bestIndividual: candidates[0],
+ recommendedCombination: {
+ ids: comboIds,
+ score: comboScore,
+ reason: 'Reference-conditioned CAD preserves visible fit, master skeletons provide stable parametrics, and the validation loop supplies physical acceptance.',
+ },
+ };
+}
+
+function bar(value: number): string {
+ return `