@dsalazardev/ai-hub-cli 0.2.0 → 0.3.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.
Files changed (78) hide show
  1. package/README.md +63 -4
  2. package/dist/backup/snapshots.js +12 -0
  3. package/dist/catalog.d.ts +8 -1
  4. package/dist/catalog.js +38 -9
  5. package/dist/cli/args.js +1 -0
  6. package/dist/cli/doctor.js +20 -11
  7. package/dist/cli/maintenance.js +21 -39
  8. package/dist/cli/memory.js +4 -0
  9. package/dist/cli/onboarding.d.ts +13 -0
  10. package/dist/cli/onboarding.js +128 -14
  11. package/dist/cli/run.d.ts +3 -1
  12. package/dist/cli/run.js +5 -4
  13. package/dist/cli/types.d.ts +3 -0
  14. package/dist/engine/engine.d.ts +19 -1
  15. package/dist/engine/engine.js +52 -0
  16. package/dist/engine/gates.d.ts +16 -0
  17. package/dist/engine/gates.js +102 -1
  18. package/dist/harness/memory/ledger.d.ts +6 -0
  19. package/dist/harness/memory/ledger.js +44 -2
  20. package/dist/manifest/manifest.d.ts +3 -1
  21. package/dist/manifest/manifest.js +16 -10
  22. package/dist/presets/materialize.d.ts +5 -0
  23. package/dist/presets/materialize.js +30 -0
  24. package/dist/speckit/binding.d.ts +15 -0
  25. package/dist/speckit/binding.js +66 -0
  26. package/dist/speckit/commands.d.ts +46 -0
  27. package/dist/speckit/commands.js +131 -0
  28. package/dist/speckit/doctor.d.ts +14 -0
  29. package/dist/speckit/doctor.js +103 -0
  30. package/dist/speckit/runner.d.ts +19 -0
  31. package/dist/speckit/runner.js +22 -0
  32. package/dist/speckit/seal.d.ts +12 -0
  33. package/dist/speckit/seal.js +47 -0
  34. package/dist/speckit/settings.d.ts +36 -0
  35. package/dist/speckit/settings.js +75 -0
  36. package/dist/speckit/state.d.ts +59 -0
  37. package/dist/speckit/state.js +279 -0
  38. package/dist/speckit/version.d.ts +8 -0
  39. package/dist/speckit/version.js +53 -0
  40. package/dist/sync/config.js +3 -2
  41. package/dist/sync/render.js +30 -0
  42. package/dist/sync/sync.d.ts +4 -0
  43. package/dist/sync/sync.js +48 -5
  44. package/dist/sync/targets.d.ts +13 -0
  45. package/dist/sync/targets.js +5 -0
  46. package/engine/engine.yaml +65 -31
  47. package/manifest.json +20 -48
  48. package/package.json +1 -1
  49. package/schema/engine.schema.json +22 -3
  50. package/schema/manifest.schema.json +10 -0
  51. package/skills/shared/analyze/SKILL.md +2 -1
  52. package/skills/shared/clarify/SKILL.md +2 -1
  53. package/skills/shared/implement/SKILL.md +2 -1
  54. package/skills/shared/plan/SKILL.md +2 -1
  55. package/skills/shared/review/SKILL.md +2 -1
  56. package/skills/shared/sdd/SKILL.md +2 -1
  57. package/skills/shared/sdd-init/SKILL.md +2 -1
  58. package/skills/shared/ship/SKILL.md +2 -1
  59. package/skills/shared/specify/SKILL.md +2 -1
  60. package/skills/shared/tasks/SKILL.md +2 -1
  61. package/skills/shared/verify/SKILL.md +2 -1
  62. package/skills/shared/speckit-analyze/SKILL.md +0 -35
  63. package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
  64. package/skills/shared/speckit-assess-define/SKILL.md +0 -36
  65. package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
  66. package/skills/shared/speckit-assess-research/SKILL.md +0 -36
  67. package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
  68. package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
  69. package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
  70. package/skills/shared/speckit-bug-test/SKILL.md +0 -37
  71. package/skills/shared/speckit-checklist/SKILL.md +0 -35
  72. package/skills/shared/speckit-clarify/SKILL.md +0 -32
  73. package/skills/shared/speckit-constitution/SKILL.md +0 -35
  74. package/skills/shared/speckit-converge/SKILL.md +0 -36
  75. package/skills/shared/speckit-implement/SKILL.md +0 -32
  76. package/skills/shared/speckit-plan/SKILL.md +0 -37
  77. package/skills/shared/speckit-specify/SKILL.md +0 -36
  78. package/skills/shared/speckit-tasks/SKILL.md +0 -34
package/README.md CHANGED
@@ -7,6 +7,11 @@ el proyecto ya usa, sin acoplarse a un lenguaje ni a un dominio concreto.
7
7
  ## Requisitos
8
8
 
9
9
  - Node.js >= 22
10
+ - [uv](https://docs.astral.sh/uv/getting-started/installation/) con `uvx` en el PATH: `init` e `install`
11
+ ejecutan Spec Kit con `uvx --from specify-cli==<versión fijada>`, sin `uv tool install`.
12
+ - `bash` para los scripts `sh` de Spec Kit (por defecto en todas las plataformas). En Windows,
13
+ Git for Windows (Git Bash) en el PATH; el lanzador `bash` de WSL sin distribución no sirve.
14
+ Alternativa: `speckit.script: ps` en `manifest.yaml` (requiere PowerShell).
10
15
 
11
16
  ## Uso rápido
12
17
 
@@ -19,8 +24,40 @@ npx @dsalazardev/ai-hub-cli doctor
19
24
  ```
20
25
 
21
26
  `init` detecta el preset según el stack del repositorio y materializa `.ai-hub/`,
22
- que es obligatoria. `install` proyecta reglas, agentes y skills a los destinos
23
- declarados en `targets`. `doctor` valida motor, preset, manifiesto y deriva.
27
+ que es obligatoria, e instala Spec Kit en la raíz con las extensiones `bug` y `assess`.
28
+ `install` proyecta reglas, agentes y skills a los destinos declarados en `targets` y
29
+ repite la instalación de Spec Kit. `doctor` valida motor, preset, manifiesto, deriva y
30
+ Spec Kit.
31
+
32
+ ## Spec Kit
33
+
34
+ Spec Kit ejecuta los 17 comandos de la metodología; AI-Hub define roles, estados y
35
+ etiquetas. El paquete fija la versión de `specify-cli`, la de las extensiones y una
36
+ fecha `--exclude-newer` para las dependencias de `uvx`. Esa fecha se mueve solo en un
37
+ release, nunca automáticamente. `manifest.yaml` puede restringir la versión dentro del rango
38
+ soportado (`speckit.version`), elegir el script (`speckit.script: sh | ps`) y la integración
39
+ por defecto (`speckit.defaultTarget`), pero no ampliar la versión ni cambiar las extensiones.
40
+
41
+ - Spec Kit se instala en `claude`, `opencode`, `pi` y `cursor` (`cursor-agent`). **Copilot
42
+ no recibe Spec Kit**: recibe agentes, reglas y la regla `ai-hub-speckit`.
43
+ - Sin `uv`, sin red y sin caché para la versión fijada, o sin `bash` con `script: sh`,
44
+ `init` sale con código 2 **antes de escribir nada** y dice qué falta.
45
+ - `init --no-speckit` instala sin Spec Kit y deja el arnés **degradado** (`doctor` sale 1).
46
+ Un `ai-hub install` posterior sin la bandera lo reactiva.
47
+ - `.specify/` y `specs/` se versionan; las copias de los comandos en las carpetas de los
48
+ asistentes no, y `install` las regenera en un clon nuevo.
49
+
50
+ ### CI
51
+
52
+ `doctor` no necesita Python, pero un clon nuevo no tiene las copias de los comandos de
53
+ Spec Kit en las carpetas de los asistentes, que están ignoradas. El job instala `uv`,
54
+ regenera con `install` y verifica:
55
+
56
+ ```yaml
57
+ - uses: astral-sh/setup-uv@v6
58
+ - run: npx @dsalazardev/ai-hub-cli install
59
+ - run: npx @dsalazardev/ai-hub-cli doctor
60
+ ```
24
61
 
25
62
  Si la detección es ambigua, el preset se elige explícitamente:
26
63
 
@@ -48,14 +85,36 @@ npx @dsalazardev/ai-hub-cli install --targets claude
48
85
  | `context` | Regenera `.planning/context-next.md` |
49
86
  | `purge` | Retira los destinos generados, con snapshot previo |
50
87
 
88
+ ## Checks de Spec Kit en `doctor`
89
+
90
+ `doctor` verifica la instalación de Spec Kit leyendo `.specify/` y calculando hashes; no
91
+ ejecuta Python ni `uvx`. En el checkout del motor todos informan `no aplica`.
92
+
93
+ | Check | Criterio | Reparación |
94
+ |---|---|---|
95
+ | `speckit:managed` | `speckit.managed` no es `false`; un arnés con `--no-speckit` es **degradado** y `doctor` sale 1 | `ai-hub install` (sin `--no-speckit`) |
96
+ | `speckit:installed` | Existe `.specify/init-options.json` | `ai-hub install` |
97
+ | `speckit:version` | `speckit_version` coincide con la versión fijada (o la que restrinja `speckit.version`) | `ai-hub install` |
98
+ | `speckit:shell` | El shell del script declarado ejecuta `echo ok` (`bash` para `sh`, PowerShell para `ps`) | Instalar Git Bash/bash, o declarar `speckit.script: ps` |
99
+ | `speckit:integrations` | `.specify/integration.json` tiene exactamente las integraciones de los destinos con Spec Kit, sin `copilot`, y la integración por defecto declarada | `ai-hub install`; si aparece `copilot`, `specify integration uninstall copilot` |
100
+ | `speckit:targets` | Informativo: `copilot: sin Spec Kit` (recibe agentes y reglas, no comandos) | — |
101
+ | `speckit:extensions` | `bug` y `assess` instalados en la versión fijada, registrados y presentes en cada destino con Spec Kit | `ai-hub install` |
102
+ | `speckit:integrity` | Cada archivo de `.specify/integrations/*.manifest.json` existe; si su hash difiere solo avisa (el próximo `init` lo sobrescribe) | `ai-hub install`; personalizar con overrides de Spec Kit, no editando plantillas |
103
+ | `speckit:collision` | Ninguna ruta de `sync.lock.json` pertenece a Spec Kit | `ai-hub sync`; no publicar skills con nombre `speckit-*` |
104
+ | `speckit:constitution` | Aviso si `.specify/memory/constitution.md` sigue siendo la plantilla | Ejecutar `/speckit.constitution` (o `/speckit-constitution`) |
105
+ | `speckit:versioning` | En un repositorio git no se ignoran la constitución, `init-options.json` ni `specs/` | Quitar esas rutas de `.gitignore` |
106
+
51
107
  ## Estructura instalada
52
108
 
53
109
  ```
54
110
  .ai-hub/ preset materializado, manifiesto y configuración (obligatoria)
55
- .planning/ estado, hallazgos y ledger de la sesión
56
- .claude/ .opencode/ .pi/ .cursor/ .github/ destinos generados según targets
111
+ .specify/ Spec Kit: constitución, plantillas, scripts, assessments y bugs (versionado)
112
+ specs/ features de Spec Kit: spec.md, plan.md, tasks.md (versionado)
113
+ .claude/ .opencode/ .pi/ .cursor/ .github/ destinos generados según targets (ignorados)
57
114
  ```
58
115
 
116
+ Una instalación no crea `.planning/`: es estado del checkout de desarrollo del motor.
117
+
59
118
  ## Procedencia de activos
60
119
 
61
120
  Ver `NOTICE.md` para el origen de skills y agentes reutilizados.
@@ -38,6 +38,11 @@ async function walkRelative(root, relative) {
38
38
  }
39
39
  return files;
40
40
  }
41
+ const SPECKIT_CONFIG_FILES = [
42
+ '.specify/init-options.json',
43
+ '.specify/integration.json',
44
+ '.specify/extensions.yml',
45
+ ];
41
46
  async function managedFiles(root) {
42
47
  const files = new Set([
43
48
  'sync.lock.json',
@@ -52,6 +57,13 @@ async function managedFiles(root) {
52
57
  for (const rel of await walkRelative(root, '.ai-hub')) {
53
58
  files.add(rel);
54
59
  }
60
+ // Estado de Spec Kit que un nuevo `specify init` podría tocar: constitución y configuración.
61
+ for (const rel of await walkRelative(root, '.specify/memory')) {
62
+ files.add(rel);
63
+ }
64
+ for (const rel of SPECKIT_CONFIG_FILES) {
65
+ files.add(rel);
66
+ }
55
67
  const result = [];
56
68
  for (const rel of [...files].sort()) {
57
69
  if (await exists(path.join(root, rel))) {
package/dist/catalog.d.ts CHANGED
@@ -16,11 +16,18 @@ export interface SelectionScope {
16
16
  }
17
17
  export interface CatalogIndex {
18
18
  skills: string[];
19
+ /** Skills del paquete que declaran `runtime: false`: siguen publicadas pero no son instalables. */
20
+ retiredSkills: string[];
19
21
  agents: string[];
20
22
  skillFiles: Record<string, string[]>;
21
23
  ruleFiles: Record<string, string[]>;
22
24
  walkCount: number;
23
25
  }
26
+ /**
27
+ * Índice de las skills instalables de un directorio de catálogo. Las skills con
28
+ * `runtime: false` siguen en el paquete pero no se listan, materializan ni proyectan.
29
+ */
30
+ export declare function buildCatalogIndexFrom(skillsDir: string): Promise<Pick<CatalogIndex, 'skills' | 'skillFiles' | 'retiredSkills'>>;
24
31
  /**
25
32
  * Índice memoizado por proceso. En tests, `AI_HUB_CATALOG_INDEX_FILE` apunta al
26
33
  * índice que construye `globalSetup`, de modo que todos los workers comparten un
@@ -31,7 +38,7 @@ export declare function catalogIndex(): Promise<CatalogIndex>;
31
38
  export declare function rebuildCatalogIndex(): Promise<CatalogIndex>;
32
39
  export declare function catalogWalkCount(): number;
33
40
  export declare function resetCatalogIndex(): void;
34
- /** Skills de catálogo: todas las del paquete menos las 17 del motor. */
41
+ /** Skills instalables del catálogo; los 17 comandos los provee Spec Kit, no el catálogo. */
35
42
  export declare function listCatalogSkills(engine: EngineManifest): Promise<string[]>;
36
43
  /** Agentes de catálogo publicados en el paquete. */
37
44
  export declare function listCatalogAgents(): Promise<string[]>;
package/dist/catalog.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { ENGINE_SKILLS_DIR } from './engine/engine.js';
5
5
  import { PACKAGE_VERSION } from './package-info.js';
6
6
  import { sha256 } from './sync/lock.js';
7
+ import { splitFrontmatter } from './frontmatter.js';
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  /** Directorio de agentes dentro del paquete (core/agents). */
9
10
  export const ENGINE_AGENTS_DIR = path.join(__dirname, '..', 'agents');
@@ -38,16 +39,43 @@ async function walk(dir) {
38
39
  }
39
40
  return files.sort();
40
41
  }
41
- async function buildCatalogIndex() {
42
- walkCount += 1;
43
- const entries = await fs.readdir(ENGINE_SKILLS_DIR, { withFileTypes: true }).catch(() => []);
42
+ /** `true` si el frontmatter de la skill declara `runtime: false` (no instalable). */
43
+ async function isRuntimeSkill(skillDir) {
44
+ const text = await fs.readFile(path.join(skillDir, 'SKILL.md'), 'utf8').catch(() => null);
45
+ if (text === null) {
46
+ return true;
47
+ }
48
+ try {
49
+ return splitFrontmatter(text, skillDir).data.runtime !== false;
50
+ }
51
+ catch {
52
+ return true;
53
+ }
54
+ }
55
+ /**
56
+ * Índice de las skills instalables de un directorio de catálogo. Las skills con
57
+ * `runtime: false` siguen en el paquete pero no se listan, materializan ni proyectan.
58
+ */
59
+ export async function buildCatalogIndexFrom(skillsDir) {
60
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true }).catch(() => []);
44
61
  const skillFiles = {};
45
- for (const entry of entries) {
62
+ const retiredSkills = [];
63
+ for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
46
64
  if (!entry.isDirectory()) {
47
65
  continue;
48
66
  }
49
- skillFiles[entry.name] = await walk(path.join(ENGINE_SKILLS_DIR, entry.name));
67
+ const dir = path.join(skillsDir, entry.name);
68
+ if (!(await isRuntimeSkill(dir))) {
69
+ retiredSkills.push(entry.name);
70
+ continue;
71
+ }
72
+ skillFiles[entry.name] = await walk(dir);
50
73
  }
74
+ return { skills: Object.keys(skillFiles).sort(), skillFiles, retiredSkills: retiredSkills.sort() };
75
+ }
76
+ async function buildCatalogIndex() {
77
+ walkCount += 1;
78
+ const { skillFiles, retiredSkills } = await buildCatalogIndexFrom(ENGINE_SKILLS_DIR);
51
79
  const ruleFiles = {};
52
80
  for (const id of RUBRIC_RULE_IDS) {
53
81
  const file = path.join(ENGINE_RULES_DIR, `${id}.md`);
@@ -60,6 +88,7 @@ async function buildCatalogIndex() {
60
88
  .filter((file) => file.endsWith('.md'))
61
89
  .map((file) => path.basename(file, '.md'))
62
90
  .sort(),
91
+ retiredSkills,
63
92
  skillFiles,
64
93
  ruleFiles,
65
94
  walkCount,
@@ -78,6 +107,7 @@ async function readIndexFile(file) {
78
107
  }
79
108
  return {
80
109
  skills: parsed.skills,
110
+ retiredSkills: Array.isArray(parsed.retiredSkills) ? parsed.retiredSkills : [],
81
111
  agents: parsed.agents,
82
112
  skillFiles: parsed.skillFiles,
83
113
  ruleFiles: parsed.ruleFiles,
@@ -121,11 +151,10 @@ export function resetCatalogIndex() {
121
151
  indexPromise = null;
122
152
  walkCount = 0;
123
153
  }
124
- /** Skills de catálogo: todas las del paquete menos las 17 del motor. */
154
+ /** Skills instalables del catálogo; los 17 comandos los provee Spec Kit, no el catálogo. */
125
155
  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));
156
+ void engine;
157
+ return (await catalogIndex()).skills;
129
158
  }
130
159
  /** Agentes de catálogo publicados en el paquete. */
131
160
  export async function listCatalogAgents() {
package/dist/cli/args.js CHANGED
@@ -13,6 +13,7 @@ const BOOLEAN_FLAGS = new Set([
13
13
  'prune',
14
14
  'auto',
15
15
  'migrate',
16
+ 'no-speckit',
16
17
  ]);
17
18
  export function parseArgs(argv) {
18
19
  const positionals = [];
@@ -4,13 +4,14 @@ import { spawnSync } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { readAiHubRc } from '../paths.js';
6
6
  import { auditSelection } from '../catalog.js';
7
- import { ENGINE_COUNTS, ENGINE_LANES, ENGINE_SKILLS_DIR } from '../engine/engine.js';
8
- import { scanCoupling, scanLegacyArtifacts } from '../engine/gates.js';
7
+ import { ENGINE_COUNTS, ENGINE_LANES } from '../engine/engine.js';
8
+ import { scanCoupling, scanLegacyArtifacts, scanReservedNamespace, scanRetiredStates } from '../engine/gates.js';
9
9
  import { checkInventory, loadManifestContext, } from '../manifest/manifest.js';
10
10
  import { checkLock, runSync } from '../sync/sync.js';
11
11
  import { CONTEXT_RELATIVE, contextHash } from './context.js';
12
12
  import { TARGETS, TOOL_IDS, isToolId } from '../sync/targets.js';
13
13
  import { sourceCheckout } from './source.js';
14
+ import { speckitDoctorChecks, speckitNotApplicable } from '../speckit/doctor.js';
14
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
16
  const COUPLING_FILE = path.join(__dirname, '..', '..', 'engine', 'coupling.json');
16
17
  async function couplingRules() {
@@ -103,19 +104,13 @@ export async function diagnose(ctx) {
103
104
  add('policy:optOuts', true, manifest.policy.optOuts.length === 0
104
105
  ? 'ninguno'
105
106
  : `opt-outs activos: ${manifest.policy.optOuts.join(', ')}`);
106
- const missingEngineSkills = [];
107
- for (const command of engine.commands) {
108
- if (!(await exists(path.join(ENGINE_SKILLS_DIR, command.skill, 'SKILL.md')))) {
109
- missingEngineSkills.push(`${command.id} -> ${command.skill}`);
110
- }
111
- }
112
107
  const audit = await auditSelection(ctx.root, manifest, preset, engine);
113
108
  const missingProjectSkills = audit.missing
114
109
  .filter((item) => item.startsWith('skill:'))
115
110
  .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(', '));
111
+ add('skills', missingProjectSkills.length === 0, missingProjectSkills.length === 0
112
+ ? `${manifest.skills.publish.length} skills de proyecto + ${engine.commands.length} comandos Spec Kit (${audit.modes.skills})`
113
+ : missingProjectSkills.map((id) => `${id} -> ${manifest.skills.source}`).join(', '));
119
114
  const missingAgents = audit.missing
120
115
  .filter((item) => item.startsWith('agent:'))
121
116
  .map((item) => item.slice('agent:'.length));
@@ -144,6 +139,12 @@ export async function diagnose(ctx) {
144
139
  const present = (await exists(direct)) || (await exists(project));
145
140
  add('mcp', present, present ? `MCP configurado (${manifest.mcp.source})` : `MCP: falta ${manifest.mcp.source}`);
146
141
  }
142
+ const speckitChecks = source.isSource
143
+ ? speckitNotApplicable()
144
+ : await speckitDoctorChecks(ctx.root, manifest, engine, ctx.exec);
145
+ for (const check of speckitChecks) {
146
+ add(check.id, check.ok, check.detail);
147
+ }
147
148
  if (source.isSource) {
148
149
  const required = ['dist', 'schema', 'manifest.json', 'engine', 'presets', 'rules', 'agents', 'skills'];
149
150
  const missing = required.filter((entry) => !source.files.includes(entry));
@@ -166,6 +167,10 @@ export async function diagnose(ctx) {
166
167
  }
167
168
  add('hooks', missingHooks.length === 0, missingHooks.length === 0 ? 'superficies presentes' : missingHooks.join(', '));
168
169
  }
170
+ const reserved = await scanReservedNamespace();
171
+ add('catalog:reserved-namespace', reserved.length === 0, reserved.length === 0
172
+ ? 'ningún activo del catálogo usa el espacio speckit-*/speckit.* de Spec Kit'
173
+ : reserved.slice(0, 5).join('; ') + (reserved.length > 5 ? ` (+${reserved.length - 5})` : ''));
169
174
  const legacy = await scanLegacyArtifacts(ctx.root);
170
175
  add('engine:no-legacy-prefix', legacy.length === 0, legacy.length === 0
171
176
  ? source.isSource
@@ -253,6 +258,10 @@ export async function diagnose(ctx) {
253
258
  }
254
259
  const coupling = await couplingRules();
255
260
  if (source.isSource) {
261
+ const retired = await scanRetiredStates(undefined, ctx.root);
262
+ add('engine:retired-states', retired.length === 0, retired.length === 0
263
+ ? 'sin vocabulario de estados retirado (ROOT_CAUSE, FIXED, READY…)'
264
+ : retired.slice(0, 5).join('; ') + (retired.length > 5 ? ` (+${retired.length - 5})` : ''));
256
265
  const findings = await scanCoupling(ctx.root, coupling.terms);
257
266
  add('engine:no-coupling', findings.length === 0, findings.length === 0
258
267
  ? 'sin términos de dominio en el motor'
@@ -5,9 +5,9 @@ import { createBackup, listBackups, purgeGenerated, restoreBackup } from '../bac
5
5
  import { readAiHubRc } from '../paths.js';
6
6
  import { loadManifestContext, writeInventory } from '../manifest/manifest.js';
7
7
  import { findPresetAsset } from '../presets/resolve.js';
8
- import { ENGINE_AGENTS, materializeAgent, materializeSkill, readScaffold, SCAFFOLD_RELATIVE, } from '../presets/materialize.js';
8
+ import { ENGINE_AGENTS, materializeAgent, materializeSkill, removeOwned, SCAFFOLD_RELATIVE, } from '../presets/materialize.js';
9
9
  import { ENGINE_SKILLS_DIR } from '../engine/engine.js';
10
- import { catalogSeal, hasFullDimension } from '../catalog.js';
10
+ import { catalogIndex, catalogSeal, hasFullDimension } from '../catalog.js';
11
11
  import { readLock } from '../sync/lock.js';
12
12
  import { runSync } from '../sync/sync.js';
13
13
  import { cliVersion, cmdInstall, writeManifest } from './onboarding.js';
@@ -15,6 +15,10 @@ import { splitItem } from './items.js';
15
15
  async function exists(file) {
16
16
  return fs.access(file).then(() => true, () => false);
17
17
  }
18
+ /** Nombres del espacio de Spec Kit: sus comandos no se añaden ni se desinstalan desde ai-hub. */
19
+ function isSpeckitCommandName(id) {
20
+ return id.startsWith('speckit-') || id.startsWith('speckit.');
21
+ }
18
22
  function markCustom(manifest, dimension) {
19
23
  manifest[dimension].selection = 'custom';
20
24
  if (hasFullDimension(manifest)) {
@@ -45,7 +49,7 @@ export async function cmdList(args, ctx) {
45
49
  rules: manifest.rules.selection ?? 'preset',
46
50
  },
47
51
  commands: engine.commands.map((command) => ({ id: command.id, lane: command.lane, role: command.role })),
48
- nativeSkills: engine.commands.map((command) => command.skill),
52
+ speckitCommands: engine.commands.map((command) => command.speckit),
49
53
  skills: manifest.skills.publish,
50
54
  agents: manifest.agents.publish,
51
55
  rules: manifest.rules.publish,
@@ -59,7 +63,7 @@ export async function cmdList(args, ctx) {
59
63
  ctx.io.out(`targets: ${manifest.targets.join(', ')}`);
60
64
  ctx.io.out(`selection: skills=${data.selection.skills} agents=${data.selection.agents} rules=${data.selection.rules}`);
61
65
  ctx.io.out(`commands (${data.commands.length}): ${data.commands.map((command) => command.id).join(', ')}`);
62
- ctx.io.out(`skills: 17 nativas + ${data.skills.length} de proyecto (${mode})`);
66
+ ctx.io.out(`skills: ${data.skills.length} de proyecto (${mode}) + ${data.speckitCommands.length} comandos Spec Kit`);
63
67
  ctx.io.out(`agents (${data.agents.length}): ${data.agents.join(', ')}`);
64
68
  ctx.io.out(`rules: ${data.rules.length + 1} universales (${data.rules.length} materializadas + preflight)`);
65
69
  return 0;
@@ -84,11 +88,14 @@ export async function cmdAdd(args, ctx) {
84
88
  }
85
89
  const { manifest, preset } = context;
86
90
  const id = parsed.id;
87
- const engineSkill = context.engine.commands.some((command) => command.skill === id);
88
- const catalogSkill = await exists(path.join(ENGINE_SKILLS_DIR, id, 'SKILL.md'));
91
+ if (isSpeckitCommandName(id)) {
92
+ ctx.io.err(`"${id}" lo provee Spec Kit; ai-hub no añade ni desinstala sus comandos`);
93
+ return 1;
94
+ }
95
+ const catalogSkill = (await catalogIndex()).skills.includes(id);
89
96
  const catalogAgent = await exists(path.join(ENGINE_AGENTS, `${id}.md`));
90
97
  const kind = parsed.kind ??
91
- (preset.skills.includes(id) || engineSkill || catalogSkill
98
+ (preset.skills.includes(id) || catalogSkill
92
99
  ? 'skill'
93
100
  : catalogAgent || preset.agents.includes(id)
94
101
  ? 'agent'
@@ -98,13 +105,14 @@ export async function cmdAdd(args, ctx) {
98
105
  return 1;
99
106
  }
100
107
  if (kind === 'skill') {
101
- if (engineSkill) {
102
- ctx.io.out(`"${id}" ya forma parte de las 17 skills del motor`);
103
- return 0;
104
- }
105
108
  const fromPreset = preset.skills.includes(id)
106
109
  ? await findPresetAsset(preset.chain, `skills/${id}`)
107
110
  : null;
111
+ if (fromPreset === null && !catalogSkill) {
112
+ // Una skill con `runtime: false` sigue en el paquete pero no es instalable.
113
+ ctx.io.err(`la skill "${id}" no pertenece al catálogo instalable (no existe o declara runtime: false)`);
114
+ return 1;
115
+ }
108
116
  const source = fromPreset ?? path.join(ENGINE_SKILLS_DIR, id);
109
117
  if (!(await exists(path.join(source, 'SKILL.md')))) {
110
118
  ctx.io.err(`no existe la skill "${id}" en ${preset.id} ni en el catálogo del motor`);
@@ -164,8 +172,8 @@ export async function cmdUninstall(args, ctx) {
164
172
  : manifest.rules.publish.includes(id) || id === 'skill-quality'
165
173
  ? 'rule'
166
174
  : 'agent');
167
- if (kind === 'skill' && context.engine.commands.some((command) => command.skill === id)) {
168
- ctx.io.err(`"${id}" es una skill del motor y no se puede desinstalar`);
175
+ if (isSpeckitCommandName(id)) {
176
+ ctx.io.err(`"${id}" lo provee Spec Kit; ai-hub no añade ni desinstala sus comandos`);
169
177
  return 1;
170
178
  }
171
179
  if (kind === 'rule' && (id === 'core' || id === 'preflight')) {
@@ -203,32 +211,6 @@ export async function cmdUninstall(args, ctx) {
203
211
  ctx.io.out(`uninstall: ${kind} "${id}" · ${sync.removed.length} archivos eliminados`);
204
212
  return 0;
205
213
  }
206
- async function removeOwned(root, relative, directory) {
207
- const absolute = path.join(root, relative);
208
- if (!(await exists(absolute))) {
209
- return;
210
- }
211
- const record = await readScaffold(root);
212
- const relativePosix = relative.split(path.sep).join('/');
213
- if (directory) {
214
- const files = record.files;
215
- const prefix = `${relativePosix}/`;
216
- const owned = Object.entries(files).filter(([name]) => name.startsWith(prefix));
217
- if (owned.length === 0) {
218
- const marker = await fs.readFile(path.join(absolute, 'SKILL.md'), 'utf8').catch(() => '');
219
- if (!marker.includes('GENERADO por ai-hub')) {
220
- return;
221
- }
222
- }
223
- await fs.rm(absolute, { recursive: true, force: true });
224
- return;
225
- }
226
- const text = await fs.readFile(absolute, 'utf8').catch(() => null);
227
- const owned = record.files[relativePosix] !== undefined;
228
- if (text === null || owned || text.includes('GENERADO por ai-hub')) {
229
- await fs.rm(absolute, { force: true });
230
- }
231
- }
232
214
  export async function cmdBackups(args, ctx) {
233
215
  const snapshots = await listBackups(ctx.root);
234
216
  if (args.flags.json) {
@@ -79,6 +79,10 @@ export async function cmdMemory(args, ctx) {
79
79
  ctx.io.out(`cambio activo: ${summary.activeChange ?? 'ninguno'}`);
80
80
  ctx.io.out(`fase: ${summary.phase}`);
81
81
  ctx.io.out(`tareas: ${summary.tasks.done}/${summary.tasks.total}`);
82
+ if (summary.feature !== null) {
83
+ ctx.io.out(`feature Spec Kit: ${summary.feature.dir} · ${summary.feature.pending} pendientes` +
84
+ (summary.feature.source === 'local' ? ' (puntero local .specify/feature.json, no versionado)' : ''));
85
+ }
82
86
  ctx.io.out(last === null
83
87
  ? 'ledger: sin eventos'
84
88
  : `último evento: ${last.event} ${last.timestamps.at} ${last.sessionId} (${last.branch ?? '-'})`);
@@ -22,13 +22,26 @@ export interface Plan extends AssetPlan {
22
22
  preset: string;
23
23
  targets: string[];
24
24
  skillCount: number;
25
+ speckitCommands: number;
25
26
  agentCount: number;
26
27
  ruleCount: number;
27
28
  mcp: boolean;
28
29
  migrate: boolean;
29
30
  write: string[];
31
+ speckit: SpeckitPlan;
30
32
  hash: string;
31
33
  }
34
+ /** Intención de Spec Kit dentro del hash del plan; el entorno (uv, shell) queda fuera. */
35
+ export interface SpeckitPlan {
36
+ managed: boolean;
37
+ package: string;
38
+ version: string;
39
+ excludeNewer: string;
40
+ script: string;
41
+ defaultIntegration: string | null;
42
+ integrations: string[];
43
+ extensions: Record<string, string>;
44
+ }
32
45
  export declare function writeManifest(root: string, manifest: ProjectManifest): Promise<void>;
33
46
  export declare function cmdInit(args: ParsedArgs, ctx: CliContext): Promise<number>;
34
47
  export declare function cmdInstall(args: ParsedArgs, ctx: CliContext): Promise<number>;