@dsalazardev/ai-hub-cli 0.1.0 → 0.2.0

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.
@@ -0,0 +1,76 @@
1
+ import { type EngineManifest } from './engine/engine.js';
2
+ import type { ProjectManifest } from './manifest/manifest.js';
3
+ import type { ResolvedPreset } from './presets/resolve.js';
4
+ /** Directorio de agentes dentro del paquete (core/agents). */
5
+ export declare const ENGINE_AGENTS_DIR: string;
6
+ /** Directorio de reglas universales dentro del paquete (core/rules). */
7
+ export declare const ENGINE_RULES_DIR: string;
8
+ export declare const RUBRIC_RULE_IDS: readonly ["skill-quality/skill-rubric", "skill-quality/skill-behavior-rubric", "skill-quality/skill-harden-rubric", "skill-quality/skill-migration-brief"];
9
+ export declare const BASELINE_RULE_IDS: readonly ["core"];
10
+ export declare const UNIVERSAL_RULE_IDS: readonly ["core", "skill-quality/skill-rubric", "skill-quality/skill-behavior-rubric", "skill-quality/skill-harden-rubric", "skill-quality/skill-migration-brief"];
11
+ export declare const PREFLIGHT_RULE_ID = "preflight";
12
+ export declare const UNIVERSAL_TARGET_RULE_IDS: readonly ["core", "skill-quality/skill-rubric", "skill-quality/skill-behavior-rubric", "skill-quality/skill-harden-rubric", "skill-quality/skill-migration-brief", "preflight"];
13
+ export type SelectionMode = 'full' | 'preset' | 'custom';
14
+ export interface SelectionScope {
15
+ selection?: SelectionMode;
16
+ }
17
+ export interface CatalogIndex {
18
+ skills: string[];
19
+ agents: string[];
20
+ skillFiles: Record<string, string[]>;
21
+ ruleFiles: Record<string, string[]>;
22
+ walkCount: number;
23
+ }
24
+ /**
25
+ * Índice memoizado por proceso. En tests, `AI_HUB_CATALOG_INDEX_FILE` apunta al
26
+ * índice que construye `globalSetup`, de modo que todos los workers comparten un
27
+ * único barrido. En producción se construye desde el paquete en ejecución.
28
+ */
29
+ export declare function catalogIndex(): Promise<CatalogIndex>;
30
+ /** Reconstruye el índice ignorando la caché de archivo; lo usa el globalSetup. */
31
+ export declare function rebuildCatalogIndex(): Promise<CatalogIndex>;
32
+ export declare function catalogWalkCount(): number;
33
+ export declare function resetCatalogIndex(): void;
34
+ /** Skills de catálogo: todas las del paquete menos las 17 del motor. */
35
+ export declare function listCatalogSkills(engine: EngineManifest): Promise<string[]>;
36
+ /** Agentes de catálogo publicados en el paquete. */
37
+ export declare function listCatalogAgents(): Promise<string[]>;
38
+ /** Archivos de una skill del catálogo. */
39
+ export declare function catalogSkillFiles(id: string): Promise<string[]>;
40
+ /** Archivos de una rúbrica universal. */
41
+ export declare function universalRuleFiles(id: string): Promise<string[]>;
42
+ export declare function normalizeRuleId(rule: string): string;
43
+ export declare function presetRuleIds(preset: ResolvedPreset): string[];
44
+ export declare function expectedRuleIds(mode: SelectionMode, preset: ResolvedPreset): string[];
45
+ export declare function selectionMode(scope: SelectionScope | undefined): SelectionMode;
46
+ export declare function sameSelection(left: SelectionMode, right: SelectionMode): boolean;
47
+ export declare function hasFullDimension(manifest: ProjectManifest): boolean;
48
+ export declare function catalogHash(manifest: ProjectManifest): string;
49
+ export declare function catalogSeal(manifest: ProjectManifest): {
50
+ version: string;
51
+ hash: string;
52
+ };
53
+ export declare function expectedSkills(mode: SelectionMode, preset: ResolvedPreset, engine: EngineManifest, catalog: string[]): string[] | null;
54
+ export declare function expectedAgents(mode: SelectionMode, preset: ResolvedPreset, catalog: string[]): string[] | null;
55
+ export declare function expectedRulesFor(mode: SelectionMode, preset: ResolvedPreset): string[] | null;
56
+ export declare function difference(expected: string[], actual: string[]): {
57
+ missing: string[];
58
+ extra: string[];
59
+ };
60
+ export interface SelectionAudit {
61
+ mode: SelectionMode;
62
+ modes: {
63
+ skills: SelectionMode;
64
+ agents: SelectionMode;
65
+ rules: SelectionMode;
66
+ };
67
+ expectedSkills: string[] | null;
68
+ expectedAgents: string[] | null;
69
+ expectedRules: string[] | null;
70
+ missing: string[];
71
+ extra: string[];
72
+ errors: string[];
73
+ warnings: string[];
74
+ newAssets: number;
75
+ }
76
+ export declare function auditSelection(root: string, manifest: ProjectManifest, preset: ResolvedPreset, engine: EngineManifest): Promise<SelectionAudit>;
@@ -0,0 +1,321 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { ENGINE_SKILLS_DIR } from './engine/engine.js';
5
+ import { PACKAGE_VERSION } from './package-info.js';
6
+ import { sha256 } from './sync/lock.js';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ /** Directorio de agentes dentro del paquete (core/agents). */
9
+ export const ENGINE_AGENTS_DIR = path.join(__dirname, '..', 'agents');
10
+ /** Directorio de reglas universales dentro del paquete (core/rules). */
11
+ export const ENGINE_RULES_DIR = path.join(__dirname, '..', 'rules');
12
+ export const RUBRIC_RULE_IDS = [
13
+ 'skill-quality/skill-rubric',
14
+ 'skill-quality/skill-behavior-rubric',
15
+ 'skill-quality/skill-harden-rubric',
16
+ 'skill-quality/skill-migration-brief',
17
+ ];
18
+ export const BASELINE_RULE_IDS = ['core'];
19
+ export const UNIVERSAL_RULE_IDS = [...BASELINE_RULE_IDS, ...RUBRIC_RULE_IDS];
20
+ export const PREFLIGHT_RULE_ID = 'preflight';
21
+ export const UNIVERSAL_TARGET_RULE_IDS = [...UNIVERSAL_RULE_IDS, PREFLIGHT_RULE_ID];
22
+ let indexPromise = null;
23
+ let walkCount = 0;
24
+ async function walk(dir) {
25
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
26
+ if (entries === null) {
27
+ return [];
28
+ }
29
+ const files = [];
30
+ for (const entry of entries) {
31
+ const full = path.join(dir, entry.name);
32
+ if (entry.isDirectory()) {
33
+ files.push(...(await walk(full)));
34
+ }
35
+ else if (entry.isFile()) {
36
+ files.push(full);
37
+ }
38
+ }
39
+ return files.sort();
40
+ }
41
+ async function buildCatalogIndex() {
42
+ walkCount += 1;
43
+ const entries = await fs.readdir(ENGINE_SKILLS_DIR, { withFileTypes: true }).catch(() => []);
44
+ const skillFiles = {};
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory()) {
47
+ continue;
48
+ }
49
+ skillFiles[entry.name] = await walk(path.join(ENGINE_SKILLS_DIR, entry.name));
50
+ }
51
+ const ruleFiles = {};
52
+ for (const id of RUBRIC_RULE_IDS) {
53
+ const file = path.join(ENGINE_RULES_DIR, `${id}.md`);
54
+ ruleFiles[id] = (await fs.access(file).then(() => true, () => false)) ? [file] : [];
55
+ }
56
+ const agentFiles = await walk(ENGINE_AGENTS_DIR);
57
+ return {
58
+ skills: Object.keys(skillFiles).sort(),
59
+ agents: agentFiles
60
+ .filter((file) => file.endsWith('.md'))
61
+ .map((file) => path.basename(file, '.md'))
62
+ .sort(),
63
+ skillFiles,
64
+ ruleFiles,
65
+ walkCount,
66
+ };
67
+ }
68
+ async function readIndexFile(file) {
69
+ try {
70
+ const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
71
+ if (!Array.isArray(parsed.skills) ||
72
+ !Array.isArray(parsed.agents) ||
73
+ typeof parsed.skillFiles !== 'object' ||
74
+ parsed.skillFiles === null ||
75
+ typeof parsed.ruleFiles !== 'object' ||
76
+ parsed.ruleFiles === null) {
77
+ return null;
78
+ }
79
+ return {
80
+ skills: parsed.skills,
81
+ agents: parsed.agents,
82
+ skillFiles: parsed.skillFiles,
83
+ ruleFiles: parsed.ruleFiles,
84
+ walkCount: typeof parsed.walkCount === 'number' ? parsed.walkCount : 1,
85
+ };
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Índice memoizado por proceso. En tests, `AI_HUB_CATALOG_INDEX_FILE` apunta al
93
+ * índice que construye `globalSetup`, de modo que todos los workers comparten un
94
+ * único barrido. En producción se construye desde el paquete en ejecución.
95
+ */
96
+ export async function catalogIndex() {
97
+ if (indexPromise === null) {
98
+ indexPromise = (async () => {
99
+ const cacheFile = process.env.AI_HUB_CATALOG_INDEX_FILE;
100
+ if (cacheFile !== undefined && cacheFile !== '') {
101
+ const cached = await readIndexFile(cacheFile);
102
+ if (cached !== null) {
103
+ walkCount = Math.max(walkCount, cached.walkCount);
104
+ return cached;
105
+ }
106
+ }
107
+ return buildCatalogIndex();
108
+ })();
109
+ }
110
+ return indexPromise;
111
+ }
112
+ /** Reconstruye el índice ignorando la caché de archivo; lo usa el globalSetup. */
113
+ export async function rebuildCatalogIndex() {
114
+ indexPromise = buildCatalogIndex();
115
+ return indexPromise;
116
+ }
117
+ export function catalogWalkCount() {
118
+ return walkCount;
119
+ }
120
+ export function resetCatalogIndex() {
121
+ indexPromise = null;
122
+ walkCount = 0;
123
+ }
124
+ /** Skills de catálogo: todas las del paquete menos las 17 del motor. */
125
+ export async function listCatalogSkills(engine) {
126
+ const index = await catalogIndex();
127
+ const engineSkills = new Set(engine.commands.map((command) => command.skill));
128
+ return index.skills.filter((skill) => !engineSkills.has(skill));
129
+ }
130
+ /** Agentes de catálogo publicados en el paquete. */
131
+ export async function listCatalogAgents() {
132
+ return (await catalogIndex()).agents;
133
+ }
134
+ /** Archivos de una skill del catálogo. */
135
+ export async function catalogSkillFiles(id) {
136
+ return (await catalogIndex()).skillFiles[id] ?? [];
137
+ }
138
+ /** Archivos de una rúbrica universal. */
139
+ export async function universalRuleFiles(id) {
140
+ return (await catalogIndex()).ruleFiles[id] ?? [];
141
+ }
142
+ export function normalizeRuleId(rule) {
143
+ return rule
144
+ .replace(/\\/g, '/')
145
+ .replace(/^\.?\//, '')
146
+ .replace(/^rules\//, '')
147
+ .replace(/\.md$/i, '');
148
+ }
149
+ export function presetRuleIds(preset) {
150
+ return [...new Set(preset.rules.map(normalizeRuleId))].sort();
151
+ }
152
+ export function expectedRuleIds(mode, preset) {
153
+ if (mode === 'preset') {
154
+ return presetRuleIds(preset);
155
+ }
156
+ return [...new Set([...UNIVERSAL_RULE_IDS, ...presetRuleIds(preset)])].sort();
157
+ }
158
+ export function selectionMode(scope) {
159
+ return scope?.selection ?? 'preset';
160
+ }
161
+ export function sameSelection(left, right) {
162
+ return left === right;
163
+ }
164
+ export function hasFullDimension(manifest) {
165
+ return (selectionMode(manifest.skills) === 'full' ||
166
+ selectionMode(manifest.agents) === 'full' ||
167
+ selectionMode(manifest.rules) === 'full');
168
+ }
169
+ export function catalogHash(manifest) {
170
+ const payload = {};
171
+ if (selectionMode(manifest.skills) === 'full') {
172
+ payload.skills = [...manifest.skills.publish].sort();
173
+ }
174
+ if (selectionMode(manifest.agents) === 'full') {
175
+ payload.agents = [...manifest.agents.publish].sort();
176
+ }
177
+ if (selectionMode(manifest.rules) === 'full') {
178
+ payload.rules = [...manifest.rules.publish].sort();
179
+ }
180
+ return sha256(JSON.stringify(payload));
181
+ }
182
+ export function catalogSeal(manifest) {
183
+ return { version: PACKAGE_VERSION, hash: catalogHash(manifest) };
184
+ }
185
+ export function expectedSkills(mode, preset, engine, catalog) {
186
+ if (mode === 'full') {
187
+ return [...catalog].sort();
188
+ }
189
+ if (mode === 'preset') {
190
+ return [...preset.skills].sort();
191
+ }
192
+ void engine;
193
+ return null;
194
+ }
195
+ export function expectedAgents(mode, preset, catalog) {
196
+ if (mode === 'full') {
197
+ return [...catalog].sort();
198
+ }
199
+ if (mode === 'preset') {
200
+ return [...preset.agents].sort();
201
+ }
202
+ return null;
203
+ }
204
+ export function expectedRulesFor(mode, preset) {
205
+ if (mode === 'custom') {
206
+ return null;
207
+ }
208
+ return expectedRuleIds(mode, preset);
209
+ }
210
+ export function difference(expected, actual) {
211
+ const expectedSet = new Set(expected);
212
+ const actualSet = new Set(actual);
213
+ return {
214
+ missing: expected.filter((id) => !actualSet.has(id)).sort(),
215
+ extra: actual.filter((id) => !expectedSet.has(id)).sort(),
216
+ };
217
+ }
218
+ async function fileExists(file) {
219
+ return fs.access(file).then(() => true, () => false);
220
+ }
221
+ export async function auditSelection(root, manifest, preset, engine) {
222
+ const modes = {
223
+ skills: selectionMode(manifest.skills),
224
+ agents: selectionMode(manifest.agents),
225
+ rules: selectionMode(manifest.rules),
226
+ };
227
+ const mode = modes.skills === modes.agents && modes.agents === modes.rules ? modes.skills : 'custom';
228
+ const errors = [];
229
+ const warnings = [];
230
+ const missing = [];
231
+ const extra = [];
232
+ const catalogSkills = await listCatalogSkills(engine);
233
+ const catalogAgents = await listCatalogAgents();
234
+ const expectedSkills = expectedSkillsFor(modes.skills, preset, engine, catalogSkills);
235
+ const expectedAgents = expectedAgentsFor(modes.agents, preset, catalogAgents);
236
+ const expectedRules = expectedRulesFor(modes.rules, preset);
237
+ let newAssets = 0;
238
+ const compare = (dimension, expected, actual) => {
239
+ if (expected === null) {
240
+ return;
241
+ }
242
+ const diff = difference(expected, actual);
243
+ missing.push(...diff.missing.map((id) => `${dimension}:${id}`));
244
+ extra.push(...diff.extra.map((id) => `${dimension}:${id}`));
245
+ if (diff.missing.length > 0) {
246
+ errors.push(`${dimension}s.publish incompleto: faltan ${diff.missing.join(', ')}`);
247
+ }
248
+ if (diff.extra.length > 0) {
249
+ errors.push(`${dimension}s.publish contiene ids fuera de la selección: ${diff.extra.join(', ')}`);
250
+ }
251
+ };
252
+ compare('skill', expectedSkills, manifest.skills.publish);
253
+ compare('agent', expectedAgents, manifest.agents.publish);
254
+ compare('rule', expectedRules, manifest.rules.publish);
255
+ const anyFull = Object.values(modes).includes('full');
256
+ if (anyFull) {
257
+ if (manifest.catalog === undefined) {
258
+ errors.push('selección full sin catalog.version/catalog.hash; ejecuta install --full o init');
259
+ }
260
+ else {
261
+ const currentHash = catalogHash(manifest);
262
+ if (currentHash !== manifest.catalog.hash) {
263
+ errors.push('full incompleto: publish editado; ejecuta install --full o restaura el manifest');
264
+ }
265
+ else if (manifest.catalog.version !== PACKAGE_VERSION) {
266
+ const expectedFull = [
267
+ ...(modes.skills === 'full' ? (expectedSkills ?? []) : []),
268
+ ...(modes.agents === 'full' ? (expectedAgents ?? []) : []),
269
+ ...(modes.rules === 'full' ? (expectedRules ?? []) : []),
270
+ ];
271
+ const actualFull = [
272
+ ...(modes.skills === 'full' ? manifest.skills.publish : []),
273
+ ...(modes.agents === 'full' ? manifest.agents.publish : []),
274
+ ...(modes.rules === 'full' ? manifest.rules.publish : []),
275
+ ];
276
+ newAssets = difference(expectedFull, actualFull).missing.length;
277
+ warnings.push(`full desactualizado: ${newAssets} activos nuevos; ejecuta upgrade`);
278
+ }
279
+ }
280
+ }
281
+ for (const skill of manifest.skills.publish) {
282
+ const file = path.join(root, manifest.skills.source, skill, 'SKILL.md');
283
+ if (!(await fileExists(file))) {
284
+ missing.push(`skill:${skill}`);
285
+ errors.push(`skills.publish "${skill}" no existe en ${manifest.skills.source}`);
286
+ }
287
+ }
288
+ for (const agent of manifest.agents.publish) {
289
+ const file = path.join(root, manifest.agents.source, `${agent}.md`);
290
+ if (!(await fileExists(file))) {
291
+ missing.push(`agent:${agent}`);
292
+ errors.push(`agents.publish "${agent}" no existe en ${manifest.agents.source}`);
293
+ }
294
+ }
295
+ for (const rule of manifest.rules.publish) {
296
+ const file = path.join(root, manifest.rules.source, `${rule}.md`);
297
+ if (!(await fileExists(file))) {
298
+ missing.push(`rule:${rule}`);
299
+ errors.push(`rules.publish "${rule}" no existe en ${manifest.rules.source}`);
300
+ }
301
+ }
302
+ return {
303
+ mode,
304
+ modes,
305
+ expectedSkills,
306
+ expectedAgents,
307
+ expectedRules,
308
+ missing: [...new Set(missing)].sort(),
309
+ extra: [...new Set(extra)].sort(),
310
+ errors,
311
+ warnings,
312
+ newAssets,
313
+ };
314
+ }
315
+ // Nombres internos para evitar colisión con las funciones exportadas de expectativas.
316
+ function expectedSkillsFor(mode, preset, engine, catalog) {
317
+ return expectedSkills(mode, preset, engine, catalog);
318
+ }
319
+ function expectedAgentsFor(mode, preset, catalog) {
320
+ return expectedAgents(mode, preset, catalog);
321
+ }
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { loadManifestContext } from '../manifest/manifest.js';
5
5
  import { readLedger, resumeLedger } from '../harness/memory/ledger.js';
6
+ import { isEngineCheckout } from '../source-checkout.js';
6
7
  export const CONTEXT_RELATIVE = '.planning/context-next.md';
7
8
  export const CONTEXT_EVENTS = 5;
8
9
  async function stateText(root) {
@@ -62,6 +63,10 @@ export async function buildContext(root) {
62
63
  return content;
63
64
  }
64
65
  export async function cmdContext(_args, ctx) {
66
+ if (!(await isEngineCheckout(ctx.root))) {
67
+ ctx.io.out('context: no aplica en instalación');
68
+ return 0;
69
+ }
65
70
  try {
66
71
  await buildContext(ctx.root);
67
72
  ctx.io.out(`[OK] Contexto generado: ${CONTEXT_RELATIVE}`);
@@ -1,7 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { readAiHubRc } from '../paths.js';
6
+ import { auditSelection } from '../catalog.js';
5
7
  import { ENGINE_COUNTS, ENGINE_LANES, ENGINE_SKILLS_DIR } from '../engine/engine.js';
6
8
  import { scanCoupling, scanLegacyArtifacts } from '../engine/gates.js';
7
9
  import { checkInventory, loadManifestContext, } from '../manifest/manifest.js';
@@ -24,6 +26,45 @@ async function couplingRules() {
24
26
  async function exists(file) {
25
27
  return fs.access(file).then(() => true, () => false);
26
28
  }
29
+ function gitOutput(root, args) {
30
+ const result = spawnSync('git', args, {
31
+ cwd: root,
32
+ encoding: 'utf8',
33
+ shell: false,
34
+ });
35
+ return {
36
+ status: result.status ?? 1,
37
+ text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
38
+ };
39
+ }
40
+ function versionedDestinations(root) {
41
+ const inside = gitOutput(root, ['rev-parse', '--is-inside-work-tree']);
42
+ if (inside.status !== 0 || inside.text !== 'true') {
43
+ return { applicable: false, tracked: [] };
44
+ }
45
+ const listed = gitOutput(root, [
46
+ 'ls-files',
47
+ '--',
48
+ '.claude',
49
+ '.opencode',
50
+ '.pi',
51
+ '.cursor',
52
+ '.github/skills',
53
+ '.github/agents',
54
+ '.github/instructions',
55
+ '.mcp.json',
56
+ '.cursor/mcp.json',
57
+ '.vscode/mcp.json',
58
+ 'opencode.json',
59
+ ]);
60
+ if (listed.status !== 0) {
61
+ return { applicable: false, tracked: [] };
62
+ }
63
+ return {
64
+ applicable: true,
65
+ tracked: listed.text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== ''),
66
+ };
67
+ }
27
68
  export async function diagnose(ctx) {
28
69
  const checks = [];
29
70
  const add = (id, ok, detail) => {
@@ -32,16 +73,18 @@ export async function diagnose(ctx) {
32
73
  const source = await sourceCheckout(ctx.root);
33
74
  let manifest = null;
34
75
  let engine = null;
76
+ let preset = null;
35
77
  try {
36
78
  const context = await loadManifestContext(ctx.root);
37
79
  manifest = context.manifest;
38
80
  engine = context.engine;
81
+ preset = context.preset;
39
82
  add('manifest', true, `manifest.yaml válido (proyecto ${manifest.project.id})`);
40
83
  }
41
84
  catch (error) {
42
85
  add('manifest', false, error instanceof Error ? error.message : String(error));
43
86
  }
44
- if (manifest && engine) {
87
+ if (manifest && engine && preset) {
45
88
  add('preset', true, `${manifest.preset.id}@${manifest.preset.version} (${manifest.preset.source})`);
46
89
  add('engine:roles', engine.roles.length === ENGINE_COUNTS.roles, `${engine.roles.length}/${ENGINE_COUNTS.roles}`);
47
90
  add('engine:commands', engine.commands.length === ENGINE_COUNTS.commands, `${engine.commands.length}/${ENGINE_COUNTS.commands}`);
@@ -60,20 +103,49 @@ export async function diagnose(ctx) {
60
103
  add('policy:optOuts', true, manifest.policy.optOuts.length === 0
61
104
  ? 'ninguno'
62
105
  : `opt-outs activos: ${manifest.policy.optOuts.join(', ')}`);
63
- const missingSkills = [];
106
+ const missingEngineSkills = [];
64
107
  for (const command of engine.commands) {
65
108
  if (!(await exists(path.join(ENGINE_SKILLS_DIR, command.skill, 'SKILL.md')))) {
66
- missingSkills.push(`${command.id} -> ${command.skill}`);
109
+ missingEngineSkills.push(`${command.id} -> ${command.skill}`);
67
110
  }
68
111
  }
69
- for (const skill of manifest.skills.publish) {
70
- if (!(await exists(path.join(ctx.root, manifest.skills.source, skill, 'SKILL.md')))) {
71
- missingSkills.push(`${skill} -> ${manifest.skills.source}`);
72
- }
112
+ const audit = await auditSelection(ctx.root, manifest, preset, engine);
113
+ const missingProjectSkills = audit.missing
114
+ .filter((item) => item.startsWith('skill:'))
115
+ .map((item) => item.slice('skill:'.length));
116
+ add('skills', missingEngineSkills.length === 0 && missingProjectSkills.length === 0, missingEngineSkills.length === 0 && missingProjectSkills.length === 0
117
+ ? `17 nativas + ${manifest.skills.publish.length} de proyecto (${audit.modes.skills})`
118
+ : [...missingEngineSkills, ...missingProjectSkills.map((id) => `${id} -> ${manifest.skills.source}`)].join(', '));
119
+ const missingAgents = audit.missing
120
+ .filter((item) => item.startsWith('agent:'))
121
+ .map((item) => item.slice('agent:'.length));
122
+ add('agents', missingAgents.length === 0, missingAgents.length === 0
123
+ ? `${manifest.agents.publish.length} agentes (${audit.modes.agents})`
124
+ : `faltan: ${missingAgents.join(', ')}`);
125
+ const missingRules = audit.missing
126
+ .filter((item) => item.startsWith('rule:'))
127
+ .map((item) => item.slice('rule:'.length));
128
+ add('rules', missingRules.length === 0, missingRules.length === 0
129
+ ? `${manifest.rules.publish.length + 1} reglas universales (${audit.modes.rules})`
130
+ : `faltan: ${missingRules.join(', ')}`);
131
+ const primaryError = audit.errors.find((error) => error.includes('publish editado')) ?? audit.errors[0];
132
+ add('completeness', audit.errors.length === 0, audit.errors.length === 0
133
+ ? `skills=${audit.modes.skills} agents=${audit.modes.agents} rules=${audit.modes.rules}`
134
+ : primaryError);
135
+ if (Object.values(audit.modes).includes('full')) {
136
+ add('full', true, audit.warnings.length > 0 ? audit.warnings.join('; ') : 'catálogo al día');
137
+ }
138
+ if (!manifest.mcp) {
139
+ add('mcp', true, 'MCP: no configurado');
140
+ }
141
+ else {
142
+ const direct = path.join(ctx.root, manifest.mcp.source);
143
+ const project = path.join(ctx.root, '.ai-hub', path.basename(manifest.mcp.source));
144
+ const present = (await exists(direct)) || (await exists(project));
145
+ add('mcp', present, present ? `MCP configurado (${manifest.mcp.source})` : `MCP: falta ${manifest.mcp.source}`);
73
146
  }
74
- add('skills', missingSkills.length === 0, missingSkills.length === 0 ? '17 skills nativas + extras' : missingSkills.join(', '));
75
147
  if (source.isSource) {
76
- const required = ['dist', 'schema', 'manifest.json', 'engine', 'presets', 'agents', 'skills'];
148
+ const required = ['dist', 'schema', 'manifest.json', 'engine', 'presets', 'rules', 'agents', 'skills'];
77
149
  const missing = required.filter((entry) => !source.files.includes(entry));
78
150
  add('package:files', missing.length === 0, missing.length === 0 ? required.join(', ') : `faltan: ${missing.join(', ')}`);
79
151
  add('package:entry', await exists(path.join(ctx.root, 'core', 'dist', 'bin', 'ai-hub.js')), 'core/dist/bin/ai-hub.js');
@@ -169,6 +241,16 @@ export async function diagnose(ctx) {
169
241
  const ignored = ignore.split(/\r?\n/).includes('.worktrees/');
170
242
  add('worktrees:ignored', ignored, ignored ? '.worktrees/ ignorado' : 'añade .worktrees/ a .gitignore');
171
243
  }
244
+ const versioning = versionedDestinations(ctx.root);
245
+ if (!versioning.applicable) {
246
+ add('versionado', true, 'no aplica (sin repositorio git)');
247
+ }
248
+ else if (versioning.tracked.length === 0) {
249
+ add('versionado', true, 'sin destinos generados versionados');
250
+ }
251
+ else {
252
+ add('versionado', true, `aviso: destinos generados versionados: ${versioning.tracked.slice(0, 5).join(', ')}${versioning.tracked.length > 5 ? ` (+${versioning.tracked.length - 5})` : ''}`);
253
+ }
172
254
  const coupling = await couplingRules();
173
255
  if (source.isSource) {
174
256
  const findings = await scanCoupling(ctx.root, coupling.terms);
@@ -1,5 +1,5 @@
1
1
  import type { ParsedArgs } from './types.js';
2
2
  export declare function splitItem(args: ParsedArgs): {
3
- kind: 'skill' | 'agent' | null;
3
+ kind: 'skill' | 'agent' | 'rule' | null;
4
4
  id: string | null;
5
5
  };
package/dist/cli/items.js CHANGED
@@ -3,7 +3,7 @@ export function splitItem(args) {
3
3
  if (!first) {
4
4
  return { kind: null, id: null };
5
5
  }
6
- if (first === 'skill' || first === 'agent') {
6
+ if (first === 'skill' || first === 'agent' || first === 'rule') {
7
7
  return { kind: first, id: second ?? null };
8
8
  }
9
9
  return { kind: null, id: first };