@ckelsoe/prompt-architect 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +116 -5
- package/MIGRATION.md +1 -1
- package/README.md +42 -45
- package/adapters/README.md +1 -1
- package/adapters/system-prompt.md +41 -11
- package/package.json +5 -4
- package/scripts/install.js +52 -22
- package/scripts/test.js +62 -40
- package/scripts/validate-skill.js +184 -56
- package/skills/prompt-architect/SKILL.md +49 -47
- package/skills/prompt-architect/assets/templates/chain-of-density_template.txt +60 -30
- package/skills/prompt-architect/assets/templates/iterative-compression_template.txt +59 -0
- package/skills/prompt-architect/references/frameworks/ape.md +2 -0
- package/skills/prompt-architect/references/frameworks/bab.md +2 -0
- package/skills/prompt-architect/references/frameworks/cai-critique-revise.md +1 -1
- package/skills/prompt-architect/references/frameworks/chain-of-density.md +95 -423
- package/skills/prompt-architect/references/frameworks/chain-of-thought.md +17 -12
- package/skills/prompt-architect/references/frameworks/co-star.md +2 -0
- package/skills/prompt-architect/references/frameworks/ctf.md +3 -1
- package/skills/prompt-architect/references/frameworks/iterative-compression.md +481 -0
- package/skills/prompt-architect/references/frameworks/pre-mortem.md +6 -6
- package/skills/prompt-architect/references/frameworks/race.md +2 -0
- package/skills/prompt-architect/references/frameworks/react.md +1 -1
- package/skills/prompt-architect/references/frameworks/reverse-role.md +1 -1
- package/skills/prompt-architect/references/frameworks/rise.md +27 -8
- package/skills/prompt-architect/references/frameworks/risen.md +2 -0
- package/skills/prompt-architect/references/frameworks/rtf.md +2 -0
- package/skills/prompt-architect/references/frameworks/skeleton-of-thought.md +1 -1
- package/skills/prompt-architect/references/frameworks/step-back.md +1 -1
- package/skills/prompt-architect/references/frameworks/tidd-ec.md +19 -6
- package/skills/prompt-architect/scripts/framework_analyzer.py +0 -807
- package/skills/prompt-architect/scripts/prompt_evaluator.py +0 -336
package/scripts/install.js
CHANGED
|
@@ -46,18 +46,24 @@ const AGENTS = [
|
|
|
46
46
|
return true;
|
|
47
47
|
} catch { return false; }
|
|
48
48
|
},
|
|
49
|
-
installPath: () =>
|
|
49
|
+
installPath: (project) => project
|
|
50
|
+
? path.join(process.cwd(), '.gemini', 'skills', SKILL_NAME)
|
|
51
|
+
: path.join(os.homedir(), '.gemini', 'skills', SKILL_NAME),
|
|
50
52
|
format: 'skill',
|
|
51
|
-
hint: () => '~/.gemini/skills/',
|
|
53
|
+
hint: (project) => project ? '.gemini/skills/ (project)' : '~/.gemini/skills/',
|
|
52
54
|
},
|
|
53
55
|
{
|
|
54
56
|
id: 'agents',
|
|
55
57
|
name: 'Agent Skills (universal)',
|
|
56
58
|
detect: () => true, // Always available
|
|
57
59
|
alwaysShow: true,
|
|
58
|
-
installPath: () =>
|
|
60
|
+
installPath: (project) => project
|
|
61
|
+
? path.join(process.cwd(), '.agents', 'skills', SKILL_NAME)
|
|
62
|
+
: path.join(os.homedir(), '.agents', 'skills', SKILL_NAME),
|
|
59
63
|
format: 'skill',
|
|
60
|
-
hint: () =>
|
|
64
|
+
hint: (project) => project
|
|
65
|
+
? '.agents/skills/ (project, 30+ compatible agents)'
|
|
66
|
+
: '~/.agents/skills/ (30+ compatible agents)',
|
|
61
67
|
},
|
|
62
68
|
{
|
|
63
69
|
id: 'windsurf',
|
|
@@ -66,6 +72,7 @@ const AGENTS = [
|
|
|
66
72
|
return fs.existsSync(path.join(process.cwd(), '.windsurfrules')) ||
|
|
67
73
|
fs.existsSync(path.join(os.homedir(), '.windsurf'));
|
|
68
74
|
},
|
|
75
|
+
// .windsurfrules is inherently project-scoped, so --project is a no-op here.
|
|
69
76
|
installPath: () => path.join(process.cwd(), '.windsurfrules'),
|
|
70
77
|
format: 'adapter',
|
|
71
78
|
adapterFile: 'for-windsurf.md',
|
|
@@ -128,12 +135,24 @@ function copyRecursive(src, dest) {
|
|
|
128
135
|
const MARKER_START = '<!-- prompt-architect-start -->';
|
|
129
136
|
const MARKER_END = '<!-- prompt-architect-end -->';
|
|
130
137
|
|
|
131
|
-
function installSkillDir(sourcePath, destPath) {
|
|
138
|
+
function installSkillDir(sourcePath, destPath, force = false) {
|
|
132
139
|
const parentDir = path.dirname(destPath);
|
|
133
140
|
if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir, { recursive: true });
|
|
134
141
|
|
|
142
|
+
// A symlink here almost always means the user has pointed this path at a
|
|
143
|
+
// working copy they are actively editing. rmSync would silently delete the
|
|
144
|
+
// link, so refuse unless they explicitly opt in with --force.
|
|
145
|
+
let linkStat = null;
|
|
146
|
+
try { linkStat = fs.lstatSync(destPath); } catch { /* does not exist */ }
|
|
147
|
+
if (linkStat && linkStat.isSymbolicLink() && !force) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`${destPath} is a symlink — refusing to replace it. ` +
|
|
150
|
+
`Remove it first, or re-run with --force if you meant to overwrite.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
// Remove existing installation
|
|
136
|
-
if (fs.existsSync(destPath)) fs.rmSync(destPath, { recursive: true, force: true });
|
|
155
|
+
if (fs.existsSync(destPath) || linkStat) fs.rmSync(destPath, { recursive: true, force: true });
|
|
137
156
|
|
|
138
157
|
copyRecursive(sourcePath, destPath);
|
|
139
158
|
|
|
@@ -154,16 +173,19 @@ function installAdapter(adaptersPath, agent, destPath) {
|
|
|
154
173
|
|
|
155
174
|
if (agent.appendMode && fs.existsSync(destPath)) {
|
|
156
175
|
const existing = fs.readFileSync(destPath, 'utf8');
|
|
157
|
-
//
|
|
176
|
+
// Strip every previously-installed block, not just the first — without the
|
|
177
|
+
// `g` flag an older duplicate would survive and accumulate on each run.
|
|
158
178
|
const cleaned = existing.replace(
|
|
159
|
-
new RegExp(`\\n?${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}\\n
|
|
179
|
+
new RegExp(`\\n?${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}\\n?`, 'g'),
|
|
160
180
|
''
|
|
161
181
|
);
|
|
162
182
|
fs.writeFileSync(destPath, cleaned + markedContent, 'utf8');
|
|
163
183
|
} else {
|
|
164
184
|
const parentDir = path.dirname(destPath);
|
|
165
185
|
if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir, { recursive: true });
|
|
166
|
-
|
|
186
|
+
// Must write the *marked* content — an unmarked first copy is invisible to
|
|
187
|
+
// the strip above and could never be reclaimed on reinstall.
|
|
188
|
+
fs.writeFileSync(destPath, markedContent, 'utf8');
|
|
167
189
|
}
|
|
168
190
|
}
|
|
169
191
|
|
|
@@ -227,15 +249,19 @@ function showHelp() {
|
|
|
227
249
|
Options:
|
|
228
250
|
-a, --all Install to all detected agents
|
|
229
251
|
-p, --project Use project-local paths where applicable
|
|
230
|
-
-f, --force
|
|
252
|
+
-f, --force Also replace a symlinked install path (refused by default)
|
|
231
253
|
-y, --yes Accept defaults (all detected agents)
|
|
232
254
|
-h, --help Show this help
|
|
255
|
+
|
|
256
|
+
Note: installing always overwrites an existing skill directory. --force is
|
|
257
|
+
only needed when the target path is a symlink, which usually means you have
|
|
258
|
+
pointed it at a working copy on purpose.
|
|
233
259
|
`);
|
|
234
260
|
}
|
|
235
261
|
|
|
236
262
|
// ─── Non-Interactive Install ─────────────────────────────────────
|
|
237
263
|
|
|
238
|
-
function installNonInteractive(agentIds, sourcePath, adaptersPath, isProject) {
|
|
264
|
+
function installNonInteractive(agentIds, sourcePath, adaptersPath, isProject, force) {
|
|
239
265
|
const version = getVersion();
|
|
240
266
|
const results = [];
|
|
241
267
|
|
|
@@ -245,7 +271,7 @@ function installNonInteractive(agentIds, sourcePath, adaptersPath, isProject) {
|
|
|
245
271
|
const destPath = agent.installPath(isProject);
|
|
246
272
|
try {
|
|
247
273
|
if (agent.format === 'skill') {
|
|
248
|
-
installSkillDir(sourcePath, destPath);
|
|
274
|
+
installSkillDir(sourcePath, destPath, force);
|
|
249
275
|
} else {
|
|
250
276
|
installAdapter(adaptersPath, agent, destPath);
|
|
251
277
|
}
|
|
@@ -278,7 +304,7 @@ function installNonInteractive(agentIds, sourcePath, adaptersPath, isProject) {
|
|
|
278
304
|
|
|
279
305
|
// ─── Interactive Install ─────────────────────────────────────────
|
|
280
306
|
|
|
281
|
-
async function installInteractive(sourcePath, adaptersPath, isProject) {
|
|
307
|
+
async function installInteractive(sourcePath, adaptersPath, isProject, force) {
|
|
282
308
|
const p = require('@clack/prompts');
|
|
283
309
|
const version = getVersion();
|
|
284
310
|
|
|
@@ -352,7 +378,7 @@ async function installInteractive(sourcePath, adaptersPath, isProject) {
|
|
|
352
378
|
|
|
353
379
|
try {
|
|
354
380
|
if (agent.format === 'skill') {
|
|
355
|
-
installSkillDir(sourcePath, destPath);
|
|
381
|
+
installSkillDir(sourcePath, destPath, force);
|
|
356
382
|
} else {
|
|
357
383
|
installAdapter(adaptersPath, agent, destPath);
|
|
358
384
|
}
|
|
@@ -405,27 +431,27 @@ async function main() {
|
|
|
405
431
|
|
|
406
432
|
// Mode 1: Specific agent flags provided
|
|
407
433
|
if (specificAgents.length > 0) {
|
|
408
|
-
installNonInteractive(specificAgents, sourcePath, adaptersPath, isProject);
|
|
434
|
+
installNonInteractive(specificAgents, sourcePath, adaptersPath, isProject, flags.force);
|
|
409
435
|
return;
|
|
410
436
|
}
|
|
411
437
|
|
|
412
438
|
// Mode 2: --all flag
|
|
413
439
|
if (flags.all) {
|
|
414
440
|
const detected = AGENTS.filter(a => a.detect()).map(a => a.id);
|
|
415
|
-
installNonInteractive(detected, sourcePath, adaptersPath, isProject);
|
|
441
|
+
installNonInteractive(detected, sourcePath, adaptersPath, isProject, flags.force);
|
|
416
442
|
return;
|
|
417
443
|
}
|
|
418
444
|
|
|
419
445
|
// Mode 3: --yes flag (accept defaults = all detected, non-interactive)
|
|
420
446
|
if (flags.yes) {
|
|
421
|
-
const detected = AGENTS.filter(a => a.detect()
|
|
422
|
-
installNonInteractive(detected
|
|
447
|
+
const detected = AGENTS.filter(a => a.detect()).map(a => a.id);
|
|
448
|
+
installNonInteractive(detected, sourcePath, adaptersPath, isProject, flags.force);
|
|
423
449
|
return;
|
|
424
450
|
}
|
|
425
451
|
|
|
426
452
|
// Mode 4: Interactive
|
|
427
453
|
if (isInteractive) {
|
|
428
|
-
await installInteractive(sourcePath, adaptersPath, isProject);
|
|
454
|
+
await installInteractive(sourcePath, adaptersPath, isProject, flags.force);
|
|
429
455
|
return;
|
|
430
456
|
}
|
|
431
457
|
|
|
@@ -435,12 +461,16 @@ async function main() {
|
|
|
435
461
|
const version = getVersion();
|
|
436
462
|
const results = [];
|
|
437
463
|
|
|
438
|
-
|
|
439
|
-
|
|
464
|
+
// Install to every detected agent — this is the path most users hit via the
|
|
465
|
+
// postinstall hook, so restricting it to one agent would silently defeat the
|
|
466
|
+
// package's multi-agent support.
|
|
467
|
+
for (const agent of AGENTS.filter(a => a.detect())) {
|
|
440
468
|
const destPath = agent.installPath(isProject);
|
|
441
469
|
try {
|
|
442
470
|
if (agent.format === 'skill') {
|
|
443
|
-
installSkillDir(sourcePath, destPath);
|
|
471
|
+
installSkillDir(sourcePath, destPath, flags.force);
|
|
472
|
+
} else {
|
|
473
|
+
installAdapter(adaptersPath, agent, destPath);
|
|
444
474
|
}
|
|
445
475
|
results.push({ agent: agent.name, path: destPath, success: true });
|
|
446
476
|
} catch (err) {
|
package/scripts/test.js
CHANGED
|
@@ -63,50 +63,34 @@ test('CHANGELOG.md exists', () => {
|
|
|
63
63
|
if (!fs.existsSync('CHANGELOG.md')) throw new Error('Not found');
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
'co-star.md',
|
|
69
|
-
'risen.md',
|
|
70
|
-
'rise.md',
|
|
71
|
-
'tidd-ec.md',
|
|
72
|
-
'rtf.md',
|
|
73
|
-
'chain-of-thought.md',
|
|
74
|
-
'chain-of-density.md',
|
|
75
|
-
];
|
|
76
|
-
frameworks.forEach(f => {
|
|
77
|
-
const fpath = path.join('skills', 'prompt-architect', 'references', 'frameworks', f);
|
|
78
|
-
if (!fs.existsSync(fpath)) {
|
|
79
|
-
throw new Error(`Framework ${f} not found`);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
66
|
+
const FRAMEWORKS_DIR = path.join('skills', 'prompt-architect', 'references', 'frameworks');
|
|
67
|
+
const TEMPLATES_DIR = path.join('skills', 'prompt-architect', 'assets', 'templates');
|
|
83
68
|
|
|
84
|
-
test('
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
'rise-ix_template.txt',
|
|
90
|
-
'tidd-ec_template.txt',
|
|
91
|
-
'rtf_template.txt',
|
|
92
|
-
'hybrid_template.txt',
|
|
93
|
-
];
|
|
94
|
-
templates.forEach(t => {
|
|
95
|
-
const tpath = path.join('skills', 'prompt-architect', 'assets', 'templates', t);
|
|
96
|
-
if (!fs.existsSync(tpath)) {
|
|
97
|
-
throw new Error(`Template ${t} not found`);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
69
|
+
test('Framework reference docs exist', () => {
|
|
70
|
+
const docs = fs.readdirSync(FRAMEWORKS_DIR).filter(f => f.endsWith('.md'));
|
|
71
|
+
if (docs.length === 0) {
|
|
72
|
+
throw new Error('No framework reference docs found');
|
|
73
|
+
}
|
|
100
74
|
});
|
|
101
75
|
|
|
102
|
-
test('
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
76
|
+
test('Every framework has a template', () => {
|
|
77
|
+
// rise.md defines two independently-selectable frameworks.
|
|
78
|
+
const multi = { 'rise.md': ['rise-ie', 'rise-ix'] };
|
|
79
|
+
const templates = new Set(fs.readdirSync(TEMPLATES_DIR));
|
|
80
|
+
const missing = [];
|
|
81
|
+
|
|
82
|
+
for (const doc of fs.readdirSync(FRAMEWORKS_DIR).filter(f => f.endsWith('.md'))) {
|
|
83
|
+
const slugs = multi[doc] || [doc.replace(/\.md$/, '')];
|
|
84
|
+
for (const slug of slugs) {
|
|
85
|
+
if (!templates.has(`${slug}_template.txt`)) missing.push(`${slug}_template.txt`);
|
|
108
86
|
}
|
|
109
|
-
}
|
|
87
|
+
}
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
throw new Error(`Missing templates: ${missing.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
if (!templates.has('hybrid_template.txt')) {
|
|
92
|
+
throw new Error('Missing hybrid_template.txt');
|
|
93
|
+
}
|
|
110
94
|
});
|
|
111
95
|
|
|
112
96
|
test('package.json has required fields', () => {
|
|
@@ -163,6 +147,44 @@ test('All adapter files exist', () => {
|
|
|
163
147
|
});
|
|
164
148
|
});
|
|
165
149
|
|
|
150
|
+
// SKILL.md <-> adapter drift guard.
|
|
151
|
+
// These two files are hand-maintained copies of the same instructions and have
|
|
152
|
+
// drifted before (the adapter was renumbered 1-7 while SKILL.md kept a duplicate
|
|
153
|
+
// step 4). Compare the sections that must stay identical.
|
|
154
|
+
test('Adapter has not drifted from SKILL.md', () => {
|
|
155
|
+
const skill = fs.readFileSync('skills/prompt-architect/SKILL.md', 'utf8');
|
|
156
|
+
const adapter = fs.readFileSync('adapters/system-prompt.md', 'utf8');
|
|
157
|
+
|
|
158
|
+
// 1. Step headings must match in number, order, and title.
|
|
159
|
+
const steps = s => (s.match(/^### \d+\. .+$/gm) || []).map(h => h.trim());
|
|
160
|
+
const skillSteps = steps(skill);
|
|
161
|
+
const adapterSteps = steps(adapter);
|
|
162
|
+
if (skillSteps.join('|') !== adapterSteps.join('|')) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Step headings differ.\n SKILL.md: ${JSON.stringify(skillSteps)}\n adapter: ${JSON.stringify(adapterSteps)}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Step numbers must be a clean 1..N with no duplicates.
|
|
169
|
+
const nums = skillSteps.map(h => parseInt(h.match(/^### (\d+)\./)[1], 10));
|
|
170
|
+
const expected = nums.map((_, i) => i + 1);
|
|
171
|
+
if (nums.join(',') !== expected.join(',')) {
|
|
172
|
+
throw new Error(`Step numbering is not sequential: ${nums.join(',')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. The intent-routing framework names must match between the two files.
|
|
176
|
+
const routed = s => [...s.matchAll(/\*\*([A-Za-z][A-Za-z0-9 +'’-]*?)\*\*/g)]
|
|
177
|
+
.map(m => m[1]).filter(n => /^[A-Z]/.test(n));
|
|
178
|
+
const missing = [...new Set(routed(skill))].filter(n => !routed(adapter).includes(n));
|
|
179
|
+
const routingNames = ['CO-STAR', 'RISEN', 'RISE-IE', 'RISE-IX', 'TIDD-EC', 'CTF', 'RTF',
|
|
180
|
+
'APE', 'BAB', 'RACE', 'CRISPE', 'BROKE', 'CARE', 'ReAct', 'RCoT', 'RPEF'];
|
|
181
|
+
const dropped = routingNames.filter(n => skill.includes(n) && !adapter.includes(n));
|
|
182
|
+
if (dropped.length > 0) {
|
|
183
|
+
throw new Error(`Frameworks in SKILL.md but missing from adapter: ${dropped.join(', ')}`);
|
|
184
|
+
}
|
|
185
|
+
void missing;
|
|
186
|
+
});
|
|
187
|
+
|
|
166
188
|
// Agent Skills compliance tests
|
|
167
189
|
test('SKILL.md has Agent Skills required fields', () => {
|
|
168
190
|
const content = fs.readFileSync('skills/prompt-architect/SKILL.md', 'utf8');
|
|
@@ -49,35 +49,49 @@ const SKILL_DIR = path.join(__dirname, '..', 'skills', 'prompt-architect');
|
|
|
49
49
|
const SKILL_FILE = path.join(SKILL_DIR, 'SKILL.md');
|
|
50
50
|
|
|
51
51
|
const REQUIRED_DIRS = [
|
|
52
|
-
'scripts',
|
|
53
52
|
'references/frameworks',
|
|
54
53
|
'assets/templates',
|
|
55
54
|
];
|
|
56
55
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
'risen.md',
|
|
60
|
-
'rise.md',
|
|
61
|
-
'tidd-ec.md',
|
|
62
|
-
'rtf.md',
|
|
63
|
-
'chain-of-thought.md',
|
|
64
|
-
'chain-of-density.md',
|
|
65
|
-
];
|
|
56
|
+
const FRAMEWORKS_DIR = path.join(SKILL_DIR, 'references', 'frameworks');
|
|
57
|
+
const TEMPLATES_DIR = path.join(SKILL_DIR, 'assets', 'templates');
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
'risen_template.txt',
|
|
70
|
-
'rise-ie_template.txt',
|
|
71
|
-
'rise-ix_template.txt',
|
|
72
|
-
'tidd-ec_template.txt',
|
|
73
|
-
'rtf_template.txt',
|
|
74
|
-
'hybrid_template.txt',
|
|
75
|
-
];
|
|
59
|
+
// Templates that are not named after a single framework.
|
|
60
|
+
const NON_FRAMEWORK_TEMPLATES = ['hybrid_template.txt'];
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
// Reference docs that define more than one independently-selectable framework.
|
|
63
|
+
// rise.md documents RISE-IE and RISE-IX, each with its own template and routing row.
|
|
64
|
+
const MULTI_FRAMEWORK_DOCS = { 'rise.md': ['rise-ie', 'rise-ix'] };
|
|
65
|
+
|
|
66
|
+
// Read the filesystem rather than hardcoding lists — a hardcoded subset is why
|
|
67
|
+
// count drift and a deleted reference doc could both pass CI silently.
|
|
68
|
+
function listFrameworkDocs() {
|
|
69
|
+
return fs.readdirSync(FRAMEWORKS_DIR).filter(f => f.endsWith('.md')).sort();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function listTemplates() {
|
|
73
|
+
return fs.readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.txt')).sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The advertised framework count: one per reference doc, except docs that
|
|
77
|
+
// define several.
|
|
78
|
+
function expectedFrameworkCount() {
|
|
79
|
+
return listFrameworkDocs().reduce(
|
|
80
|
+
(n, doc) => n + (MULTI_FRAMEWORK_DOCS[doc] ? MULTI_FRAMEWORK_DOCS[doc].length : 1),
|
|
81
|
+
0
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Every template slug a framework doc implies.
|
|
86
|
+
function expectedTemplateSlugs() {
|
|
87
|
+
const slugs = [];
|
|
88
|
+
for (const doc of listFrameworkDocs()) {
|
|
89
|
+
const base = doc.replace(/\.md$/, '');
|
|
90
|
+
if (MULTI_FRAMEWORK_DOCS[doc]) slugs.push(...MULTI_FRAMEWORK_DOCS[doc]);
|
|
91
|
+
else slugs.push(base);
|
|
92
|
+
}
|
|
93
|
+
return slugs;
|
|
94
|
+
}
|
|
81
95
|
|
|
82
96
|
let errorCount = 0;
|
|
83
97
|
let warningCount = 0;
|
|
@@ -115,47 +129,59 @@ function validateSkill() {
|
|
|
115
129
|
|
|
116
130
|
// Check framework files
|
|
117
131
|
info('\nValidating framework files...');
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Warn if framework file is too small
|
|
129
|
-
if (stats.size < 5000) {
|
|
130
|
-
warning(`Framework ${framework} seems small (< 5 KB)`);
|
|
131
|
-
warningCount++;
|
|
132
|
-
}
|
|
132
|
+
const frameworkDocs = listFrameworkDocs();
|
|
133
|
+
if (frameworkDocs.length === 0) {
|
|
134
|
+
error('No framework reference docs found');
|
|
135
|
+
errorCount++;
|
|
136
|
+
}
|
|
137
|
+
frameworkDocs.forEach(framework => {
|
|
138
|
+
const stats = fs.statSync(path.join(FRAMEWORKS_DIR, framework));
|
|
139
|
+
if (stats.size < 5000) {
|
|
140
|
+
warning(`Framework ${framework} seems small (< 5 KB)`);
|
|
141
|
+
warningCount++;
|
|
133
142
|
}
|
|
134
143
|
});
|
|
144
|
+
success(`${frameworkDocs.length} framework reference docs found`);
|
|
135
145
|
|
|
136
|
-
// Check template
|
|
146
|
+
// Check every framework has its template, and no template is orphaned
|
|
137
147
|
info('\nValidating template files...');
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
148
|
+
const templates = listTemplates();
|
|
149
|
+
const templateSet = new Set(templates);
|
|
150
|
+
let templateErrors = 0;
|
|
151
|
+
|
|
152
|
+
for (const slug of expectedTemplateSlugs()) {
|
|
153
|
+
const expected = `${slug}_template.txt`;
|
|
154
|
+
if (!templateSet.has(expected)) {
|
|
155
|
+
error(`Template missing for framework "${slug}": ${expected}`);
|
|
142
156
|
errorCount++;
|
|
143
|
-
|
|
144
|
-
success(`Template found: ${template}`);
|
|
157
|
+
templateErrors++;
|
|
145
158
|
}
|
|
146
|
-
}
|
|
159
|
+
}
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
const expectedSet = new Set([
|
|
162
|
+
...expectedTemplateSlugs().map(s => `${s}_template.txt`),
|
|
163
|
+
...NON_FRAMEWORK_TEMPLATES,
|
|
164
|
+
]);
|
|
165
|
+
for (const template of templates) {
|
|
166
|
+
if (!expectedSet.has(template)) {
|
|
167
|
+
error(`Orphaned template with no corresponding framework doc: ${template}`);
|
|
154
168
|
errorCount++;
|
|
155
|
-
|
|
156
|
-
success(`Script found: ${script}`);
|
|
169
|
+
templateErrors++;
|
|
157
170
|
}
|
|
158
|
-
}
|
|
171
|
+
}
|
|
172
|
+
for (const template of NON_FRAMEWORK_TEMPLATES) {
|
|
173
|
+
if (!templateSet.has(template)) {
|
|
174
|
+
error(`Required non-framework template missing: ${template}`);
|
|
175
|
+
errorCount++;
|
|
176
|
+
templateErrors++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (templateErrors === 0) {
|
|
180
|
+
success(`${templates.length} templates found, all matched to frameworks`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Cross-check the advertised count against what is actually on disk
|
|
184
|
+
validateFrameworkCount();
|
|
159
185
|
|
|
160
186
|
// Check package structure
|
|
161
187
|
info('\nValidating package structure...');
|
|
@@ -170,6 +196,107 @@ function validateSkill() {
|
|
|
170
196
|
|
|
171
197
|
}
|
|
172
198
|
|
|
199
|
+
// Assert the advertised framework count matches the filesystem, everywhere it
|
|
200
|
+
// is stated. This is the check whose absence let "27" survive to v3.2.2 while
|
|
201
|
+
// 28 frameworks shipped.
|
|
202
|
+
function validateFrameworkCount() {
|
|
203
|
+
info('\nValidating advertised framework count...');
|
|
204
|
+
const actual = expectedFrameworkCount();
|
|
205
|
+
const root = path.join(__dirname, '..');
|
|
206
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
207
|
+
|
|
208
|
+
const declared = (pkg.claudeCode && pkg.claudeCode.frameworks) || [];
|
|
209
|
+
if (declared.length !== actual) {
|
|
210
|
+
error(`package.json claudeCode.frameworks lists ${declared.length} frameworks, but ${actual} exist on disk`);
|
|
211
|
+
errorCount++;
|
|
212
|
+
} else {
|
|
213
|
+
success(`claudeCode.frameworks matches disk (${actual})`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Any prose that says "<N> ... framework" or "<N>-framework" must say N = actual.
|
|
217
|
+
const proseSites = [
|
|
218
|
+
'package.json',
|
|
219
|
+
'.claude-plugin/plugin.json',
|
|
220
|
+
'.claude-plugin/marketplace.json',
|
|
221
|
+
'README.md',
|
|
222
|
+
'adapters/system-prompt.md',
|
|
223
|
+
path.join('skills', 'prompt-architect', 'SKILL.md'),
|
|
224
|
+
];
|
|
225
|
+
const countPattern = /(\d+)[\s-]+(?:research-backed\s+)?frameworks?\b/gi;
|
|
226
|
+
let mismatches = 0;
|
|
227
|
+
|
|
228
|
+
for (const rel of proseSites) {
|
|
229
|
+
const full = path.join(root, rel);
|
|
230
|
+
if (!fs.existsSync(full)) continue;
|
|
231
|
+
const lines = fs.readFileSync(full, 'utf8').split('\n');
|
|
232
|
+
lines.forEach((line, i) => {
|
|
233
|
+
let m;
|
|
234
|
+
countPattern.lastIndex = 0;
|
|
235
|
+
while ((m = countPattern.exec(line)) !== null) {
|
|
236
|
+
const n = parseInt(m[1], 10);
|
|
237
|
+
// Ignore small numbers — those are phrases like "two frameworks", or
|
|
238
|
+
// "2 frameworks" inside an explanatory sentence, not the headline count.
|
|
239
|
+
if (n >= 10 && n !== actual) {
|
|
240
|
+
error(`${rel}:${i + 1} advertises ${n} frameworks, but ${actual} exist — "${m[0]}"`);
|
|
241
|
+
errorCount++;
|
|
242
|
+
mismatches++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (mismatches === 0) {
|
|
248
|
+
success(`All advertised framework counts agree (${actual})`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// CLAUDE.md claims CI validates the SKILL.md version. It did not. It does now,
|
|
253
|
+
// along with every other site that carries the version.
|
|
254
|
+
function validateVersionSites() {
|
|
255
|
+
info('\nValidating version consistency across all sites...');
|
|
256
|
+
const root = path.join(__dirname, '..');
|
|
257
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
258
|
+
const expected = pkg.version;
|
|
259
|
+
|
|
260
|
+
const sites = [];
|
|
261
|
+
|
|
262
|
+
sites.push({ name: 'package.json claudeCode.version', value: pkg.claudeCode && pkg.claudeCode.version });
|
|
263
|
+
|
|
264
|
+
const pluginPath = path.join(root, '.claude-plugin', 'plugin.json');
|
|
265
|
+
if (fs.existsSync(pluginPath)) {
|
|
266
|
+
sites.push({ name: 'plugin.json version', value: JSON.parse(fs.readFileSync(pluginPath, 'utf8')).version });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const marketPath = path.join(root, '.claude-plugin', 'marketplace.json');
|
|
270
|
+
if (fs.existsSync(marketPath)) {
|
|
271
|
+
const market = JSON.parse(fs.readFileSync(marketPath, 'utf8'));
|
|
272
|
+
(market.plugins || []).forEach((p, i) => {
|
|
273
|
+
sites.push({ name: `marketplace.json plugins[${i}].version`, value: p.version });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (fs.existsSync(SKILL_FILE)) {
|
|
278
|
+
const fm = fs.readFileSync(SKILL_FILE, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
279
|
+
const m = fm && fm[1].match(/^\s*version:\s*["']?([^"'\r\n]+)["']?\s*$/m);
|
|
280
|
+
sites.push({ name: 'SKILL.md metadata.version', value: m ? m[1].trim() : undefined });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let bad = 0;
|
|
284
|
+
for (const site of sites) {
|
|
285
|
+
if (site.value === undefined) {
|
|
286
|
+
error(`${site.name} is missing — cannot verify it matches package.json (${expected})`);
|
|
287
|
+
errorCount++;
|
|
288
|
+
bad++;
|
|
289
|
+
} else if (site.value !== expected) {
|
|
290
|
+
error(`${site.name} is ${site.value}, expected ${expected}`);
|
|
291
|
+
errorCount++;
|
|
292
|
+
bad++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (bad === 0) {
|
|
296
|
+
success(`All ${sites.length + 1} version sites agree (${expected})`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
173
300
|
function validateSkillFile() {
|
|
174
301
|
const content = fs.readFileSync(SKILL_FILE, 'utf8');
|
|
175
302
|
|
|
@@ -185,7 +312,7 @@ function validateSkillFile() {
|
|
|
185
312
|
const frontmatter = frontmatterMatch[1];
|
|
186
313
|
|
|
187
314
|
// Check required fields
|
|
188
|
-
const requiredFields = ['name', 'description'];
|
|
315
|
+
const requiredFields = ['name', 'description', 'version'];
|
|
189
316
|
requiredFields.forEach(field => {
|
|
190
317
|
if (!frontmatter.includes(`${field}:`)) {
|
|
191
318
|
error(`SKILL.md missing required field: ${field}`);
|
|
@@ -330,6 +457,7 @@ function validatePluginManifest() {
|
|
|
330
457
|
try {
|
|
331
458
|
validateSkill();
|
|
332
459
|
validatePluginManifest();
|
|
460
|
+
validateVersionSites();
|
|
333
461
|
|
|
334
462
|
// Final summary
|
|
335
463
|
log('\n' + '='.repeat(50), 'blue');
|