@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.
Files changed (91) hide show
  1. package/README.md +63 -4
  2. package/dist/backup/snapshots.js +12 -0
  3. package/dist/catalog.d.ts +83 -0
  4. package/dist/catalog.js +350 -0
  5. package/dist/cli/args.js +1 -0
  6. package/dist/cli/context.js +5 -0
  7. package/dist/cli/doctor.js +105 -14
  8. package/dist/cli/items.d.ts +1 -1
  9. package/dist/cli/items.js +1 -1
  10. package/dist/cli/maintenance.js +105 -51
  11. package/dist/cli/memory.js +9 -0
  12. package/dist/cli/onboarding.d.ts +33 -1
  13. package/dist/cli/onboarding.js +450 -54
  14. package/dist/cli/run.d.ts +3 -1
  15. package/dist/cli/run.js +10 -7
  16. package/dist/cli/types.d.ts +3 -0
  17. package/dist/concurrency.d.ts +5 -0
  18. package/dist/concurrency.js +23 -0
  19. package/dist/engine/engine.d.ts +19 -1
  20. package/dist/engine/engine.js +52 -0
  21. package/dist/engine/gates.d.ts +16 -0
  22. package/dist/engine/gates.js +154 -15
  23. package/dist/harness/memory/ledger.d.ts +6 -0
  24. package/dist/harness/memory/ledger.js +44 -2
  25. package/dist/manifest/manifest.d.ts +20 -1
  26. package/dist/manifest/manifest.js +35 -11
  27. package/dist/presets/materialize.d.ts +14 -1
  28. package/dist/presets/materialize.js +132 -11
  29. package/dist/speckit/binding.d.ts +15 -0
  30. package/dist/speckit/binding.js +66 -0
  31. package/dist/speckit/commands.d.ts +46 -0
  32. package/dist/speckit/commands.js +131 -0
  33. package/dist/speckit/doctor.d.ts +14 -0
  34. package/dist/speckit/doctor.js +103 -0
  35. package/dist/speckit/runner.d.ts +19 -0
  36. package/dist/speckit/runner.js +22 -0
  37. package/dist/speckit/seal.d.ts +12 -0
  38. package/dist/speckit/seal.js +47 -0
  39. package/dist/speckit/settings.d.ts +36 -0
  40. package/dist/speckit/settings.js +75 -0
  41. package/dist/speckit/state.d.ts +59 -0
  42. package/dist/speckit/state.js +279 -0
  43. package/dist/speckit/version.d.ts +8 -0
  44. package/dist/speckit/version.js +53 -0
  45. package/dist/sync/config.js +3 -2
  46. package/dist/sync/render.js +35 -1
  47. package/dist/sync/sync.d.ts +4 -0
  48. package/dist/sync/sync.js +68 -25
  49. package/dist/sync/targets.d.ts +13 -0
  50. package/dist/sync/targets.js +5 -0
  51. package/engine/engine.yaml +65 -31
  52. package/manifest.json +28 -47
  53. package/package.json +2 -1
  54. package/presets/angular-express/preset.yaml +2 -4
  55. package/presets/base/preset.yaml +1 -3
  56. package/presets/generic-ts/preset.yaml +1 -3
  57. package/presets/python/preset.yaml +1 -3
  58. package/rules/skill-quality/skill-behavior-rubric.md +63 -0
  59. package/rules/skill-quality/skill-harden-rubric.md +102 -0
  60. package/rules/skill-quality/skill-migration-brief.md +200 -0
  61. package/rules/skill-quality/skill-rubric.md +40 -0
  62. package/schema/engine.schema.json +22 -3
  63. package/schema/manifest.schema.json +22 -0
  64. package/skills/shared/analyze/SKILL.md +2 -1
  65. package/skills/shared/clarify/SKILL.md +2 -1
  66. package/skills/shared/implement/SKILL.md +2 -1
  67. package/skills/shared/plan/SKILL.md +2 -1
  68. package/skills/shared/review/SKILL.md +2 -1
  69. package/skills/shared/sdd/SKILL.md +2 -1
  70. package/skills/shared/sdd-init/SKILL.md +2 -1
  71. package/skills/shared/ship/SKILL.md +2 -1
  72. package/skills/shared/specify/SKILL.md +2 -1
  73. package/skills/shared/tasks/SKILL.md +2 -1
  74. package/skills/shared/verify/SKILL.md +2 -1
  75. package/skills/shared/speckit-analyze/SKILL.md +0 -35
  76. package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
  77. package/skills/shared/speckit-assess-define/SKILL.md +0 -36
  78. package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
  79. package/skills/shared/speckit-assess-research/SKILL.md +0 -36
  80. package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
  81. package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
  82. package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
  83. package/skills/shared/speckit-bug-test/SKILL.md +0 -37
  84. package/skills/shared/speckit-checklist/SKILL.md +0 -35
  85. package/skills/shared/speckit-clarify/SKILL.md +0 -32
  86. package/skills/shared/speckit-constitution/SKILL.md +0 -35
  87. package/skills/shared/speckit-converge/SKILL.md +0 -36
  88. package/skills/shared/speckit-implement/SKILL.md +0 -32
  89. package/skills/shared/speckit-plan/SKILL.md +0 -37
  90. package/skills/shared/speckit-specify/SKILL.md +0 -36
  91. package/skills/shared/speckit-tasks/SKILL.md +0 -34
@@ -15,4 +15,17 @@ export interface MaterializeResult {
15
15
  collisions: string[];
16
16
  }
17
17
  export declare function readScaffold(root: string): Promise<ScaffoldRecord>;
18
- export declare function materializePreset(root: string, preset: ResolvedPreset): Promise<MaterializeResult>;
18
+ export declare function materializePreset(root: string, preset: ResolvedPreset, selectedRules?: string[]): Promise<MaterializeResult>;
19
+ /** Copia una skill del catálogo o del preset a `.ai-hub/skills/<id>`. */
20
+ export declare function materializeSkill(root: string, id: string, sourceDir?: string, sourceKey?: string): Promise<MaterializeResult>;
21
+ /** Copia un agente del catálogo o del preset a `.ai-hub/agents/<id>.md`. */
22
+ export declare function materializeAgent(root: string, id: string, sourceFile?: string, sourceKey?: string): Promise<MaterializeResult>;
23
+ /** Copia las 4 rúbricas universales desde el paquete a `.ai-hub/rules/skill-quality/`. */
24
+ export declare function materializeUniversalRules(root: string, preset: ResolvedPreset, selectedRules?: string[]): Promise<MaterializeResult>;
25
+ /** Copia skills y agentes seleccionados del catálogo. */
26
+ export declare function materializeCatalog(root: string, skillIds: string[], agentIds: string[]): Promise<MaterializeResult>;
27
+ /**
28
+ * Borra un activo materializado solo si lo escribió ai-hub (registro de scaffold o marcador);
29
+ * un archivo del proyecto se conserva.
30
+ */
31
+ export declare function removeOwned(root: string, relative: string, directory: boolean): Promise<void>;
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
3
+ import { mapLimit } from '../concurrency.js';
4
+ import { ENGINE_SKILLS_DIR } from '../engine/engine.js';
5
+ import { ENGINE_AGENTS_DIR, ENGINE_RULES_DIR, RUBRIC_RULE_IDS, normalizeRuleId, } from '../catalog.js';
4
6
  import { sha256 } from '../sync/lock.js';
5
7
  import { findPresetAsset } from './resolve.js';
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- export const ENGINE_AGENTS = path.join(__dirname, '..', '..', 'agents');
8
+ export const ENGINE_AGENTS = ENGINE_AGENTS_DIR;
8
9
  export const SCAFFOLD_RELATIVE = '.ai-hub/.scaffold.json';
9
10
  async function exists(file) {
10
11
  return fs.access(file).then(() => true, () => false);
@@ -38,10 +39,11 @@ export async function readScaffold(root) {
38
39
  }
39
40
  async function writeScaffold(root, record) {
40
41
  const file = path.join(root, SCAFFOLD_RELATIVE);
42
+ const files = Object.fromEntries(Object.entries(record.files).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)));
41
43
  await fs.mkdir(path.dirname(file), { recursive: true });
42
- await fs.writeFile(file, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
44
+ await fs.writeFile(file, `${JSON.stringify({ version: 1, files }, null, 2)}\n`, 'utf8');
43
45
  }
44
- async function copyOnce(root, source, destination, record, result) {
46
+ async function copyOnce(root, source, destination, record, result, sourceKey = source) {
45
47
  const content = await fs.readFile(source, 'utf8');
46
48
  const hash = sha256(content);
47
49
  const abs = path.join(root, destination);
@@ -49,6 +51,7 @@ async function copyOnce(root, source, destination, record, result) {
49
51
  const previous = record.files[destination];
50
52
  const current = sha256(await fs.readFile(abs, 'utf8').catch(() => ''));
51
53
  if (previous && previous.sha256 === current) {
54
+ previous.source = sourceKey;
52
55
  result.kept.push(destination);
53
56
  return;
54
57
  }
@@ -57,18 +60,22 @@ async function copyOnce(root, source, destination, record, result) {
57
60
  }
58
61
  await fs.mkdir(path.dirname(abs), { recursive: true });
59
62
  await fs.writeFile(abs, content, 'utf8');
60
- record.files[destination] = { sha256: hash, source };
63
+ record.files[destination] = { sha256: hash, source: sourceKey };
61
64
  result.written.push(destination);
62
65
  }
63
- export async function materializePreset(root, preset) {
66
+ export async function materializePreset(root, preset, selectedRules) {
64
67
  const result = { written: [], kept: [], collisions: [] };
65
68
  const record = await readScaffold(root);
69
+ const wantedRules = selectedRules === undefined ? null : new Set(selectedRules);
66
70
  for (const rule of preset.rules) {
71
+ if (wantedRules !== null && !wantedRules.has(normalizeRuleId(rule))) {
72
+ continue;
73
+ }
67
74
  const source = await findPresetAsset(preset.chain, rule);
68
75
  if (source === null) {
69
76
  throw new Error(`el preset "${preset.id}" declara la regla "${rule}" pero no existe`);
70
77
  }
71
- await copyOnce(root, source, `.ai-hub/rules/${path.basename(rule)}`, record, result);
78
+ await copyOnce(root, source, `.ai-hub/rules/${normalizeRuleId(rule)}.md`, record, result, `@preset/${preset.id}/${rule.replace(/\\/g, '/')}`);
72
79
  }
73
80
  for (const agent of preset.agents) {
74
81
  const fromPreset = await findPresetAsset(preset.chain, `agents/${agent}.md`);
@@ -76,13 +83,13 @@ export async function materializePreset(root, preset) {
76
83
  if (!(await exists(source))) {
77
84
  throw new Error(`el agente "${agent}" no existe ni en el preset ni en el motor`);
78
85
  }
79
- await copyOnce(root, source, `.ai-hub/agents/${agent}.md`, record, result);
86
+ await copyOnce(root, source, `.ai-hub/agents/${agent}.md`, record, result, fromPreset ? `@preset/${preset.id}/agents/${agent}.md` : `@engine/agents/${agent}.md`);
80
87
  }
81
88
  const harness = await findPresetAsset(preset.chain, 'harness');
82
89
  if (harness !== null) {
83
90
  for (const source of await walk(harness)) {
84
91
  const relative = path.relative(harness, source).split(path.sep).join('/');
85
- await copyOnce(root, source, `.ai-hub/harness/${relative}`, record, result);
92
+ await copyOnce(root, source, `.ai-hub/harness/${relative}`, record, result, `@preset/${preset.id}/harness/${relative}`);
86
93
  }
87
94
  }
88
95
  for (const skill of preset.skills) {
@@ -93,9 +100,123 @@ export async function materializePreset(root, preset) {
93
100
  const files = await walk(dir);
94
101
  for (const source of files) {
95
102
  const relative = path.relative(dir, source).split(path.sep).join('/');
96
- await copyOnce(root, source, `.ai-hub/skills/${skill}/${relative}`, record, result);
103
+ await copyOnce(root, source, `.ai-hub/skills/${skill}/${relative}`, record, result, `@preset/${preset.id}/skills/${skill}/${relative}`);
97
104
  }
98
105
  }
99
106
  await writeScaffold(root, record);
100
107
  return result;
101
108
  }
109
+ /** Copia una skill del catálogo o del preset a `.ai-hub/skills/<id>`. */
110
+ export async function materializeSkill(root, id, sourceDir = path.join(ENGINE_SKILLS_DIR, id), sourceKey = `@engine/skills/shared/${id}`) {
111
+ const result = { written: [], kept: [], collisions: [] };
112
+ const record = await readScaffold(root);
113
+ if (!(await exists(sourceDir))) {
114
+ throw new Error(`la skill "${id}" no existe en ${sourceDir}`);
115
+ }
116
+ for (const source of await walk(sourceDir)) {
117
+ const relative = path.relative(sourceDir, source).split(path.sep).join('/');
118
+ await copyOnce(root, source, `.ai-hub/skills/${id}/${relative}`, record, result, `${sourceKey}/${relative}`);
119
+ }
120
+ await writeScaffold(root, record);
121
+ return result;
122
+ }
123
+ /** Copia un agente del catálogo o del preset a `.ai-hub/agents/<id>.md`. */
124
+ export async function materializeAgent(root, id, sourceFile = path.join(ENGINE_AGENTS, `${id}.md`), sourceKey = `@engine/agents/${id}.md`) {
125
+ const result = { written: [], kept: [], collisions: [] };
126
+ const record = await readScaffold(root);
127
+ if (!(await exists(sourceFile))) {
128
+ throw new Error(`el agente "${id}" no existe en ${sourceFile}`);
129
+ }
130
+ await copyOnce(root, sourceFile, `.ai-hub/agents/${id}.md`, record, result, sourceKey);
131
+ await writeScaffold(root, record);
132
+ return result;
133
+ }
134
+ /** Copia las 4 rúbricas universales desde el paquete a `.ai-hub/rules/skill-quality/`. */
135
+ export async function materializeUniversalRules(root, preset, selectedRules = [...RUBRIC_RULE_IDS]) {
136
+ const result = { written: [], kept: [], collisions: [] };
137
+ const record = await readScaffold(root);
138
+ const core = await findPresetAsset(preset.chain, 'rules/core.md');
139
+ if (core === null) {
140
+ throw new Error(`el preset "${preset.id}" no declara rules/core.md`);
141
+ }
142
+ await copyOnce(root, core, '.ai-hub/rules/core.md', record, result, `@preset/${preset.id}/rules/core.md`);
143
+ const wanted = new Set(selectedRules.filter((id) => id.startsWith('skill-quality/')));
144
+ for (const id of RUBRIC_RULE_IDS) {
145
+ if (!wanted.has(id)) {
146
+ continue;
147
+ }
148
+ const source = path.join(ENGINE_RULES_DIR, `${id}.md`);
149
+ if (!(await exists(source))) {
150
+ throw new Error(`falta la rúbrica universal "${id}" en ${ENGINE_RULES_DIR}`);
151
+ }
152
+ await copyOnce(root, source, `.ai-hub/rules/${id}.md`, record, result, `@engine/rules/${id}.md`);
153
+ }
154
+ await writeScaffold(root, record);
155
+ return result;
156
+ }
157
+ /** Copia skills y agentes seleccionados del catálogo. */
158
+ export async function materializeCatalog(root, skillIds, agentIds) {
159
+ const result = { written: [], kept: [], collisions: [] };
160
+ const record = await readScaffold(root);
161
+ const copies = [];
162
+ for (const id of [...skillIds].sort()) {
163
+ const sourceDir = path.join(ENGINE_SKILLS_DIR, id);
164
+ if (!(await exists(sourceDir))) {
165
+ throw new Error(`la skill de catálogo "${id}" no existe en ${ENGINE_SKILLS_DIR}`);
166
+ }
167
+ for (const source of await walk(sourceDir)) {
168
+ const relative = path.relative(sourceDir, source).split(path.sep).join('/');
169
+ copies.push({
170
+ source,
171
+ destination: `.ai-hub/skills/${id}/${relative}`,
172
+ sourceKey: `@engine/skills/shared/${id}/${relative}`,
173
+ });
174
+ }
175
+ }
176
+ for (const id of [...agentIds].sort()) {
177
+ const source = path.join(ENGINE_AGENTS, `${id}.md`);
178
+ if (!(await exists(source))) {
179
+ throw new Error(`el agente de catálogo "${id}" no existe en ${ENGINE_AGENTS}`);
180
+ }
181
+ copies.push({
182
+ source,
183
+ destination: `.ai-hub/agents/${id}.md`,
184
+ sourceKey: `@engine/agents/${id}.md`,
185
+ });
186
+ }
187
+ await mapLimit(copies, 24, async (copy) => {
188
+ await copyOnce(root, copy.source, copy.destination, record, result, copy.sourceKey);
189
+ });
190
+ await writeScaffold(root, record);
191
+ return result;
192
+ }
193
+ /**
194
+ * Borra un activo materializado solo si lo escribió ai-hub (registro de scaffold o marcador);
195
+ * un archivo del proyecto se conserva.
196
+ */
197
+ export async function removeOwned(root, relative, directory) {
198
+ const absolute = path.join(root, relative);
199
+ if (!(await exists(absolute))) {
200
+ return;
201
+ }
202
+ const record = await readScaffold(root);
203
+ const relativePosix = relative.split(path.sep).join('/');
204
+ if (directory) {
205
+ const files = record.files;
206
+ const prefix = `${relativePosix}/`;
207
+ const owned = Object.entries(files).filter(([name]) => name.startsWith(prefix));
208
+ if (owned.length === 0) {
209
+ const marker = await fs.readFile(path.join(absolute, 'SKILL.md'), 'utf8').catch(() => '');
210
+ if (!marker.includes('GENERADO por ai-hub')) {
211
+ return;
212
+ }
213
+ }
214
+ await fs.rm(absolute, { recursive: true, force: true });
215
+ return;
216
+ }
217
+ const text = await fs.readFile(absolute, 'utf8').catch(() => null);
218
+ const owned = record.files[relativePosix] !== undefined;
219
+ if (text === null || owned || text.includes('GENERADO por ai-hub')) {
220
+ await fs.rm(absolute, { force: true });
221
+ }
222
+ }
@@ -0,0 +1,15 @@
1
+ import type { EngineManifest } from '../engine/engine.js';
2
+ import { type ToolId } from '../sync/targets.js';
3
+ /**
4
+ * Regla generada que enlaza Spec Kit con AI-Hub: roles por comando, invocación en la notación
5
+ * de cada destino, efecto de cada estado de salida sobre las etiquetas, rutas canónicas y sello.
6
+ * Se llama `ai-hub-speckit` y no `speckit-binding` porque en Pi el directorio de reglas
7
+ * (`.pi/prompts`) es también el de comandos de Spec Kit, donde `speckit-*` está reservado.
8
+ */
9
+ export declare const BINDING_RULE_ID = "ai-hub-speckit";
10
+ export declare const BINDING_SOURCE = "@engine/engine/engine.yaml";
11
+ export declare const BINDING_MARKER_SOURCE = "engine/engine.yaml";
12
+ /** Estados de salida en el orden en que aparecen en los comandos. */
13
+ export declare function exitStates(engine: EngineManifest): string[];
14
+ /** Cuerpo de la regla para un destino. Copilot no tiene Spec Kit y su copia no invoca comandos. */
15
+ export declare function bindingRule(engine: EngineManifest, tool: ToolId): string;
@@ -0,0 +1,66 @@
1
+ import { TARGETS } from '../sync/targets.js';
2
+ import { invocationFor } from './commands.js';
3
+ /**
4
+ * Regla generada que enlaza Spec Kit con AI-Hub: roles por comando, invocación en la notación
5
+ * de cada destino, efecto de cada estado de salida sobre las etiquetas, rutas canónicas y sello.
6
+ * Se llama `ai-hub-speckit` y no `speckit-binding` porque en Pi el directorio de reglas
7
+ * (`.pi/prompts`) es también el de comandos de Spec Kit, donde `speckit-*` está reservado.
8
+ */
9
+ export const BINDING_RULE_ID = 'ai-hub-speckit';
10
+ export const BINDING_SOURCE = '@engine/engine/engine.yaml';
11
+ export const BINDING_MARKER_SOURCE = 'engine/engine.yaml';
12
+ /** Estados de salida en el orden en que aparecen en los comandos. */
13
+ export function exitStates(engine) {
14
+ const states = [];
15
+ for (const command of engine.commands) {
16
+ for (const state of command.exit) {
17
+ if (!states.includes(state)) {
18
+ states.push(state);
19
+ }
20
+ }
21
+ }
22
+ return states;
23
+ }
24
+ function stateTable(engine) {
25
+ return [
26
+ '## Estados de salida y etiquetas',
27
+ '',
28
+ '| Estado | Efecto sobre la etiqueta |',
29
+ '|---|---|',
30
+ ...exitStates(engine).map((state) => `| \`${state}\` | ${engine.exit_effects[state]} |`),
31
+ '',
32
+ 'Ningún agente mueve una etiqueta. `hermes-control` aplica el efecto; ante `KILL` (assess-decide) o',
33
+ '`INVALID` (bug-assess) solo **propone** `ai::done` y un humano autorizado lo confirma.',
34
+ `Se mantienen las ${engine.labels.length} etiquetas: ${engine.labels.map((label) => `\`${label}\``).join(', ')}.`,
35
+ ];
36
+ }
37
+ const ARTIFACTS = [
38
+ '## Artefactos canónicos (rutas relativas a la raíz del repositorio)',
39
+ '',
40
+ '- Constitución: `.specify/memory/constitution.md`',
41
+ '- Assessment: `.specify/assessments/<slug>/{intake,research,problem,concept,decision}.md`',
42
+ '- Feature: `specs/<feature>/{spec,plan,tasks}.md` (Spec Kit crea `<feature>` como `NNN-slug`)',
43
+ '- Bug: `.specify/bugs/<slug>/{assessment,fix,test}.md`',
44
+ ];
45
+ const SEAL = [
46
+ '## Sello de `ai::spec-ready`',
47
+ '',
48
+ 'SHA-256 sobre `specs/<feature>/plan.md`, `specs/<feature>/spec.md` y `specs/<feature>/tasks.md`:',
49
+ 'rutas relativas a la raíz, ordenadas, separador `/`, contenido con LF y sin BOM; por archivo',
50
+ '`ruta + "\\n" + bytes`. Cualquier cambio posterior invalida la aprobación y una feature con',
51
+ 'alguno de los tres archivos ausente no se puede sellar.',
52
+ ];
53
+ /** Cuerpo de la regla para un destino. Copilot no tiene Spec Kit y su copia no invoca comandos. */
54
+ export function bindingRule(engine, tool) {
55
+ const speckit = TARGETS[tool].speckit;
56
+ const lines = ['# Spec Kit en AI-Hub: roles, estados y etiquetas', ''];
57
+ if (speckit === null) {
58
+ lines.push(`Este destino (${tool}) no tiene Spec Kit instalado: sus comandos no están disponibles aquí.`, 'La ejecución ocurre en un destino con Spec Kit (Claude, OpenCode, Pi o Cursor) o por la CLI de Spec Kit.', 'Esta regla solo informa de cómo se interpretan sus resultados.', '', ...stateTable(engine), '', ...ARTIFACTS, '', ...SEAL, '');
59
+ return lines.join('\n');
60
+ }
61
+ lines.push(`Spec Kit (\`${engine.speckit.package}\` ${engine.speckit.pin}) ejecuta los ${engine.commands.length} comandos.`, 'AI-Hub define qué rol invoca cada uno, cómo se interpreta su salida y qué etiqueta se aplica.', '', '## Comandos', '', '| Comando | Invocación | Carril | Rol | Artefactos | Estados de salida |', '|---|---|---|---|---|---|', ...engine.commands.map((command) => {
62
+ const roles = [command.role, ...(command.co_roles ?? [])].join(', ');
63
+ return `| \`${command.speckit}\` | \`${invocationFor(command.speckit, speckit.separator)}\` | ${command.lane} | ${roles} | ${command.artifacts.join(', ')} | ${command.exit.join(', ')} |`;
64
+ }), '', 'Un agente solo invoca los comandos asignados a su rol; un comando de otro rol se rechaza.', '', ...stateTable(engine), '', ...ARTIFACTS, '', ...SEAL, '');
65
+ return lines.join('\n');
66
+ }
@@ -0,0 +1,46 @@
1
+ import { TARGETS } from '../sync/targets.js';
2
+ import { type ProcessRunner } from './runner.js';
3
+ import type { SpeckitSettings } from './settings.js';
4
+ /** Argumentos de `uvx` que fijan el paquete y la fecha de resolución. */
5
+ export declare function uvxPrefix(settings: SpeckitSettings): string[];
6
+ /**
7
+ * Secuencia de instalación (design D3, paso 9). Cada elemento son los argumentos de `uvx`.
8
+ * Nunca incluye `extension add --force`: borra la extensión de la integración anterior.
9
+ * `integration use` registra de forma aditiva las extensiones ya instaladas.
10
+ */
11
+ export declare function speckitSequence(settings: SpeckitSettings): string[][];
12
+ export interface SpeckitEnvironment {
13
+ uvx: 'found' | 'missing';
14
+ cached: 'yes' | 'no' | 'unknown';
15
+ shell: 'found' | 'missing';
16
+ shellCommand: string;
17
+ }
18
+ /**
19
+ * Ejecuta el shell en lugar de buscarlo en el PATH: en Windows `bash` puede ser el lanzador
20
+ * de WSL, que existe pero no ejecuta sin una distribución instalada.
21
+ */
22
+ export declare function probeShell(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<{
23
+ ok: boolean;
24
+ command: string;
25
+ }>;
26
+ /** Sonda del entorno para el plan: describe la máquina y no entra en el hash. */
27
+ export declare function probeEnvironment(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<SpeckitEnvironment>;
28
+ export interface PreflightResult {
29
+ ok: boolean;
30
+ problems: string[];
31
+ }
32
+ /**
33
+ * Preflight (design D3, paso 6). Se ejecuta antes de escribir cualquier archivo.
34
+ * Comprueba uv, la resolución de la versión fijada (caché o red) y el shell del script declarado.
35
+ */
36
+ export declare function speckitPreflight(settings: SpeckitSettings, runner: ProcessRunner, cwd: string): Promise<PreflightResult>;
37
+ export interface SpeckitRunResult {
38
+ ok: boolean;
39
+ steps: string[][];
40
+ failure: string | null;
41
+ }
42
+ /** Ejecuta la secuencia de instalación en `root`. Se detiene en el primer paso fallido. */
43
+ export declare function runSpeckitSequence(settings: SpeckitSettings, runner: ProcessRunner, root: string): Promise<SpeckitRunResult>;
44
+ /** Comando de Spec Kit en la notación de un destino: `/speckit-bug-assess` o `/speckit.bug.assess`. */
45
+ export declare function invocationFor(canonical: string, separator: '-' | '.'): string;
46
+ export declare function speckitTargetOf(tool: keyof typeof TARGETS): (typeof TARGETS)[keyof typeof TARGETS]['speckit'];
@@ -0,0 +1,131 @@
1
+ import { TARGETS } from '../sync/targets.js';
2
+ import { succeeded } from './runner.js';
3
+ /** Argumentos de `uvx` que fijan el paquete y la fecha de resolución. */
4
+ export function uvxPrefix(settings) {
5
+ return ['--from', `${settings.package}==${settings.version}`, '--exclude-newer', settings.excludeNewer, 'specify'];
6
+ }
7
+ /**
8
+ * Secuencia de instalación (design D3, paso 9). Cada elemento son los argumentos de `uvx`.
9
+ * Nunca incluye `extension add --force`: borra la extensión de la integración anterior.
10
+ * `integration use` registra de forma aditiva las extensiones ya instaladas.
11
+ */
12
+ export function speckitSequence(settings) {
13
+ if (settings.defaultIntegration === null) {
14
+ return [];
15
+ }
16
+ const prefix = uvxPrefix(settings);
17
+ const extensions = Object.keys(settings.extensions)
18
+ .sort()
19
+ .reverse()
20
+ .flatMap((id) => ['--extension', id]);
21
+ const steps = [
22
+ [
23
+ ...prefix,
24
+ 'init',
25
+ '.',
26
+ '--here',
27
+ '--force',
28
+ '--non-interactive',
29
+ '--integration',
30
+ settings.defaultIntegration,
31
+ '--script',
32
+ settings.script,
33
+ ...extensions,
34
+ ],
35
+ ];
36
+ for (const integration of settings.integrations.slice(1)) {
37
+ steps.push([...prefix, 'integration', 'install', integration, '--script', settings.script]);
38
+ steps.push([...prefix, 'integration', 'use', integration]);
39
+ }
40
+ if (settings.integrations.length > 1) {
41
+ steps.push([...prefix, 'integration', 'use', settings.defaultIntegration]);
42
+ }
43
+ return steps;
44
+ }
45
+ function shellCandidates(settings) {
46
+ return settings.script === 'ps'
47
+ ? [
48
+ { command: 'pwsh', args: ['-NoProfile', '-Command', 'echo ok'] },
49
+ { command: 'powershell', args: ['-NoProfile', '-Command', 'echo ok'] },
50
+ ]
51
+ : [{ command: 'bash', args: ['-c', 'echo ok'] }];
52
+ }
53
+ /**
54
+ * Ejecuta el shell en lugar de buscarlo en el PATH: en Windows `bash` puede ser el lanzador
55
+ * de WSL, que existe pero no ejecuta sin una distribución instalada.
56
+ */
57
+ export async function probeShell(settings, runner, cwd) {
58
+ const candidates = shellCandidates(settings);
59
+ for (const candidate of candidates) {
60
+ const result = await runner(candidate.command, candidate.args, { cwd, timeoutMs: 30_000 });
61
+ if (succeeded(result) && result.stdout.trim() === 'ok') {
62
+ return { ok: true, command: candidate.command };
63
+ }
64
+ }
65
+ return { ok: false, command: candidates[0].command };
66
+ }
67
+ async function uvxAvailable(runner, cwd) {
68
+ return succeeded(await runner('uvx', ['--version'], { cwd, timeoutMs: 30_000 }));
69
+ }
70
+ /** Sonda del entorno para el plan: describe la máquina y no entra en el hash. */
71
+ export async function probeEnvironment(settings, runner, cwd) {
72
+ const uvx = await uvxAvailable(runner, cwd);
73
+ let cached = 'unknown';
74
+ if (uvx) {
75
+ const offline = await runner('uvx', ['--offline', ...uvxPrefix(settings), '--version'], { cwd, timeoutMs: 60_000 });
76
+ cached = succeeded(offline) ? 'yes' : 'no';
77
+ }
78
+ const shell = await probeShell(settings, runner, cwd);
79
+ return {
80
+ uvx: uvx ? 'found' : 'missing',
81
+ cached,
82
+ shell: shell.ok ? 'found' : 'missing',
83
+ shellCommand: shell.command,
84
+ };
85
+ }
86
+ /**
87
+ * Preflight (design D3, paso 6). Se ejecuta antes de escribir cualquier archivo.
88
+ * Comprueba uv, la resolución de la versión fijada (caché o red) y el shell del script declarado.
89
+ */
90
+ export async function speckitPreflight(settings, runner, cwd) {
91
+ const problems = [];
92
+ if (!(await uvxAvailable(runner, cwd))) {
93
+ problems.push('uv no está disponible (uvx no está en el PATH). Instálalo: https://docs.astral.sh/uv/getting-started/installation/ ' +
94
+ '— o repite con --no-speckit para instalar sin Spec Kit (el arnés quedará degradado).');
95
+ return { ok: false, problems };
96
+ }
97
+ const resolved = await runner('uvx', [...uvxPrefix(settings), '--version'], { cwd });
98
+ if (!succeeded(resolved)) {
99
+ problems.push(`no se pudo resolver ${settings.package}==${settings.version} con uvx: sin red y sin caché, o la versión no existe. ` +
100
+ 'Ejecuta una vez con red (uvx descarga y cachea la versión fijada) o usa --no-speckit.' +
101
+ (resolved.stderr.trim() !== '' ? `\n${resolved.stderr.trim().split(/\r?\n/).slice(-3).join('\n')}` : ''));
102
+ }
103
+ const shell = await probeShell(settings, runner, cwd);
104
+ if (!shell.ok) {
105
+ problems.push(settings.script === 'ps'
106
+ ? 'PowerShell no está disponible (pwsh o powershell) y speckit.script es ps. Instala PowerShell o usa speckit.script: sh.'
107
+ : 'bash no está disponible o no ejecuta `bash -c "echo ok"`, y speckit.script es sh. ' +
108
+ 'En Windows instala Git for Windows (Git Bash) y ponlo en el PATH; el lanzador de WSL sin distribución no sirve. ' +
109
+ 'Alternativa: declara speckit.script: ps en manifest.yaml.');
110
+ }
111
+ return { ok: problems.length === 0, problems };
112
+ }
113
+ /** Ejecuta la secuencia de instalación en `root`. Se detiene en el primer paso fallido. */
114
+ export async function runSpeckitSequence(settings, runner, root) {
115
+ const steps = speckitSequence(settings);
116
+ for (const step of steps) {
117
+ const result = await runner('uvx', step, { cwd: root });
118
+ if (!succeeded(result)) {
119
+ const detail = (result.error ?? result.stderr ?? result.stdout).trim().split(/\r?\n/).slice(-5).join('\n');
120
+ return { ok: false, steps, failure: `uvx ${step.join(' ')} falló (${result.status ?? 'sin código'}): ${detail}` };
121
+ }
122
+ }
123
+ return { ok: true, steps, failure: null };
124
+ }
125
+ /** Comando de Spec Kit en la notación de un destino: `/speckit-bug-assess` o `/speckit.bug.assess`. */
126
+ export function invocationFor(canonical, separator) {
127
+ return `/${canonical.replace(/\./g, separator)}`;
128
+ }
129
+ export function speckitTargetOf(tool) {
130
+ return TARGETS[tool].speckit;
131
+ }
@@ -0,0 +1,14 @@
1
+ import type { EngineManifest } from '../engine/engine.js';
2
+ import type { ProjectManifest } from '../manifest/manifest.js';
3
+ import type { ProcessRunner } from './runner.js';
4
+ export interface SpeckitCheck {
5
+ id: string;
6
+ ok: boolean;
7
+ detail: string;
8
+ }
9
+ /** Ids de los checks de Spec Kit, en el orden en que `doctor` los imprime. */
10
+ export declare const SPECKIT_CHECK_IDS: readonly ["speckit:managed", "speckit:installed", "speckit:version", "speckit:shell", "speckit:integrations", "speckit:extensions", "speckit:integrity", "speckit:collision", "speckit:constitution", "speckit:versioning"];
11
+ /** Checks de doctor para una instalación (dev-cli "Doctor verifies the Spec Kit installation"). */
12
+ export declare function speckitDoctorChecks(root: string, manifest: ProjectManifest, engine: EngineManifest, runner: ProcessRunner): Promise<SpeckitCheck[]>;
13
+ /** En el checkout del motor los checks de Spec Kit no aplican: su .specify/ es del mantenedor. */
14
+ export declare function speckitNotApplicable(): SpeckitCheck[];
@@ -0,0 +1,103 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readLock } from '../sync/lock.js';
3
+ import { probeShell } from './commands.js';
4
+ import { effectiveSpeckit } from './settings.js';
5
+ import { checkExtensions, checkIntegrations, checkIntegrity, constitutionIsTemplate, inReservedNamespace, readSpeckitState, speckitOwnedPaths, } from './state.js';
6
+ /** Ids de los checks de Spec Kit, en el orden en que `doctor` los imprime. */
7
+ export const SPECKIT_CHECK_IDS = [
8
+ 'speckit:managed',
9
+ 'speckit:installed',
10
+ 'speckit:version',
11
+ 'speckit:shell',
12
+ 'speckit:integrations',
13
+ 'speckit:extensions',
14
+ 'speckit:integrity',
15
+ 'speckit:collision',
16
+ 'speckit:constitution',
17
+ 'speckit:versioning',
18
+ ];
19
+ /** Estado de Spec Kit que el consumidor debe versionar: si git lo ignora, se pierde. */
20
+ const VERSIONED_STATE = ['.specify/memory/constitution.md', '.specify/init-options.json', 'specs/'];
21
+ function list(items, limit = 5) {
22
+ return items.slice(0, limit).join('; ') + (items.length > limit ? ` (+${items.length - limit})` : '');
23
+ }
24
+ function gitIgnored(root, rel) {
25
+ const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, encoding: 'utf8', shell: false });
26
+ if (inside.status !== 0 || (inside.stdout ?? '').trim() !== 'true') {
27
+ return null;
28
+ }
29
+ const result = spawnSync('git', ['check-ignore', '-q', '--no-index', rel], { cwd: root, encoding: 'utf8', shell: false });
30
+ return result.status === 0;
31
+ }
32
+ /** Checks de doctor para una instalación (dev-cli "Doctor verifies the Spec Kit installation"). */
33
+ export async function speckitDoctorChecks(root, manifest, engine, runner) {
34
+ const checks = [];
35
+ const add = (id, ok, detail) => {
36
+ checks.push({ id, ok, detail });
37
+ };
38
+ const settings = effectiveSpeckit(manifest, engine);
39
+ const copilot = manifest.targets.includes('copilot');
40
+ if (!settings.managed) {
41
+ add('speckit:managed', false, 'degradado: speckit.managed es false (--no-speckit); ejecuta ai-hub install para instalar Spec Kit');
42
+ for (const id of SPECKIT_CHECK_IDS.slice(1)) {
43
+ add(id, true, 'no aplica: Spec Kit no gestionado');
44
+ }
45
+ return checks;
46
+ }
47
+ add('speckit:managed', true, `gestionado: ${settings.package} ${settings.version} vía uvx`);
48
+ const state = await readSpeckitState(root);
49
+ add('speckit:installed', state.initOptions !== null, state.initOptions !== null ? '.specify/init-options.json presente' : 'falta .specify/init-options.json; ejecuta ai-hub install');
50
+ const installed = state.initOptions?.speckit_version;
51
+ add('speckit:version', installed === settings.version, installed === settings.version
52
+ ? `${installed}`
53
+ : `instalada ${installed ?? '(ninguna)'}; se esperaba ${settings.version}; ejecuta ai-hub install`);
54
+ const shell = await probeShell(settings, runner, root);
55
+ add('speckit:shell', shell.ok, shell.ok
56
+ ? `${shell.command} ejecuta los scripts ${settings.script}`
57
+ : settings.script === 'ps'
58
+ ? 'PowerShell (pwsh o powershell) no está disponible para los scripts ps'
59
+ : 'bash no está disponible o no ejecuta; en Windows instala Git Bash, o declara speckit.script: ps');
60
+ const integrations = checkIntegrations(settings, state);
61
+ add('speckit:integrations', integrations.length === 0, integrations.length === 0
62
+ ? `${settings.integrations.join(', ')} (por defecto ${settings.defaultIntegration ?? '-'})`
63
+ : list(integrations));
64
+ if (copilot) {
65
+ add('speckit:targets', true, 'copilot: sin Spec Kit (recibe agentes y reglas de AI-Hub; sus comandos se ejecutan en otro destino)');
66
+ }
67
+ const extensions = await checkExtensions(root, settings, state);
68
+ add('speckit:extensions', extensions.length === 0, extensions.length === 0
69
+ ? Object.entries(settings.extensions)
70
+ .map(([id, version]) => `${id} ${version}`)
71
+ .join(', ') + ` en ${settings.targets.length} destinos`
72
+ : list(extensions));
73
+ const integrity = await checkIntegrity(root, state);
74
+ add('speckit:integrity', integrity.missing.length === 0, integrity.missing.length > 0
75
+ ? `faltan ${integrity.missing.length} archivos de Spec Kit: ${list(integrity.missing)}; ejecuta ai-hub install`
76
+ : integrity.modified.length > 0
77
+ ? `aviso: ${integrity.modified.length} archivos de Spec Kit modificados; el próximo init los sobrescribe (usa overrides): ${list(integrity.modified)}`
78
+ : `${integrity.checked} archivos de Spec Kit íntegros`);
79
+ const owned = await speckitOwnedPaths(root, manifest.targets);
80
+ const lock = await readLock(root).catch(() => null);
81
+ const shared = Object.keys(lock?.files ?? {})
82
+ .filter((rel) => owned.has(rel) || (state.present && inReservedNamespace(rel, manifest.targets)))
83
+ .sort();
84
+ add('speckit:collision', shared.length === 0, shared.length === 0 ? 'ninguna ruta de sync.lock.json pertenece a Spec Kit' : `rutas compartidas con Spec Kit: ${list(shared)}`);
85
+ const template = await constitutionIsTemplate(root);
86
+ add('speckit:constitution', true, template === true
87
+ ? 'aviso: la constitución sigue siendo la plantilla; ejecuta /speckit.constitution'
88
+ : template === null
89
+ ? 'aviso: no hay .specify/memory/constitution.md'
90
+ : 'constitución propia del proyecto');
91
+ const ignored = VERSIONED_STATE.filter((rel) => gitIgnored(root, rel) === true);
92
+ const applicable = gitIgnored(root, VERSIONED_STATE[0]) !== null;
93
+ add('speckit:versioning', ignored.length === 0, !applicable
94
+ ? 'no aplica (sin repositorio git)'
95
+ : ignored.length === 0
96
+ ? 'constitución, init-options.json y specs/ versionables'
97
+ : `git ignora estado del proyecto: ${ignored.join(', ')}; quítalo de .gitignore`);
98
+ return checks;
99
+ }
100
+ /** En el checkout del motor los checks de Spec Kit no aplican: su .specify/ es del mantenedor. */
101
+ export function speckitNotApplicable() {
102
+ return SPECKIT_CHECK_IDS.map((id) => ({ id, ok: true, detail: 'no aplica en el checkout del motor' }));
103
+ }
@@ -0,0 +1,19 @@
1
+ export interface RunResult {
2
+ /** Código de salida; `null` si el proceso no llegó a ejecutarse o fue interrumpido. */
3
+ status: number | null;
4
+ stdout: string;
5
+ stderr: string;
6
+ /** Error de arranque (por ejemplo `ENOENT` cuando el ejecutable no existe). */
7
+ error?: string;
8
+ }
9
+ export interface RunOptions {
10
+ cwd: string;
11
+ timeoutMs?: number;
12
+ }
13
+ /**
14
+ * Ejecuta un proceso externo. Los argumentos viajan como array, nunca a través de un shell,
15
+ * para que las rutas con espacios no se interpreten. La entrada estándar se cierra.
16
+ */
17
+ export type ProcessRunner = (command: string, args: string[], options: RunOptions) => Promise<RunResult>;
18
+ export declare const defaultRunner: ProcessRunner;
19
+ export declare function succeeded(result: RunResult): boolean;
@@ -0,0 +1,22 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ const DEFAULT_TIMEOUT_MS = 300_000;
3
+ export const defaultRunner = async (command, args, options) => {
4
+ const result = spawnSync(command, args, {
5
+ cwd: options.cwd,
6
+ shell: false,
7
+ encoding: 'utf8',
8
+ input: '',
9
+ windowsHide: true,
10
+ timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
11
+ env: { ...process.env, PYTHONIOENCODING: 'utf-8', NO_COLOR: '1' },
12
+ });
13
+ return {
14
+ status: result.status,
15
+ stdout: result.stdout ?? '',
16
+ stderr: result.stderr ?? '',
17
+ ...(result.error ? { error: result.error.message } : {}),
18
+ };
19
+ };
20
+ export function succeeded(result) {
21
+ return result.error === undefined && result.status === 0;
22
+ }