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: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ jobs:
run: bun run check

- name: Validate agent skills and plugin packages
run: bun scripts/validate-skills.ts
run: |
bun test scripts/clawhub-ignore-policy.test.ts
bun scripts/validate-skills.ts

- name: Type check
run: bun run check-types
Expand Down
47 changes: 47 additions & 0 deletions scripts/clawhub-ignore-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, test } from 'bun:test'
import { readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { clawHubRequiredIgnorePatterns, validateClawHubIgnorePolicy } from './clawhub-ignore-policy'

const repositoryRoot = resolve(import.meta.dir, '..')
const canonicalPolicy = `${clawHubRequiredIgnorePatterns.join('\n')}\n`

describe('ClawHub ignore policy', () => {
test.each(['pascal-3d', 'furniture-fit'])('%s uses the protected policy', (skillName) => {
const content = readFileSync(
join(repositoryRoot, 'skills', skillName, '.clawhubignore'),
'utf8',
)
expect(validateClawHubIgnorePolicy(content)).toEqual([])
})

test.each([
['a broad re-inclusion', '!*'],
['a protected directory re-inclusion', '!dist/'],
['a nested protected file re-inclusion', '!screenshots/public.png'],
['a whitespace-prefixed re-inclusion', ' !.env.example'],
])('rejects %s rule appended after the exclusions', (_label, reinclude) => {
expect(validateClawHubIgnorePolicy(`${canonicalPolicy}${reinclude}\n`)).toContain(
`.clawhubignore must not contain re-inclusion rule ${reinclude.trim()}`,
)
})

test('rejects a later legacy ignore file that could override the canonical policy', () => {
expect(validateClawHubIgnorePolicy(canonicalPolicy, true)).toContain(
'.clawdhubignore must not coexist with the canonical ignore policy',
)
})

test('reports a missing protected pattern', () => {
const incompletePolicy = canonicalPolicy.replace('screenshots/\n', '')
expect(validateClawHubIgnorePolicy(incompletePolicy)).toContain(
'.clawhubignore is missing screenshots/',
)
})

test('rejects an empty canonical policy even when a legacy file exists', () => {
const failures = validateClawHubIgnorePolicy('', true)
expect(failures).toContain('.clawhubignore is missing .env*')
expect(failures).toContain('.clawdhubignore must not coexist with the canonical ignore policy')
})
})
43 changes: 43 additions & 0 deletions scripts/clawhub-ignore-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const clawHubRequiredIgnorePatterns = [
'.env*',
'.next/',
'dist/',
'node_modules/',
'coverage/',
'test-results/',
'playwright-report/',
'screenshots/',
'*.lock',
'*.lockb',
'package-lock.json',
'pnpm-lock.yaml',
'yarn.lock',
] as const

export function validateClawHubIgnorePolicy(
content: string,
hasLegacyIgnoreFile = false,
): string[] {
const patterns = content
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('#'))
const patternSet = new Set(patterns)
const failures: string[] = []

for (const pattern of clawHubRequiredIgnorePatterns) {
if (!patternSet.has(pattern)) failures.push(`.clawhubignore is missing ${pattern}`)
}

for (const pattern of patterns) {
if (pattern.startsWith('!')) {
failures.push(`.clawhubignore must not contain re-inclusion rule ${pattern}`)
}
}

if (hasLegacyIgnoreFile) {
failures.push('.clawdhubignore must not coexist with the canonical ignore policy')
}

return failures
}
9 changes: 9 additions & 0 deletions scripts/validate-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node
import { dirname, extname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { XMLParser, XMLValidator } from 'fast-xml-parser'
import { validateClawHubIgnorePolicy } from './clawhub-ignore-policy'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const skillNames = ['pascal-3d', 'furniture-fit'] as const
Expand Down Expand Up @@ -204,6 +205,14 @@ for (const entry of readdirSync(join(root, 'skills'), { withFileTypes: true }))
for (const skillName of skillNames) {
const skillRoot = join(root, 'skills', skillName)
const skillFile = join(skillRoot, 'SKILL.md')
const clawHubIgnoreFile = join(skillRoot, '.clawhubignore')
const clawHubIgnoreContent = read(clawHubIgnoreFile)
for (const policyFailure of validateClawHubIgnorePolicy(
clawHubIgnoreContent,
existsSync(join(skillRoot, '.clawdhubignore')),
)) {
fail(`${skillName}: ${policyFailure}`)
}
const content = read(skillFile)
const fields = frontmatter(content, skillFile)
if (fields.name !== skillName) fail(`${skillName}: frontmatter name does not match directory`)
Expand Down
3 changes: 2 additions & 1 deletion skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ The `source-reviewed` date records a code and public-documentation review. The `
## Validate the source package

```bash
bun test scripts/clawhub-ignore-policy.test.ts
bun scripts/validate-skills.ts
claude plugin validate . --strict
```

The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage.
The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage.
2 changes: 1 addition & 1 deletion skills/VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This release retains the portable root Agent Plugins manifest, Codex compatibili

Both skills require Pascal MCP tools for their tool-backed workflows. OpenAI's [submission guide](https://developers.openai.com/plugins/deploy/submission) therefore places this package on the **With MCP** route, combining the hosted MCP server with the skills, rather than Skills only. This source is not submission-ready: the production endpoint has not passed OpenAI Scan Tools for this candidate, OAuth-compatible reviewer access and credentials have not been prepared, domain verification has not been completed, and the positive review cases do not yet name provisioned disposable fixtures with stable identifiers and reset instructions. The review suite records those blockers explicitly. Platform access, verified publisher identity, countries, policy attestations, review, approval, and the developer's separate publish action also remain external. The validator follows the current [submission error reference](https://developers.openai.com/plugins/deploy/submission-errors) for the checked metadata constraints.

For ClawHub, both canonical skill folders pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround.
For ClawHub, both canonical skill folders carry scoped `.clawhubignore` policies that reject later re-inclusion rules and the legacy override file. Focused tests cover broad, directory, nested-file, and whitespace-prefixed negation attempts. Both folders pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround.

These checks do not create a ClawHub publisher or release, accept the mandatory MIT-0 publication terms, run ClawHub's post-upload security scanners, prove a live Pascal MCP connection from OpenClaw, or establish installs, useful tasks, or retention. Those remain separate release and adoption evidence.

Expand Down
13 changes: 13 additions & 0 deletions skills/furniture-fit/.clawhubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.env*
.next/
dist/
node_modules/
coverage/
test-results/
playwright-report/
screenshots/
*.lock
*.lockb
package-lock.json
pnpm-lock.yaml
yarn.lock
13 changes: 13 additions & 0 deletions skills/pascal-3d/.clawhubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.env*
.next/
dist/
node_modules/
coverage/
test-results/
playwright-report/
screenshots/
*.lock
*.lockb
package-lock.json
pnpm-lock.yaml
yarn.lock
Loading