@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/dist/cli/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { cmdWorktrees } from './worktrees.js';
|
|
|
14
14
|
import { cliVersion } from './onboarding.js';
|
|
15
15
|
import { sourceCheckout } from './source.js';
|
|
16
16
|
import { processIO } from './types.js';
|
|
17
|
+
import { defaultRunner } from '../speckit/runner.js';
|
|
17
18
|
function printLockCheck(result, ctx) {
|
|
18
19
|
const header = `destinos esperados ${result.expectedCount} / actuales ${result.actualCount} · sources ${result.expectedSourceCount}/${result.actualSourceCount}`;
|
|
19
20
|
const extraCategories = result.breakdown.other > 0 ? `, other ${result.breakdown.other}` : '';
|
|
@@ -101,8 +102,8 @@ async function cmdCheck(_args, ctx) {
|
|
|
101
102
|
}
|
|
102
103
|
export const COMMANDS = {
|
|
103
104
|
init: { summary: 'Detecta el stack, elige preset y aplica un plan reproducible', handler: cmdInit },
|
|
104
|
-
install: { summary: 'Materializa el preset y sincroniza
|
|
105
|
-
add: { summary: 'Añade una skill o
|
|
105
|
+
install: { summary: 'Materializa el catálogo o el preset según la selección y sincroniza', handler: cmdInstall },
|
|
106
|
+
add: { summary: 'Añade una skill o cualquier agente del catálogo', handler: cmdAdd },
|
|
106
107
|
sync: { summary: 'Proyecta las fuentes a los destinos; --check informa deriva', handler: cmdSync },
|
|
107
108
|
drift: { summary: 'Informa deriva de hash sin escribir', handler: cmdDrift },
|
|
108
109
|
list: { summary: 'Lista preset, targets, comandos, skills y agentes', handler: cmdList },
|
|
@@ -110,7 +111,7 @@ export const COMMANDS = {
|
|
|
110
111
|
check: { summary: 'Alias de doctor', handler: cmdCheck },
|
|
111
112
|
backups: { summary: 'Lista los snapshots disponibles', handler: cmdBackups },
|
|
112
113
|
restore: { summary: 'Restaura un snapshot', handler: cmdRestore },
|
|
113
|
-
uninstall: { summary: 'Elimina una skill o
|
|
114
|
+
uninstall: { summary: 'Elimina una skill, agente o regla añadida', handler: cmdUninstall },
|
|
114
115
|
purge: { summary: 'Elimina el arnés tras crear un snapshot', handler: cmdPurge },
|
|
115
116
|
upgrade: { summary: 'Actualiza el paquete y re-sincroniza', handler: cmdUpgrade },
|
|
116
117
|
repair: { summary: 'Reaplica solo lo que falta o está roto', handler: cmdRepair },
|
|
@@ -132,16 +133,18 @@ export function printHelp(io) {
|
|
|
132
133
|
io.out(` ${name.padEnd(10)} ${COMMANDS[name].summary}`);
|
|
133
134
|
}
|
|
134
135
|
io.out('');
|
|
136
|
+
io.out('add agent acepta cualquier agente del catálogo, no solo los del preset.');
|
|
137
|
+
io.out('Publicación: prepublishOnly ejecuta build, manifest:build y npm test (unit + integración).');
|
|
135
138
|
io.out('Compatibilidad: verify, judge (fuera del catálogo).');
|
|
136
139
|
}
|
|
137
|
-
export async function createContext(cwd, io) {
|
|
140
|
+
export async function createContext(cwd, io, exec = defaultRunner) {
|
|
138
141
|
try {
|
|
139
142
|
const location = resolveAiHubDir(cwd);
|
|
140
|
-
return { cwd, root: location.dir, source: location.source, io };
|
|
143
|
+
return { cwd, root: location.dir, source: location.source, io, exec };
|
|
141
144
|
}
|
|
142
145
|
catch (error) {
|
|
143
146
|
if (error instanceof AiHubDirNotFoundError) {
|
|
144
|
-
return { cwd, root: cwd, source: null, io };
|
|
147
|
+
return { cwd, root: cwd, source: null, io, exec };
|
|
145
148
|
}
|
|
146
149
|
throw error;
|
|
147
150
|
}
|
|
@@ -150,7 +153,7 @@ export async function runCli(argv, options = {}) {
|
|
|
150
153
|
const io = options.io ?? processIO;
|
|
151
154
|
const cwd = options.cwd ?? process.cwd();
|
|
152
155
|
const [command, ...rest] = argv;
|
|
153
|
-
const ctx = await createContext(cwd, io);
|
|
156
|
+
const ctx = await createContext(cwd, io, options.exec);
|
|
154
157
|
if (!command) {
|
|
155
158
|
printHelp(io);
|
|
156
159
|
return 2;
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AiHubSource } from '../paths.js';
|
|
2
|
+
import type { ProcessRunner } from '../speckit/runner.js';
|
|
2
3
|
export interface CliIO {
|
|
3
4
|
out(message: string): void;
|
|
4
5
|
err(message: string): void;
|
|
@@ -8,6 +9,8 @@ export interface CliContext {
|
|
|
8
9
|
root: string;
|
|
9
10
|
source: AiHubSource | null;
|
|
10
11
|
io: CliIO;
|
|
12
|
+
/** Ejecutor de procesos externos (uvx, bash); los tests lo sustituyen. */
|
|
13
|
+
exec: ProcessRunner;
|
|
11
14
|
}
|
|
12
15
|
export interface ParsedArgs {
|
|
13
16
|
positionals: string[];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ejecuta `fn` sobre `items` con un máximo de promesas en vuelo, preservando
|
|
3
|
+
* el orden de los resultados. Se usa para el I/O masivo de sync y materialización.
|
|
4
|
+
*/
|
|
5
|
+
export declare function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ejecuta `fn` sobre `items` con un máximo de promesas en vuelo, preservando
|
|
3
|
+
* el orden de los resultados. Se usa para el I/O masivo de sync y materialización.
|
|
4
|
+
*/
|
|
5
|
+
export async function mapLimit(items, limit, fn) {
|
|
6
|
+
if (items.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const results = new Array(items.length);
|
|
10
|
+
let next = 0;
|
|
11
|
+
const workers = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const index = next;
|
|
14
|
+
next += 1;
|
|
15
|
+
if (index >= items.length) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
results[index] = await fn(items[index], index);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
await Promise.all(workers);
|
|
22
|
+
return results;
|
|
23
|
+
}
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -28,15 +28,33 @@ export interface EngineCommand {
|
|
|
28
28
|
exit: string[];
|
|
29
29
|
role: string;
|
|
30
30
|
co_roles?: string[];
|
|
31
|
-
|
|
31
|
+
/** Id canónico del comando en Spec Kit, en notación con punto (`speckit.bug.assess`). */
|
|
32
|
+
speckit: string;
|
|
32
33
|
}
|
|
34
|
+
export type SpeckitScript = 'sh' | 'ps';
|
|
35
|
+
/** Dependencia de Spec Kit fijada por el paquete; un proyecto solo puede restringirla. */
|
|
36
|
+
export interface EngineSpeckit {
|
|
37
|
+
package: string;
|
|
38
|
+
pin: string;
|
|
39
|
+
supported: string;
|
|
40
|
+
excludeNewer: string;
|
|
41
|
+
script: SpeckitScript;
|
|
42
|
+
extensions: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/** Extensiones de Spec Kit que proveen los 8 comandos de assessment y bug fix. */
|
|
45
|
+
export declare const REQUIRED_SPECKIT_EXTENSIONS: readonly ["assess", "bug"];
|
|
33
46
|
export interface EngineManifest {
|
|
34
47
|
version: 2;
|
|
35
48
|
roles: EngineRole[];
|
|
36
49
|
commands: EngineCommand[];
|
|
50
|
+
/** Efecto de cada estado de salida sobre las etiquetas; tabla única proyectada en la regla de enlace. */
|
|
51
|
+
exit_effects: Record<string, string>;
|
|
37
52
|
labels: string[];
|
|
38
53
|
targets: string[];
|
|
54
|
+
speckit: EngineSpeckit;
|
|
39
55
|
}
|
|
56
|
+
/** Estados de salida que el vocabulario normalizado de Spec Kit retiró. */
|
|
57
|
+
export declare const RETIRED_EXIT_STATES: readonly ["ROOT_CAUSE", "NEEDS_DATA", "FIXED", "RESOLVED", "OPEN", "DRAFT", "READY"];
|
|
40
58
|
export declare class EngineError extends Error {
|
|
41
59
|
readonly errors: string[];
|
|
42
60
|
constructor(errors: string[]);
|
package/dist/engine/engine.js
CHANGED
|
@@ -3,12 +3,17 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Ajv } from 'ajv';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
|
+
import { satisfiesRange } from '../speckit/version.js';
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const ENGINE_PATH = path.join(__dirname, '..', '..', 'engine', 'engine.yaml');
|
|
8
9
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schema', 'engine.schema.json');
|
|
9
10
|
export const ENGINE_SKILLS_DIR = path.join(__dirname, '..', '..', 'skills', 'shared');
|
|
10
11
|
export const ENGINE_COUNTS = { roles: 8, commands: 17, labels: 7 };
|
|
11
12
|
export const ENGINE_LANES = { assessment: 5, feature: 9, bug: 3 };
|
|
13
|
+
/** Extensiones de Spec Kit que proveen los 8 comandos de assessment y bug fix. */
|
|
14
|
+
export const REQUIRED_SPECKIT_EXTENSIONS = ['assess', 'bug'];
|
|
15
|
+
/** Estados de salida que el vocabulario normalizado de Spec Kit retiró. */
|
|
16
|
+
export const RETIRED_EXIT_STATES = ['ROOT_CAUSE', 'NEEDS_DATA', 'FIXED', 'RESOLVED', 'OPEN', 'DRAFT', 'READY'];
|
|
12
17
|
export class EngineError extends Error {
|
|
13
18
|
errors;
|
|
14
19
|
constructor(errors) {
|
|
@@ -104,6 +109,53 @@ export function crossValidate(engine) {
|
|
|
104
109
|
errors.push(`lane "${lane}" debe tener ${count} comandos (tiene ${actual})`);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
112
|
+
for (const id of findDuplicateIds(engine.commands.map((command) => command.speckit))) {
|
|
113
|
+
errors.push(`id de Spec Kit duplicado: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
const retired = new Set(RETIRED_EXIT_STATES);
|
|
116
|
+
const states = new Set();
|
|
117
|
+
for (const command of engine.commands) {
|
|
118
|
+
for (const state of command.exit) {
|
|
119
|
+
states.add(state);
|
|
120
|
+
if (retired.has(state)) {
|
|
121
|
+
errors.push(`commands "${command.id}".exit usa el estado retirado ${state}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const effects = engine.exit_effects ?? {};
|
|
126
|
+
for (const state of [...states].sort()) {
|
|
127
|
+
if (!(state in effects)) {
|
|
128
|
+
errors.push(`exit_effects no define el efecto de ${state}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const state of Object.keys(effects).sort()) {
|
|
132
|
+
if (!states.has(state)) {
|
|
133
|
+
errors.push(`exit_effects.${state} no corresponde a ningún estado de salida`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
errors.push(...validateEngineSpeckit(engine.speckit));
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
function validateEngineSpeckit(speckit) {
|
|
140
|
+
if (speckit === undefined) {
|
|
141
|
+
return ['falta el bloque speckit'];
|
|
142
|
+
}
|
|
143
|
+
const errors = [];
|
|
144
|
+
for (const field of ['package', 'pin', 'supported', 'excludeNewer', 'script']) {
|
|
145
|
+
if (typeof speckit[field] !== 'string' || speckit[field] === '') {
|
|
146
|
+
errors.push(`falta speckit.${field}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const extension of REQUIRED_SPECKIT_EXTENSIONS) {
|
|
150
|
+
if (typeof speckit.extensions?.[extension] !== 'string') {
|
|
151
|
+
errors.push(`falta speckit.extensions.${extension}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (typeof speckit.pin === 'string' &&
|
|
155
|
+
typeof speckit.supported === 'string' &&
|
|
156
|
+
!satisfiesRange(speckit.pin, speckit.supported)) {
|
|
157
|
+
errors.push(`speckit.pin ${speckit.pin} está fuera de speckit.supported "${speckit.supported}"`);
|
|
158
|
+
}
|
|
107
159
|
return errors;
|
|
108
160
|
}
|
|
109
161
|
export function commandById(engine, id) {
|
package/dist/engine/gates.d.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
export declare function scanLegacyArtifacts(root: string): Promise<string[]>;
|
|
2
2
|
export declare function scanCoupling(root: string, terms: string[]): Promise<string[]>;
|
|
3
|
+
/** Prefijos que Spec Kit reserva en los directorios de comandos de cada destino. */
|
|
4
|
+
export declare const SPECKIT_RESERVED_PREFIXES: readonly ["speckit-", "speckit."];
|
|
5
|
+
/**
|
|
6
|
+
* Gate del catálogo publicado: ninguna skill, agente o regla puede proyectarse con un nombre
|
|
7
|
+
* del espacio reservado de Spec Kit (`speckit-*` / `speckit.*`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function scanReservedNamespace(dirs?: {
|
|
10
|
+
skills: string;
|
|
11
|
+
agents: string;
|
|
12
|
+
rules: string;
|
|
13
|
+
}): Promise<string[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Gate del vocabulario de estados retirado (`ROOT_CAUSE`, `FIXED`, `READY`…): devuelve
|
|
16
|
+
* `ruta:línea vocabulario obsoleto "TOKEN"` relativo a `root`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanRetiredStates(files: string[] | undefined, root: string): Promise<string[]>;
|
package/dist/engine/gates.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isEngineCheckout } from '../source-checkout.js';
|
|
4
|
+
import { ENGINE_AGENTS_DIR, ENGINE_RULES_DIR, catalogIndex } from '../catalog.js';
|
|
5
|
+
import { ENGINE_SKILLS_DIR } from './engine.js';
|
|
4
6
|
const LEGACY_SOURCE_DIRS = ['core/presets', 'core/skills', 'core/agents'];
|
|
5
7
|
const LEGACY_TARGET_DIRS = [
|
|
6
8
|
'.claude',
|
|
@@ -16,6 +18,23 @@ const TEXT_FILE = /\.(md|mdc|ya?ml|json|ts|js|mjs|sh|ps1|txt)$/i;
|
|
|
16
18
|
function isLegacyName(name) {
|
|
17
19
|
return name === 'opsx' || name === 'openspec' || name.startsWith('opsx-') || name.startsWith('openspec-');
|
|
18
20
|
}
|
|
21
|
+
async function scanFile(file, relative, findings) {
|
|
22
|
+
const name = path.basename(file);
|
|
23
|
+
if (isLegacyName(name)) {
|
|
24
|
+
findings.push(`${relative} (nombre legacy)`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!TEXT_FILE.test(name)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
31
|
+
for (const marker of LEGACY_CONTENT) {
|
|
32
|
+
if (text.includes(marker)) {
|
|
33
|
+
findings.push(`${relative} (contiene "${marker}")`);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
async function walk(dir, relative, findings) {
|
|
20
39
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
21
40
|
if (entries === null) {
|
|
@@ -31,27 +50,46 @@ async function walk(dir, relative, findings) {
|
|
|
31
50
|
await walk(path.join(dir, entry.name), childRelative, findings);
|
|
32
51
|
continue;
|
|
33
52
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
53
|
+
if (entry.isFile()) {
|
|
54
|
+
await scanFile(path.join(dir, entry.name), childRelative, findings);
|
|
36
55
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function scanInstalledCatalog(findings) {
|
|
59
|
+
const index = await catalogIndex();
|
|
60
|
+
for (const id of index.skills) {
|
|
61
|
+
for (const file of index.skillFiles[id] ?? []) {
|
|
62
|
+
const relative = `@engine/skills/shared/${id}/${path
|
|
63
|
+
.relative(path.join(ENGINE_SKILLS_DIR, id), file)
|
|
64
|
+
.split(path.sep)
|
|
65
|
+
.join('/')}`;
|
|
66
|
+
await scanFile(file, relative, findings);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const id of index.agents) {
|
|
70
|
+
await scanFile(path.join(ENGINE_AGENTS_DIR, `${id}.md`), `@engine/agents/${id}.md`, findings);
|
|
71
|
+
}
|
|
72
|
+
for (const id of Object.keys(index.ruleFiles)) {
|
|
73
|
+
for (const file of index.ruleFiles[id] ?? []) {
|
|
74
|
+
await scanFile(file, `@engine/rules/${id}.md`, findings);
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
export async function scanLegacyArtifacts(root) {
|
|
47
79
|
const findings = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
const engineCheckout = await isEngineCheckout(root);
|
|
81
|
+
if (engineCheckout) {
|
|
82
|
+
// En el checkout del motor los destinos de agente son el taller del mantenedor.
|
|
83
|
+
for (const relative of LEGACY_SOURCE_DIRS) {
|
|
84
|
+
await walk(path.join(root, ...relative.split('/')), relative, findings);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// En una instalación se escanean el proyecto, los destinos y el catálogo del paquete.
|
|
89
|
+
for (const relative of ['.ai-hub', ...LEGACY_TARGET_DIRS]) {
|
|
90
|
+
await walk(path.join(root, ...relative.split('/')), relative, findings);
|
|
91
|
+
}
|
|
92
|
+
await scanInstalledCatalog(findings);
|
|
55
93
|
}
|
|
56
94
|
return findings.sort();
|
|
57
95
|
}
|
|
@@ -91,3 +129,104 @@ export async function scanCoupling(root, terms) {
|
|
|
91
129
|
}
|
|
92
130
|
return findings.sort();
|
|
93
131
|
}
|
|
132
|
+
/** Prefijos que Spec Kit reserva en los directorios de comandos de cada destino. */
|
|
133
|
+
export const SPECKIT_RESERVED_PREFIXES = ['speckit-', 'speckit.'];
|
|
134
|
+
function reservedName(name) {
|
|
135
|
+
return SPECKIT_RESERVED_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Gate del catálogo publicado: ninguna skill, agente o regla puede proyectarse con un nombre
|
|
139
|
+
* del espacio reservado de Spec Kit (`speckit-*` / `speckit.*`).
|
|
140
|
+
*/
|
|
141
|
+
export async function scanReservedNamespace(dirs = {
|
|
142
|
+
skills: ENGINE_SKILLS_DIR,
|
|
143
|
+
agents: ENGINE_AGENTS_DIR,
|
|
144
|
+
rules: ENGINE_RULES_DIR,
|
|
145
|
+
}) {
|
|
146
|
+
const findings = [];
|
|
147
|
+
for (const entry of await fs.readdir(dirs.skills, { withFileTypes: true }).catch(() => [])) {
|
|
148
|
+
if (entry.isDirectory() && reservedName(entry.name)) {
|
|
149
|
+
findings.push(`skills/shared/${entry.name} (espacio reservado de Spec Kit)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const [kind, dir] of [['agents', dirs.agents], ['rules', dirs.rules]]) {
|
|
153
|
+
for (const name of await listNames(dir)) {
|
|
154
|
+
if (reservedName(path.basename(name))) {
|
|
155
|
+
findings.push(`${kind}/${name} (espacio reservado de Spec Kit)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return findings.sort();
|
|
160
|
+
}
|
|
161
|
+
async function listNames(dir, prefix = '') {
|
|
162
|
+
const names = [];
|
|
163
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
164
|
+
const rel = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
names.push(...(await listNames(path.join(dir, entry.name), rel)));
|
|
167
|
+
}
|
|
168
|
+
else if (entry.isFile()) {
|
|
169
|
+
names.push(rel);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return names;
|
|
173
|
+
}
|
|
174
|
+
/** Estados retirados que solo existían como estado de salida: se marcan en cualquier contexto. */
|
|
175
|
+
const RETIRED_ALWAYS = ['ROOT_CAUSE', 'NEEDS_DATA'];
|
|
176
|
+
/** Estados retirados que también son palabras corrientes: se marcan solo usados como estado. */
|
|
177
|
+
const RETIRED_IN_STATE_CONTEXT = ['FIXED', 'RESOLVED', 'OPEN', 'DRAFT', 'READY'];
|
|
178
|
+
const STATE_CONTEXT = /\b(estados?|exit|exits|termina(n)? en|ends? in|outcome|veredicto|verdict)\b/i;
|
|
179
|
+
function retiredInLine(line) {
|
|
180
|
+
const found = [];
|
|
181
|
+
const words = new Set(line.split(/[^A-Za-z0-9_-]+/));
|
|
182
|
+
for (const token of RETIRED_ALWAYS) {
|
|
183
|
+
if (words.has(token)) {
|
|
184
|
+
found.push(token);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const token of RETIRED_IN_STATE_CONTEXT) {
|
|
188
|
+
const quoted = line.includes(`\`${token}\``);
|
|
189
|
+
const inContext = STATE_CONTEXT.test(line) && words.has(token);
|
|
190
|
+
if (quoted || inContext) {
|
|
191
|
+
found.push(token);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return found;
|
|
195
|
+
}
|
|
196
|
+
/** Archivos que el gate revisa en el checkout del motor: motor, documentación y catálogo publicado. */
|
|
197
|
+
async function retiredStateFiles(root) {
|
|
198
|
+
const files = [
|
|
199
|
+
path.join(root, 'core', 'engine', 'engine.yaml'),
|
|
200
|
+
path.join(root, 'AGENTS.md'),
|
|
201
|
+
path.join(root, 'README.md'),
|
|
202
|
+
path.join(root, 'core', 'README.md'),
|
|
203
|
+
];
|
|
204
|
+
for (const dir of ['core/rules', 'core/agents', 'core/skills/shared']) {
|
|
205
|
+
for (const rel of await listNames(path.join(root, ...dir.split('/')))) {
|
|
206
|
+
if (TEXT_FILE.test(rel)) {
|
|
207
|
+
files.push(path.join(root, ...dir.split('/'), ...rel.split('/')));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return files;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Gate del vocabulario de estados retirado (`ROOT_CAUSE`, `FIXED`, `READY`…): devuelve
|
|
215
|
+
* `ruta:línea vocabulario obsoleto "TOKEN"` relativo a `root`.
|
|
216
|
+
*/
|
|
217
|
+
export async function scanRetiredStates(files, root) {
|
|
218
|
+
const findings = [];
|
|
219
|
+
for (const file of files ?? (await retiredStateFiles(root))) {
|
|
220
|
+
const text = await fs.readFile(file, 'utf8').catch(() => null);
|
|
221
|
+
if (text === null) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
225
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
226
|
+
for (const token of retiredInLine(line)) {
|
|
227
|
+
findings.push(`${rel}:${index + 1} vocabulario obsoleto "${token}"`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
}
|
|
@@ -49,5 +49,11 @@ export interface ResumeSummary {
|
|
|
49
49
|
done: number;
|
|
50
50
|
};
|
|
51
51
|
state: ProjectState | null;
|
|
52
|
+
/** Feature de Spec Kit activa y de dónde salió: `state` (versionado) o `local` (.specify/feature.json). */
|
|
53
|
+
feature: {
|
|
54
|
+
dir: string;
|
|
55
|
+
source: 'state' | 'local';
|
|
56
|
+
pending: number;
|
|
57
|
+
} | null;
|
|
52
58
|
}
|
|
53
59
|
export declare function resumeLedger(root: string): Promise<ResumeSummary>;
|
|
@@ -114,6 +114,44 @@ export function queryLedger(events, filter) {
|
|
|
114
114
|
return events.filter((event) => (filter.changeId === undefined || event.changeId === filter.changeId) &&
|
|
115
115
|
(filter.sessionId === undefined || event.sessionId === filter.sessionId));
|
|
116
116
|
}
|
|
117
|
+
const FEATURE_PREFIX = 'specs/';
|
|
118
|
+
/**
|
|
119
|
+
* Feature de Spec Kit: la nombra `.planning/state.json` (`activeChange: specs/<feature>`) o,
|
|
120
|
+
* si no, el puntero local `.specify/feature.json`. Solo se referencia la ruta; nunca se copia
|
|
121
|
+
* el contenido de la feature a `.planning/`.
|
|
122
|
+
*/
|
|
123
|
+
async function activeFeature(root, state) {
|
|
124
|
+
if (state?.activeChange?.startsWith(FEATURE_PREFIX)) {
|
|
125
|
+
return { dir: state.activeChange.replace(/\/+$/, ''), source: 'state' };
|
|
126
|
+
}
|
|
127
|
+
if (state?.activeChange) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const raw = await fs.readFile(path.join(root, '.specify', 'feature.json'), 'utf8').catch(() => null);
|
|
131
|
+
if (raw === null) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
return typeof parsed.feature_directory === 'string' && parsed.feature_directory.startsWith(FEATURE_PREFIX)
|
|
137
|
+
? { dir: parsed.feature_directory.replace(/\/+$/, ''), source: 'local' }
|
|
138
|
+
: null;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function countTasks(root, dir) {
|
|
145
|
+
const text = await fs.readFile(path.join(root, ...dir.split('/'), 'tasks.md'), 'utf8').catch(() => null);
|
|
146
|
+
if (text === null) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const lines = text.split(/\r?\n/);
|
|
150
|
+
return {
|
|
151
|
+
total: lines.filter((line) => /^\s*-\s*\[[ xX]\]/.test(line)).length,
|
|
152
|
+
done: lines.filter((line) => /^\s*-\s*\[[xX]\]/.test(line)).length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
117
155
|
export async function resumeLedger(root) {
|
|
118
156
|
const events = await readLedger(root, 1);
|
|
119
157
|
const last = events.length > 0 ? events[events.length - 1] : null;
|
|
@@ -126,11 +164,15 @@ export async function resumeLedger(root) {
|
|
|
126
164
|
throw error;
|
|
127
165
|
}
|
|
128
166
|
}
|
|
167
|
+
const feature = await activeFeature(root, state);
|
|
168
|
+
const featureTasks = feature === null ? null : await countTasks(root, feature.dir);
|
|
169
|
+
const tasks = featureTasks ?? state?.tasks ?? { total: 0, done: 0 };
|
|
129
170
|
return {
|
|
130
171
|
last,
|
|
131
|
-
activeChange: state?.activeChange ?? null,
|
|
172
|
+
activeChange: state?.activeChange ?? feature?.dir ?? null,
|
|
132
173
|
phase: state?.phase ?? 'idle',
|
|
133
|
-
tasks
|
|
174
|
+
tasks,
|
|
134
175
|
state,
|
|
176
|
+
feature: feature === null ? null : { ...feature, pending: tasks.total - tasks.done },
|
|
135
177
|
};
|
|
136
178
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ENGINE_COUNTS, type EngineManifest } from '../engine/engine.js';
|
|
2
2
|
import { type ResolvedPreset } from '../presets/resolve.js';
|
|
3
3
|
import type { PresetDangerPattern } from '../presets/schema.js';
|
|
4
|
+
import type { SelectionMode } from '../catalog.js';
|
|
5
|
+
import { type ProjectSpeckit } from '../speckit/settings.js';
|
|
4
6
|
export declare const MANIFEST_FILE = "manifest.yaml";
|
|
5
7
|
export declare const INVENTORY_RELATIVE = "core/manifest.json";
|
|
6
8
|
export { ENGINE_COUNTS };
|
|
@@ -38,18 +40,26 @@ export interface ProjectManifest {
|
|
|
38
40
|
skills: {
|
|
39
41
|
source: string;
|
|
40
42
|
publish: string[];
|
|
43
|
+
selection?: SelectionMode;
|
|
41
44
|
};
|
|
42
45
|
agents: {
|
|
43
46
|
source: string;
|
|
44
47
|
publish: string[];
|
|
48
|
+
selection?: SelectionMode;
|
|
45
49
|
};
|
|
46
50
|
rules: {
|
|
47
51
|
source: string;
|
|
48
52
|
publish: string[];
|
|
53
|
+
selection?: SelectionMode;
|
|
54
|
+
};
|
|
55
|
+
catalog?: {
|
|
56
|
+
version: string;
|
|
57
|
+
hash: string;
|
|
49
58
|
};
|
|
50
59
|
mcp?: {
|
|
51
60
|
source: string;
|
|
52
61
|
};
|
|
62
|
+
speckit?: ProjectSpeckit;
|
|
53
63
|
}
|
|
54
64
|
export declare class ManifestError extends Error {
|
|
55
65
|
readonly errors: string[];
|
|
@@ -86,10 +96,19 @@ export interface ManifestInventory {
|
|
|
86
96
|
id: string;
|
|
87
97
|
lane: string;
|
|
88
98
|
role: string;
|
|
89
|
-
|
|
99
|
+
speckit: string;
|
|
90
100
|
}>;
|
|
91
101
|
labels: string[];
|
|
92
102
|
targets: string[];
|
|
103
|
+
selection?: {
|
|
104
|
+
skills: SelectionMode;
|
|
105
|
+
agents: SelectionMode;
|
|
106
|
+
rules: SelectionMode;
|
|
107
|
+
};
|
|
108
|
+
catalog?: {
|
|
109
|
+
version: string;
|
|
110
|
+
hash: string;
|
|
111
|
+
} | null;
|
|
93
112
|
agents: string[];
|
|
94
113
|
skills: string[];
|
|
95
114
|
rules: string[];
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { PACKAGE_NAME } from '../package-info.js';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { Ajv } from 'ajv';
|
|
6
7
|
import { parse } from 'yaml';
|
|
7
|
-
import { ENGINE_COUNTS,
|
|
8
|
+
import { ENGINE_COUNTS, loadEngine } from '../engine/engine.js';
|
|
8
9
|
import { frontmatterString, splitFrontmatter } from '../frontmatter.js';
|
|
9
10
|
import { resolvePreset } from '../presets/resolve.js';
|
|
11
|
+
import { validateProjectSpeckit } from '../speckit/settings.js';
|
|
10
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schema', 'manifest.schema.json');
|
|
12
14
|
const ENGINE_AGENTS = path.join(__dirname, '..', '..', 'agents');
|
|
@@ -68,6 +70,18 @@ export async function loadManifestContext(root) {
|
|
|
68
70
|
return validateManifestData(await readManifestFile(root), root);
|
|
69
71
|
}
|
|
70
72
|
export async function validateManifestData(data, root = process.cwd()) {
|
|
73
|
+
if (typeof data === 'object' && data !== null && 'speckit' in data) {
|
|
74
|
+
// Los campos del motor se explican antes del schema, que solo diría "additional property".
|
|
75
|
+
const record = data;
|
|
76
|
+
const targets = Array.isArray(record.targets)
|
|
77
|
+
? record.targets.filter((item) => typeof item === 'string')
|
|
78
|
+
: [];
|
|
79
|
+
const block = record.speckit;
|
|
80
|
+
const speckitErrors = validateProjectSpeckit(block, targets, await loadEngine());
|
|
81
|
+
if (speckitErrors.length > 0) {
|
|
82
|
+
throw new ManifestError(speckitErrors);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
71
85
|
const validate = await validator();
|
|
72
86
|
if (!validate(data)) {
|
|
73
87
|
throw new ManifestError((validate.errors ?? []).map((error) => {
|
|
@@ -104,13 +118,6 @@ export async function validateManifestData(data, root = process.cwd()) {
|
|
|
104
118
|
errors.push(`skills.publish "${skill}" no existe en ${manifest.skills.source}`);
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
|
-
for (const skill of new Set(engine.commands.map((command) => command.skill))) {
|
|
108
|
-
const file = path.join(ENGINE_SKILLS_DIR, skill, 'SKILL.md');
|
|
109
|
-
skillFiles.add(file);
|
|
110
|
-
if (!(await exists(file))) {
|
|
111
|
-
errors.push(`skill del motor "${skill}" no existe en ${ENGINE_SKILLS_DIR}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
121
|
for (const file of skillFiles) {
|
|
115
122
|
if (await exists(file)) {
|
|
116
123
|
await validateCatalogFile(file, errors);
|
|
@@ -148,15 +155,20 @@ export async function validateManifestData(data, root = process.cwd()) {
|
|
|
148
155
|
return { manifest, engine, preset };
|
|
149
156
|
}
|
|
150
157
|
export function inventoryPath(root) {
|
|
151
|
-
|
|
158
|
+
// En el checkout del motor el inventario vive en core/ porque ese es el paquete que se publica.
|
|
159
|
+
// En un proyecto consumidor pertenece a .ai-hub/, junto al arnes instalado.
|
|
160
|
+
const engineMarker = path.join(root, 'core', 'src', 'paths.ts');
|
|
161
|
+
return existsSync(engineMarker)
|
|
162
|
+
? path.join(root, INVENTORY_RELATIVE)
|
|
163
|
+
: path.join(root, '.ai-hub', 'manifest.json');
|
|
152
164
|
}
|
|
153
165
|
export function buildInventory(manifest, engine) {
|
|
154
166
|
const roles = engine.roles.map((role) => role.id).sort();
|
|
155
167
|
const commands = [...engine.commands]
|
|
156
168
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
157
|
-
.map((command) => ({ id: command.id, lane: command.lane, role: command.role,
|
|
169
|
+
.map((command) => ({ id: command.id, lane: command.lane, role: command.role, speckit: command.speckit }));
|
|
158
170
|
const labels = [...engine.labels].sort();
|
|
159
|
-
const skills = [...new Set(
|
|
171
|
+
const skills = [...new Set(manifest.skills.publish)].sort();
|
|
160
172
|
const agents = [...manifest.agents.publish].sort();
|
|
161
173
|
const rules = [...manifest.rules.publish].sort();
|
|
162
174
|
return {
|
|
@@ -169,6 +181,18 @@ export function buildInventory(manifest, engine) {
|
|
|
169
181
|
commands,
|
|
170
182
|
labels,
|
|
171
183
|
targets: [...manifest.targets].sort(),
|
|
184
|
+
...(manifest.skills.selection !== undefined ||
|
|
185
|
+
manifest.agents.selection !== undefined ||
|
|
186
|
+
manifest.rules.selection !== undefined
|
|
187
|
+
? {
|
|
188
|
+
selection: {
|
|
189
|
+
skills: manifest.skills.selection ?? 'preset',
|
|
190
|
+
agents: manifest.agents.selection ?? 'preset',
|
|
191
|
+
rules: manifest.rules.selection ?? 'preset',
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
...(manifest.catalog !== undefined ? { catalog: manifest.catalog } : {}),
|
|
172
196
|
agents,
|
|
173
197
|
skills,
|
|
174
198
|
rules,
|