Merge main into v2.0: consolidate critique skill with scoring, personas, and detection

Merges 54 commits from main including factory-based build system, Trae support,
improved skill descriptions, and security hardening. Consolidates the critique
skill to combine v2.0's sub-agent architecture and automated anti-pattern
detection with main's Nielsen heuristics scoring, cognitive load assessment,
persona-based testing, and structured follow-up workflow. Fixes browser detector
build to create target directory after skill sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-27 22:20:04 -07:00
co-authored by Claude Opus 4.6
375 changed files with 47897 additions and 5781 deletions
-24
View File
@@ -1,24 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Agents Transformer (VS Code Copilot + Antigravity)
* Output: .agents/skills/{name}/SKILL.md
*/
export function transformAgents(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'agents',
displayName: 'Agents',
configDir: '.agents',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.userInvokable) obj['user-invokable'] = true;
if (skill.userInvokable && skill.args && skill.args.length > 0) {
const hints = skill.args.map(arg =>
arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`
);
obj['argument-hint'] = hints.join(' ');
}
return obj;
},
}, skills, distDir, options);
}
-23
View File
@@ -1,23 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Claude Code Transformer
* Output: .claude/skills/{name}/SKILL.md
*/
export function transformClaudeCode(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'claude-code',
displayName: 'Claude Code',
configDir: '.claude',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.userInvokable) obj['user-invokable'] = true;
if (skill.args && skill.args.length > 0) obj.args = skill.args;
if (skill.license) obj.license = skill.license;
if (skill.compatibility) obj.compatibility = skill.compatibility;
if (skill.metadata) obj.metadata = skill.metadata;
if (skill.allowedTools) obj['allowed-tools'] = skill.allowedTools;
return obj;
},
}, skills, distDir, options);
}
-31
View File
@@ -1,31 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Codex Transformer
* Output: .codex/skills/{name}/SKILL.md
* User-invokable: {{argname}} becomes $ARGNAME, argument-hint in frontmatter
*/
export function transformCodex(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'codex',
displayName: 'Codex',
configDir: '.codex',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.userInvokable && skill.args && skill.args.length > 0) {
const hints = skill.args.map(arg =>
arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`
);
obj['argument-hint'] = hints.join(' ');
}
if (skill.license) obj.license = skill.license;
return obj;
},
transformBody: (body, skill) => {
if (skill.userInvokable) {
return body.replace(/\{\{([^}]+)\}\}/g, (_, argName) => `$${argName.toUpperCase()}`);
}
return body;
},
}, skills, distDir, options);
}
-18
View File
@@ -1,18 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Cursor Transformer
* Output: .cursor/skills/{name}/SKILL.md
*/
export function transformCursor(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'cursor',
displayName: 'Cursor',
configDir: '.cursor',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.license) obj.license = skill.license;
return obj;
},
}, skills, distDir, options);
}
+115
View File
@@ -0,0 +1,115 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences, PROVIDER_PLACEHOLDERS } from '../utils.js';
/**
* Map from frontmatter field name to extraction spec.
*
* - sourceKey: property name on the skill object
* - yamlKey: key name in YAML frontmatter
* - condition: if provided, field is only emitted when this returns true
* - value: if provided, use this instead of skill[sourceKey]
*/
const FIELD_SPECS = {
'user-invocable': {
sourceKey: 'userInvocable',
yamlKey: 'user-invocable',
condition: (skill) => skill.userInvocable,
value: () => true,
},
'argument-hint': {
sourceKey: 'argumentHint',
yamlKey: 'argument-hint',
condition: (skill) => skill.userInvocable && skill.argumentHint,
},
license: {
sourceKey: 'license',
yamlKey: 'license',
},
compatibility: {
sourceKey: 'compatibility',
yamlKey: 'compatibility',
},
metadata: {
sourceKey: 'metadata',
yamlKey: 'metadata',
},
'allowed-tools': {
sourceKey: 'allowedTools',
yamlKey: 'allowed-tools',
},
};
/**
* Create a transformer function for a given provider config.
*
* @param {Object} config - Provider configuration from providers.js
* @returns {Function} transform(skills, distDir, options?)
*/
export function createTransformer(config) {
const { provider, configDir, displayName, frontmatterFields = [], bodyTransform, placeholderProvider } = config;
const placeholderKey = placeholderProvider || provider;
const activeFields = frontmatterFields
.map((name) => FIELD_SPECS[name])
.filter(Boolean);
return function transform(skills, distDir, options = {}) {
const { prefix = '', outputSuffix = '' } = options;
const providerDir = path.join(distDir, `${provider}${outputSuffix}`);
const skillsDir = path.join(providerDir, `${configDir}/skills`);
cleanDir(providerDir);
ensureDir(skillsDir);
const allSkillNames = skills.map((s) => s.name);
const commandNames = skills
.filter((s) => s.userInvocable)
.map((s) => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
const skillDir = path.join(skillsDir, skillName);
// Build frontmatter
const frontmatterObj = {
name: skillName,
description: skill.description,
};
for (const spec of activeFields) {
if (spec.condition && !spec.condition(skill)) continue;
const val = spec.value ? spec.value(skill) : skill[spec.sourceKey];
if (val) frontmatterObj[spec.yamlKey] = val;
}
const frontmatter = generateYamlFrontmatter(frontmatterObj);
// Build body
const cmdPrefix = (PROVIDER_PLACEHOLDERS[placeholderKey] || {}).command_prefix || '/';
let skillBody = replacePlaceholders(skill.body, placeholderKey, commandNames, allSkillNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames, cmdPrefix);
if (bodyTransform) skillBody = bodyTransform(skillBody, skill);
const content = `${frontmatter}\n\n${skillBody}`;
writeFile(path.join(skillDir, 'SKILL.md'), content);
// Copy reference files
if (skill.references && skill.references.length > 0) {
const refDir = path.join(skillDir, 'reference');
ensureDir(refDir);
for (const ref of skill.references) {
const refContent = replacePlaceholders(ref.content, placeholderKey, [], allSkillNames);
writeFile(path.join(refDir, `${ref.name}.md`), refContent);
refCount++;
}
}
}
const userInvocableCount = skills.filter((s) => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`${displayName}${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
};
}
-24
View File
@@ -1,24 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Gemini Transformer
* Output: .gemini/skills/{name}/SKILL.md
* User-invokable: {{arg}} placeholders become {{args}}
*/
export function transformGemini(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'gemini',
displayName: 'Gemini',
configDir: '.gemini',
buildFrontmatter: (skill, skillName) => ({
name: skillName,
description: skill.description,
}),
transformBody: (body, skill) => {
if (skill.userInvokable) {
return body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
}
return body;
},
}, skills, distDir, options);
}
+13 -8
View File
@@ -1,8 +1,13 @@
export { transformCursor } from './cursor.js';
export { transformClaudeCode } from './claude-code.js';
export { transformGemini } from './gemini.js';
export { transformCodex } from './codex.js';
export { transformAgents } from './agents.js';
export { transformKiro } from './kiro.js';
export { transformOpenCode } from "./opencode.js";
export { transformPi } from './pi.js';
import { createTransformer } from './factory.js';
import { PROVIDERS } from './providers.js';
export const transformCursor = createTransformer(PROVIDERS.cursor);
export const transformClaudeCode = createTransformer(PROVIDERS['claude-code']);
export const transformGemini = createTransformer(PROVIDERS.gemini);
export const transformCodex = createTransformer(PROVIDERS.codex);
export const transformAgents = createTransformer(PROVIDERS.agents);
export const transformKiro = createTransformer(PROVIDERS.kiro);
export const transformOpenCode = createTransformer(PROVIDERS.opencode);
export const transformPi = createTransformer(PROVIDERS.pi);
export { createTransformer, PROVIDERS };
-20
View File
@@ -1,20 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Kiro Transformer
* Output: .kiro/skills/{name}/SKILL.md
*/
export function transformKiro(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'kiro',
displayName: 'Kiro',
configDir: '.kiro',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.license) obj.license = skill.license;
if (skill.compatibility) obj.compatibility = skill.compatibility;
if (skill.metadata) obj.metadata = skill.metadata;
return obj;
},
}, skills, distDir, options);
}
-23
View File
@@ -1,23 +0,0 @@
import { transformProvider } from './shared.js';
/**
* OpenCode Transformer
* Output: .opencode/skills/{name}/SKILL.md
*/
export function transformOpenCode(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'opencode',
displayName: 'OpenCode',
configDir: '.opencode',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.userInvokable) obj['user-invokable'] = true;
if (skill.args && skill.args.length > 0) obj.args = skill.args;
if (skill.license) obj.license = skill.license;
if (skill.compatibility) obj.compatibility = skill.compatibility;
if (skill.metadata) obj.metadata = skill.metadata;
if (skill.allowedTools) obj['allowed-tools'] = skill.allowedTools;
return obj;
},
}, skills, distDir, options);
}
-20
View File
@@ -1,20 +0,0 @@
import { transformProvider } from './shared.js';
/**
* Pi Transformer
* Output: .pi/skills/{name}/SKILL.md
*/
export function transformPi(skills, distDir, patterns = null, options = {}) {
transformProvider({
provider: 'pi',
displayName: 'Pi',
configDir: '.pi',
buildFrontmatter: (skill, skillName) => {
const obj = { name: skillName, description: skill.description };
if (skill.license) obj.license = skill.license;
if (skill.compatibility) obj.compatibility = skill.compatibility;
if (skill.metadata) obj.metadata = skill.metadata;
return obj;
},
}, skills, distDir, options);
}
+73
View File
@@ -0,0 +1,73 @@
/**
* Provider configurations for the transformer factory.
*
* Each config specifies:
* - provider: key into PROVIDER_PLACEHOLDERS (e.g. 'claude-code')
* - configDir: dot-directory name (e.g. '.claude')
* - displayName: human-readable name for log output (e.g. 'Claude Code')
* - frontmatterFields: which optional fields to emit beyond name + description
* - bodyTransform: optional function (body, skill) => transformed body
*/
export const PROVIDERS = {
cursor: {
provider: 'cursor',
configDir: '.cursor',
displayName: 'Cursor',
frontmatterFields: ['license', 'compatibility', 'metadata'],
},
'claude-code': {
provider: 'claude-code',
configDir: '.claude',
displayName: 'Claude Code',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
gemini: {
provider: 'gemini',
configDir: '.gemini',
displayName: 'Gemini',
frontmatterFields: [],
},
codex: {
provider: 'codex',
configDir: '.codex',
displayName: 'Codex',
frontmatterFields: ['argument-hint', 'license'],
},
agents: {
provider: 'agents',
configDir: '.agents',
displayName: 'Agents',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
},
kiro: {
provider: 'kiro',
configDir: '.kiro',
displayName: 'Kiro',
frontmatterFields: ['license', 'compatibility', 'metadata'],
},
opencode: {
provider: 'opencode',
configDir: '.opencode',
displayName: 'OpenCode',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
},
pi: {
provider: 'pi',
configDir: '.pi',
displayName: 'Pi',
frontmatterFields: ['license', 'compatibility', 'metadata', 'allowed-tools'],
},
'trae-cn': {
provider: 'trae-cn',
configDir: '.trae-cn',
displayName: 'Trae China',
placeholderProvider: 'trae',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
},
trae: {
provider: 'trae',
configDir: '.trae',
displayName: 'Trae',
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
},
};