@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.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +116 -5
  4. package/MIGRATION.md +1 -1
  5. package/README.md +42 -45
  6. package/adapters/README.md +1 -1
  7. package/adapters/system-prompt.md +41 -11
  8. package/package.json +5 -4
  9. package/scripts/install.js +52 -22
  10. package/scripts/test.js +62 -40
  11. package/scripts/validate-skill.js +184 -56
  12. package/skills/prompt-architect/SKILL.md +49 -47
  13. package/skills/prompt-architect/assets/templates/chain-of-density_template.txt +60 -30
  14. package/skills/prompt-architect/assets/templates/iterative-compression_template.txt +59 -0
  15. package/skills/prompt-architect/references/frameworks/ape.md +2 -0
  16. package/skills/prompt-architect/references/frameworks/bab.md +2 -0
  17. package/skills/prompt-architect/references/frameworks/cai-critique-revise.md +1 -1
  18. package/skills/prompt-architect/references/frameworks/chain-of-density.md +95 -423
  19. package/skills/prompt-architect/references/frameworks/chain-of-thought.md +17 -12
  20. package/skills/prompt-architect/references/frameworks/co-star.md +2 -0
  21. package/skills/prompt-architect/references/frameworks/ctf.md +3 -1
  22. package/skills/prompt-architect/references/frameworks/iterative-compression.md +481 -0
  23. package/skills/prompt-architect/references/frameworks/pre-mortem.md +6 -6
  24. package/skills/prompt-architect/references/frameworks/race.md +2 -0
  25. package/skills/prompt-architect/references/frameworks/react.md +1 -1
  26. package/skills/prompt-architect/references/frameworks/reverse-role.md +1 -1
  27. package/skills/prompt-architect/references/frameworks/rise.md +27 -8
  28. package/skills/prompt-architect/references/frameworks/risen.md +2 -0
  29. package/skills/prompt-architect/references/frameworks/rtf.md +2 -0
  30. package/skills/prompt-architect/references/frameworks/skeleton-of-thought.md +1 -1
  31. package/skills/prompt-architect/references/frameworks/step-back.md +1 -1
  32. package/skills/prompt-architect/references/frameworks/tidd-ec.md +19 -6
  33. package/skills/prompt-architect/scripts/framework_analyzer.py +0 -807
  34. package/skills/prompt-architect/scripts/prompt_evaluator.py +0 -336
@@ -46,18 +46,24 @@ const AGENTS = [
46
46
  return true;
47
47
  } catch { return false; }
48
48
  },
49
- installPath: () => path.join(os.homedir(), '.gemini', 'skills', SKILL_NAME),
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: () => path.join(os.homedir(), '.agents', 'skills', SKILL_NAME),
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: () => '~/.agents/skills/ (30+ compatible agents)',
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
- // Remove old content if present
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
- fs.writeFileSync(destPath, adapterContent, 'utf8');
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 Overwrite existing installations
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() && !a.alwaysShow).map(a => a.id);
422
- installNonInteractive(detected.length > 0 ? detected : ['claude'], sourcePath, adaptersPath, isProject);
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
- for (const agent of AGENTS) {
439
- if (agent.id !== 'claude') continue;
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
- test('All framework files exist', () => {
67
- const frameworks = [
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('All templates exist', () => {
85
- const templates = [
86
- 'co-star_template.txt',
87
- 'risen_template.txt',
88
- 'rise-ie_template.txt',
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('Python scripts exist', () => {
103
- const scripts = ['framework_analyzer.py', 'prompt_evaluator.py'];
104
- scripts.forEach(s => {
105
- const spath = path.join('skills', 'prompt-architect', 'scripts', s);
106
- if (!fs.existsSync(spath)) {
107
- throw new Error(`Script ${s} not found`);
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 REQUIRED_FRAMEWORKS = [
58
- 'co-star.md',
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
- const REQUIRED_TEMPLATES = [
68
- 'co-star_template.txt',
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
- const REQUIRED_SCRIPTS = [
78
- 'framework_analyzer.py',
79
- 'prompt_evaluator.py',
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
- REQUIRED_FRAMEWORKS.forEach(framework => {
119
- const frameworkPath = path.join(SKILL_DIR, 'references', 'frameworks', framework);
120
- if (!fs.existsSync(frameworkPath)) {
121
- error(`Framework file missing: ${framework}`);
122
- errorCount++;
123
- } else {
124
- const stats = fs.statSync(frameworkPath);
125
- const sizeKB = (stats.size / 1024).toFixed(2);
126
- success(`Framework found: ${framework} (${sizeKB} KB)`);
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 files
146
+ // Check every framework has its template, and no template is orphaned
137
147
  info('\nValidating template files...');
138
- REQUIRED_TEMPLATES.forEach(template => {
139
- const templatePath = path.join(SKILL_DIR, 'assets', 'templates', template);
140
- if (!fs.existsSync(templatePath)) {
141
- error(`Template file missing: ${template}`);
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
- } else {
144
- success(`Template found: ${template}`);
157
+ templateErrors++;
145
158
  }
146
- });
159
+ }
147
160
 
148
- // Check script files
149
- info('\nValidating script files...');
150
- REQUIRED_SCRIPTS.forEach(script => {
151
- const scriptPath = path.join(SKILL_DIR, 'scripts', script);
152
- if (!fs.existsSync(scriptPath)) {
153
- error(`Script file missing: ${script}`);
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
- } else {
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');