@dsalazardev/ai-hub-cli 0.1.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.
- package/README.md +63 -4
- package/dist/backup/snapshots.js +12 -0
- package/dist/catalog.d.ts +83 -0
- package/dist/catalog.js +350 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/context.js +5 -0
- package/dist/cli/doctor.js +105 -14
- package/dist/cli/items.d.ts +1 -1
- package/dist/cli/items.js +1 -1
- package/dist/cli/maintenance.js +105 -51
- package/dist/cli/memory.js +9 -0
- package/dist/cli/onboarding.d.ts +33 -1
- package/dist/cli/onboarding.js +450 -54
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/run.js +10 -7
- package/dist/cli/types.d.ts +3 -0
- package/dist/concurrency.d.ts +5 -0
- package/dist/concurrency.js +23 -0
- package/dist/engine/engine.d.ts +19 -1
- package/dist/engine/engine.js +52 -0
- package/dist/engine/gates.d.ts +16 -0
- package/dist/engine/gates.js +154 -15
- package/dist/harness/memory/ledger.d.ts +6 -0
- package/dist/harness/memory/ledger.js +44 -2
- package/dist/manifest/manifest.d.ts +20 -1
- package/dist/manifest/manifest.js +35 -11
- package/dist/presets/materialize.d.ts +14 -1
- package/dist/presets/materialize.js +132 -11
- package/dist/speckit/binding.d.ts +15 -0
- package/dist/speckit/binding.js +66 -0
- package/dist/speckit/commands.d.ts +46 -0
- package/dist/speckit/commands.js +131 -0
- package/dist/speckit/doctor.d.ts +14 -0
- package/dist/speckit/doctor.js +103 -0
- package/dist/speckit/runner.d.ts +19 -0
- package/dist/speckit/runner.js +22 -0
- package/dist/speckit/seal.d.ts +12 -0
- package/dist/speckit/seal.js +47 -0
- package/dist/speckit/settings.d.ts +36 -0
- package/dist/speckit/settings.js +75 -0
- package/dist/speckit/state.d.ts +59 -0
- package/dist/speckit/state.js +279 -0
- package/dist/speckit/version.d.ts +8 -0
- package/dist/speckit/version.js +53 -0
- package/dist/sync/config.js +3 -2
- package/dist/sync/render.js +35 -1
- package/dist/sync/sync.d.ts +4 -0
- package/dist/sync/sync.js +68 -25
- package/dist/sync/targets.d.ts +13 -0
- package/dist/sync/targets.js +5 -0
- package/engine/engine.yaml +65 -31
- package/manifest.json +28 -47
- package/package.json +2 -1
- package/presets/angular-express/preset.yaml +2 -4
- package/presets/base/preset.yaml +1 -3
- package/presets/generic-ts/preset.yaml +1 -3
- package/presets/python/preset.yaml +1 -3
- package/rules/skill-quality/skill-behavior-rubric.md +63 -0
- package/rules/skill-quality/skill-harden-rubric.md +102 -0
- package/rules/skill-quality/skill-migration-brief.md +200 -0
- package/rules/skill-quality/skill-rubric.md +40 -0
- package/schema/engine.schema.json +22 -3
- package/schema/manifest.schema.json +22 -0
- package/skills/shared/analyze/SKILL.md +2 -1
- package/skills/shared/clarify/SKILL.md +2 -1
- package/skills/shared/implement/SKILL.md +2 -1
- package/skills/shared/plan/SKILL.md +2 -1
- package/skills/shared/review/SKILL.md +2 -1
- package/skills/shared/sdd/SKILL.md +2 -1
- package/skills/shared/sdd-init/SKILL.md +2 -1
- package/skills/shared/ship/SKILL.md +2 -1
- package/skills/shared/specify/SKILL.md +2 -1
- package/skills/shared/tasks/SKILL.md +2 -1
- package/skills/shared/verify/SKILL.md +2 -1
- package/skills/shared/speckit-analyze/SKILL.md +0 -35
- package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
- package/skills/shared/speckit-assess-define/SKILL.md +0 -36
- package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
- package/skills/shared/speckit-assess-research/SKILL.md +0 -36
- package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
- package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
- package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
- package/skills/shared/speckit-bug-test/SKILL.md +0 -37
- package/skills/shared/speckit-checklist/SKILL.md +0 -35
- package/skills/shared/speckit-clarify/SKILL.md +0 -32
- package/skills/shared/speckit-constitution/SKILL.md +0 -35
- package/skills/shared/speckit-converge/SKILL.md +0 -36
- package/skills/shared/speckit-implement/SKILL.md +0 -32
- package/skills/shared/speckit-plan/SKILL.md +0 -37
- package/skills/shared/speckit-specify/SKILL.md +0 -36
- 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
|
|
23
|
-
declarados en `targets
|
|
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
|
-
.
|
|
56
|
-
|
|
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.
|
package/dist/backup/snapshots.js
CHANGED
|
@@ -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))) {
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
/** Skills del paquete que declaran `runtime: false`: siguen publicadas pero no son instalables. */
|
|
20
|
+
retiredSkills: string[];
|
|
21
|
+
agents: string[];
|
|
22
|
+
skillFiles: Record<string, string[]>;
|
|
23
|
+
ruleFiles: Record<string, string[]>;
|
|
24
|
+
walkCount: number;
|
|
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'>>;
|
|
31
|
+
/**
|
|
32
|
+
* Índice memoizado por proceso. En tests, `AI_HUB_CATALOG_INDEX_FILE` apunta al
|
|
33
|
+
* índice que construye `globalSetup`, de modo que todos los workers comparten un
|
|
34
|
+
* único barrido. En producción se construye desde el paquete en ejecución.
|
|
35
|
+
*/
|
|
36
|
+
export declare function catalogIndex(): Promise<CatalogIndex>;
|
|
37
|
+
/** Reconstruye el índice ignorando la caché de archivo; lo usa el globalSetup. */
|
|
38
|
+
export declare function rebuildCatalogIndex(): Promise<CatalogIndex>;
|
|
39
|
+
export declare function catalogWalkCount(): number;
|
|
40
|
+
export declare function resetCatalogIndex(): void;
|
|
41
|
+
/** Skills instalables del catálogo; los 17 comandos los provee Spec Kit, no el catálogo. */
|
|
42
|
+
export declare function listCatalogSkills(engine: EngineManifest): Promise<string[]>;
|
|
43
|
+
/** Agentes de catálogo publicados en el paquete. */
|
|
44
|
+
export declare function listCatalogAgents(): Promise<string[]>;
|
|
45
|
+
/** Archivos de una skill del catálogo. */
|
|
46
|
+
export declare function catalogSkillFiles(id: string): Promise<string[]>;
|
|
47
|
+
/** Archivos de una rúbrica universal. */
|
|
48
|
+
export declare function universalRuleFiles(id: string): Promise<string[]>;
|
|
49
|
+
export declare function normalizeRuleId(rule: string): string;
|
|
50
|
+
export declare function presetRuleIds(preset: ResolvedPreset): string[];
|
|
51
|
+
export declare function expectedRuleIds(mode: SelectionMode, preset: ResolvedPreset): string[];
|
|
52
|
+
export declare function selectionMode(scope: SelectionScope | undefined): SelectionMode;
|
|
53
|
+
export declare function sameSelection(left: SelectionMode, right: SelectionMode): boolean;
|
|
54
|
+
export declare function hasFullDimension(manifest: ProjectManifest): boolean;
|
|
55
|
+
export declare function catalogHash(manifest: ProjectManifest): string;
|
|
56
|
+
export declare function catalogSeal(manifest: ProjectManifest): {
|
|
57
|
+
version: string;
|
|
58
|
+
hash: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function expectedSkills(mode: SelectionMode, preset: ResolvedPreset, engine: EngineManifest, catalog: string[]): string[] | null;
|
|
61
|
+
export declare function expectedAgents(mode: SelectionMode, preset: ResolvedPreset, catalog: string[]): string[] | null;
|
|
62
|
+
export declare function expectedRulesFor(mode: SelectionMode, preset: ResolvedPreset): string[] | null;
|
|
63
|
+
export declare function difference(expected: string[], actual: string[]): {
|
|
64
|
+
missing: string[];
|
|
65
|
+
extra: string[];
|
|
66
|
+
};
|
|
67
|
+
export interface SelectionAudit {
|
|
68
|
+
mode: SelectionMode;
|
|
69
|
+
modes: {
|
|
70
|
+
skills: SelectionMode;
|
|
71
|
+
agents: SelectionMode;
|
|
72
|
+
rules: SelectionMode;
|
|
73
|
+
};
|
|
74
|
+
expectedSkills: string[] | null;
|
|
75
|
+
expectedAgents: string[] | null;
|
|
76
|
+
expectedRules: string[] | null;
|
|
77
|
+
missing: string[];
|
|
78
|
+
extra: string[];
|
|
79
|
+
errors: string[];
|
|
80
|
+
warnings: string[];
|
|
81
|
+
newAssets: number;
|
|
82
|
+
}
|
|
83
|
+
export declare function auditSelection(root: string, manifest: ProjectManifest, preset: ResolvedPreset, engine: EngineManifest): Promise<SelectionAudit>;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
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
|
+
import { splitFrontmatter } from './frontmatter.js';
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
/** Directorio de agentes dentro del paquete (core/agents). */
|
|
10
|
+
export const ENGINE_AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
11
|
+
/** Directorio de reglas universales dentro del paquete (core/rules). */
|
|
12
|
+
export const ENGINE_RULES_DIR = path.join(__dirname, '..', 'rules');
|
|
13
|
+
export const RUBRIC_RULE_IDS = [
|
|
14
|
+
'skill-quality/skill-rubric',
|
|
15
|
+
'skill-quality/skill-behavior-rubric',
|
|
16
|
+
'skill-quality/skill-harden-rubric',
|
|
17
|
+
'skill-quality/skill-migration-brief',
|
|
18
|
+
];
|
|
19
|
+
export const BASELINE_RULE_IDS = ['core'];
|
|
20
|
+
export const UNIVERSAL_RULE_IDS = [...BASELINE_RULE_IDS, ...RUBRIC_RULE_IDS];
|
|
21
|
+
export const PREFLIGHT_RULE_ID = 'preflight';
|
|
22
|
+
export const UNIVERSAL_TARGET_RULE_IDS = [...UNIVERSAL_RULE_IDS, PREFLIGHT_RULE_ID];
|
|
23
|
+
let indexPromise = null;
|
|
24
|
+
let walkCount = 0;
|
|
25
|
+
async function walk(dir) {
|
|
26
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
27
|
+
if (entries === null) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const files = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const full = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
files.push(...(await walk(full)));
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile()) {
|
|
37
|
+
files.push(full);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|
|
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(() => []);
|
|
61
|
+
const skillFiles = {};
|
|
62
|
+
const retiredSkills = [];
|
|
63
|
+
for (const entry of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
64
|
+
if (!entry.isDirectory()) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
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);
|
|
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);
|
|
79
|
+
const ruleFiles = {};
|
|
80
|
+
for (const id of RUBRIC_RULE_IDS) {
|
|
81
|
+
const file = path.join(ENGINE_RULES_DIR, `${id}.md`);
|
|
82
|
+
ruleFiles[id] = (await fs.access(file).then(() => true, () => false)) ? [file] : [];
|
|
83
|
+
}
|
|
84
|
+
const agentFiles = await walk(ENGINE_AGENTS_DIR);
|
|
85
|
+
return {
|
|
86
|
+
skills: Object.keys(skillFiles).sort(),
|
|
87
|
+
agents: agentFiles
|
|
88
|
+
.filter((file) => file.endsWith('.md'))
|
|
89
|
+
.map((file) => path.basename(file, '.md'))
|
|
90
|
+
.sort(),
|
|
91
|
+
retiredSkills,
|
|
92
|
+
skillFiles,
|
|
93
|
+
ruleFiles,
|
|
94
|
+
walkCount,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function readIndexFile(file) {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
100
|
+
if (!Array.isArray(parsed.skills) ||
|
|
101
|
+
!Array.isArray(parsed.agents) ||
|
|
102
|
+
typeof parsed.skillFiles !== 'object' ||
|
|
103
|
+
parsed.skillFiles === null ||
|
|
104
|
+
typeof parsed.ruleFiles !== 'object' ||
|
|
105
|
+
parsed.ruleFiles === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
skills: parsed.skills,
|
|
110
|
+
retiredSkills: Array.isArray(parsed.retiredSkills) ? parsed.retiredSkills : [],
|
|
111
|
+
agents: parsed.agents,
|
|
112
|
+
skillFiles: parsed.skillFiles,
|
|
113
|
+
ruleFiles: parsed.ruleFiles,
|
|
114
|
+
walkCount: typeof parsed.walkCount === 'number' ? parsed.walkCount : 1,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Índice memoizado por proceso. En tests, `AI_HUB_CATALOG_INDEX_FILE` apunta al
|
|
123
|
+
* índice que construye `globalSetup`, de modo que todos los workers comparten un
|
|
124
|
+
* único barrido. En producción se construye desde el paquete en ejecución.
|
|
125
|
+
*/
|
|
126
|
+
export async function catalogIndex() {
|
|
127
|
+
if (indexPromise === null) {
|
|
128
|
+
indexPromise = (async () => {
|
|
129
|
+
const cacheFile = process.env.AI_HUB_CATALOG_INDEX_FILE;
|
|
130
|
+
if (cacheFile !== undefined && cacheFile !== '') {
|
|
131
|
+
const cached = await readIndexFile(cacheFile);
|
|
132
|
+
if (cached !== null) {
|
|
133
|
+
walkCount = Math.max(walkCount, cached.walkCount);
|
|
134
|
+
return cached;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return buildCatalogIndex();
|
|
138
|
+
})();
|
|
139
|
+
}
|
|
140
|
+
return indexPromise;
|
|
141
|
+
}
|
|
142
|
+
/** Reconstruye el índice ignorando la caché de archivo; lo usa el globalSetup. */
|
|
143
|
+
export async function rebuildCatalogIndex() {
|
|
144
|
+
indexPromise = buildCatalogIndex();
|
|
145
|
+
return indexPromise;
|
|
146
|
+
}
|
|
147
|
+
export function catalogWalkCount() {
|
|
148
|
+
return walkCount;
|
|
149
|
+
}
|
|
150
|
+
export function resetCatalogIndex() {
|
|
151
|
+
indexPromise = null;
|
|
152
|
+
walkCount = 0;
|
|
153
|
+
}
|
|
154
|
+
/** Skills instalables del catálogo; los 17 comandos los provee Spec Kit, no el catálogo. */
|
|
155
|
+
export async function listCatalogSkills(engine) {
|
|
156
|
+
void engine;
|
|
157
|
+
return (await catalogIndex()).skills;
|
|
158
|
+
}
|
|
159
|
+
/** Agentes de catálogo publicados en el paquete. */
|
|
160
|
+
export async function listCatalogAgents() {
|
|
161
|
+
return (await catalogIndex()).agents;
|
|
162
|
+
}
|
|
163
|
+
/** Archivos de una skill del catálogo. */
|
|
164
|
+
export async function catalogSkillFiles(id) {
|
|
165
|
+
return (await catalogIndex()).skillFiles[id] ?? [];
|
|
166
|
+
}
|
|
167
|
+
/** Archivos de una rúbrica universal. */
|
|
168
|
+
export async function universalRuleFiles(id) {
|
|
169
|
+
return (await catalogIndex()).ruleFiles[id] ?? [];
|
|
170
|
+
}
|
|
171
|
+
export function normalizeRuleId(rule) {
|
|
172
|
+
return rule
|
|
173
|
+
.replace(/\\/g, '/')
|
|
174
|
+
.replace(/^\.?\//, '')
|
|
175
|
+
.replace(/^rules\//, '')
|
|
176
|
+
.replace(/\.md$/i, '');
|
|
177
|
+
}
|
|
178
|
+
export function presetRuleIds(preset) {
|
|
179
|
+
return [...new Set(preset.rules.map(normalizeRuleId))].sort();
|
|
180
|
+
}
|
|
181
|
+
export function expectedRuleIds(mode, preset) {
|
|
182
|
+
if (mode === 'preset') {
|
|
183
|
+
return presetRuleIds(preset);
|
|
184
|
+
}
|
|
185
|
+
return [...new Set([...UNIVERSAL_RULE_IDS, ...presetRuleIds(preset)])].sort();
|
|
186
|
+
}
|
|
187
|
+
export function selectionMode(scope) {
|
|
188
|
+
return scope?.selection ?? 'preset';
|
|
189
|
+
}
|
|
190
|
+
export function sameSelection(left, right) {
|
|
191
|
+
return left === right;
|
|
192
|
+
}
|
|
193
|
+
export function hasFullDimension(manifest) {
|
|
194
|
+
return (selectionMode(manifest.skills) === 'full' ||
|
|
195
|
+
selectionMode(manifest.agents) === 'full' ||
|
|
196
|
+
selectionMode(manifest.rules) === 'full');
|
|
197
|
+
}
|
|
198
|
+
export function catalogHash(manifest) {
|
|
199
|
+
const payload = {};
|
|
200
|
+
if (selectionMode(manifest.skills) === 'full') {
|
|
201
|
+
payload.skills = [...manifest.skills.publish].sort();
|
|
202
|
+
}
|
|
203
|
+
if (selectionMode(manifest.agents) === 'full') {
|
|
204
|
+
payload.agents = [...manifest.agents.publish].sort();
|
|
205
|
+
}
|
|
206
|
+
if (selectionMode(manifest.rules) === 'full') {
|
|
207
|
+
payload.rules = [...manifest.rules.publish].sort();
|
|
208
|
+
}
|
|
209
|
+
return sha256(JSON.stringify(payload));
|
|
210
|
+
}
|
|
211
|
+
export function catalogSeal(manifest) {
|
|
212
|
+
return { version: PACKAGE_VERSION, hash: catalogHash(manifest) };
|
|
213
|
+
}
|
|
214
|
+
export function expectedSkills(mode, preset, engine, catalog) {
|
|
215
|
+
if (mode === 'full') {
|
|
216
|
+
return [...catalog].sort();
|
|
217
|
+
}
|
|
218
|
+
if (mode === 'preset') {
|
|
219
|
+
return [...preset.skills].sort();
|
|
220
|
+
}
|
|
221
|
+
void engine;
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
export function expectedAgents(mode, preset, catalog) {
|
|
225
|
+
if (mode === 'full') {
|
|
226
|
+
return [...catalog].sort();
|
|
227
|
+
}
|
|
228
|
+
if (mode === 'preset') {
|
|
229
|
+
return [...preset.agents].sort();
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
export function expectedRulesFor(mode, preset) {
|
|
234
|
+
if (mode === 'custom') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return expectedRuleIds(mode, preset);
|
|
238
|
+
}
|
|
239
|
+
export function difference(expected, actual) {
|
|
240
|
+
const expectedSet = new Set(expected);
|
|
241
|
+
const actualSet = new Set(actual);
|
|
242
|
+
return {
|
|
243
|
+
missing: expected.filter((id) => !actualSet.has(id)).sort(),
|
|
244
|
+
extra: actual.filter((id) => !expectedSet.has(id)).sort(),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
async function fileExists(file) {
|
|
248
|
+
return fs.access(file).then(() => true, () => false);
|
|
249
|
+
}
|
|
250
|
+
export async function auditSelection(root, manifest, preset, engine) {
|
|
251
|
+
const modes = {
|
|
252
|
+
skills: selectionMode(manifest.skills),
|
|
253
|
+
agents: selectionMode(manifest.agents),
|
|
254
|
+
rules: selectionMode(manifest.rules),
|
|
255
|
+
};
|
|
256
|
+
const mode = modes.skills === modes.agents && modes.agents === modes.rules ? modes.skills : 'custom';
|
|
257
|
+
const errors = [];
|
|
258
|
+
const warnings = [];
|
|
259
|
+
const missing = [];
|
|
260
|
+
const extra = [];
|
|
261
|
+
const catalogSkills = await listCatalogSkills(engine);
|
|
262
|
+
const catalogAgents = await listCatalogAgents();
|
|
263
|
+
const expectedSkills = expectedSkillsFor(modes.skills, preset, engine, catalogSkills);
|
|
264
|
+
const expectedAgents = expectedAgentsFor(modes.agents, preset, catalogAgents);
|
|
265
|
+
const expectedRules = expectedRulesFor(modes.rules, preset);
|
|
266
|
+
let newAssets = 0;
|
|
267
|
+
const compare = (dimension, expected, actual) => {
|
|
268
|
+
if (expected === null) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const diff = difference(expected, actual);
|
|
272
|
+
missing.push(...diff.missing.map((id) => `${dimension}:${id}`));
|
|
273
|
+
extra.push(...diff.extra.map((id) => `${dimension}:${id}`));
|
|
274
|
+
if (diff.missing.length > 0) {
|
|
275
|
+
errors.push(`${dimension}s.publish incompleto: faltan ${diff.missing.join(', ')}`);
|
|
276
|
+
}
|
|
277
|
+
if (diff.extra.length > 0) {
|
|
278
|
+
errors.push(`${dimension}s.publish contiene ids fuera de la selección: ${diff.extra.join(', ')}`);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
compare('skill', expectedSkills, manifest.skills.publish);
|
|
282
|
+
compare('agent', expectedAgents, manifest.agents.publish);
|
|
283
|
+
compare('rule', expectedRules, manifest.rules.publish);
|
|
284
|
+
const anyFull = Object.values(modes).includes('full');
|
|
285
|
+
if (anyFull) {
|
|
286
|
+
if (manifest.catalog === undefined) {
|
|
287
|
+
errors.push('selección full sin catalog.version/catalog.hash; ejecuta install --full o init');
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const currentHash = catalogHash(manifest);
|
|
291
|
+
if (currentHash !== manifest.catalog.hash) {
|
|
292
|
+
errors.push('full incompleto: publish editado; ejecuta install --full o restaura el manifest');
|
|
293
|
+
}
|
|
294
|
+
else if (manifest.catalog.version !== PACKAGE_VERSION) {
|
|
295
|
+
const expectedFull = [
|
|
296
|
+
...(modes.skills === 'full' ? (expectedSkills ?? []) : []),
|
|
297
|
+
...(modes.agents === 'full' ? (expectedAgents ?? []) : []),
|
|
298
|
+
...(modes.rules === 'full' ? (expectedRules ?? []) : []),
|
|
299
|
+
];
|
|
300
|
+
const actualFull = [
|
|
301
|
+
...(modes.skills === 'full' ? manifest.skills.publish : []),
|
|
302
|
+
...(modes.agents === 'full' ? manifest.agents.publish : []),
|
|
303
|
+
...(modes.rules === 'full' ? manifest.rules.publish : []),
|
|
304
|
+
];
|
|
305
|
+
newAssets = difference(expectedFull, actualFull).missing.length;
|
|
306
|
+
warnings.push(`full desactualizado: ${newAssets} activos nuevos; ejecuta upgrade`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const skill of manifest.skills.publish) {
|
|
311
|
+
const file = path.join(root, manifest.skills.source, skill, 'SKILL.md');
|
|
312
|
+
if (!(await fileExists(file))) {
|
|
313
|
+
missing.push(`skill:${skill}`);
|
|
314
|
+
errors.push(`skills.publish "${skill}" no existe en ${manifest.skills.source}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const agent of manifest.agents.publish) {
|
|
318
|
+
const file = path.join(root, manifest.agents.source, `${agent}.md`);
|
|
319
|
+
if (!(await fileExists(file))) {
|
|
320
|
+
missing.push(`agent:${agent}`);
|
|
321
|
+
errors.push(`agents.publish "${agent}" no existe en ${manifest.agents.source}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const rule of manifest.rules.publish) {
|
|
325
|
+
const file = path.join(root, manifest.rules.source, `${rule}.md`);
|
|
326
|
+
if (!(await fileExists(file))) {
|
|
327
|
+
missing.push(`rule:${rule}`);
|
|
328
|
+
errors.push(`rules.publish "${rule}" no existe en ${manifest.rules.source}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
mode,
|
|
333
|
+
modes,
|
|
334
|
+
expectedSkills,
|
|
335
|
+
expectedAgents,
|
|
336
|
+
expectedRules,
|
|
337
|
+
missing: [...new Set(missing)].sort(),
|
|
338
|
+
extra: [...new Set(extra)].sort(),
|
|
339
|
+
errors,
|
|
340
|
+
warnings,
|
|
341
|
+
newAssets,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
// Nombres internos para evitar colisión con las funciones exportadas de expectativas.
|
|
345
|
+
function expectedSkillsFor(mode, preset, engine, catalog) {
|
|
346
|
+
return expectedSkills(mode, preset, engine, catalog);
|
|
347
|
+
}
|
|
348
|
+
function expectedAgentsFor(mode, preset, catalog) {
|
|
349
|
+
return expectedAgents(mode, preset, catalog);
|
|
350
|
+
}
|
package/dist/cli/args.js
CHANGED
package/dist/cli/context.js
CHANGED
|
@@ -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}`);
|