From ccc47afe28e3649203bdf84b8447355c5cc147c5 Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Thu, 10 Sep 2026 01:57:20 -0400 Subject: [PATCH] feat(skills): add human-openable fit prechecks --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- scripts/validate-skills.ts | 66 ++++++++++++++++++- skills/furniture-fit/SKILL.md | 13 +++- skills/furniture-fit/evals/evals.json | 62 +++++++++++++++++ .../examples/insufficient-evidence.md | 2 + .../examples/no-sign-in-dimension-precheck.md | 34 ++++++++++ .../references/report-template.md | 9 +++ 10 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 skills/furniture-fit/examples/no-sign-in-dimension-precheck.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 974a9cf31..eb1cf9bcd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-marketplace.json", "name": "pascal", - "version": "0.1.7", + "version": "0.1.8", "description": "Public Pascal workflows for MCP-capable agents.", "owner": { "name": "Pascal" @@ -11,7 +11,7 @@ "name": "pascal-agent-skills", "source": "./", "description": "Create and inspect editable Pascal scenes and run bounded furniture footprint assessments with blocker-aware next actions.", - "version": "0.1.7", + "version": "0.1.8", "category": "productivity", "skills": ["./skills/pascal-3d", "./skills/furniture-fit"] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1e9d7a7db..a19bb48e4 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin.json", "name": "pascal-agent-skills", "displayName": "Pascal agent skills", - "version": "0.1.7", + "version": "0.1.8", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { "name": "Pascal" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 326964eda..29ab94cae 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pascal-agent-skills", - "version": "0.1.7", + "version": "0.1.8", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { "name": "Pascal", diff --git a/plugin.json b/plugin.json index a8533aea8..9fc8bf783 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "pascal-agent-skills", - "version": "0.1.7", + "version": "0.1.8", "description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.", "author": { "name": "Pascal", diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index 8e7127f62..36c7dbd9f 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -12,8 +12,8 @@ import { const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const skillNames = ['pascal-3d', 'furniture-fit'] as const -const skillVersions = { 'pascal-3d': '0.1.0', 'furniture-fit': '0.1.3' } as const -const pluginVersion = '0.1.7' +const skillVersions = { 'pascal-3d': '0.1.0', 'furniture-fit': '0.1.4' } as const +const pluginVersion = '0.1.8' const portablePluginSchema = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json' const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/ @@ -360,6 +360,68 @@ for (const entry of readdirSync(furnitureExamplesRoot, { withFileTypes: true })) } } +const furniturePrecheckExample = read( + join(furnitureExamplesRoot, 'no-sign-in-dimension-precheck.md'), +) +const furniturePrecheckUrlMatches = furniturePrecheckExample.match( + /https:\/\/editor\.pascal\.app\/tools\/furniture-fit\?[^\s]+/gu, +) +if (furniturePrecheckUrlMatches?.length !== 1) { + fail('furniture-fit no-sign-in pre-check example must contain exactly one canonical URL') +} else { + const precheckUrl = new URL(furniturePrecheckUrlMatches[0]!) + const allowedPrecheckKeys = [ + 'clearance', + 'entry', + 'itemDepth', + 'itemWidth', + 'roomDepth', + 'roomWidth', + 'shared', + 'unit', + ] + if ( + precheckUrl.origin !== 'https://editor.pascal.app' || + precheckUrl.pathname !== '/tools/furniture-fit' + ) { + fail('furniture-fit no-sign-in pre-check must use the canonical HTTPS calculator URL') + } + if ( + JSON.stringify([...precheckUrl.searchParams.keys()].sort()) !== + JSON.stringify(allowedPrecheckKeys) + ) { + fail('furniture-fit no-sign-in pre-check must use only the fixed query keys') + } + if ( + precheckUrl.searchParams.get('entry') !== 'agent_report' || + precheckUrl.searchParams.get('shared') !== '1' || + !['cm', 'in'].includes(precheckUrl.searchParams.get('unit') ?? '') + ) { + fail('furniture-fit no-sign-in pre-check must carry fixed attribution and a supported unit') + } + for (const key of ['roomWidth', 'roomDepth', 'itemWidth', 'itemDepth']) { + const value = Number(precheckUrl.searchParams.get(key)) + if (!(Number.isFinite(value) && value > 0 && value <= 1_000_000)) { + fail(`furniture-fit no-sign-in pre-check ${key} must be within the runtime bounds`) + } + } + const clearance = Number(precheckUrl.searchParams.get('clearance')) + if (!(Number.isFinite(clearance) && clearance >= 0 && clearance <= 1_000_000)) { + fail('furniture-fit no-sign-in pre-check clearance must be within the runtime bounds') + } +} + +for (const requiredBoundary of [ + 'Open dimension-only footprint pre-check', + 'entry=agent_report', + 'Opening the link sends the visible measurement query to `editor.pascal.app`', + 'Never put a project, revision, graph hash, node ID, address, person, account, workspace, credential, signed URL, `flow_id`, or arbitrary scene text in the URL.', +]) { + if (!furnitureSkill.includes(requiredBoundary)) { + fail(`furniture-fit skill is missing no-sign-in pre-check boundary: ${requiredBoundary}`) + } +} + type FurnitureDecisionContext = { has_passing_footprint?: unknown has_failing_requested_pose?: unknown diff --git a/skills/furniture-fit/SKILL.md b/skills/furniture-fit/SKILL.md index a4e440b2c..c2dc8da31 100644 --- a/skills/furniture-fit/SKILL.md +++ b/skills/furniture-fit/SKILL.md @@ -3,8 +3,8 @@ name: furniture-fit description: Assess whether furniture fits in a measured Pascal room or layout. Use this skill for sofa, table, bed, cabinet, appliance, staging, placement, collision, clearance, or rotated-footprint questions. Produce a tool-backed spatial report that distinguishes footprint fit from unsupported height, door-swing, assembly, and delivery-route claims, and return insufficient evidence when dimensions or scale are missing. compatibility: Requires a Pascal MCP connection for verified scene checks. Can still produce an input-gap report when the scene or measurements are unavailable. metadata: - version: "0.1.3" - source-reviewed: "2026-09-09" + version: "0.1.4" + source-reviewed: "2026-09-10" native-host-validation: "package-checks-only" openclaw: homepage: https://editor.pascal.app/docs/developers/mcp @@ -151,6 +151,14 @@ Use the exact report shape in [references/report-template.md](references/report- - one blocker-aware `nextAction` with its required input, exact available context, authority, and cost boundary; - the exact `editorUrl` returned by Pascal when a persistent project is involved. +When the user asks for a hosted link, or explicitly confirms that these measurements may be sent to Pascal, an eligible report can include an **Open dimension-only footprint pre-check** link. Eligibility requires exact positive dimensions no greater than `1,000,000` for one rectangular room footprint and one rectangular item footprint. Use the user's original `cm` or `in` values when they are exact; otherwise convert measured meter values to centimeters without rounding away meaningful precision. Use the user's explicit uniform room-boundary clearance when one was supplied. Item-to-item spacing from `check_collisions.minimumClearance` is a different constraint and must not be copied into this link. Use `clearance=0` only for a bare dimensional fit or when the user explicitly requested no added room-boundary clearance. Build only this fixed URL shape, with standard URL encoding: + +```text +https://editor.pascal.app/tools/furniture-fit?entry=agent_report&roomWidth=&roomDepth=&itemWidth=&itemDepth=&clearance=&unit=&shared=1 +``` + +The link recomputes only an empty axis-aligned rectangular footprint at 0° and 90° with uniform per-side room-boundary clearance. Label it as a separate dimension-only pre-check, not as the scene-backed verdict. Omit it when the room is irregular; dimensions are missing, ambiguous, inferred, or over the calculator limit; any directional or asymmetric clearance was requested; the user has not authorized sending private or local measurements to Pascal; or the requested conclusion depends on a tested position, existing objects, doors, height, delivery, or another scene-specific constraint. Opening the link sends the visible measurement query to `editor.pascal.app` and can leave it in browser history and service request logs. Never put a project, revision, graph hash, node ID, address, person, account, workspace, credential, signed URL, `flow_id`, or arbitrary scene text in the URL. Use `unavailable` plus the first reason when the link cannot represent the inputs safely. + Before sending the report, compare its numeric inputs and source IDs against both the user's constraint record and the actual tool output. Copy level, zone, item, candidate, and project IDs exactly; do not recreate them from memory. A missing requested check must be identified as incomplete, even when a narrower calculation passes. The examples are synthetic and illustrate correct claim boundaries: @@ -160,3 +168,4 @@ The examples are synthetic and illustrate correct claim boundaries: - [examples/all-tested-poses-fail.md](examples/all-tested-poses-fail.md) - [examples/insufficient-evidence.md](examples/insufficient-evidence.md) - [examples/unproven-height-metadata.md](examples/unproven-height-metadata.md) +- [examples/no-sign-in-dimension-precheck.md](examples/no-sign-in-dimension-precheck.md) diff --git a/skills/furniture-fit/evals/evals.json b/skills/furniture-fit/evals/evals.json index 1b7bcf7b1..ef68e9be2 100644 --- a/skills/furniture-fit/evals/evals.json +++ b/skills/furniture-fit/evals/evals.json @@ -239,6 +239,68 @@ "Returns exactly one check_alternate_pose nextAction naming position [1.20, 0, 0.80] and 90 degrees.", "Labels the alternate proposed and unverified and requires fresh containment, collision, requested-clearance, and applicable door checks before a pass." ] + }, + { + "id": 13, + "prompt": "A user supplied an exact empty rectangular room 144 in wide by 120 in deep, a sofa footprint 84 in wide by 36 in deep, and 8 in of room-boundary clearance on every side. They explicitly ask for a Pascal link using these measurements. Return the bounded furniture-fit report and a human-openable pre-check.", + "expected_output": "Keeps the scene-backed report separate and includes the exact no-sign-in Pascal dimension-only pre-check URL using only the supplied inch values and fixed attribution keys.", + "files": [], + "semantic_case": "no-sign-in-dimension-precheck-link", + "expectations": [ + "Labels the link Open dimension-only footprint pre-check.", + "Uses exactly https://editor.pascal.app/tools/furniture-fit with roomWidth=144, roomDepth=120, itemWidth=84, itemDepth=36, clearance=8, unit=in, shared=1, and entry=agent_report.", + "States that the public calculator does not carry the scene-backed verdict or project-specific collision, door, height, delivery, or mesh evidence.", + "Discloses that opening the link sends the visible measurement query to Pascal and can retain it in browser history and service request logs.", + "Does not place project, revision, graph hash, node, identity, credential, address, signed URL, flow, prompt, or arbitrary scene data in the URL." + ] + }, + { + "id": 14, + "prompt": "The user authorizes sending the measurements to Pascal. The measured room is L-shaped inside a 400 cm by 350 cm bounding box, the exact sofa footprint is 210 cm by 95 cm, and the requested room-boundary clearance is 20 cm on every side. Include any safe human-openable pre-check in the report.", + "expected_output": "Omits the dimension-only calculator URL because the rectangular uniform-clearance calculator cannot represent the evidence, while retaining the scene-backed report.", + "files": [], + "semantic_case": "no-sign-in-dimension-precheck-unrepresentable", + "expectations": [ + "Marks Open dimension-only footprint pre-check unavailable and gives the first unrepresentable-input reason.", + "Does not flatten the irregular room, pose-specific obstacle, or unequal clearances into a misleading rectangular link.", + "Does not expose project, revision, graph hash, node, identity, credential, address, signed URL, flow, prompt, or arbitrary scene data." + ] + }, + { + "id": 15, + "prompt": "The user authorizes sending the measurements to Pascal. The exact rectangular room is 400 cm by 350 cm, the exact sofa footprint is 210 cm by 95 cm, and the user requests zero added room-boundary clearance. The requested scene pose overlaps a fixed kitchen island. The user asks whether that placement works and asks for a Pascal link.", + "expected_output": "Keeps the scene-backed collision failure and omits the empty-room calculator link because it would contradict the requested conclusion.", + "files": [], + "semantic_case": "dimension-precheck-scene-conflict", + "expectations": [ + "Marks Open dimension-only footprint pre-check unavailable because the requested conclusion depends on the fixed-island collision.", + "Does not produce a link whose empty-room verdict could contradict the scene-backed result.", + "Names the scene-dependent collision as the reason the narrower calculator is unavailable." + ] + }, + { + "id": 16, + "prompt": "The user authorizes sending the measurements to Pascal. The exact rectangular room is 400 cm by 350 cm and the exact sofa footprint is 210 cm by 95 cm, but the user requires 60 cm only in front of the sofa and 10 cm at the sides. They ask for a report and a Pascal link.", + "expected_output": "Omits the dimension-only link because the calculator cannot represent directional clearance without changing the request.", + "files": [], + "semantic_case": "dimension-precheck-asymmetric-clearance", + "expectations": [ + "Marks Open dimension-only footprint pre-check unavailable because the requested clearances are directional.", + "Does not replace the directional values with zero or with item-to-item minimum clearance.", + "Retains the exact directional constraint in the scene-backed report." + ] + }, + { + "id": 17, + "prompt": "A generated coordinate import reports a rectangular room width of 1000001 cm and otherwise exact dimensions. The user asks for a Pascal pre-check link.", + "expected_output": "Omits the link because the room width exceeds the calculator's accepted maximum instead of emitting a link that silently falls back to defaults.", + "files": [], + "semantic_case": "dimension-precheck-over-limit", + "expectations": [ + "Marks Open dimension-only footprint pre-check unavailable because one value exceeds 1000000.", + "Does not clamp, round, or replace the value with a default.", + "Does not emit a calculator URL." + ] } ] } diff --git a/skills/furniture-fit/examples/insufficient-evidence.md b/skills/furniture-fit/examples/insufficient-evidence.md index f3537b881..69c70718b 100644 --- a/skills/furniture-fit/examples/insufficient-evidence.md +++ b/skills/furniture-fit/examples/insufficient-evidence.md @@ -16,6 +16,8 @@ Stop there. Do not add conditional maximum-size, fit, height, route, or alternat Do not create a placeholder with guessed dimensions and report it as a verified fit. +**Open dimension-only footprint pre-check:** unavailable — the room and item footprints are not exact rectangular measurements. + ```yaml nextAction: kind: request_measurement diff --git a/skills/furniture-fit/examples/no-sign-in-dimension-precheck.md b/skills/furniture-fit/examples/no-sign-in-dimension-precheck.md new file mode 100644 index 000000000..a8ec844e8 --- /dev/null +++ b/skills/furniture-fit/examples/no-sign-in-dimension-precheck.md @@ -0,0 +1,34 @@ +# Synthetic example: no-sign-in dimension pre-check + +## Inputs + +- Rectangular room: 400 cm wide × 350 cm deep +- Rectangular sofa footprint: 210 cm wide × 95 cm deep +- Uniform requested clearance: 20 cm on every side +- Exact source unit: centimeters +- The user explicitly asked for a Pascal link using these measurements +- No project, person, address, workspace, or private scene value is required for the pre-check + +## Report excerpt + +**Open dimension-only footprint pre-check:** https://editor.pascal.app/tools/furniture-fit?entry=agent_report&roomWidth=400&roomDepth=350&itemWidth=210&itemDepth=95&clearance=20&unit=cm&shared=1 + +This no-sign-in link sends the visible measurements to `editor.pascal.app` and may retain them in browser history and service request logs. It recomputes only the stated empty rectangular room and item footprints at 0° and 90° with 20 cm on every side. It does not carry or prove the report's project, position, collisions, existing-object spacing, doors, height, delivery route, detailed mesh, or scene-backed verdict. + +Do not add project IDs, revisions, graph hashes, node IDs, addresses, people, accounts, workspaces, credentials, signed URLs, flow IDs, or scene labels to the query string. + +```yaml +nextAction: + kind: check_related_item_or_pose + task: Check one other exact rectangular item footprint in the same measured room. + requiredInput: One exact item width and depth in centimeters. + context: + projectId: null + revision: null + graphHash: null + levelId: null + zoneId: null + itemId: null + authority: Read-only; no account or workspace changes, publication, save, or project mutation authorized. + cost: No rendering, generation, paid operation, or additional spending authorized. +``` diff --git a/skills/furniture-fit/references/report-template.md b/skills/furniture-fit/references/report-template.md index cd5d73b05..48d782573 100644 --- a/skills/furniture-fit/references/report-template.md +++ b/skills/furniture-fit/references/report-template.md @@ -73,6 +73,15 @@ The next action is optional. Do not execute it, create or switch accounts/worksp - Changed node IDs: - Editor URL returned by Pascal: +## Open dimension-only footprint pre-check + +- URL: `https://editor.pascal.app/tools/furniture-fit?entry=agent_report&roomWidth=&roomDepth=&itemWidth=&itemDepth=&clearance=&unit=&shared=1` | unavailable +- Representation: exact rectangular room and item footprints at 0° and 90° with one uniform per-side room-boundary clearance; use zero when none was requested +- Difference from the report: this no-sign-in calculator assumes an empty rectangular room and does not carry the project, pose, collisions, doors, height, delivery route, detailed mesh, or scene-backed verdict +- Unavailable reason: first missing, private, or unrepresentable input | not applicable + +Include the URL only after the user asks for it or confirms that the measurements may be sent to Pascal. Every represented dimension must be exact, positive, no greater than `1,000,000`, and safe to disclose; clearance may be zero. Omit it for directional clearance or whenever scene-specific evidence changes the requested conclusion. Never reuse item-to-item spacing as room-boundary clearance. Opening the link sends its visible measurement query to `editor.pascal.app` and can retain it in browser history and service request logs. Keep its query keys fixed. Never add project, revision, graph hash, node, address, person, account, workspace, credential, signed URL, `flow_id`, or arbitrary scene values. + Use `footprint` in the verdict sentence. Never turn untested rows into an unqualified purchase, delivery, safety, or code-compliance assurance. An empty issue list with missing geometry is not a pass. State `not checked` or `insufficient evidence` and name the missing geometry. A read-only candidate is absent from `verify_scene`; do not borrow that tool's clean result for the candidate.