Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"plugins": [
{
"name": "pascal-agent-skills",
"source": "./",
"source": "./skills",
"description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.",
"author": {
"name": "Pascal",
Expand All @@ -18,7 +18,7 @@
},
"version": "0.1.8",
"category": "productivity",
"skills": ["./skills/pascal-3d", "./skills/furniture-fit"]
"skills": ["./pascal-3d", "./furniture-fit"]
}
]
}
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"],
"skills": "./skills/",
"interface": {
"displayName": "Pascal agent skills",
"displayName": "Pascal",
"shortDescription": "Build 3D scenes and check fit",
"longDescription": "Use Pascal's MCP tools to work with editable building scenes, validate and save results, and produce bounded furniture footprint reports with explicit evidence, limitations, and blocker-aware next actions.",
"developerName": "Pascal",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pascal-agent-skills",
"displayName": "Pascal agent skills",
"displayName": "Pascal",
"version": "0.1.8",
"description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.",
"author": {
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

### Fixes

- The Claude Code plugin root is now `skills/` instead of the repository root, so installing `pascal-agent-skills@pascal` copies the two skill bundles and their MCP configuration instead of caching the whole monorepo and running `bun install` against the root lockfile ([#832](https://github.com/pascalorg/editor/pull/832))
- Preserve custom scene materials across save, load, clone, fork, and live sync. Materials were dropped at every persistence boundary, so a scene reopened with default surfaces. Collections were dropped on MCP import for the same reason ([#597](https://github.com/pascalorg/editor/pull/597)) by [@ShiroKSH](https://github.com/ShiroKSH)
- Wall junction mitering is now deterministic for exactly-collinear walls, so identical scenes produce identical geometry regardless of node iteration order ([#596](https://github.com/pascalorg/editor/pull/596)) by [@tomatotomata](https://github.com/tomatotomata)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Claude Code users can install the same canonical skill source as a plugin:
/plugin install pascal-agent-skills@pascal
```

The Claude plugin also supplies the local `pascal mcp connect` server. Install and start the Pascal CLI first, and keep `pascal` on the `PATH` used to launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically.
The Claude plugin also supplies the local `pascal mcp connect` server. Install and start the Pascal CLI first, and keep `pascal` on the `PATH` used to launch Claude Code. This local connector needs no Pascal account or API key and does not upload projects automatically. Its plugin root is this repository's `skills/` directory, so an install copies only the skill bundles and their plugin metadata rather than the repository.

Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Remove the manual entry before reloading or restarting Claude Code so only the plugin owns the connection lifecycle:

Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"extensions": {
"com.openai": {
"interface": {
"displayName": "Pascal agent skills",
"displayName": "Pascal",
"shortDescription": "Build 3D scenes and check fit",
"longDescription": "Use Pascal's MCP tools to work with editable building scenes, validate and save results, and produce bounded furniture footprint reports with explicit evidence, limitations, and blocker-aware next actions.",
"developerName": "Pascal",
Expand Down
8 changes: 4 additions & 4 deletions scripts/claude-mcp-config-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { validateClaudeMcpPolicy } from './claude-mcp-config-policy'

const repositoryRoot = resolve(import.meta.dir, '..')
const canonicalConfig = JSON.parse(
readFileSync(join(repositoryRoot, '.mcp.json'), 'utf8'),
readFileSync(join(repositoryRoot, 'skills', '.mcp.json'), 'utf8'),
) as unknown
const canonicalPlugin = JSON.parse(
readFileSync(join(repositoryRoot, '.claude-plugin', 'plugin.json'), 'utf8'),
readFileSync(join(repositoryRoot, 'skills', '.claude-plugin', 'plugin.json'), 'utf8'),
) as Record<string, unknown>
const marketplace = JSON.parse(
readFileSync(join(repositoryRoot, '.claude-plugin', 'marketplace.json'), 'utf8'),
Expand Down Expand Up @@ -79,8 +79,8 @@ describe('Claude plugin MCP configuration', () => {
canonicalMarketplaceEntry,
),
).toEqual([
'.mcp.json pascal server command must be pascal',
'.mcp.json pascal server args must be exactly ["mcp", "connect"]',
'skills/.mcp.json pascal server command must be pascal',
'skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]',
])
})

Expand Down
13 changes: 7 additions & 6 deletions scripts/claude-mcp-config-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,32 @@ export function validateClaudeMcpPolicy(
): string[] {
const failures: string[] = []
if (!isRecord(config) || !hasExactKeys(config, ['mcpServers'])) {
return ['.mcp.json must contain only the mcpServers object']
return ['skills/.mcp.json must contain only the mcpServers object']
}

const servers = config.mcpServers
if (!isRecord(servers) || !hasExactKeys(servers, ['pascal'])) {
return ['.mcp.json must contain exactly one server named pascal']
return ['skills/.mcp.json must contain exactly one server named pascal']
}

const pascal = servers.pascal
if (!isRecord(pascal) || !hasExactKeys(pascal, ['type', 'command', 'args'])) {
failures.push(
'.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed',
'skills/.mcp.json pascal server must contain only type, command, and args; remote or credential fields are not allowed',
)
return failures
}

if (pascal.type !== 'stdio') failures.push('.mcp.json pascal server type must be stdio')
if (pascal.command !== 'pascal') failures.push('.mcp.json pascal server command must be pascal')
if (pascal.type !== 'stdio') failures.push('skills/.mcp.json pascal server type must be stdio')
if (pascal.command !== 'pascal')
failures.push('skills/.mcp.json pascal server command must be pascal')
if (
!Array.isArray(pascal.args) ||
pascal.args.length !== 2 ||
pascal.args[0] !== 'mcp' ||
pascal.args[1] !== 'connect'
) {
failures.push('.mcp.json pascal server args must be exactly ["mcp", "connect"]')
failures.push('skills/.mcp.json pascal server args must be exactly ["mcp", "connect"]')
}

if (!isRecord(pluginManifest)) {
Expand Down
40 changes: 25 additions & 15 deletions scripts/validate-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ for (const discoveryFailure of validatePublicSkillDiscoverySurface(
}

for (const entry of readdirSync(join(root, 'skills'), { withFileTypes: true })) {
// skills/ is also the Claude plugin root, so its dot-entries carry plugin metadata, not bundles.
if (entry.name.startsWith('.')) continue
if (entry.isDirectory() && !skillNames.includes(entry.name as (typeof skillNames)[number])) {
fail(`OpenAI skills directory contains an unexpected non-skill directory: ${entry.name}`)
}
Expand Down Expand Up @@ -820,9 +822,10 @@ for (const item of publishingCases) {
if (positivePublishingCases < 5) fail('Publishing suite needs at least 5 positive cases')
if (negativePublishingCases < 3) fail('Publishing suite needs at least 3 negative cases')

const claudePlugin = parseJson(join(root, '.claude-plugin', 'plugin.json'))
const claudePluginRoot = join(root, 'skills')
const claudePlugin = parseJson(join(claudePluginRoot, '.claude-plugin', 'plugin.json'))
const claudeMarketplace = parseJson(join(root, '.claude-plugin', 'marketplace.json'))
const claudeMcpConfig = parseJson(join(root, '.mcp.json'))
const claudeMcpConfig = parseJson(join(claudePluginRoot, '.mcp.json'))
const portableMcpConfig = parseJson(join(root, 'mcp.json'))
const portablePlugin = parseJson(join(root, 'plugin.json'))
const codexPlugin = parseJson(join(root, '.codex-plugin', 'plugin.json'))
Expand Down Expand Up @@ -907,7 +910,7 @@ if (Object.keys(portableMcpConfig).sort().join(',') !== '$schema,mcpServers') {
fail('Portable mcp.json must contain only $schema and mcpServers')
}
if (canonicalJson(portableMcpConfig.mcpServers) !== canonicalJson(claudeMcpConfig.mcpServers)) {
fail('Portable mcp.json and .mcp.json must declare the same mcpServers block')
fail('Portable mcp.json and skills/.mcp.json must declare the same mcpServers block')
}

const portablePascalServer = (portableMcpConfig.mcpServers as Record<string, unknown> | undefined)
Expand Down Expand Up @@ -1097,23 +1100,30 @@ if (!Array.isArray(marketplacePlugins) || marketplacePlugins.length !== 1) {
for (const configFailure of validateClaudeMcpPolicy(claudeMcpConfig, claudePlugin, plugin)) {
fail(configFailure)
}
if (plugin.source !== './') fail('Claude marketplace plugin must use the repository root')
// The plugin root must stay skills/: a repository-root source makes Claude Code cache the whole
// monorepo and run bun install against the root lockfile on every install.
if (plugin.source !== './skills') {
fail('Claude marketplace plugin must use the skills directory as its plugin root')
}
// A listed skills array is the complete set Claude Code loads for the entry, so it must equal
// every packaged bundle; a new skills/<name>/SKILL.md is otherwise installed but never loaded.
const bundledSkillPaths = readdirSync(join(root, 'skills'), { withFileTypes: true })
const bundledSkillPaths = readdirSync(claudePluginRoot, { withFileTypes: true })
.filter(
(entry) => entry.isDirectory() && existsSync(join(root, 'skills', entry.name, 'SKILL.md')),
(entry) => entry.isDirectory() && existsSync(join(claudePluginRoot, entry.name, 'SKILL.md')),
)
.map((entry) => `./skills/${entry.name}`)
.map((entry) => `./${entry.name}`)
.sort()
const packagedSkills = plugin.skills
const listedSkillPaths = Array.isArray(packagedSkills)
? [...packagedSkills].map(String).sort()
: []
if (listedSkillPaths.join(',') !== bundledSkillPaths.join(',')) {
fail(
`Claude marketplace skills must list exactly the packaged bundles ${bundledSkillPaths.join(', ')}; found ${listedSkillPaths.join(', ') || 'none'}`,
)
for (const [label, descriptor] of [
['Claude marketplace', plugin],
['Claude plugin manifest', claudePlugin],
] as const) {
const declared = descriptor.skills
const declaredPaths = Array.isArray(declared) ? [...declared].map(String).sort() : []
if (declaredPaths.join(',') !== bundledSkillPaths.join(',')) {
fail(
`${label} skills must list exactly the packaged bundles ${bundledSkillPaths.join(', ')}; found ${declaredPaths.join(', ') || 'none'}`,
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin.json",
"name": "pascal-agent-skills",
"displayName": "Pascal agent skills",
"displayName": "Pascal",
"version": "0.1.8",
"description": "Create, inspect, validate, and assess furniture layouts with bounded next actions in Pascal through MCP.",
"author": {
Expand All @@ -12,5 +12,6 @@
"homepage": "https://editor.pascal.app/docs/developers/mcp",
"repository": "https://github.com/pascalorg/editor",
"license": "MIT",
"keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"]
"keywords": ["pascal", "3d", "architecture", "mcp", "furniture", "spatial"],
"skills": ["./pascal-3d", "./furniture-fit"]
}
File renamed without changes.
6 changes: 3 additions & 3 deletions skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ codex plugin marketplace add pascalorg/editor
codex plugin add pascal-agent-skills@pascal
```

The Claude plugin installs the instructions from the canonical `skills/` directory and supplies one local stdio server that runs `pascal mcp connect`. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically. Codex and individually installed skills still use the setup reference included in either skill.
The Claude plugin installs the instructions from the canonical `skills/` directory and supplies one local stdio server that runs `pascal mcp connect`. This `skills/` directory is itself the Claude plugin root, so an install copies only the two skill bundles and their plugin metadata rather than the repository. Install and start the Pascal CLI first, and keep `pascal` on Claude Code's `PATH`. The bundled local connector needs no Pascal account or API key and does not upload projects automatically. Codex and individually installed skills still use the setup reference included in either skill.

Claude Code 2.1.258 loads both the user-scoped `pascal` server created by `pascal mcp setup claude` and the plugin-provided server. Run `claude mcp remove --scope user pascal` before reloading or restarting Claude Code so only the plugin owns the connection lifecycle. Use `/mcp` to remove or disable any project- or local-scoped Pascal connection too. Leaving both connections active violates the one-active-agent-client-per-local-service requirement. For a hosted Pascal project, disable the plugin-provided local server in `/mcp`, then configure the hosted endpoint from the setup reference.

Plugin installation alone never creates an account, uploads a project, or authorizes paid work.

The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same server from `.mcp.json`. The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex.
The root [`plugin.json`](../plugin.json) is the portable Agent Plugins manifest used for OpenAI submission, and the root [`mcp.json`](../mcp.json) is the only MCP configuration path Codex and Cursor read; Claude Code reads the same server from [`.mcp.json`](.mcp.json) in its `skills/` plugin root, next to [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json). The repository keeps `.codex-plugin/plugin.json` as a compatibility fallback and validates that both expose the same OpenAI listing metadata. Public-directory submission, review, and publication are separate external steps; a Git marketplace install does not make the plugin publicly listed in ChatGPT or Codex.

The npm `beta` CLI remains on the older runtime contract. For the read-only `check_collisions.candidate` capability used by the current furniture workflow, follow the checksum-verified [GitHub preview instructions](pascal-3d/references/setup.md#verified-github-preview). The preview archive is published on GitHub, not npm.

Expand Down Expand Up @@ -111,4 +111,4 @@ bun run skills:validate

Run `claude plugin validate . --strict` manually as well. It stays out of the script and out of CI because it needs the Claude Code CLI, which is not installed on every runner.

The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact credential-free Claude local MCP configuration, an identical `mcp.json` server block for Codex and Cursor, a Gemini CLI manifest that runs the same command at the same version, a Claude marketplace skills list that equals the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage.
The repository validator checks the exact two-skill public discovery surface, keeps contributor-only workflows internal, and checks frontmatter, semantic skill versions, bundled links and their heading anchors, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the exact credential-free Claude local MCP configuration, an identical `mcp.json` server block for Codex and Cursor, a Gemini CLI manifest that runs the same command at the same version, Claude marketplace and plugin skills lists that equal the packaged bundles exactly, the publishing suite, the exact 46-tool OpenAI annotation and justification packet, portable and compatibility manifest consistency with one plugin version, description, and author across every plugin descriptor, OpenAI public-directory metadata limits including its documented interface fields, bundled branding assets, and accidental private-path or credential leakage.
Loading