mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-23 03:27:50 +09:00
fix(skills): scope host compatibility to target projections
This commit is contained in:
31 files changed
+1482
-36
No files matched your search
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: skill-host-compat
|
||||
description: Lint ECC skills for Codex/Cursor-safe frontmatter and Claude-only substitutions before a skill PR.
|
||||
---
|
||||
|
||||
# Skill Host Compatibility
|
||||
|
||||
Run the same host-compat gate CI uses on `skills/` before opening a skill PR.
|
||||
The check is for Codex and Cursor copies, not Claude-only runtime quality.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Adding or editing a skill under `skills/`
|
||||
- Copying a skill into `.agents/skills/` or `.cursor/skills/`
|
||||
- A review asks whether a skill is safe to install into Codex or Cursor
|
||||
- CI reports `validate-skill-host-compat.js` findings
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Canonical skills live in `skills/`. Codex loads `.agents/skills/`. Cursor loads a
|
||||
smaller `.cursor/skills/` subset. CONTRIBUTING documents that sync as manual.
|
||||
|
||||
`validate-skills.js` only checks that `SKILL.md` exists and that `name` /
|
||||
`description` are usable YAML. `tests/ci/codex-skill-surface.test.js` only
|
||||
sees `.agents/skills/`. This script is the cross-tree gate:
|
||||
|
||||
1. Claude-only substitutions in executable `bash` / `sh` / `zsh` fences
|
||||
(`$CLAUDE_SESSION_ID`, `$CLAUDE_SKILL_DIR`, `$CLAUDE_PLUGIN_ROOT`,
|
||||
`$CLAUDE_PROJECT_DIR`, `$ARGUMENTS`) fail. Codex leaves those tokens
|
||||
as literal text. In a Codex copy, rewrite `$CLAUDE_PROJECT_DIR` to
|
||||
`$(pwd)` or an explicit path.
|
||||
2. A Codex copy may only use `name`, `description`, `metadata`, `license`,
|
||||
and `allowed-tools`. `origin: ECC` may stay on the canonical `skills/`
|
||||
copy and must be stripped on the Codex copy.
|
||||
3. A skill listed in `agents/openai.yaml` that has a `skills/` directory must
|
||||
have `.agents/skills/<name>/`. Cursor copies are optional; when present,
|
||||
`name:` must match the directory.
|
||||
4. CONTRIBUTING checklist items (When to Activate, Anti-Patterns, name
|
||||
matches directory, 500/800 line caps) warn. They fail only with
|
||||
`--strict-checklist`.
|
||||
|
||||
`validate-skills.js` frontmatter WARN default is unchanged (#1663).
|
||||
|
||||
## Code Examples
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js
|
||||
```
|
||||
|
||||
Print counts without failing:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js --inventory
|
||||
```
|
||||
|
||||
Promote checklist warnings:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js --strict-checklist
|
||||
```
|
||||
|
||||
Slash command:
|
||||
|
||||
```
|
||||
$skill-host-compat
|
||||
$skill-host-compat --inventory
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Codex and Cursor projections must resolve Claude-only substitutions in executable blocks. Canonical Claude-scoped examples remain valid. POSIX `$1` and `${1}` parameters are ambiguous and require review rather than failing this gate. This fence is the
|
||||
forbidden pattern; do not use these unresolved projection examples:
|
||||
|
||||
```bash
|
||||
echo "$CLAUDE_SESSION_ID"
|
||||
ls "$CLAUDE_SKILL_DIR"
|
||||
node "$CLAUDE_PLUGIN_ROOT/scripts/setup.js" $ARGUMENTS
|
||||
cd "$CLAUDE_PROJECT_DIR"
|
||||
```
|
||||
|
||||
Do not add `version:` or `origin:` to a Codex copy under `.agents/skills/`.
|
||||
Do not treat a passing `validate-skills.js` run as Codex-safe. Do not require
|
||||
every `skills/` entry to have a Cursor copy.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep canonical `origin: ECC` if the CONTRIBUTING template calls for it.
|
||||
- Drop `origin`, `version`, and `argument-hint` on the Codex copy.
|
||||
- Show host-portable commands in bash fences (`npx`, `ecc`, or a resolved
|
||||
root). Rewrite `$CLAUDE_PROJECT_DIR` in Codex copies to `$(pwd)` or an
|
||||
explicit path. Keep valid Claude-scoped forms in canonical instructions; resolve host variables in the target projection.
|
||||
- Run this script before `npm test` when the change is a new skill.
|
||||
- Use `--inventory` to see checklist warning volume without failing the tree.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `skill-comply` — runtime behavioral traces, not host-portability
|
||||
- `skill-stocktake` — holistic audit of installed `~/.claude/skills`
|
||||
- `skill-scout` — search existing skills before creating a new one
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Skill Host Compat"
|
||||
short_description: "Lint skills for Codex-safe frontmatter and substitutions"
|
||||
brand_color: "#6366F1"
|
||||
default_prompt: "Use $skill-host-compat to lint skills for Codex/Cursor-safe frontmatter and Claude-only substitutions."
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"source": "./",
|
||||
"description": "Harness-native ECC operator layer - 71 agents, 302 skills, 95 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||
"description": "Harness-native ECC operator layer - 71 agents, 303 skills, 96 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
|
||||
"version": "2.2.3",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ecc",
|
||||
"version": "2.2.3",
|
||||
"description": "Harness-native ECC plugin for engineering teams - 71 agents, 302 skills, 95 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
|
||||
"description": "Harness-native ECC plugin for engineering teams - 71 agents, 303 skills, 96 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
|
||||
"author": {
|
||||
"name": "Affaan Mustafa",
|
||||
"url": "https://x.com/affaanmustafa"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ This file provides Gemini CLI with the baseline ECC workflow, review standards,
|
||||
|
||||
## Overview
|
||||
|
||||
ECC is a cross-harness coding system with 71 specialized agents, 302 skills, and 95 commands.
|
||||
ECC is a cross-harness coding system with 71 specialized agents, 303 skills, and 96 commands.
|
||||
|
||||
Gemini support is currently focused on a strong project-local instruction layer via `.gemini/GEMINI.md`, plus the shared MCP catalog and package-manager setup assets shipped by the installer.
|
||||
|
||||
|
||||
@@ -211,6 +211,10 @@ jobs:
|
||||
run: node scripts/ci/validate-skills.js
|
||||
continue-on-error: false
|
||||
|
||||
- name: Validate skill host compatibility
|
||||
run: node scripts/ci/validate-skill-host-compat.js
|
||||
continue-on-error: false
|
||||
|
||||
- name: Validate install manifests
|
||||
run: node scripts/ci/validate-install-manifests.js
|
||||
continue-on-error: false
|
||||
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
- name: Validate skills
|
||||
run: node scripts/ci/validate-skills.js
|
||||
|
||||
- name: Validate skill host compatibility
|
||||
run: node scripts/ci/validate-skill-host-compat.js
|
||||
|
||||
- name: Validate install manifests
|
||||
run: node scripts/ci/validate-install-manifests.js
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ECC — Agent Instructions
|
||||
|
||||
This is a **production-ready AI coding plugin** providing 71 specialized agents, 302 skills, 95 commands, and automated hook workflows for software development.
|
||||
This is a **production-ready AI coding plugin** providing 71 specialized agents, 303 skills, 96 commands, and automated hook workflows for software development.
|
||||
|
||||
**Version:** 2.2.3
|
||||
|
||||
@@ -202,8 +202,8 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
|
||||
|
||||
```
|
||||
agents/ — 71 specialized subagents
|
||||
skills/ — 302 workflow skills and domain knowledge
|
||||
commands/ — 95 slash commands
|
||||
skills/ — 303 workflow skills and domain knowledge
|
||||
commands/ — 96 slash commands
|
||||
hooks/ — Trigger-based automations
|
||||
rules/ — Always-follow guidelines (common + per-language)
|
||||
scripts/ — Cross-platform Node.js utilities
|
||||
|
||||
@@ -180,6 +180,7 @@ These lifecycle commands are also available through the `ecc` CLI.
|
||||
| `/instinct-import` | Import instincts from a file or URL |
|
||||
| `/skill-create` | Analyse local git history → generate a reusable skill |
|
||||
| `/skill-health` | Skill portfolio health dashboard with analytics |
|
||||
| `/skill-host-compat` | Lint skills for Codex-safe frontmatter and Claude substitutions |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -140,13 +140,13 @@ Instead of rebuilding that process in every prompt, you install it once and make
|
||||
|
||||
ECC is MIT-licensed open source. It works best with Claude Code today, has native Codex and Qoder plugin paths, and provides capability-limited adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. See the [support status matrix](#platform-support) before assuming feature parity.
|
||||
|
||||
Access to 71 agents, 302 skills, and 95 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work.
|
||||
Access to 71 agents, 303 skills, and 96 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work.
|
||||
|
||||
| Included | Count | What it gives you |
|
||||
| ---------------- | ----------: | ------------------------------------------------------------------------------------ |
|
||||
| Agents | 71 agents | Planning, review, build repair, security, architecture, and domain work |
|
||||
| Skills | 302 skills | TDD, research, security, docs, frontend, data, ML, operations, and more |
|
||||
| Commands | 95 commands | Convenient entry points while ECC moves to a skills-first surface |
|
||||
| Skills | 303 skills | TDD, research, security, docs, frontend, data, ML, operations, and more |
|
||||
| Commands | 96 commands | Convenient entry points while ECC moves to a skills-first surface |
|
||||
| Hooks and memory | Runtime | Enforcement, session summaries, continuous learning, instincts, and context controls |
|
||||
| Rules | Selective | Always-loaded standards you choose by language or project |
|
||||
| AgentShield | Included | Scanning for prompts, hooks, MCP config, permissions, secrets, and agent files |
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**完成!** 你现在可以使用 71 个代理、302 个技能和 95 个命令。
|
||||
**完成!** 你现在可以使用 71 个代理、303 个技能和 96 个命令。
|
||||
|
||||
### multi-* 命令需要额外配置
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Soul
|
||||
|
||||
## Core Identity
|
||||
ECC is a production-ready AI coding plugin with 71 specialized agents, 302 skills, 95 commands, and automated hook workflows for software development.
|
||||
ECC is a production-ready AI coding plugin with 71 specialized agents, 303 skills, 96 commands, and automated hook workflows for software development.
|
||||
|
||||
## Core Principles
|
||||
1. **Agent-First** — route work to the right specialist as early as possible.
|
||||
|
||||
@@ -147,6 +147,7 @@ skills:
|
||||
- security-review
|
||||
- security-scan
|
||||
- skill-comply
|
||||
- skill-host-compat
|
||||
- skill-stocktake
|
||||
- springboot-patterns
|
||||
- springboot-security
|
||||
@@ -261,6 +262,7 @@ commands:
|
||||
- setup-pm
|
||||
- skill-create
|
||||
- skill-health
|
||||
- skill-host-compat
|
||||
- test-coverage
|
||||
- token-card
|
||||
- update-codemaps
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: skill-host-compat
|
||||
description: Lint ECC skills for Codex/Cursor-safe frontmatter and Claude-only substitutions before a skill PR.
|
||||
command: true
|
||||
---
|
||||
|
||||
# Skill Host Compatibility
|
||||
|
||||
Lint curated `skills/` for Codex-safe frontmatter and Claude-only substitutions.
|
||||
|
||||
## Implementation
|
||||
|
||||
Run the host-compat validator:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)}")}"
|
||||
node "$ECC_ROOT/scripts/ci/validate-skill-host-compat.js"
|
||||
```
|
||||
|
||||
Inventory counts without failing:
|
||||
|
||||
```bash
|
||||
ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(function(){var p=require('path'),f=require('fs'),o=require('os');var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var d=p.join(o.homedir(),'.claude');function L(x){try{return require(p.join(x,'scripts','lib','resolve-ecc-root')).resolveEccRoot()}catch(_){return null}}var r=L(d);if(r)return r;var s=['ecc','ecc@ecc','marketplaces/ecc','everything-claude-code','everything-claude-code@everything-claude-code','marketplaces/everything-claude-code'];for(var i=0;i<s.length;i++){r=L(p.join(d,'plugins',s[i]));if(r)return r}try{var g=['ecc','everything-claude-code'];for(var j=0;j<g.length;j++){var c=p.join(d,'plugins','cache',g[j]);var O=f.readdirSync(c);for(var k=0;k<O.length;k++){var q=p.join(c,O[k]);var V=f.readdirSync(q);for(var m=0;m<V.length;m++){r=L(p.join(q,V[m]));if(r)return r}}}}catch(_){}return d})();console.log(r)}")}"
|
||||
node "$ECC_ROOT/scripts/ci/validate-skill-host-compat.js" --inventory
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/skill-host-compat # Fail on substitutions and Codex drift
|
||||
/skill-host-compat --inventory # JSON counts, exit 0
|
||||
/skill-host-compat --strict-checklist
|
||||
```
|
||||
|
||||
## What to Do
|
||||
|
||||
1. Run `scripts/ci/validate-skill-host-compat.js` from the ECC root
|
||||
2. Show ERROR and WARN lines to the user
|
||||
3. For substitution errors, resolve Claude env vars in the affected Codex/Cursor projection. Rewrite `$CLAUDE_PROJECT_DIR` to `$(pwd)` or an explicit path
|
||||
4. For Codex-key errors, drop `origin` / `version` / `argument-hint` on the copy
|
||||
@@ -37,6 +37,7 @@ This document lists each slash command and the primary agent(s) or skills it inv
|
||||
| `/promote` | — | continuous-learning-v2 |
|
||||
| `/projects` | — | continuous-learning-v2 |
|
||||
| `/skill-create` | — | skill-create-output script, git history |
|
||||
| `/skill-host-compat` | — | validate-skill-host-compat.js, Codex/Cursor skill lint |
|
||||
| `/checkpoint` | — | verification-loop skill |
|
||||
| `/verify` | — | verification-loop skill |
|
||||
| `/eval` | — | eval-harness skill |
|
||||
@@ -71,6 +72,7 @@ This document lists each slash command and the primary agent(s) or skills it inv
|
||||
- **verification-loop**: `/checkpoint`, `/verify`
|
||||
- **eval-harness**: `/eval`
|
||||
- **security-scan**: `/security-scan` (runs AgentShield)
|
||||
- **skill-host-compat**: `/skill-host-compat`
|
||||
- **strategic-compact**: suggested at compaction points (hooks)
|
||||
- **unified-memory**: `ecc memory ...` and the opt-in `ecc-memory-mcp` server
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"totalCommands": 95,
|
||||
"totalCommands": 96,
|
||||
"commands": [
|
||||
{
|
||||
"command": "aside",
|
||||
@@ -981,6 +981,17 @@
|
||||
"skills": [],
|
||||
"path": "commands/skill-health.md"
|
||||
},
|
||||
{
|
||||
"command": "skill-host-compat",
|
||||
"description": "Lint ECC skills for Codex/Cursor-safe frontmatter and Claude-only substitutions before a skill PR.",
|
||||
"type": "general",
|
||||
"primaryAgents": [],
|
||||
"allAgents": [],
|
||||
"skills": [
|
||||
"skill-host-compat"
|
||||
],
|
||||
"path": "commands/skill-host-compat.md"
|
||||
},
|
||||
{
|
||||
"command": "test-coverage",
|
||||
"description": "Analyze coverage, identify gaps, and generate missing tests toward the target threshold.",
|
||||
@@ -1040,7 +1051,7 @@
|
||||
"statistics": {
|
||||
"byType": {
|
||||
"build": 2,
|
||||
"general": 11,
|
||||
"general": 12,
|
||||
"orchestration": 11,
|
||||
"planning": 2,
|
||||
"refactoring": 1,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# ECC — Agent Talimatları
|
||||
|
||||
Bu, yazılım geliştirme için 71 özel agent, 302 skill, 95 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
|
||||
Bu, yazılım geliştirme için 71 özel agent, 303 skill, 96 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
|
||||
|
||||
**Sürüm:** 2.2.3
|
||||
|
||||
@@ -142,8 +142,8 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl
|
||||
|
||||
```
|
||||
agents/ — 71 özel subagent
|
||||
skills/ — 302 iş akışı skillleri ve alan bilgisi
|
||||
commands/ — 95 slash command
|
||||
skills/ — 303 iş akışı skillleri ve alan bilgisi
|
||||
commands/ — 96 slash command
|
||||
hooks/ — Tetikleyici tabanlı otomasyonlar
|
||||
rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel)
|
||||
scripts/ — Platformlar arası Node.js yardımcı programları
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# ECC — 智能体指令
|
||||
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 71 个专业代理、302 项技能、95 条命令以及自动化钩子工作流,用于软件开发。
|
||||
这是一个**生产就绪的 AI 编码插件**,提供 71 个专业代理、303 项技能、96 条命令以及自动化钩子工作流,用于软件开发。
|
||||
|
||||
**版本:** 2.2.3
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
|
||||
```
|
||||
agents/ — 71 个专业子代理
|
||||
skills/ — 302 个工作流技能和领域知识
|
||||
commands/ — 95 个斜杠命令
|
||||
skills/ — 303 个工作流技能和领域知识
|
||||
commands/ — 96 个斜杠命令
|
||||
hooks/ — 基于触发的自动化
|
||||
rules/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||
scripts/ — 跨平台 Node.js 实用工具
|
||||
|
||||
@@ -264,7 +264,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
|
||||
/plugin list ecc@ecc
|
||||
```
|
||||
|
||||
**搞定!** 你现在可以使用 71 个智能体、302 项技能和 95 个命令了。
|
||||
**搞定!** 你现在可以使用 71 个智能体、303 项技能和 96 个命令了。
|
||||
|
||||
***
|
||||
|
||||
@@ -1177,8 +1177,8 @@ opencode
|
||||
| 功能特性 | Claude Code | OpenCode | 状态 |
|
||||
|---------|---------------|----------|--------|
|
||||
| 智能体 | PASS: 71 个 | PASS: 12 个 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 95 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 302 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 命令 | PASS: 96 个 | PASS: 35 个 | **Claude Code 领先** |
|
||||
| 技能 | PASS: 303 项 | PASS: 37 项 | **Claude Code 领先** |
|
||||
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
|
||||
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
|
||||
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
|
||||
@@ -1285,8 +1285,8 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|
||||
| 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode |
|
||||
|---------|-----------------------|------------|-----------|----------|
|
||||
| **智能体** | 71 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
|
||||
| **命令** | 95 | 共享 | 基于指令 | 35 |
|
||||
| **技能** | 302 | 共享 | 10 (原生格式) | 37 |
|
||||
| **命令** | 96 | 共享 | 基于指令 | 35 |
|
||||
| **技能** | 303 | 共享 | 10 (原生格式) | 37 |
|
||||
| **钩子事件** | 8 种类型 | 15 种类型 | SessionStart(1 种类型) | 11 种类型 |
|
||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 |
|
||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"id": "hand-seeded",
|
||||
"source": "manual-curation-pending-regeneration"
|
||||
},
|
||||
"registryDigest": "1d84b5f3852a014edbaa36c038561382f6e11b7b69a011d8801c7b8abd22fbb7",
|
||||
"registryDigest": "577a749c4ea19a03c2cd5263f1b1797cc45d395cae2b934b307c912bb2a97059",
|
||||
"schemaVersion": 1,
|
||||
"triggers": {
|
||||
"skill:api-connector-builder": [
|
||||
|
||||
@@ -65,7 +65,9 @@
|
||||
"paths": [
|
||||
"commands",
|
||||
"scripts/harness-audit.js",
|
||||
"scripts/skills-health.js"
|
||||
"scripts/skills-health.js",
|
||||
"scripts/ci/validate-skill-host-compat.js",
|
||||
"scripts/ci/validate-skills.js"
|
||||
],
|
||||
"targets": [
|
||||
"claude",
|
||||
@@ -369,6 +371,7 @@
|
||||
"skills/plankton-code-quality",
|
||||
"skills/production-audit",
|
||||
"skills/skill-comply",
|
||||
"skills/skill-host-compat",
|
||||
"skills/skill-scout",
|
||||
"skills/skill-stocktake",
|
||||
"skills/strategic-compact",
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"security-scan": "wraps AgentShield commercial product",
|
||||
"seo": "SEO/marketing",
|
||||
"skill-comply": "runs agent rosters for compliance checks",
|
||||
"skill-host-compat": "runs an ECC repo CI script (scripts/ci/validate-skill-host-compat.js)",
|
||||
"skill-scout": "network searches of skill marketplaces",
|
||||
"skill-stocktake": "subagent-based Claude skill audit",
|
||||
"social-graph-ranker": "social graph (X and LinkedIn)",
|
||||
@@ -438,6 +439,7 @@
|
||||
"setup-pm.md": "runs an ECC repo script (scripts/setup-package-manager.js)",
|
||||
"skill-create.md": "Claude Code skill authoring plus instincts",
|
||||
"skill-health.md": "ECC skill analytics dashboard",
|
||||
"skill-host-compat.md": "runs an ECC repo CI script (scripts/ci/validate-skill-host-compat.js)",
|
||||
"vue-review.md": "invokes ECC agent roster (vue-reviewer)",
|
||||
"token-card.md": "Requires the tokenchit CLI and provider session logs via the token-card skill."
|
||||
}
|
||||
|
||||
+5
-2
@@ -113,6 +113,8 @@
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/ci/supply-chain-advisory-sources.js",
|
||||
"scripts/ci/validate-skill-host-compat.js",
|
||||
"scripts/ci/validate-skills.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/profile.js",
|
||||
"scripts/auto-update.js",
|
||||
@@ -355,6 +357,7 @@
|
||||
"skills/seo/",
|
||||
"skills/skill-scout/",
|
||||
"skills/skill-comply/",
|
||||
"skills/skill-host-compat/",
|
||||
"skills/skill-stocktake/",
|
||||
"skills/social-graph-ranker/",
|
||||
"skills/springboot-patterns/",
|
||||
@@ -513,10 +516,10 @@
|
||||
"orchestrate:status": "node scripts/orchestration-status.js",
|
||||
"orchestrate:worker": "bash scripts/orchestrate-codex-worker.sh",
|
||||
"orchestrate:tmux": "node scripts/orchestrate-worktrees.js",
|
||||
"test:validators": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check",
|
||||
"test:validators": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-skill-host-compat.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check",
|
||||
"test:js": "node tests/run-all.js",
|
||||
"test:python": "python3 -m pytest tests/ -q",
|
||||
"test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js",
|
||||
"test": "node scripts/ci/check-unicode-safety.js && node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-skill-host-compat.js && node scripts/ci/validate-hooks.js && node scripts/ci/check-hooks-schema-keys.js && node scripts/ci/validate-install-manifests.js && node scripts/ci/validate-context-profiles.js && node scripts/ci/validate-no-personal-paths.js && npm run catalog:check && npm run command-registry:check && node tests/run-all.js",
|
||||
"coverage:js": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
|
||||
"coverage:python": "python3 -m pytest tests/ -q --cov=src --cov-report=term --cov-fail-under=80",
|
||||
"coverage": "c8 --all --include=\"scripts/**/*.js\" --include=\"scripts/**/*.mjs\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
# Curation
|
||||
|
||||
pi/core includes 128 of 302 skills and 24 of 95 commands from the root of ECC.
|
||||
pi/core includes 128 of 303 skills and 24 of 96 commands from the root of ECC.
|
||||
Everything excluded is listed here with its reason.
|
||||
|
||||
## Rules
|
||||
@@ -163,6 +163,7 @@ Exclude anything that:
|
||||
| `security-scan` | wraps AgentShield commercial product |
|
||||
| `seo` | SEO/marketing |
|
||||
| `skill-comply` | runs agent rosters for compliance checks |
|
||||
| `skill-host-compat` | runs an ECC repo CI script (scripts/ci/validate-skill-host-compat.js) |
|
||||
| `skill-scout` | network searches of skill marketplaces |
|
||||
| `skill-stocktake` | subagent-based Claude skill audit |
|
||||
| `social-graph-ranker` | social graph (X and LinkedIn) |
|
||||
@@ -267,6 +268,7 @@ Exclude anything that:
|
||||
| `setup-pm` | runs an ECC repo script (scripts/setup-package-manager.js) |
|
||||
| `skill-create` | Claude Code skill authoring plus instincts |
|
||||
| `skill-health` | ECC skill analytics dashboard |
|
||||
| `skill-host-compat` | runs an ECC repo CI script (scripts/ci/validate-skill-host-compat.js) |
|
||||
| `vue-review` | invokes ECC agent roster (vue-reviewer) |
|
||||
| `token-card` | Requires the tokenchit CLI and provider session logs via the token-card skill. |
|
||||
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lint curated skills for host-portable frontmatter and substitutions.
|
||||
*
|
||||
* Checks:
|
||||
* 1. Claude-only substitutions in Codex/Cursor executable bash/sh/zsh fences
|
||||
* (${CLAUDE_SESSION_ID}, ${CLAUDE_SKILL_DIR}, ${CLAUDE_PLUGIN_ROOT},
|
||||
* ${CLAUDE_PROJECT_DIR}, $ARGUMENTS). Codex and a bare shell leave
|
||||
* those tokens literal. Rewrite ${CLAUDE_PROJECT_DIR} in Codex copies
|
||||
* to $(pwd) or an explicit path.
|
||||
* 2. Codex-copy frontmatter allowlist on .agents/skills/<name>/SKILL.md
|
||||
* (name, description, metadata, license, allowed-tools). origin/version
|
||||
* may remain on the canonical skills/ copy.
|
||||
* 3. Subset drift: agents/openai.yaml names a skill that is missing from
|
||||
* .agents/skills/, or a Codex/Cursor copy name: does not match its
|
||||
* directory.
|
||||
* 4. CONTRIBUTING checklist (WARN unless --strict-checklist): When to
|
||||
* Activate, Anti-Patterns, name matches directory, 500/800 line caps.
|
||||
*
|
||||
* Default invocation is fail-closed on (1)-(3). Pass --inventory to print
|
||||
* JSON counts and exit 0. Do not flip validate-skills.js to --strict;
|
||||
* that file's WARN default is load-bearing for #1663.
|
||||
*
|
||||
* Scope: canonical checklist plus host-specific Codex/Cursor projections.
|
||||
* POSIX positional parameters ($1, ${1}) are ambiguous and never fail this gate. If skills/ is absent,
|
||||
* exit 0 (nothing to validate).
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { extractFrontmatter, inspectFrontmatter } = require('./validate-skills');
|
||||
|
||||
const DEFAULT_ROOT = path.join(__dirname, '../..');
|
||||
|
||||
const ALLOWED_CODEX_FRONTMATTER_KEYS = new Set([
|
||||
'allowed-tools',
|
||||
'description',
|
||||
'license',
|
||||
'metadata',
|
||||
'name',
|
||||
]);
|
||||
|
||||
const CLAUDE_SUBSTITUTION_PATTERNS = [
|
||||
{ token: '${CLAUDE_SESSION_ID}', regex: /\$\{CLAUDE_SESSION_ID\}/g },
|
||||
{ token: '$CLAUDE_SESSION_ID', regex: /\$CLAUDE_SESSION_ID\b/g },
|
||||
{ token: '${CLAUDE_SKILL_DIR}', regex: /\$\{CLAUDE_SKILL_DIR\}/g },
|
||||
{ token: '$CLAUDE_SKILL_DIR', regex: /\$CLAUDE_SKILL_DIR\b/g },
|
||||
{ token: '${CLAUDE_PLUGIN_ROOT}', regex: /\$\{CLAUDE_PLUGIN_ROOT\}/g },
|
||||
{ token: '$CLAUDE_PLUGIN_ROOT', regex: /\$CLAUDE_PLUGIN_ROOT\b/g },
|
||||
{ token: '${CLAUDE_PROJECT_DIR}', regex: /\$\{CLAUDE_PROJECT_DIR\}/g },
|
||||
{ token: '$CLAUDE_PROJECT_DIR', regex: /\$CLAUDE_PROJECT_DIR\b/g },
|
||||
{ token: '$ARGUMENTS', regex: /\$ARGUMENTS\b|\$\{ARGUMENTS\}/g },
|
||||
|
||||
];
|
||||
|
||||
const EXECUTABLE_FENCE_LANGS = new Set(['bash', 'sh', 'zsh']);
|
||||
const CHECKLIST_CODES = new Set([
|
||||
'missing-when-to-activate',
|
||||
'missing-anti-patterns',
|
||||
'name-mismatch',
|
||||
'line-cap-500',
|
||||
'line-cap-800',
|
||||
]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
root: DEFAULT_ROOT,
|
||||
inventory: false,
|
||||
strictChecklist: false,
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--inventory') {
|
||||
args.inventory = true;
|
||||
} else if (arg === '--strict-checklist') {
|
||||
args.strictChecklist = true;
|
||||
} else if (arg === '--root') {
|
||||
const value = argv[i + 1];
|
||||
if (!value) {
|
||||
throw new Error('--root requires a directory path');
|
||||
}
|
||||
args.root = value;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function listSkillDirs(dir) {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function readFileIfPresent(filePath) {
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
function unquoteYamlScalar(value) {
|
||||
if (value === undefined || value === null) return '';
|
||||
const trimmed = String(value).trim();
|
||||
if (
|
||||
(trimmed.startsWith('"') && trimmed.endsWith('"'))
|
||||
|| (trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||
) {
|
||||
return trimmed.slice(1, -1);
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function extractTopLevelList(yamlSource, key) {
|
||||
const lines = String(yamlSource).replace(/^\uFEFF/, '').split(/\r?\n/);
|
||||
const results = [];
|
||||
let collecting = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!collecting) {
|
||||
if (line.trim() === `${key}:`) {
|
||||
collecting = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[A-Za-z0-9_-]+:\s*/.test(line)) {
|
||||
break;
|
||||
}
|
||||
|
||||
const match = line.match(/^\s*-\s+(.+?)\s*$/);
|
||||
if (match) {
|
||||
results.push(unquoteYamlScalar(match[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function listOpenaiYamlSkills(root) {
|
||||
const filePath = path.join(root, 'agents', 'openai.yaml');
|
||||
const source = readFileIfPresent(filePath);
|
||||
if (source === null) return [];
|
||||
return extractTopLevelList(source, 'skills');
|
||||
}
|
||||
|
||||
function stripFrontmatter(content) {
|
||||
const fm = extractFrontmatter(content);
|
||||
if (!fm.present) {
|
||||
return { frontmatter: fm, body: content.replace(/^\uFEFF/, ''), bodyOffset: 0 };
|
||||
}
|
||||
const match = content.replace(/^\uFEFF/, '').match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/);
|
||||
const bodyOffset = match ? match[0].length : 0;
|
||||
return {
|
||||
frontmatter: fm,
|
||||
body: content.replace(/^\uFEFF/, '').slice(bodyOffset),
|
||||
bodyOffset,
|
||||
};
|
||||
}
|
||||
|
||||
function findExecutableFences(body) {
|
||||
const fences = [];
|
||||
const fenceRe = /^```([^\r\n]*)\r?\n([\s\S]*?)^```[ \t]*(?:\r?\n|$)/gm;
|
||||
let match = fenceRe.exec(body);
|
||||
while (match) {
|
||||
const info = (match[1] || '').trim();
|
||||
const lang = info === '' ? '' : info.split(/\s+/)[0].toLowerCase();
|
||||
const content = match[2];
|
||||
const executable = EXECUTABLE_FENCE_LANGS.has(lang)
|
||||
|| (lang === '' && /^\s*#!/.test(content));
|
||||
if (executable) {
|
||||
fences.push({
|
||||
lang: lang || 'shebang',
|
||||
content,
|
||||
start: match.index,
|
||||
});
|
||||
}
|
||||
match = fenceRe.exec(body);
|
||||
}
|
||||
return fences;
|
||||
}
|
||||
|
||||
function precedingAllowsToken(body, fenceStart) {
|
||||
const prefix = body.slice(0, fenceStart).replace(/\s+$/, '');
|
||||
const lines = prefix.split(/\r?\n/);
|
||||
const window = lines.slice(-3).join('\n');
|
||||
return /anti-pattern/i.test(window) || /do not/i.test(window);
|
||||
}
|
||||
|
||||
function findSubstitutionTokens(fenceContent) {
|
||||
return CLAUDE_SUBSTITUTION_PATTERNS.filter(pattern => {
|
||||
pattern.regex.lastIndex = 0;
|
||||
return pattern.regex.test(fenceContent);
|
||||
}).map(pattern => pattern.token);
|
||||
}
|
||||
|
||||
function inspectProjectionSubstitutions(skillName, content, treeLabel, findings) {
|
||||
const { body } = stripFrontmatter(content);
|
||||
for (const fence of findExecutableFences(body)) {
|
||||
if (precedingAllowsToken(body, fence.start)) continue;
|
||||
for (const token of findSubstitutionTokens(fence.content)) {
|
||||
pushFinding(findings, { severity: 'error', code: 'substitution', skill: skillName,
|
||||
token, message: `${treeLabel}/${substitutionMessage(skillName, fence.lang, token)}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countLines(content) {
|
||||
if (content.length === 0) return 0;
|
||||
return content.replace(/^\uFEFF/, '').split(/\r?\n/).length;
|
||||
}
|
||||
|
||||
function relSkillMd(skillName) {
|
||||
return `${skillName}/SKILL.md`;
|
||||
}
|
||||
|
||||
function pushFinding(findings, finding) {
|
||||
findings.push(finding);
|
||||
}
|
||||
|
||||
function substitutionMessage(skillName, fenceLang, token) {
|
||||
const base = `${relSkillMd(skillName)} - ${fenceLang} fence contains Claude-only substitution ${token}`;
|
||||
if (token === '${CLAUDE_PROJECT_DIR}' || token === '$CLAUDE_PROJECT_DIR') {
|
||||
return `${base}; rewrite to $(pwd) or an explicit path`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function inspectCanonicalSkill(skillName, content, findings) {
|
||||
const { frontmatter, body } = stripFrontmatter(content);
|
||||
const values = frontmatter.present ? inspectFrontmatter(frontmatter.lines).values : {};
|
||||
const declaredName = unquoteYamlScalar(values.name || '');
|
||||
const lineCount = countLines(content);
|
||||
|
||||
if (declaredName && declaredName !== skillName) {
|
||||
pushFinding(findings, {
|
||||
severity: 'warn',
|
||||
code: 'name-mismatch',
|
||||
skill: skillName,
|
||||
message: `${relSkillMd(skillName)} - frontmatter name '${declaredName}' does not match directory '${skillName}'`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^##\s+When to Activate\b/m.test(body)) {
|
||||
pushFinding(findings, {
|
||||
severity: 'warn',
|
||||
code: 'missing-when-to-activate',
|
||||
skill: skillName,
|
||||
message: `${relSkillMd(skillName)} - missing '## When to Activate' section`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!/^##\s+Anti-Patterns\b/m.test(body)) {
|
||||
pushFinding(findings, {
|
||||
severity: 'warn',
|
||||
code: 'missing-anti-patterns',
|
||||
skill: skillName,
|
||||
message: `${relSkillMd(skillName)} - missing '## Anti-Patterns' section`,
|
||||
});
|
||||
}
|
||||
|
||||
if (lineCount > 800) {
|
||||
pushFinding(findings, {
|
||||
severity: 'warn',
|
||||
code: 'line-cap-800',
|
||||
skill: skillName,
|
||||
message: `${relSkillMd(skillName)} - body is ${lineCount} lines (CONTRIBUTING max 800)`,
|
||||
});
|
||||
} else if (lineCount > 500) {
|
||||
pushFinding(findings, {
|
||||
severity: 'warn',
|
||||
code: 'line-cap-500',
|
||||
skill: skillName,
|
||||
message: `${relSkillMd(skillName)} - body is ${lineCount} lines (CONTRIBUTING target 500)`,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function inspectCopyName(skillName, content, treeLabel, findings) {
|
||||
const { frontmatter } = stripFrontmatter(content);
|
||||
if (!frontmatter.present) return;
|
||||
const declaredName = unquoteYamlScalar(inspectFrontmatter(frontmatter.lines).values.name || '');
|
||||
if (declaredName && declaredName !== skillName) {
|
||||
pushFinding(findings, {
|
||||
severity: 'error',
|
||||
code: 'copy-name-mismatch',
|
||||
skill: skillName,
|
||||
message: `${treeLabel}/${relSkillMd(skillName)} - frontmatter name '${declaredName}' does not match directory '${skillName}'`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function inspectCodexCopy(skillName, content, findings) {
|
||||
const { frontmatter } = stripFrontmatter(content);
|
||||
inspectCopyName(skillName, content, '.agents/skills', findings);
|
||||
inspectProjectionSubstitutions(skillName, content, '.agents/skills', findings);
|
||||
if (!frontmatter.present) return;
|
||||
|
||||
const keys = Object.keys(inspectFrontmatter(frontmatter.lines).values);
|
||||
const unexpected = keys.filter(key => !ALLOWED_CODEX_FRONTMATTER_KEYS.has(key));
|
||||
if (unexpected.length > 0) {
|
||||
pushFinding(findings, {
|
||||
severity: 'error',
|
||||
code: 'codex-key',
|
||||
skill: skillName,
|
||||
message: `.agents/skills/${relSkillMd(skillName)} - Codex-forbidden frontmatter key${unexpected.length === 1 ? '' : 's'}: ${unexpected.join(', ')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function validateSkillHostCompat(options = {}) {
|
||||
const root = options.root || DEFAULT_ROOT;
|
||||
const inventory = Boolean(options.inventory);
|
||||
const strictChecklist = Boolean(options.strictChecklist);
|
||||
const skillsDir = path.join(root, 'skills');
|
||||
const codexDir = path.join(root, '.agents', 'skills');
|
||||
const cursorDir = path.join(root, '.cursor', 'skills');
|
||||
const findings = [];
|
||||
|
||||
if (!fs.existsSync(skillsDir)) {
|
||||
return {
|
||||
ok: true,
|
||||
skipped: true,
|
||||
message: 'No curated skills directory (skills/), skipping',
|
||||
findings: [],
|
||||
counts: emptyCounts(),
|
||||
};
|
||||
}
|
||||
|
||||
const canonicalSkills = listSkillDirs(skillsDir);
|
||||
const codexSkills = listSkillDirs(codexDir);
|
||||
const cursorSkills = listSkillDirs(cursorDir);
|
||||
const openaiListed = listOpenaiYamlSkills(root);
|
||||
const skillsWithCanonical = new Set(canonicalSkills);
|
||||
|
||||
for (const skillName of canonicalSkills) {
|
||||
const skillMd = path.join(skillsDir, skillName, 'SKILL.md');
|
||||
const content = readFileIfPresent(skillMd);
|
||||
if (content === null) continue;
|
||||
inspectCanonicalSkill(skillName, content, findings);
|
||||
}
|
||||
|
||||
for (const skillName of openaiListed) {
|
||||
if (!skillsWithCanonical.has(skillName)) continue;
|
||||
const copyMd = path.join(codexDir, skillName, 'SKILL.md');
|
||||
if (!fs.existsSync(copyMd)) {
|
||||
pushFinding(findings, {
|
||||
severity: 'error',
|
||||
code: 'missing-codex-copy',
|
||||
skill: skillName,
|
||||
message: `agents/openai.yaml lists '${skillName}' but .agents/skills/${skillName}/ is missing`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const skillName of codexSkills) {
|
||||
const copyMd = path.join(codexDir, skillName, 'SKILL.md');
|
||||
const content = readFileIfPresent(copyMd);
|
||||
if (content === null) continue;
|
||||
inspectCodexCopy(skillName, content, findings);
|
||||
}
|
||||
|
||||
for (const skillName of cursorSkills) {
|
||||
const copyMd = path.join(cursorDir, skillName, 'SKILL.md');
|
||||
const content = readFileIfPresent(copyMd);
|
||||
if (content === null) continue;
|
||||
inspectCopyName(skillName, content, '.cursor/skills', findings);
|
||||
inspectProjectionSubstitutions(skillName, content, '.cursor/skills', findings);
|
||||
}
|
||||
|
||||
const resolved = findings.map(finding => {
|
||||
if (strictChecklist && CHECKLIST_CODES.has(finding.code) && finding.severity === 'warn') {
|
||||
return { ...finding, severity: 'error' };
|
||||
}
|
||||
return finding;
|
||||
});
|
||||
|
||||
const counts = summarizeCounts(canonicalSkills.length, resolved);
|
||||
const errorCount = resolved.filter(finding => finding.severity === 'error').length;
|
||||
|
||||
return {
|
||||
ok: inventory || errorCount === 0,
|
||||
skipped: false,
|
||||
inventory,
|
||||
findings: resolved,
|
||||
counts,
|
||||
message: inventory
|
||||
? JSON.stringify(counts)
|
||||
: formatSummary(canonicalSkills.length, counts),
|
||||
};
|
||||
}
|
||||
|
||||
function emptyCounts() {
|
||||
return {
|
||||
skillsScanned: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
substitutions: 0,
|
||||
codexKeys: 0,
|
||||
missingCodexCopies: 0,
|
||||
copyNameMismatches: 0,
|
||||
checklistWarnings: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeCounts(skillsScanned, findings) {
|
||||
const counts = emptyCounts();
|
||||
counts.skillsScanned = skillsScanned;
|
||||
for (const finding of findings) {
|
||||
if (finding.severity === 'error') counts.errors += 1;
|
||||
if (finding.severity === 'warn') counts.warnings += 1;
|
||||
if (finding.code === 'substitution') counts.substitutions += 1;
|
||||
if (finding.code === 'codex-key') counts.codexKeys += 1;
|
||||
if (finding.code === 'missing-codex-copy') counts.missingCodexCopies += 1;
|
||||
if (finding.code === 'copy-name-mismatch') counts.copyNameMismatches += 1;
|
||||
if (CHECKLIST_CODES.has(finding.code)) counts.checklistWarnings += 1;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function formatSummary(skillCount, counts) {
|
||||
let msg = `Validated host compatibility for ${skillCount} skill directories`;
|
||||
const extras = [];
|
||||
if (counts.errors > 0) extras.push(`${counts.errors} error${counts.errors === 1 ? '' : 's'}`);
|
||||
if (counts.warnings > 0) extras.push(`${counts.warnings} warning${counts.warnings === 1 ? '' : 's'}`);
|
||||
if (extras.length > 0) msg += ` (${extras.join(', ')})`;
|
||||
return msg;
|
||||
}
|
||||
|
||||
function reportFindings(result, stdout, stderr) {
|
||||
if (result.skipped) {
|
||||
stdout.write(`${result.message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.inventory) {
|
||||
stdout.write(`${result.message}\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const finding of result.findings) {
|
||||
const line = `${finding.severity === 'error' ? 'ERROR' : 'WARN'}: ${finding.message}\n`;
|
||||
if (finding.severity === 'error') stderr.write(line);
|
||||
else stderr.write(line);
|
||||
}
|
||||
stdout.write(`${result.message}\n`);
|
||||
}
|
||||
|
||||
function run(argv = process.argv.slice(2), options = {}) {
|
||||
const stdout = options.stdout || process.stdout;
|
||||
const stderr = options.stderr || process.stderr;
|
||||
|
||||
try {
|
||||
const args = parseArgs(argv);
|
||||
const result = validateSkillHostCompat({
|
||||
root: options.root || args.root,
|
||||
inventory: args.inventory,
|
||||
strictChecklist: args.strictChecklist,
|
||||
});
|
||||
reportFindings(result, stdout, stderr);
|
||||
return result.ok ? 0 : 1;
|
||||
} catch (error) {
|
||||
stderr.write(`ERROR: ${error.message}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
process.exit(run());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ALLOWED_CODEX_FRONTMATTER_KEYS,
|
||||
CLAUDE_SUBSTITUTION_PATTERNS,
|
||||
extractTopLevelList,
|
||||
findExecutableFences,
|
||||
parseArgs,
|
||||
run,
|
||||
validateSkillHostCompat,
|
||||
};
|
||||
@@ -379,9 +379,16 @@ function validateSkills() {
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
try {
|
||||
validateSkills();
|
||||
} catch (error) {
|
||||
console.error(`ERROR: ${error.message}`);
|
||||
process.exit(1);
|
||||
module.exports = {
|
||||
extractFrontmatter,
|
||||
inspectFrontmatter,
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
validateSkills();
|
||||
} catch (error) {
|
||||
console.error(`ERROR: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const path = require('path');
|
||||
const { assertWithinTrustedRoot } = require('../path-safety');
|
||||
|
||||
function isCodexUserConfig(plan, operation) {
|
||||
if (plan.adapter.id !== 'codex-home' || operation.kind !== 'copy-file') {
|
||||
if (!plan.adapter || plan.adapter.id !== 'codex-home' || operation.kind !== 'copy-file') {
|
||||
return false;
|
||||
}
|
||||
const relativePath = path.relative(plan.targetRoot, operation.destinationPath);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: skill-host-compat
|
||||
description: Lint ECC skills for Codex/Cursor-safe frontmatter and Claude-only substitutions before a skill PR.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# Skill Host Compatibility
|
||||
|
||||
Run the same host-compat gate CI uses on `skills/` before opening a skill PR.
|
||||
The check is for Codex and Cursor copies, not Claude-only runtime quality.
|
||||
|
||||
## When to Activate
|
||||
|
||||
- Adding or editing a skill under `skills/`
|
||||
- Copying a skill into `.agents/skills/` or `.cursor/skills/`
|
||||
- A review asks whether a skill is safe to install into Codex or Cursor
|
||||
- CI reports `validate-skill-host-compat.js` findings
|
||||
|
||||
## Core Concepts
|
||||
|
||||
Canonical skills live in `skills/`. Codex loads `.agents/skills/`. Cursor loads a
|
||||
smaller `.cursor/skills/` subset. CONTRIBUTING documents that sync as manual.
|
||||
|
||||
`validate-skills.js` only checks that `SKILL.md` exists and that `name` /
|
||||
`description` are usable YAML. `tests/ci/codex-skill-surface.test.js` only
|
||||
sees `.agents/skills/`. This script is the cross-tree gate:
|
||||
|
||||
1. Claude-only substitutions in executable `bash` / `sh` / `zsh` fences
|
||||
in Codex/Cursor projections (`${CLAUDE_SESSION_ID}`, `${CLAUDE_SKILL_DIR}`, `${CLAUDE_PLUGIN_ROOT}`,
|
||||
`${CLAUDE_PROJECT_DIR}`, `$ARGUMENTS`) fail. Codex leaves those tokens
|
||||
as literal text. In a Codex copy, rewrite `${CLAUDE_PROJECT_DIR}` to
|
||||
`$(pwd)` or an explicit path.
|
||||
2. A Codex copy may only use `name`, `description`, `metadata`, `license`,
|
||||
and `allowed-tools`. `origin: ECC` may stay on the canonical `skills/`
|
||||
copy and must be stripped on the Codex copy.
|
||||
3. A skill listed in `agents/openai.yaml` that has a `skills/` directory must
|
||||
have `.agents/skills/<name>/`. Cursor copies are optional; when present,
|
||||
`name:` must match the directory.
|
||||
4. CONTRIBUTING checklist items (When to Activate, Anti-Patterns, name
|
||||
matches directory, 500/800 line caps) warn. They fail only with
|
||||
`--strict-checklist`.
|
||||
|
||||
`validate-skills.js` frontmatter WARN default is unchanged (#1663).
|
||||
|
||||
## Code Examples
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js
|
||||
```
|
||||
|
||||
Print counts without failing:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js --inventory
|
||||
```
|
||||
|
||||
Promote checklist warnings:
|
||||
|
||||
```bash
|
||||
node scripts/ci/validate-skill-host-compat.js --strict-checklist
|
||||
```
|
||||
|
||||
Slash command:
|
||||
|
||||
```
|
||||
/skill-host-compat
|
||||
/skill-host-compat --inventory
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
Codex and Cursor projections must resolve Claude-only substitutions in executable blocks. Canonical Claude-scoped examples remain valid. POSIX `$1` and `${1}` parameters are ambiguous and require review rather than failing this gate. This fence is the
|
||||
forbidden pattern:
|
||||
|
||||
```bash
|
||||
echo "${CLAUDE_SESSION_ID}"
|
||||
ls "${CLAUDE_SKILL_DIR}"
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/setup.js" $ARGUMENTS
|
||||
cd "${CLAUDE_PROJECT_DIR}"
|
||||
```
|
||||
|
||||
Do not add `version:` or `origin:` to a Codex copy under `.agents/skills/`.
|
||||
Do not treat a passing `validate-skills.js` run as Codex-safe. Do not require
|
||||
every `skills/` entry to have a Cursor copy.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep canonical `origin: ECC` if the CONTRIBUTING template calls for it.
|
||||
- Drop `origin`, `version`, and `argument-hint` on the Codex copy.
|
||||
- Show host-portable commands in bash fences (`npx`, `ecc`, or a resolved
|
||||
root). Rewrite `${CLAUDE_PROJECT_DIR}` in Codex copies to `$(pwd)` or an
|
||||
explicit path. Keep valid Claude-scoped forms in canonical instructions; resolve host variables in the target projection.
|
||||
- Run this script before `npm test` when the change is a new skill.
|
||||
- Use `--inventory` to see checklist warning volume without failing the tree.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `skill-comply` — runtime behavioral traces, not host-portability
|
||||
- `skill-stocktake` — holistic audit of installed `~/.claude/skills`
|
||||
- `skill-scout` — search existing skills before creating a new one
|
||||
@@ -0,0 +1,664 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fixture tests for scripts/ci/validate-skill-host-compat.js.
|
||||
*
|
||||
* Uses tmp dirs, not the live catalog.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const validatorPath = path.join(repoRoot, 'scripts', 'ci', 'validate-skill-host-compat.js');
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createRoot() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'skill-host-compat-'));
|
||||
}
|
||||
|
||||
function cleanup(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeSkill(root, skillName, body, options = {}) {
|
||||
const tree = options.tree || 'skills';
|
||||
const extraFrontmatter = options.frontmatter || {};
|
||||
const dir = path.join(root, tree, skillName);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const lines = ['---', `name: ${options.name || skillName}`];
|
||||
if (options.description !== false) {
|
||||
lines.push(`description: ${options.description || `Fixture skill ${skillName}`}`);
|
||||
}
|
||||
for (const [key, value] of Object.entries(extraFrontmatter)) {
|
||||
lines.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push('---', '');
|
||||
lines.push(body);
|
||||
fs.writeFileSync(path.join(dir, 'SKILL.md'), `${lines.join('\n')}\n`);
|
||||
}
|
||||
|
||||
function writeOpenaiYaml(root, skillNames) {
|
||||
const dir = path.join(root, 'agents');
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const body = ['skills:', ...skillNames.map(name => ` - ${name}`)].join('\n');
|
||||
fs.writeFileSync(path.join(dir, 'openai.yaml'), `${body}\n`);
|
||||
}
|
||||
|
||||
function runValidator(root, extraArgs = []) {
|
||||
return spawnSync('node', [validatorPath, '--root', root, ...extraArgs], {
|
||||
encoding: 'utf8',
|
||||
cwd: repoRoot,
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
function checklistBody(title) {
|
||||
return [
|
||||
`# ${title}`,
|
||||
'',
|
||||
'## When to Activate',
|
||||
'',
|
||||
'- Writing or reviewing a skill PR',
|
||||
'',
|
||||
'## Anti-Patterns',
|
||||
'',
|
||||
'- Shipping a Codex copy with extra frontmatter keys',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
console.log('\n=== Testing validate-skill-host-compat.js ===\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function check(name, fn) {
|
||||
if (test(name, fn)) passed += 1;
|
||||
else failed += 1;
|
||||
}
|
||||
|
||||
check('exports extractFrontmatter and inspectFrontmatter without running the CLI', () => {
|
||||
const skills = require('../../scripts/ci/validate-skills');
|
||||
assert.strictEqual(typeof skills.extractFrontmatter, 'function');
|
||||
assert.strictEqual(typeof skills.inspectFrontmatter, 'function');
|
||||
const fm = skills.extractFrontmatter('---\nname: demo\ndescription: hi\n---\n# Demo\n');
|
||||
assert.strictEqual(fm.present, true);
|
||||
const inspected = skills.inspectFrontmatter(fm.lines);
|
||||
assert.strictEqual(inspected.values.name, 'demo');
|
||||
});
|
||||
|
||||
check('happy: skill with name/description only, no Codex copy required, exits 0', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'portable', checklistBody('Portable'));
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
assert.ok(result.stdout.includes('Validated host compatibility for 1'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('happy: Codex copy whose frontmatter is the allowlist only, exits 0', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'portable', checklistBody('Portable'), {
|
||||
frontmatter: { origin: 'ECC' },
|
||||
});
|
||||
writeSkill(root, 'portable', checklistBody('Portable'), {
|
||||
tree: '.agents/skills',
|
||||
});
|
||||
writeOpenaiYaml(root, ['portable']);
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: bash fence contains ${CLAUDE_SESSION_ID}', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'leaky',
|
||||
`${checklistBody('Leaky')}\n## Code Examples\n\n\`\`\`bash\necho "\${CLAUDE_SESSION_ID}"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'leaky', fs.readFileSync(path.join(root, 'skills', 'leaky', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('ERROR:'));
|
||||
assert.ok(result.stderr.includes('leaky'));
|
||||
assert.ok(result.stderr.includes('${CLAUDE_SESSION_ID}'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: bash fence contains ${CLAUDE_PROJECT_DIR}', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'proj',
|
||||
`${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`bash\ncd "\${CLAUDE_PROJECT_DIR}"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'proj', fs.readFileSync(path.join(root, 'skills', 'proj', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('ERROR:'));
|
||||
assert.ok(result.stderr.includes('proj'));
|
||||
assert.ok(result.stderr.includes('${CLAUDE_PROJECT_DIR}'));
|
||||
assert.ok(result.stderr.includes('rewrite to $(pwd) or an explicit path'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: $CLAUDE_PROJECT_DIR used as a substitution in a bash fence', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'proj',
|
||||
`${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`sh\ncd "$CLAUDE_PROJECT_DIR"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'proj', fs.readFileSync(path.join(root, 'skills', 'proj', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('$CLAUDE_PROJECT_DIR'));
|
||||
assert.ok(result.stderr.includes('rewrite to $(pwd) or an explicit path'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('bash fence $(pwd) is accepted in place of CLAUDE_PROJECT_DIR', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'proj',
|
||||
`${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`bash\ncd "$(pwd)"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'proj', fs.readFileSync(path.join(root, 'skills', 'proj', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
assert.ok(!result.stderr.includes('CLAUDE_PROJECT_DIR'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('bash fence variables that only contain CLAUDE_PROJECT_DIR are accepted', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'proj',
|
||||
`${checklistBody('Proj')}\n## Code Examples\n\n\`\`\`bash\ncd "$MY_CLAUDE_PROJECT_DIR"\ncd "\${MY_CLAUDE_PROJECT_DIR}"\nls "$CLAUDE_PROJECT_DIR_TMP"\nls "\${CLAUDE_PROJECT_DIR_TMP}"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'proj', fs.readFileSync(path.join(root, 'skills', 'proj', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('prose mention of CLAUDE_PROJECT_DIR does not fail', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'prose',
|
||||
`${checklistBody('Prose')}\nThe project root is \`CLAUDE_PROJECT_DIR\`, then cwd.\n`
|
||||
);
|
||||
writeSkill(root, 'prose', fs.readFileSync(path.join(root, 'skills', 'prose', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: $ARGUMENTS used as a substitution in a bash fence', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'args',
|
||||
`${checklistBody('Args')}\n## Code Examples\n\n\`\`\`bash\necho "$ARGUMENTS"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'args', fs.readFileSync(path.join(root, 'skills', 'args', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('args'));
|
||||
assert.ok(result.stderr.includes('$ARGUMENTS'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: .agents/skills/foo/SKILL.md has version:', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'foo', checklistBody('Foo'));
|
||||
writeSkill(root, 'foo', checklistBody('Foo'), {
|
||||
tree: '.agents/skills',
|
||||
frontmatter: { version: '1.0.0' },
|
||||
});
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('version'));
|
||||
assert.ok(result.stderr.includes('.agents/skills/foo/SKILL.md'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('fail: agents/openai.yaml lists foo but .agents/skills/foo/ is missing', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'foo', checklistBody('Foo'));
|
||||
writeOpenaiYaml(root, ['foo']);
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('agents/openai.yaml lists \'foo\''));
|
||||
assert.ok(result.stderr.includes('.agents/skills/foo/'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('warn-only: missing ## When to Activate exits 0 unless --strict-checklist', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'thin',
|
||||
'# Thin\n\n## Anti-Patterns\n\n- Missing activation heading\n'
|
||||
);
|
||||
const warnResult = runValidator(root);
|
||||
assert.strictEqual(warnResult.status, 0, warnResult.stderr);
|
||||
assert.ok(warnResult.stderr.includes('WARN:'));
|
||||
assert.ok(warnResult.stderr.includes('When to Activate'));
|
||||
|
||||
const strictResult = runValidator(root, ['--strict-checklist']);
|
||||
assert.strictEqual(strictResult.status, 1);
|
||||
assert.ok(strictResult.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('anti-pattern fence still matches when the do-not line wraps', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'wrapped',
|
||||
[
|
||||
'# Wrapped',
|
||||
'',
|
||||
'## When to Activate',
|
||||
'',
|
||||
'- Reviewing a skill PR',
|
||||
'',
|
||||
'## Anti-Patterns',
|
||||
'',
|
||||
'Do not put Claude-only substitutions in bash blocks. This fence is the',
|
||||
'forbidden pattern:',
|
||||
'',
|
||||
'```bash',
|
||||
'echo "${CLAUDE_SESSION_ID}"',
|
||||
'```',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('anti-pattern fence that quotes the token as forbidden exits 0', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'quoted',
|
||||
[
|
||||
'# Quoted',
|
||||
'',
|
||||
'## When to Activate',
|
||||
'',
|
||||
'- Reviewing a skill PR',
|
||||
'',
|
||||
'## Anti-Patterns',
|
||||
'',
|
||||
'Do not put Claude-only substitutions in bash blocks:',
|
||||
'',
|
||||
'```bash',
|
||||
'echo "${CLAUDE_SESSION_ID}"',
|
||||
'```',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('empty skills/ tmp root exits 0', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
fs.mkdirSync(path.join(root, 'skills'));
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(result.stdout.includes('Validated host compatibility for 0'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('missing skills directory skips with exit 0', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(result.stdout.includes('skipping'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('origin on canonical copy is allowed; origin on Codex copy fails', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'foo', checklistBody('Foo'), { frontmatter: { origin: 'ECC' } });
|
||||
writeSkill(root, 'foo', checklistBody('Foo'), {
|
||||
tree: '.agents/skills',
|
||||
frontmatter: { origin: 'ECC' },
|
||||
});
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('origin'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('Cursor copy name mismatch is an error; extra Cursor copies are not required', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'alpha', checklistBody('Alpha'));
|
||||
writeSkill(root, 'alpha', checklistBody('Alpha'), {
|
||||
tree: '.cursor/skills',
|
||||
name: 'beta',
|
||||
});
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('.cursor/skills/alpha/SKILL.md'));
|
||||
assert.ok(result.stderr.includes('does not match directory'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('--inventory prints JSON counts and exits 0 even with errors', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'leaky',
|
||||
`${checklistBody('Leaky')}\n## Code Examples\n\n\`\`\`bash\necho "$CLAUDE_SKILL_DIR"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'leaky', fs.readFileSync(path.join(root, 'skills', 'leaky', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root, ['--inventory']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const counts = JSON.parse(result.stdout.trim());
|
||||
assert.strictEqual(counts.skillsScanned, 1);
|
||||
assert.ok(counts.substitutions >= 1);
|
||||
assert.ok(counts.errors >= 1);
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('empty-language shebang fence is scanned; markdown fence is not', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'mixed',
|
||||
[
|
||||
'# Mixed',
|
||||
'',
|
||||
'## When to Activate',
|
||||
'',
|
||||
'- Reviewing fences',
|
||||
'',
|
||||
'## Anti-Patterns',
|
||||
'',
|
||||
'- Putting substitutions in bash',
|
||||
'',
|
||||
'```markdown',
|
||||
'Parse $ARGUMENTS in a command file, not in a skill bash block.',
|
||||
'```',
|
||||
'',
|
||||
'```',
|
||||
'#!/bin/sh',
|
||||
'echo "$CLAUDE_PLUGIN_ROOT"',
|
||||
'```',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
writeSkill(root, 'mixed', fs.readFileSync(path.join(root, 'skills', 'mixed', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('$CLAUDE_PLUGIN_ROOT'));
|
||||
assert.ok(!result.stderr.includes('$ARGUMENTS'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('Codex copy name mismatch is an error', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'foo', checklistBody('Foo'));
|
||||
writeSkill(root, 'foo', checklistBody('Foo'), {
|
||||
tree: '.agents/skills',
|
||||
name: 'bar',
|
||||
});
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('.agents/skills/foo/SKILL.md'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('--root without a path exits 1', () => {
|
||||
const result = spawnSync('node', [validatorPath, '--root'], {
|
||||
encoding: 'utf8',
|
||||
cwd: repoRoot,
|
||||
timeout: 10000,
|
||||
});
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('--root requires a directory path'));
|
||||
});
|
||||
|
||||
check('canonical name mismatch warns and does not fail', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(root, 'foo', checklistBody('Foo'), { name: 'bar' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(result.stderr.includes('WARN:'));
|
||||
assert.ok(result.stderr.includes('does not match directory'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('missing Anti-Patterns heading warns', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'thin',
|
||||
'# Thin\n\n## When to Activate\n\n- Reviewing a skill\n'
|
||||
);
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0);
|
||||
assert.ok(result.stderr.includes('Anti-Patterns'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('body over 500 lines warns; over 800 stays warn unless --strict-checklist', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
const overFive = `${checklistBody('Long')}\n${'line\n'.repeat(500)}`;
|
||||
writeSkill(root, 'longish', overFive);
|
||||
const warnResult = runValidator(root);
|
||||
assert.strictEqual(warnResult.status, 0, warnResult.stderr);
|
||||
assert.ok(warnResult.stderr.includes('500'));
|
||||
|
||||
cleanup(root);
|
||||
const root2 = createRoot();
|
||||
const overEight = `${checklistBody('Huge')}\n${'line\n'.repeat(800)}`;
|
||||
writeSkill(root2, 'huge', overEight);
|
||||
const hugeWarn = runValidator(root2);
|
||||
assert.strictEqual(hugeWarn.status, 0, hugeWarn.stderr);
|
||||
assert.ok(hugeWarn.stderr.includes('800'));
|
||||
const hugeStrict = runValidator(root2, ['--strict-checklist']);
|
||||
assert.strictEqual(hugeStrict.status, 1);
|
||||
cleanup(root2);
|
||||
} finally {
|
||||
if (fs.existsSync(root)) cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('bash fence ${1} is accepted as an ambiguous POSIX parameter', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'pos',
|
||||
`${checklistBody('Pos')}\n## Code Examples\n\n\`\`\`bash\necho "\${1}"\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'pos', fs.readFileSync(path.join(root, 'skills', 'pos', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('POSIX VAR=$1 assignment is not a Claude substitution', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'posix',
|
||||
`${checklistBody('Posix')}\n## Code Examples\n\n\`\`\`bash\nmove_dir() {\n SOURCE=$1\n DEST=$2\n}\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'posix', fs.readFileSync(path.join(root, 'skills', 'posix', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('Perl/awk $1 numeric compare is not a Claude substitution', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'cover',
|
||||
`${checklistBody('Cover')}\n## Code Examples\n\n\`\`\`bash\nperl -ne 'if (/Total.*?(\\d+)/) { exit 1 if $1 < 80 }'\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'cover', fs.readFileSync(path.join(root, 'skills', 'cover', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('echo $1 in a bash fence is accepted as an ambiguous POSIX parameter', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
writeSkill(
|
||||
root,
|
||||
'echoone',
|
||||
`${checklistBody('Echo')}\n## Code Examples\n\n\`\`\`bash\necho $1\n\`\`\`\n`
|
||||
);
|
||||
writeSkill(root, 'echoone', fs.readFileSync(path.join(root, 'skills', 'echoone', 'SKILL.md'), 'utf8').replace(/^---\n[\s\S]*?\n---\n/, ''), { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!result.stderr.includes('ERROR:'));
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
check('canonical Claude examples remain available while unresolved Codex projections fail', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
const body = `${checklistBody('Scoped')}\n## Workflow\n\n\`\`\`bash\necho "$CLAUDE_SKILL_DIR"\n\`\`\`\n`;
|
||||
writeSkill(root, 'scoped', body);
|
||||
assert.strictEqual(runValidator(root).status, 0);
|
||||
writeSkill(root, 'scoped', body, { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 1);
|
||||
assert.ok(result.stderr.includes('.agents/skills/scoped/SKILL.md'));
|
||||
} finally { cleanup(root); }
|
||||
});
|
||||
|
||||
check('POSIX function arguments $1 and ${1} pass in the Codex projection', () => {
|
||||
const root = createRoot();
|
||||
try {
|
||||
const body = `${checklistBody('Positional')}\n## Workflow\n\n\`\`\`sh\nprint_arg() { printf '%s\\n' "$1" "\${1}"; }\nprint_arg hello\n\`\`\`\n`;
|
||||
writeSkill(root, 'positional', body);
|
||||
writeSkill(root, 'positional', body, { tree: '.agents/skills' });
|
||||
const result = runValidator(root);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
} finally { cleanup(root); }
|
||||
});
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -453,7 +453,7 @@ else failed++;
|
||||
// A case-folded spelling, because macOS resolves `$venv/bin/python` to a committed
|
||||
// `Python` while git matches index pathspecs case-sensitively. Skipped where the
|
||||
// filesystem is case-sensitive and the two names cannot collide.
|
||||
if (fs.existsSync(__filename.toUpperCase()) || fs.existsSync(__filename.toLowerCase())) {
|
||||
if (fs.existsSync(__filename.toUpperCase()) && fs.existsSync(__filename.toLowerCase())) {
|
||||
if (
|
||||
test('pre-push refuses a tracked interpreter committed under a folded case', () => {
|
||||
const { result, calls } = runHermeticPythonPrePush({
|
||||
|
||||
@@ -71,6 +71,8 @@ function buildExpectedPublishPaths(repoRoot) {
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/ci/supply-chain-advisory-sources.js",
|
||||
"scripts/ci/validate-skill-host-compat.js",
|
||||
"scripts/ci/validate-skills.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/profile.js",
|
||||
"scripts/control-pane.js",
|
||||
@@ -227,6 +229,8 @@ function main() {
|
||||
"scripts/catalog.js",
|
||||
"scripts/ci/scan-supply-chain-iocs.js",
|
||||
"scripts/ci/supply-chain-advisory-sources.js",
|
||||
"scripts/ci/validate-skill-host-compat.js",
|
||||
"scripts/ci/validate-skills.js",
|
||||
"scripts/consult.js",
|
||||
"scripts/profile.js",
|
||||
"scripts/lib/context-profiles.js",
|
||||
@@ -303,6 +307,7 @@ function main() {
|
||||
"schemas/memory.schema.json",
|
||||
"skills/backend-patterns/SKILL.md",
|
||||
"skills/skill-comply/SKILL.md",
|
||||
"skills/skill-host-compat/SKILL.md",
|
||||
"skills/unified-memory/SKILL.md",
|
||||
"scripts/agent-convert.js",
|
||||
"scripts/lib/agent-ir.js",
|
||||
|
||||
Reference in new issue
Block a user