siesa-agents 2.1.82 → 2.1.84
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/bin/folder-mappings.js +79 -0
- package/bin/install.js +358 -88
- package/bin/prepare-publish.js +20 -13
- package/bin/restore-folders.js +16 -13
- package/claude/agents/sa-code-review.md +1 -0
- package/claude/agents/sa-dev-story.md +1 -0
- package/claude/commands/sa-quick-dev.md +58 -0
- package/package.json +3 -4
- package/siesa-agents/bmm/workflows/4-implementation/dev-story/workflow_ext.md +16 -0
- package/siesa-agents/resources/sonar/sonar-rules.md +267 -0
- package/vscode/settings.json +5 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// folder-mappings.js — Single source of truth for the source→target mapping used by the
|
|
2
|
+
// installer (install.js) and the publish helpers (prepare-publish.js / restore-folders.js).
|
|
3
|
+
//
|
|
4
|
+
// Background — the dual-mirror pattern this repo uses:
|
|
5
|
+
// Every folder that ships with the npm package exists twice in the dev tree:
|
|
6
|
+
// - The "dotted/underscored" version used by the dev workflow:
|
|
7
|
+
// `.claude/`, `.github/`, `.gemini/`, `.resources/`, `.mcp.json`, `.vscode/`,
|
|
8
|
+
// `_bmad/`, `_siesa-agents/`
|
|
9
|
+
// - The "flat" version that is what npm publish actually packages and ships:
|
|
10
|
+
// `claude/`, `github/`, `gemini/`, `resources/`, `mcp.json`, `vscode/`,
|
|
11
|
+
// `bmad/`, `siesa-agents/`
|
|
12
|
+
// When you edit anything under `.claude/skills/`, you must mirror the change into
|
|
13
|
+
// `claude/skills/`. The installer reads from the flat (tarball) names and writes to the
|
|
14
|
+
// dotted (user project) names.
|
|
15
|
+
//
|
|
16
|
+
// Why both? npm's `files` array historically had quirks with dot-prefixed entries
|
|
17
|
+
// (`.github/` would sometimes ship, sometimes not, depending on npm version and registry
|
|
18
|
+
// rules). Keeping a flat copy guarantees the file ships, while keeping the dotted copy
|
|
19
|
+
// keeps the dev workflow honest (`.claude/` is what Claude Code reads, `.github/` is what
|
|
20
|
+
// GitHub Actions reads).
|
|
21
|
+
//
|
|
22
|
+
// Three views of the same data:
|
|
23
|
+
// - installerMappings() used by bin/install.js: { source = tarball name,
|
|
24
|
+
// target = user project name }
|
|
25
|
+
// - publishRenames() used by bin/prepare-publish.js. Currently empty — see comment
|
|
26
|
+
// on the FOLDERS array.
|
|
27
|
+
// - restoreRenames() used by bin/restore-folders.js. Currently empty — see comment.
|
|
28
|
+
//
|
|
29
|
+
// Add new folders here and the rest of the pipeline picks them up. If a new folder needs
|
|
30
|
+
// pre-publish renaming (only when there's no flat mirror in dev), add a `devSource`.
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
// Each entry describes one shipping unit:
|
|
35
|
+
// - tarball: name inside the published npm tarball (no leading `.` or `_`)
|
|
36
|
+
// - userProject: name in the user's project after `npx siesa-agents`
|
|
37
|
+
// - devSource: optional — pre-publish rename source. Leave it `null` when the dev tree
|
|
38
|
+
// already maintains both the dotted AND the flat copy (current strategy
|
|
39
|
+
// for every folder shipped today). Only set it if you want the publish
|
|
40
|
+
// pipeline to physically rename the dev folder before `npm publish`.
|
|
41
|
+
const FOLDERS = [
|
|
42
|
+
{ tarball: 'bmad', userProject: '_bmad', devSource: null },
|
|
43
|
+
{ tarball: 'siesa-agents', userProject: '_siesa-agents', devSource: null },
|
|
44
|
+
{ tarball: 'vscode', userProject: '.vscode', devSource: null },
|
|
45
|
+
{ tarball: 'github', userProject: '.github', devSource: null },
|
|
46
|
+
{ tarball: 'claude', userProject: '.claude', devSource: null },
|
|
47
|
+
{ tarball: 'gemini', userProject: '.gemini', devSource: null },
|
|
48
|
+
{ tarball: 'resources', userProject: '.resources', devSource: null },
|
|
49
|
+
{ tarball: 'mcp.json', userProject: '.mcp.json', devSource: null },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Shape expected by install.js: { source, target } where source is the tarball name and
|
|
53
|
+
// target is what to call it in the user's project.
|
|
54
|
+
function installerMappings() {
|
|
55
|
+
return FOLDERS.map(f => ({ source: f.tarball, target: f.userProject }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Shape expected by prepare-publish.js: { from, to } where from is the dev name and to is
|
|
59
|
+
// the tarball name. Empty when the dev tree already maintains dual-mirror copies.
|
|
60
|
+
function publishRenames() {
|
|
61
|
+
return FOLDERS
|
|
62
|
+
.filter(f => f.devSource && f.devSource !== f.tarball)
|
|
63
|
+
.map(f => ({ from: f.devSource, to: f.tarball }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Inverse of publishRenames: restore tarball names back to dev names. Empty in the
|
|
67
|
+
// dual-mirror strategy.
|
|
68
|
+
function restoreRenames() {
|
|
69
|
+
return FOLDERS
|
|
70
|
+
.filter(f => f.devSource && f.devSource !== f.tarball)
|
|
71
|
+
.map(f => ({ from: f.tarball, to: f.devSource }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
FOLDERS,
|
|
76
|
+
installerMappings,
|
|
77
|
+
publishRenames,
|
|
78
|
+
restoreRenames,
|
|
79
|
+
};
|
package/bin/install.js
CHANGED
|
@@ -2,27 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const { sourceMapsEnabled } = require('process');
|
|
6
|
-
const readline = require('readline');
|
|
7
5
|
const { execSync } = require('child_process');
|
|
6
|
+
const { installerMappings } = require('./folder-mappings');
|
|
7
|
+
|
|
8
|
+
// @clack/prompts es ESM-only, así que desde este install.js CJS lo cargamos con
|
|
9
|
+
// dynamic `import()`. Cachéamos la promesa para que la primera carga pague el
|
|
10
|
+
// costo de resolución y las siguientes sean instantáneas.
|
|
11
|
+
let _clackPromise = null;
|
|
12
|
+
function loadClack() {
|
|
13
|
+
if (!_clackPromise) {
|
|
14
|
+
_clackPromise = import('@clack/prompts');
|
|
15
|
+
}
|
|
16
|
+
return _clackPromise;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// En Windows oculta/des-oculta una carpeta o archivo (atributo +h/-h). Sirve para
|
|
20
|
+
// que las carpetas de staging `.sa-new`/`.sa-old` no aparezcan en el explorador
|
|
21
|
+
// durante el breve lapso en que existen — verlas crearse y borrarse se ve raro.
|
|
22
|
+
// Best-effort: si `attrib` falla no pasa nada (es puramente cosmético). No-op
|
|
23
|
+
// fuera de Windows (en Unix el prefijo `.` ya las oculta en la mayoría de FMs).
|
|
24
|
+
function setHidden(targetPath, hidden) {
|
|
25
|
+
if (process.platform !== 'win32') return;
|
|
26
|
+
try {
|
|
27
|
+
execSync(`attrib ${hidden ? '+h' : '-h'} "${targetPath}"`, { stdio: 'ignore' });
|
|
28
|
+
} catch (_) { /* cosmético: ignorar */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Maneja la cancelación de un prompt clack (Ctrl+C / Esc). Sale limpio con
|
|
32
|
+
// código 0 — no es un error, el usuario explícitamente abortó.
|
|
33
|
+
async function bailIfCancel(value) {
|
|
34
|
+
const clack = await loadClack();
|
|
35
|
+
if (clack.isCancel(value)) {
|
|
36
|
+
clack.cancel('Instalación cancelada por el usuario.');
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse argumentos CLI básicos. Soporta:
|
|
43
|
+
// --yes / -y Modo no-interactivo: usa defaults sensatos en cualquier prompt
|
|
44
|
+
// en vez de leer de stdin. Defaults: "hacer backup de modificados"
|
|
45
|
+
// (opción 2) y "no hay repo remoto" (opción 2). También se activa
|
|
46
|
+
// automáticamente cuando stdin no es un TTY (CI, pipes).
|
|
47
|
+
// --version Imprime la versión del paquete y sale.
|
|
48
|
+
// --help / -h Imprime ayuda breve y sale.
|
|
49
|
+
function parseCliArgs(argv) {
|
|
50
|
+
const out = { yes: false, version: false, help: false };
|
|
51
|
+
for (const a of argv) {
|
|
52
|
+
if (a === '--yes' || a === '-y') out.yes = true;
|
|
53
|
+
else if (a === '--version') out.version = true;
|
|
54
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Deep-merge de dos objetos JSON. Source gana en conflictos; los arrays se
|
|
60
|
+
// reemplazan completos (no se hace merge elemento-a-elemento). Pensado para
|
|
61
|
+
// `.mcp.json`: los MCPs que el usuario agregó (no existen en el paquete) sobreviven
|
|
62
|
+
// al update mientras los nativos se actualizan a la versión del paquete.
|
|
63
|
+
function deepMergeJson(target, source) {
|
|
64
|
+
if (source === null || typeof source !== 'object') return source;
|
|
65
|
+
if (target === null || typeof target !== 'object') return source;
|
|
66
|
+
if (Array.isArray(source) || Array.isArray(target)) return source;
|
|
67
|
+
const out = { ...target };
|
|
68
|
+
for (const key of Object.keys(source)) {
|
|
69
|
+
out[key] = deepMergeJson(target[key], source[key]);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
8
73
|
|
|
9
74
|
class SiesaBmadInstaller {
|
|
10
|
-
constructor() {
|
|
11
|
-
// Definir las carpetas primero (nombres en el paquete vs nombres finales)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Lista de archivos que se preservan automáticamente (no se crean backups)
|
|
75
|
+
constructor(cliArgs = {}) {
|
|
76
|
+
// Definir las carpetas primero (nombres en el paquete vs nombres finales).
|
|
77
|
+
// Source of truth: bin/folder-mappings.js
|
|
78
|
+
this.folderMappings = installerMappings();
|
|
79
|
+
|
|
80
|
+
// Archivos que el instalador PRESERVA: no se sobrescriben si ya existen, no se
|
|
81
|
+
// reportan como "modificados" y no se les hace backup. El usuario los personaliza
|
|
82
|
+
// localmente y son suyos.
|
|
83
|
+
// - data/technical-preferences.md → preferencias del usuario.
|
|
84
|
+
// - settings.json → `.vscode/settings.json` (la única carpeta del paquete con
|
|
85
|
+
// un settings.json en su raíz es `.vscode`, así que el match es inequívoco).
|
|
86
|
+
// El usuario ajusta su editor; el instalador no debe pisarlo ni molestar.
|
|
24
87
|
this.ignoredFiles = [
|
|
25
|
-
'data/technical-preferences.md'
|
|
88
|
+
'data/technical-preferences.md',
|
|
89
|
+
'settings.json'
|
|
26
90
|
];
|
|
27
91
|
|
|
28
92
|
// Directorios base que se deben crear siempre durante la instalación
|
|
@@ -33,9 +97,14 @@ class SiesaBmadInstaller {
|
|
|
33
97
|
this.targetDir = process.cwd();
|
|
34
98
|
// Intentar múltiples ubicaciones posibles para el paquete
|
|
35
99
|
this.packageDir = this.findPackageDir();
|
|
36
|
-
|
|
100
|
+
|
|
37
101
|
// Almacenamiento temporal para contenido de archivos ignorados
|
|
38
102
|
this.preservedContent = new Map();
|
|
103
|
+
|
|
104
|
+
// Flag no-interactivo: opt-in via --yes/-y o auto-detectado cuando stdin no es TTY
|
|
105
|
+
// (típicamente CI, npx detrás de pipes, scripts automatizados). Si está activo, los
|
|
106
|
+
// prompts usan defaults en vez de bloquearse esperando input.
|
|
107
|
+
this.nonInteractive = Boolean(cliArgs.yes) || !process.stdin.isTTY;
|
|
39
108
|
}
|
|
40
109
|
|
|
41
110
|
|
|
@@ -121,27 +190,39 @@ class SiesaBmadInstaller {
|
|
|
121
190
|
}
|
|
122
191
|
|
|
123
192
|
async promptHasRepository() {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
if (this.nonInteractive) {
|
|
194
|
+
console.log('\n (modo no-interactivo: sin repositorio remoto)');
|
|
195
|
+
return '2';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const clack = await loadClack();
|
|
199
|
+
const answer = await clack.select({
|
|
200
|
+
message: '¿Tienes un repositorio remoto que quieras vincular ahora?',
|
|
201
|
+
options: [
|
|
202
|
+
{ value: '2', label: 'No, lo configuraré después', hint: 'recomendado si todavía no creaste el repo' },
|
|
203
|
+
{ value: '1', label: 'Sí, tengo URL del repo remoto' },
|
|
204
|
+
],
|
|
205
|
+
initialValue: '2',
|
|
134
206
|
});
|
|
207
|
+
return bailIfCancel(answer);
|
|
135
208
|
}
|
|
136
209
|
|
|
137
210
|
async promptRepositoryUrl() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
211
|
+
if (this.nonInteractive) {
|
|
212
|
+
// En modo no-interactivo nunca deberíamos llegar aquí (promptHasRepository
|
|
213
|
+
// devuelve '2'), pero por defensa devolvemos cadena vacía.
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
const clack = await loadClack();
|
|
217
|
+
const answer = await clack.text({
|
|
218
|
+
message: 'URL del repositorio',
|
|
219
|
+
placeholder: 'https://github.com/<org>/<repo>.git',
|
|
220
|
+
validate: (value) => {
|
|
221
|
+
if (!value || value.trim() === '') return 'La URL no puede estar vacía';
|
|
222
|
+
},
|
|
144
223
|
});
|
|
224
|
+
const checked = await bailIfCancel(answer);
|
|
225
|
+
return checked.trim();
|
|
145
226
|
}
|
|
146
227
|
|
|
147
228
|
async installGitignore() {
|
|
@@ -149,7 +230,9 @@ class SiesaBmadInstaller {
|
|
|
149
230
|
const entries = [
|
|
150
231
|
'node_modules/',
|
|
151
232
|
'.claude/commands/get-features/oauth-config.json',
|
|
152
|
-
'.claude/commands/get-features/tokens.json'
|
|
233
|
+
'.claude/commands/get-features/tokens.json',
|
|
234
|
+
'.vscode/settings.json',
|
|
235
|
+
'.claude.json'
|
|
153
236
|
];
|
|
154
237
|
|
|
155
238
|
let content = entries.join('\n') + '\n';
|
|
@@ -268,6 +351,13 @@ class SiesaBmadInstaller {
|
|
|
268
351
|
const modifiedFiles = [];
|
|
269
352
|
|
|
270
353
|
for (const mapping of this.folderMappings) {
|
|
354
|
+
// .mcp.json no entra en la detección de modificaciones: se gestiona con
|
|
355
|
+
// deep-merge JSON en performAtomicUpdate / copyWithBackupPreservation, que
|
|
356
|
+
// preserva los MCPs personalizados del usuario y actualiza los nativos.
|
|
357
|
+
// Marcarlo como "modificado" forzaría el flujo backup-y-reemplaza y
|
|
358
|
+
// destruiría los MCPs custom.
|
|
359
|
+
if (mapping.target === '.mcp.json') continue;
|
|
360
|
+
|
|
271
361
|
const sourcePath = path.join(this.packageDir, mapping.source);
|
|
272
362
|
const targetPath = path.join(this.targetDir, mapping.target);
|
|
273
363
|
|
|
@@ -284,9 +374,13 @@ class SiesaBmadInstaller {
|
|
|
284
374
|
|
|
285
375
|
if (fs.existsSync(targetFile)) {
|
|
286
376
|
try {
|
|
287
|
-
// Comparar contenido
|
|
288
|
-
|
|
289
|
-
|
|
377
|
+
// Comparar contenido normalizando CRLF→LF. Sin esta normalización, en Windows
|
|
378
|
+
// con `core.autocrlf=true` cualquier archivo de texto cuya copia local fue
|
|
379
|
+
// checkout con CRLF aparece como "modificado" frente al paquete (que tiene LF),
|
|
380
|
+
// y el instalador pide backup en cada corrida aunque el usuario no haya tocado
|
|
381
|
+
// nada.
|
|
382
|
+
const sourceContent = (await fs.readFile(sourceFile, 'utf8')).replace(/\r\n/g, '\n');
|
|
383
|
+
const targetContent = (await fs.readFile(targetFile, 'utf8')).replace(/\r\n/g, '\n');
|
|
290
384
|
|
|
291
385
|
if (sourceContent !== targetContent) {
|
|
292
386
|
modifiedFiles.push({
|
|
@@ -297,7 +391,8 @@ class SiesaBmadInstaller {
|
|
|
297
391
|
});
|
|
298
392
|
}
|
|
299
393
|
} catch (error) {
|
|
300
|
-
// Si no se puede leer como texto, comparar como buffer (archivos binarios)
|
|
394
|
+
// Si no se puede leer como texto, comparar como buffer (archivos binarios).
|
|
395
|
+
// Para binarios la normalización CRLF no aplica.
|
|
301
396
|
const sourceBuffer = await fs.readFile(sourceFile);
|
|
302
397
|
const targetBuffer = await fs.readFile(targetFile);
|
|
303
398
|
|
|
@@ -341,22 +436,26 @@ class SiesaBmadInstaller {
|
|
|
341
436
|
}
|
|
342
437
|
|
|
343
438
|
async promptUser(modifiedFiles) {
|
|
439
|
+
// modifiedFiles solo contiene archivos QUE EXISTEN EN EL PAQUETE y cuyo
|
|
440
|
+
// contenido difiere del target. Es decir: archivos del paquete (skills nativas,
|
|
441
|
+
// configs nativas, etc.) que el usuario modificó localmente. Los archivos que
|
|
442
|
+
// el usuario AGREGÓ y no están en el paquete (skills custom, flows propios,
|
|
443
|
+
// MCPs adicionales) NO aparecen aquí — se preservan automáticamente en
|
|
444
|
+
// performAtomicUpdate / copyWithBackupPreservation sin pedir permiso.
|
|
445
|
+
const nonIgnoredFiles = modifiedFiles.filter(f => !f.is_ignored);
|
|
446
|
+
if (nonIgnoredFiles.length === 0) return '2';
|
|
344
447
|
|
|
345
|
-
|
|
346
|
-
if (!hasNonIgnoredFiles) return '2'
|
|
347
|
-
|
|
348
|
-
console.log('\n⚠️ Se detectaron archivos modificados:');
|
|
448
|
+
console.log(`\n⚠️ Detecté ${nonIgnoredFiles.length} archivo(s) del paquete Siesa-Agents que modificaste localmente:`);
|
|
349
449
|
|
|
350
450
|
// Agrupar por carpeta
|
|
351
451
|
const filesByFolder = {};
|
|
352
|
-
|
|
452
|
+
nonIgnoredFiles.forEach(item => {
|
|
353
453
|
if (!filesByFolder[item.folder]) {
|
|
354
454
|
filesByFolder[item.folder] = [];
|
|
355
455
|
}
|
|
356
456
|
filesByFolder[item.folder].push(item.file);
|
|
357
457
|
});
|
|
358
458
|
|
|
359
|
-
// Mostrar archivos modificados por carpeta
|
|
360
459
|
Object.keys(filesByFolder).forEach(folder => {
|
|
361
460
|
console.log(`\n📁 ${folder}:`);
|
|
362
461
|
filesByFolder[folder].forEach(file => {
|
|
@@ -364,21 +463,24 @@ class SiesaBmadInstaller {
|
|
|
364
463
|
});
|
|
365
464
|
});
|
|
366
465
|
|
|
367
|
-
console.log('\n
|
|
368
|
-
console.log('
|
|
369
|
-
console.log('2. Hacer backup de archivos modificados (se agregarán con sufijo _bk)');
|
|
466
|
+
console.log('\nℹ️ Tus skills/flows/MCPs propios (los que NO vienen en el paquete) se');
|
|
467
|
+
console.log(' preservan automáticamente — no aparecen en la lista de arriba.');
|
|
370
468
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
469
|
+
if (this.nonInteractive) {
|
|
470
|
+
console.log('\n (modo no-interactivo: backup de tus cambios)');
|
|
471
|
+
return '2';
|
|
472
|
+
}
|
|
375
473
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
474
|
+
const clack = await loadClack();
|
|
475
|
+
const answer = await clack.select({
|
|
476
|
+
message: '¿Qué hago con TUS cambios sobre los archivos del paquete?',
|
|
477
|
+
options: [
|
|
478
|
+
{ value: '2', label: 'Backup y actualizar', hint: 'guarda tus cambios con sufijo _bk + trae la versión del paquete (recomendado)' },
|
|
479
|
+
{ value: '1', label: 'Reemplazar', hint: 'pierdes tus modificaciones — sin backup' },
|
|
480
|
+
],
|
|
481
|
+
initialValue: '2',
|
|
381
482
|
});
|
|
483
|
+
return bailIfCancel(answer);
|
|
382
484
|
}
|
|
383
485
|
|
|
384
486
|
async backupModifiedFiles(modifiedFiles) {
|
|
@@ -419,22 +521,31 @@ class SiesaBmadInstaller {
|
|
|
419
521
|
|
|
420
522
|
// Primer intento: archivo_bk.ext
|
|
421
523
|
const basicBackupPath = path.join(dir, `${name}_bk${ext}`);
|
|
422
|
-
|
|
423
|
-
// Si no existe, usar el nombre básico
|
|
424
524
|
if (!fs.existsSync(basicBackupPath)) {
|
|
425
525
|
return basicBackupPath;
|
|
426
526
|
}
|
|
427
527
|
|
|
428
|
-
// Si ya existe _bk, crear versión con timestamp
|
|
528
|
+
// Si ya existe _bk, crear versión con timestamp. Incluimos milisegundos para
|
|
529
|
+
// evitar colisiones cuando dos backups del mismo archivo ocurren en el mismo
|
|
530
|
+
// segundo (p.ej. instalador corriendo en bucle / tests). Y por si dos backups
|
|
531
|
+
// caen en el mismo ms (esencialmente imposible pero defensivo), agregamos un
|
|
532
|
+
// contador incremental.
|
|
429
533
|
const now = new Date();
|
|
430
534
|
const timestamp = now.getFullYear().toString() +
|
|
431
535
|
(now.getMonth() + 1).toString().padStart(2, '0') +
|
|
432
536
|
now.getDate().toString().padStart(2, '0') + '_' +
|
|
433
537
|
now.getHours().toString().padStart(2, '0') +
|
|
434
538
|
now.getMinutes().toString().padStart(2, '0') +
|
|
435
|
-
now.getSeconds().toString().padStart(2, '0')
|
|
436
|
-
|
|
437
|
-
|
|
539
|
+
now.getSeconds().toString().padStart(2, '0') +
|
|
540
|
+
now.getMilliseconds().toString().padStart(3, '0');
|
|
541
|
+
|
|
542
|
+
let candidate = path.join(dir, `${name}_bk_${timestamp}${ext}`);
|
|
543
|
+
let counter = 1;
|
|
544
|
+
while (fs.existsSync(candidate)) {
|
|
545
|
+
candidate = path.join(dir, `${name}_bk_${timestamp}_${counter}${ext}`);
|
|
546
|
+
counter++;
|
|
547
|
+
}
|
|
548
|
+
return candidate;
|
|
438
549
|
}
|
|
439
550
|
|
|
440
551
|
async performUpdateWithBackups() {
|
|
@@ -454,21 +565,33 @@ class SiesaBmadInstaller {
|
|
|
454
565
|
async copyWithBackupPreservation(sourcePath, targetPath) {
|
|
455
566
|
// Obtener todos los archivos backup existentes
|
|
456
567
|
const backupFiles = await this.findBackupFiles(targetPath);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
568
|
+
|
|
569
|
+
const sourceIsFile = (await fs.stat(sourcePath)).isFile();
|
|
570
|
+
|
|
571
|
+
if (sourceIsFile) {
|
|
572
|
+
// File mapping (.mcp.json u otros). Para .mcp.json hacemos deep-merge JSON
|
|
573
|
+
// para preservar MCPs personalizados; para cualquier otro archivo,
|
|
574
|
+
// copia directa (source-wins).
|
|
575
|
+
await this.applyFileMapping(sourcePath, targetPath, targetPath);
|
|
576
|
+
} else {
|
|
577
|
+
// Folder mapping. fs.copy con overwrite=true sobre el target es un merge:
|
|
578
|
+
// actualiza archivos que ya existían y agrega los nuevos, dejando intactos
|
|
579
|
+
// los archivos del target que no aparecen en el source (skills custom,
|
|
580
|
+
// flujos agregados por el ingeniero, etc.).
|
|
581
|
+
await fs.copy(sourcePath, targetPath, {
|
|
582
|
+
overwrite: true,
|
|
583
|
+
recursive: true,
|
|
584
|
+
filter: (src) => {
|
|
585
|
+
const relativePath = path.relative(sourcePath, src);
|
|
586
|
+
// No sobrescribir archivos ignorados si ya existen
|
|
587
|
+
if (this.ignoredFiles.includes(relativePath)) {
|
|
588
|
+
const targetFile = path.join(targetPath, relativePath);
|
|
589
|
+
return !fs.existsSync(targetFile);
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
468
592
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
});
|
|
593
|
+
});
|
|
594
|
+
}
|
|
472
595
|
|
|
473
596
|
// Restaurar los archivos backup
|
|
474
597
|
for (const backupFile of backupFiles) {
|
|
@@ -485,6 +608,30 @@ class SiesaBmadInstaller {
|
|
|
485
608
|
}
|
|
486
609
|
}
|
|
487
610
|
|
|
611
|
+
// Aplica un mapping de archivo (no carpeta). Para `.mcp.json` hace deep-merge
|
|
612
|
+
// JSON con source-wins: actualiza los servidores MCP nativos a la versión del
|
|
613
|
+
// paquete pero conserva los MCPs adicionales que el usuario haya configurado.
|
|
614
|
+
// Para cualquier otro archivo, copia directa del source.
|
|
615
|
+
// El parámetro `outputPath` permite usar este método tanto en el camino atómico
|
|
616
|
+
// (escribiendo a `<target>.sa-new`) como en el camino con backups (escribiendo
|
|
617
|
+
// directamente al target).
|
|
618
|
+
async applyFileMapping(sourcePath, targetPath, outputPath) {
|
|
619
|
+
const isMcpJson = path.basename(targetPath) === '.mcp.json';
|
|
620
|
+
if (isMcpJson && fs.existsSync(targetPath)) {
|
|
621
|
+
try {
|
|
622
|
+
const sourceJson = JSON.parse(await fs.readFile(sourcePath, 'utf8'));
|
|
623
|
+
const targetJson = JSON.parse(await fs.readFile(targetPath, 'utf8'));
|
|
624
|
+
const merged = deepMergeJson(targetJson, sourceJson);
|
|
625
|
+
await fs.writeFile(outputPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
626
|
+
console.log('🔀 .mcp.json: merge — MCPs nativos actualizados, MCPs personalizados preservados');
|
|
627
|
+
return;
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.warn(`⚠️ No se pudo hacer merge de .mcp.json (${err.message}). Se usará la versión del paquete.`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
await fs.copy(sourcePath, outputPath, { overwrite: true });
|
|
633
|
+
}
|
|
634
|
+
|
|
488
635
|
async findBackupFiles(targetPath) {
|
|
489
636
|
if (!fs.existsSync(targetPath)) {
|
|
490
637
|
return [];
|
|
@@ -566,23 +713,121 @@ class SiesaBmadInstaller {
|
|
|
566
713
|
if (hasBackups) {
|
|
567
714
|
await this.performUpdateWithBackups();
|
|
568
715
|
} else {
|
|
569
|
-
//
|
|
570
|
-
//
|
|
716
|
+
// Update atómico con merge:
|
|
717
|
+
// 1. Staging arranca como CLON del target actual — preserva skills custom
|
|
718
|
+
// en .claude/skills/, flujos agregados por el ingeniero, y cualquier
|
|
719
|
+
// archivo no nativo que el usuario haya puesto dentro de las carpetas
|
|
720
|
+
// del paquete.
|
|
721
|
+
// 2. Source se sobrepone al staging (actualiza archivos nativos).
|
|
722
|
+
// 3. Para .mcp.json: deep-merge JSON con source-wins — los MCPs nativos
|
|
723
|
+
// se actualizan, los MCPs adicionales del usuario sobreviven.
|
|
724
|
+
// 4. Swap atómico: target → .sa-old, .sa-new → target, borrar .sa-old.
|
|
725
|
+
//
|
|
726
|
+
// El flujo anterior borraba el destino antes de copiar — si el proceso moría
|
|
727
|
+
// a mitad (Ctrl+C, OOM, crash), el usuario se quedaba sin instalación. Con
|
|
728
|
+
// el staging, una interrupción deja el target intacto y solo basura en
|
|
729
|
+
// .sa-new que se limpia en la próxima corrida.
|
|
730
|
+
//
|
|
731
|
+
// Trade-off conocido: archivos nativos que el paquete eliminó en una versión
|
|
732
|
+
// nueva permanecen en el target tras el update — no podemos distinguir
|
|
733
|
+
// entre "nativo removido en upstream" y "custom del usuario". El usuario
|
|
734
|
+
// limpia artefactos obsoletos a mano si los detecta.
|
|
571
735
|
await this.preserveIgnoredFiles();
|
|
572
|
-
|
|
736
|
+
await this.performAtomicUpdate();
|
|
737
|
+
await this.restoreIgnoredFiles();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async performAtomicUpdate() {
|
|
742
|
+
const STAGING_SUFFIX = '.sa-new';
|
|
743
|
+
const OLD_SUFFIX = '.sa-old';
|
|
744
|
+
|
|
745
|
+
// Phase 1: limpiar restos de runs anteriores que murieron mid-update.
|
|
746
|
+
for (const mapping of this.folderMappings) {
|
|
747
|
+
const target = path.join(this.targetDir, mapping.target);
|
|
748
|
+
for (const suffix of [STAGING_SUFFIX, OLD_SUFFIX]) {
|
|
749
|
+
const stale = target + suffix;
|
|
750
|
+
if (fs.existsSync(stale)) {
|
|
751
|
+
try { await fs.remove(stale); } catch (_) {}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Phase 2: stage cada mapping en `<target>.sa-new`.
|
|
757
|
+
// - Folder mapping: clonar target actual al staging y después overlay del
|
|
758
|
+
// source. fs.copy con overwrite=true es un merge — el clon preserva los
|
|
759
|
+
// archivos del usuario (skills custom, MCPs, etc.) y el overlay del
|
|
760
|
+
// source actualiza los archivos nativos del paquete.
|
|
761
|
+
// - File mapping `.mcp.json`: deep-merge JSON con source-wins (preserva
|
|
762
|
+
// MCPs personalizados, actualiza nativos). Otros file mappings: copia
|
|
763
|
+
// directa del source.
|
|
764
|
+
// Si falla cualquier mapping, rollback antes de tocar los targets reales.
|
|
765
|
+
const staged = [];
|
|
766
|
+
try {
|
|
573
767
|
for (const mapping of this.folderMappings) {
|
|
768
|
+
const sourcePath = path.join(this.packageDir, mapping.source);
|
|
574
769
|
const targetPath = path.join(this.targetDir, mapping.target);
|
|
770
|
+
const stagingPath = targetPath + STAGING_SUFFIX;
|
|
575
771
|
|
|
576
|
-
if (fs.existsSync(
|
|
577
|
-
|
|
772
|
+
if (!fs.existsSync(sourcePath)) {
|
|
773
|
+
console.warn(`⚠️ ${mapping.source} no encontrado en el paquete`);
|
|
774
|
+
continue;
|
|
578
775
|
}
|
|
776
|
+
|
|
777
|
+
const sourceIsFile = (await fs.stat(sourcePath)).isFile();
|
|
778
|
+
|
|
779
|
+
if (sourceIsFile) {
|
|
780
|
+
await this.applyFileMapping(sourcePath, targetPath, stagingPath);
|
|
781
|
+
} else {
|
|
782
|
+
// Clonar target al staging primero (preserva customs), después overlay
|
|
783
|
+
// del source (actualiza nativos).
|
|
784
|
+
if (fs.existsSync(targetPath)) {
|
|
785
|
+
await fs.copy(targetPath, stagingPath, { overwrite: true, recursive: true });
|
|
786
|
+
}
|
|
787
|
+
await fs.copy(sourcePath, stagingPath, {
|
|
788
|
+
overwrite: true,
|
|
789
|
+
recursive: true,
|
|
790
|
+
filter: (src) => {
|
|
791
|
+
const relativePath = path.relative(sourcePath, src);
|
|
792
|
+
// No sobrescribir archivos ignorados si ya existen en el target real.
|
|
793
|
+
if (this.ignoredFiles.includes(relativePath)) {
|
|
794
|
+
const realTargetFile = path.join(targetPath, relativePath);
|
|
795
|
+
return !fs.existsSync(realTargetFile);
|
|
796
|
+
}
|
|
797
|
+
return true;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
// Ocultar el staging mientras existe (cosmético, solo Windows).
|
|
802
|
+
setHidden(stagingPath, true);
|
|
803
|
+
staged.push({ targetPath, stagingPath });
|
|
804
|
+
}
|
|
805
|
+
} catch (err) {
|
|
806
|
+
// Rollback: borrar lo que sí alcanzó a copiarse al staging.
|
|
807
|
+
for (const { stagingPath } of staged) {
|
|
808
|
+
try { await fs.remove(stagingPath); } catch (_) {}
|
|
579
809
|
}
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
580
812
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
813
|
+
// Phase 3: swap atómico. Por cada mapping ya staged, rename target → `.sa-old`,
|
|
814
|
+
// rename `.sa-new` → target, borrar `.sa-old`. fs.move es rename donde es posible.
|
|
815
|
+
for (const { targetPath, stagingPath } of staged) {
|
|
816
|
+
const oldPath = targetPath + OLD_SUFFIX;
|
|
817
|
+
|
|
818
|
+
if (fs.existsSync(targetPath)) {
|
|
819
|
+
await fs.move(targetPath, oldPath, { overwrite: true });
|
|
820
|
+
setHidden(oldPath, true); // ocultar el viejo mientras se borra
|
|
821
|
+
}
|
|
822
|
+
await fs.move(stagingPath, targetPath, { overwrite: true });
|
|
823
|
+
// El rename hereda el atributo oculto del staging — limpiarlo para que el
|
|
824
|
+
// target final sea visible.
|
|
825
|
+
setHidden(targetPath, false);
|
|
826
|
+
if (fs.existsSync(oldPath)) {
|
|
827
|
+
try { await fs.remove(oldPath); } catch (e) {
|
|
828
|
+
console.warn(`⚠️ No se pudo borrar ${oldPath} (se limpiará en la próxima corrida): ${e.message}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
586
831
|
}
|
|
587
832
|
}
|
|
588
833
|
|
|
@@ -660,9 +905,34 @@ class SiesaBmadInstaller {
|
|
|
660
905
|
}
|
|
661
906
|
}
|
|
662
907
|
|
|
908
|
+
function printHelp() {
|
|
909
|
+
console.log('Usage: npx siesa-agents [options]\n');
|
|
910
|
+
console.log('Options:');
|
|
911
|
+
console.log(' -y, --yes Modo no-interactivo. Usa defaults sensatos en cualquier prompt');
|
|
912
|
+
console.log(' (no preguntar por repo remoto, hacer backup de modificados).');
|
|
913
|
+
console.log(' Se activa automáticamente cuando stdin no es un TTY.');
|
|
914
|
+
console.log(' --version Imprime la versión del paquete y sale.');
|
|
915
|
+
console.log(' -h, --help Muestra esta ayuda.\n');
|
|
916
|
+
console.log('Environment:');
|
|
917
|
+
console.log(' SIESA_DEBUG=1 Muestra información de diagnóstico para el descubrimiento');
|
|
918
|
+
console.log(' del directorio del paquete.');
|
|
919
|
+
}
|
|
920
|
+
|
|
663
921
|
// Ejecutar instalación si el script es llamado directamente
|
|
664
922
|
if (require.main === module) {
|
|
665
|
-
const
|
|
923
|
+
const cliArgs = parseCliArgs(process.argv.slice(2));
|
|
924
|
+
|
|
925
|
+
if (cliArgs.help) {
|
|
926
|
+
printHelp();
|
|
927
|
+
process.exit(0);
|
|
928
|
+
}
|
|
929
|
+
if (cliArgs.version) {
|
|
930
|
+
const pkg = require('../package.json');
|
|
931
|
+
console.log(pkg.version);
|
|
932
|
+
process.exit(0);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const installer = new SiesaBmadInstaller(cliArgs);
|
|
666
936
|
installer.install();
|
|
667
937
|
}
|
|
668
938
|
|
package/bin/prepare-publish.js
CHANGED
|
@@ -2,26 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { publishRenames } = require('./folder-mappings');
|
|
5
6
|
|
|
6
7
|
const rootDir = path.dirname(__dirname);
|
|
7
8
|
|
|
8
|
-
// Renombrar carpetas
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{ from: '.github', to: 'github' }
|
|
13
|
-
];
|
|
9
|
+
// Renombrar carpetas dev-side a sus nombres "flat" antes de `npm publish`. Cuáles se
|
|
10
|
+
// renombran lo decide bin/folder-mappings.js (un solo source of truth compartido con
|
|
11
|
+
// install.js y restore-folders.js).
|
|
12
|
+
const folderMappings = publishRenames();
|
|
14
13
|
|
|
15
14
|
console.log('📦 Preparando carpetas para publicación (siesa-agents)...');
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (folderMappings.length === 0) {
|
|
17
|
+
console.log('ℹ️ El repo mantiene un mirror flat de cada carpeta (claude/, github/, bmad/, …)');
|
|
18
|
+
console.log(' junto al dotted/underscored (.claude/, .github/, _bmad/, …). No hace falta');
|
|
19
|
+
console.log(' renombrar nada antes de `npm publish` — los archivos flat ya están listos.');
|
|
20
|
+
console.log(' Si en el futuro se elimina algún mirror, declara su devSource en');
|
|
21
|
+
console.log(' bin/folder-mappings.js y este script lo renombrará automáticamente.');
|
|
22
|
+
} else {
|
|
23
|
+
for (const mapping of folderMappings) {
|
|
24
|
+
const fromPath = path.join(rootDir, mapping.from);
|
|
25
|
+
const toPath = path.join(rootDir, mapping.to);
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
if (fs.existsSync(fromPath)) {
|
|
28
|
+
console.log(`📁 Renombrando ${mapping.from} -> ${mapping.to}`);
|
|
29
|
+
fs.moveSync(fromPath, toPath);
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
console.log('✅ Carpetas preparadas para publicación');
|
|
34
|
+
console.log('✅ Carpetas preparadas para publicación');
|
package/bin/restore-folders.js
CHANGED
|
@@ -2,26 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { restoreRenames } = require('./folder-mappings');
|
|
5
6
|
|
|
6
7
|
const rootDir = path.dirname(__dirname);
|
|
7
8
|
|
|
8
|
-
// Restaurar nombres originales de las carpetas
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{ from: 'vscode', to: '.vscode' },
|
|
12
|
-
{ from: 'github', to: '.github' }
|
|
13
|
-
];
|
|
9
|
+
// Restaurar nombres originales de las carpetas dev tras `npm publish`. Cuáles se
|
|
10
|
+
// restauran lo decide bin/folder-mappings.js (compartido con install.js y prepare-publish.js).
|
|
11
|
+
const folderMappings = restoreRenames();
|
|
14
12
|
|
|
15
13
|
console.log('🔄 Restaurando nombres originales de carpetas...');
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
if (folderMappings.length === 0) {
|
|
16
|
+
console.log('ℹ️ Nada que restaurar — prepare-publish.js no renombró nada porque el repo');
|
|
17
|
+
console.log(' mantiene mirrors flat junto a los dotted/underscored. Ver bin/folder-mappings.js.');
|
|
18
|
+
} else {
|
|
19
|
+
for (const mapping of folderMappings) {
|
|
20
|
+
const fromPath = path.join(rootDir, mapping.from);
|
|
21
|
+
const toPath = path.join(rootDir, mapping.to);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
if (fs.existsSync(fromPath)) {
|
|
24
|
+
console.log(`📁 Restaurando ${mapping.from} -> ${mapping.to}`);
|
|
25
|
+
fs.moveSync(fromPath, toPath);
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
console.log('✅ Carpetas restauradas');
|
|
30
|
+
console.log('✅ Carpetas restauradas');
|
|
@@ -20,6 +20,7 @@ NO modifiques ningún archivo dentro de `.claude/agent-memory/` — es de SOLO L
|
|
|
20
20
|
- Sé directo, funcional y breve.
|
|
21
21
|
- Si encuentras issues que puedes auto-corregir, corrígelos directamente sin pedir confirmación.
|
|
22
22
|
- Valida compliance contra los estándares cargados: folder structure correcta, naming conventions, uso de DateTimeOffset (no DateTime), UUID PKs, FluentValidation, patrones DDD, etc.
|
|
23
|
+
- **Git — NO auto-gitflow:** Cuando este agente es invocado desde `sa-quick-dev`, está **prohibido** crear ramas, hacer commits, push, pull requests, worktrees ni cualquier operación git automática. Todos los cambios (incluidas las auto-correcciones) se aplican directamente sobre la rama activa del repositorio al momento de la invocación. El versionamiento es responsabilidad exclusiva del usuario.
|
|
23
24
|
|
|
24
25
|
## Ejecución
|
|
25
26
|
|
|
@@ -20,6 +20,7 @@ NO modifiques ningún archivo dentro de `.claude/agent-memory/` — es de SOLO L
|
|
|
20
20
|
- Sé directo, funcional y breve.
|
|
21
21
|
- Implementa SOLO lo que los acceptance criteria y las tareas de la historia requieren. Nada más.
|
|
22
22
|
- Todo código DEBE seguir los estándares cargados: folder structure, naming conventions, tech stack, patterns (DDD entities, value objects, CQRS, FluentValidation, DateTimeOffset, UUID PKs, etc.).
|
|
23
|
+
- **Git — NO auto-gitflow:** Cuando este agente es invocado desde `sa-quick-dev`, está **prohibido** crear ramas, hacer commits, push, pull requests, worktrees ni cualquier operación git automática. Todos los cambios se aplican directamente sobre la rama activa del repositorio al momento de la invocación. El versionamiento es responsabilidad exclusiva del usuario.
|
|
23
24
|
|
|
24
25
|
## Ejecución
|
|
25
26
|
|
|
@@ -99,6 +99,64 @@ Modo: epic-level
|
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
+
## Reglas generales del pipeline
|
|
103
|
+
|
|
104
|
+
### Regla 1 — Observabilidad obligatoria en cada sub-agente
|
|
105
|
+
|
|
106
|
+
Cada sub-agente que ejecuta un workflow BMAD (`sa-create-story`, `sa-dev-story`, `sa-code-review`) **DEBE honrar las instrucciones de observabilidad** definidas en su respectivo `workflow_ext.md`. Estas instrucciones emiten eventos de telemetría (`sa-emit.js`) a GCP en momentos específicos del flujo y son la fuente de todos los reportes de métricas del equipo.
|
|
107
|
+
|
|
108
|
+
**Workflows instrumentados y eventos que se deben emitir:**
|
|
109
|
+
|
|
110
|
+
| Sub-agente | `workflow_ext.md` | Eventos emitidos |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `sa-create-story` | `_siesa-agents/bmm/workflows/4-implementation/create-story/workflow_ext.md` | `workflow.started`, `status.changed` (backlog → ready-for-dev), `workflow.finished` |
|
|
113
|
+
| `sa-dev-story` | `_siesa-agents/bmm/workflows/4-implementation/dev-story/workflow_ext.md` | `workflow.started`, `status.changed` (in-progress → review), `workflow.finished` |
|
|
114
|
+
| `sa-code-review` | `_siesa-agents/bmm/workflows/4-implementation/code-review/workflow_ext.md` | `workflow.started`, `fix.started`/`fix.finished` (cuando el usuario acepta auto_fix / action_items / show_details), `status.changed` (review → done o → in-progress), `workflow.finished` |
|
|
115
|
+
|
|
116
|
+
**Prohibido a cualquier sub-agente bajo este orquestador:**
|
|
117
|
+
|
|
118
|
+
- ❌ Saltarse, comentar, condicionar u omitir cualquier comando `node ... sa-emit.js ...` del `workflow_ext.md` correspondiente.
|
|
119
|
+
- ❌ Diferir la emisión al "final del pipeline" o agruparla en batch. Los eventos son **lifecycle markers** y solo tienen sentido en el instante exacto que el `workflow_ext.md` indica (`workflow.finished` mide `duration_ms` desde el `workflow.started` correspondiente; si lo retrasas, la métrica queda corrompida).
|
|
120
|
+
|
|
121
|
+
**Manejo de errores de `sa-emit.js`:**
|
|
122
|
+
|
|
123
|
+
Si la llamada a `sa-emit.js` falla (gateway caído, credenciales mal configuradas, etc.), el sub-agente debe:
|
|
124
|
+
|
|
125
|
+
1. Registrar el fallo como advertencia **no-bloqueante**.
|
|
126
|
+
2. **Continuar el workflow normalmente** (regla universal documentada en cada `workflow_ext.md`: *"observability must never block the workflow"*).
|
|
127
|
+
3. NUNCA reemplazar la llamada por su omisión deliberada — el evento queda buffereado localmente en `~/.claude/observability/buffer/events.jsonl` y se reenvía automáticamente cuando el transporte se recupera.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Reglas generales del pipeline
|
|
132
|
+
|
|
133
|
+
### Regla 1 — Observabilidad obligatoria en cada sub-agente
|
|
134
|
+
|
|
135
|
+
Cada sub-agente que ejecuta un workflow BMAD (`sa-create-story`, `sa-dev-story`, `sa-code-review`) **DEBE honrar las instrucciones de observabilidad** definidas en su respectivo `workflow_ext.md`. Estas instrucciones emiten eventos de telemetría (`sa-emit.js`) a GCP en momentos específicos del flujo y son la fuente de todos los reportes de métricas del equipo.
|
|
136
|
+
|
|
137
|
+
**Workflows instrumentados y eventos que se deben emitir:**
|
|
138
|
+
|
|
139
|
+
| Sub-agente | `workflow_ext.md` | Eventos emitidos |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `sa-create-story` | `_siesa-agents/bmm/workflows/4-implementation/create-story/workflow_ext.md` | `workflow.started`, `status.changed` (backlog → ready-for-dev), `workflow.finished` |
|
|
142
|
+
| `sa-dev-story` | `_siesa-agents/bmm/workflows/4-implementation/dev-story/workflow_ext.md` | `workflow.started`, `status.changed` (in-progress → review), `workflow.finished` |
|
|
143
|
+
| `sa-code-review` | `_siesa-agents/bmm/workflows/4-implementation/code-review/workflow_ext.md` | `workflow.started`, `fix.started`/`fix.finished` (cuando el usuario acepta auto_fix / action_items / show_details), `status.changed` (review → done o → in-progress), `workflow.finished` |
|
|
144
|
+
|
|
145
|
+
**Prohibido a cualquier sub-agente bajo este orquestador:**
|
|
146
|
+
|
|
147
|
+
- ❌ Saltarse, comentar, condicionar u omitir cualquier comando `node ... sa-emit.js ...` del `workflow_ext.md` correspondiente.
|
|
148
|
+
- ❌ Diferir la emisión al "final del pipeline" o agruparla en batch. Los eventos son **lifecycle markers** y solo tienen sentido en el instante exacto que el `workflow_ext.md` indica (`workflow.finished` mide `duration_ms` desde el `workflow.started` correspondiente; si lo retrasas, la métrica queda corrompida).
|
|
149
|
+
|
|
150
|
+
**Manejo de errores de `sa-emit.js`:**
|
|
151
|
+
|
|
152
|
+
Si la llamada a `sa-emit.js` falla (gateway caído, credenciales mal configuradas, etc.), el sub-agente debe:
|
|
153
|
+
|
|
154
|
+
1. Registrar el fallo como advertencia **no-bloqueante**.
|
|
155
|
+
2. **Continuar el workflow normalmente** (regla universal documentada en cada `workflow_ext.md`: *"observability must never block the workflow"*).
|
|
156
|
+
3. NUNCA reemplazar la llamada por su omisión deliberada — el evento queda buffereado localmente en `~/.claude/observability/buffer/events.jsonl` y se reenvía automáticamente cuando el transporte se recupera.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
102
160
|
## PASO 1 — Loop de procesamiento por historia
|
|
103
161
|
|
|
104
162
|
Para CADA historia pendiente de la épica seleccionada, ejecuta secuencialmente los sub-agentes dedicados. Cada historia completa su ciclo completo antes de pasar a la siguiente.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "siesa-agents",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.84",
|
|
4
4
|
"description": "Paquete para instalar y configurar agentes SIESA en tu proyecto",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"github/**/*",
|
|
27
27
|
"claude/**/*",
|
|
28
28
|
"gemini/**/*",
|
|
29
|
-
"kiro/**/*",
|
|
30
29
|
"bin/**/*",
|
|
31
30
|
"resources/**/*",
|
|
32
31
|
"README.md",
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
"node": ">=14.0.0"
|
|
37
36
|
},
|
|
38
37
|
"dependencies": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
38
|
+
"@clack/prompts": "^0.11.0",
|
|
39
|
+
"fs-extra": "^11.1.1"
|
|
41
40
|
}
|
|
42
41
|
}
|
|
@@ -147,3 +147,19 @@ node "{project_root}/_siesa-agents/observability/scripts/sa-emit.js" --event wor
|
|
|
147
147
|
|
|
148
148
|
Before marking a story as complete, verify the Siesa-specific checklist items at:
|
|
149
149
|
`@_siesa-agents/bmm/workflows/4-implementation/dev-story/checklist_ext.md`
|
|
150
|
+
|
|
151
|
+
# MANDATORY RULE: SONARQUBE CODE QUALITY CONSIDERATIONS
|
|
152
|
+
|
|
153
|
+
**TRIGGER:** Every time the dev-story workflow implements or modifies code.
|
|
154
|
+
|
|
155
|
+
**CRITICAL INSTRUCTION:** BEFORE writing or modifying any code, YOU MUST **USE YOUR READ TOOL** to open and read the FULL contents of the SonarQube rules file at:
|
|
156
|
+
`_siesa-agents/resources/sonar/sonar-rules.md`
|
|
157
|
+
|
|
158
|
+
> NOTE ON THE MIRROR COPY: This reference path is ALWAYS `_siesa-agents/resources/sonar/sonar-rules.md`,
|
|
159
|
+
|
|
160
|
+
## 8. Apply Sonar rules throughout implementation
|
|
161
|
+
|
|
162
|
+
1. Assimilate the rules in `sonar-rules.md` (security, reliability, maintainability findings based on real SonarQube results) and keep them in memory during the entire implementation.
|
|
163
|
+
2. You MUST enforce these rules in every piece of code you write or modify — never produce code that introduces a known SonarQube violation (e.g., hardcoded secrets, unhandled exceptions, code smells covered by the file).
|
|
164
|
+
3. BEFORE marking the story as review/complete, validate your changes against the final checklist defined in `sonar-rules.md`.
|
|
165
|
+
4. If the file does not exist for the current project, acknowledge it and proceed normally (Sonar enforcement is best-effort and must never block the workflow).
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Sonar Rules — Reglas transversales de calidad y seguridad
|
|
2
|
+
|
|
3
|
+
> Reglas obligatorias para cualquier agente de IA (Claude Code, Cursor, Copilot, etc.) que genere o modifique código en cualquier proyecto SIESA.
|
|
4
|
+
> Basado en hallazgos reales de SonarQube y prácticas de seguridad para .NET / C#.
|
|
5
|
+
>
|
|
6
|
+
> **Stack y versiones:** la fuente de verdad es `_siesa-agents/resources/architecture/` (.NET 10, PostgreSQL 18+, EF Core 10, xUnit, etc.). NO uses frameworks, motores de BD ni versiones distintas a los definidos allí.
|
|
7
|
+
>
|
|
8
|
+
> **Antes de generar código, lee este archivo completo.**
|
|
9
|
+
> **Antes de hacer commit, valida el checklist final.**
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. Reglas críticas de seguridad (NUNCA violar)
|
|
14
|
+
|
|
15
|
+
### 1.1 Secretos y credenciales
|
|
16
|
+
|
|
17
|
+
**Prohibido absolutamente:**
|
|
18
|
+
- Hardcodear passwords, API keys, connection strings con credenciales, encryption keys, tokens, certificados.
|
|
19
|
+
- Subir `appsettings.json` o cualquier archivo con valores sensibles reales.
|
|
20
|
+
- Dejar credenciales en comentarios, ejemplos o tests.
|
|
21
|
+
|
|
22
|
+
**Cómo hacerlo bien:**
|
|
23
|
+
|
|
24
|
+
```jsonc
|
|
25
|
+
// appsettings.json — SOLO placeholders o valores no sensibles
|
|
26
|
+
{
|
|
27
|
+
"DATABASE_PROVIDER": "postgres",
|
|
28
|
+
"DATABASE_URL": "", // ← se inyecta en runtime
|
|
29
|
+
"ENCRYPTION_KEY": "", // ← se inyecta en runtime
|
|
30
|
+
"API_PORT": 3005
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Variables de entorno (producción / staging) — PostgreSQL / Npgsql
|
|
36
|
+
export DATABASE_URL="Host=...;Port=5432;Database=...;Username=...;Password=...;"
|
|
37
|
+
export ENCRYPTION_KEY="..."
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# User Secrets (desarrollo local)
|
|
42
|
+
dotnet user-secrets init
|
|
43
|
+
dotnet user-secrets set "DATABASE_URL" "Host=localhost;Port=5432;Database=...;Username=...;Password=...;"
|
|
44
|
+
dotnet user-secrets set "ENCRYPTION_KEY" "..."
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**En producción:** Azure Key Vault, AWS Secrets Manager, o GCP Secret Manager (si el proyecto ya usa GCP, **preferir Secret Manager de GCP**).
|
|
48
|
+
|
|
49
|
+
**Si encuentras un secreto hardcodeado:** no solo lo muevas — **márcalo como comprometido** y avisa al humano para rotarlo. Una vez en Git, asume que está filtrado.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### 1.2 Regex siempre con timeout
|
|
54
|
+
|
|
55
|
+
Toda expresión regular **debe** especificar `matchTimeout` para prevenir ReDoS (Regular Expression Denial of Service).
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
// ❌ MAL — vulnerable a ReDoS
|
|
59
|
+
private static readonly Regex IdFormatoPattern =
|
|
60
|
+
new(@"^[A-Za-z0-9_]{1,40}$", RegexOptions.Compiled);
|
|
61
|
+
|
|
62
|
+
// ✅ BIEN — con timeout explícito
|
|
63
|
+
private static readonly Regex IdFormatoPattern =
|
|
64
|
+
new(@"^[A-Za-z0-9_]{1,40}$", RegexOptions.Compiled, TimeSpan.FromSeconds(2));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Aplica a:**
|
|
68
|
+
- `Regex` estáticos `readonly`
|
|
69
|
+
- `Regex` instanciados ad-hoc
|
|
70
|
+
- `Regex.IsMatch()`, `Regex.Match()`, `Regex.Replace()` estáticos → preferir instancias con timeout
|
|
71
|
+
|
|
72
|
+
**Timeout recomendado:** entre `100ms` (validaciones rápidas) y `2s` (parsing complejo). Nunca `Timeout.InfiniteTimeSpan`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### 1.3 HTTPS y configuración CORS
|
|
77
|
+
|
|
78
|
+
**HTTP solo en `localhost` o `127.0.0.1` para desarrollo.** Cualquier otro origen → HTTPS.
|
|
79
|
+
|
|
80
|
+
```csharp
|
|
81
|
+
// ❌ MAL — http://app-dev.example.com:3000 es un dominio público con HTTP
|
|
82
|
+
var defaultOrigins = new[]
|
|
83
|
+
{
|
|
84
|
+
"http://localhost:5173", // ok, es localhost
|
|
85
|
+
"http://app-dev.example.com:3000" // ← NO, debe ser https
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ✅ BIEN
|
|
89
|
+
var defaultOrigins = new[]
|
|
90
|
+
{
|
|
91
|
+
"http://localhost:5173",
|
|
92
|
+
"https://app-dev.example.com:3000"
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**CORS — reglas:**
|
|
97
|
+
- Nunca `AllowAnyOrigin()` en producción.
|
|
98
|
+
- Lista explícita de orígenes, idealmente leída de configuración.
|
|
99
|
+
- No combinar `AllowAnyOrigin()` con `AllowCredentials()` (es invalido y peligroso).
|
|
100
|
+
- `AllowAnyHeader()` y `AllowAnyMethod()` aceptables solo si el conjunto de orígenes está restringido.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### 1.4 Configuración del archivo `appsettings.json`
|
|
105
|
+
|
|
106
|
+
Solo valores **no sensibles**:
|
|
107
|
+
- ✅ Puertos, hostnames públicos, feature flags, niveles de log, nombres de bucket
|
|
108
|
+
- ❌ Passwords, connection strings con credenciales, encryption keys, JWT secrets, API tokens
|
|
109
|
+
|
|
110
|
+
Si un valor sensible necesita un default visible, usa cadena vacía `""` o un placeholder claro como `"__SET_IN_ENV__"`, **nunca un valor real ni de ejemplo creíble**.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 2. Reglas para contenedores (Docker)
|
|
115
|
+
|
|
116
|
+
### 2.1 Nunca correr como root
|
|
117
|
+
|
|
118
|
+
```dockerfile
|
|
119
|
+
# ❌ MAL — implícitamente root
|
|
120
|
+
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
|
121
|
+
COPY --from=build /app/publish .
|
|
122
|
+
ENTRYPOINT ["dotnet", "MyApp.API.dll"]
|
|
123
|
+
|
|
124
|
+
# ✅ BIEN — usuario no privilegiado
|
|
125
|
+
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
|
126
|
+
RUN groupadd -r app && useradd -r -g app -u 1001 app
|
|
127
|
+
WORKDIR /app
|
|
128
|
+
COPY --from=build --chown=app:app /app/publish .
|
|
129
|
+
USER app
|
|
130
|
+
ENTRYPOINT ["dotnet", "MyApp.API.dll"]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2.2 Otras reglas Docker
|
|
134
|
+
|
|
135
|
+
- Tags específicos en `FROM`, nunca `:latest`.
|
|
136
|
+
- Multi-stage build para no incluir SDK en la imagen final.
|
|
137
|
+
- `.dockerignore` actualizado (excluir `bin/`, `obj/`, `.git/`, `appsettings.Development.json`, secretos).
|
|
138
|
+
- `COPY` específico, evitar `COPY . .` sin `.dockerignore`.
|
|
139
|
+
- `HEALTHCHECK` definido.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 3. Reglas de código C# / .NET
|
|
144
|
+
|
|
145
|
+
### 3.1 Manejo de excepciones
|
|
146
|
+
|
|
147
|
+
```csharp
|
|
148
|
+
// ❌ MAL
|
|
149
|
+
try { ... } catch (Exception ex) { _logger.LogError(ex.Message); }
|
|
150
|
+
|
|
151
|
+
// ✅ BIEN — capturar específico, loguear con contexto, re-lanzar o convertir
|
|
152
|
+
try { ... }
|
|
153
|
+
catch (NpgsqlException ex)
|
|
154
|
+
{
|
|
155
|
+
_logger.LogError(ex, "Failed to query {Entity} with id {Id}", entityName, id);
|
|
156
|
+
throw;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Capturar excepciones específicas, no `Exception` genérico (salvo en handlers globales).
|
|
161
|
+
- Loguear con structured logging (`LogError(ex, "msg {Param}", value)`), no concatenar.
|
|
162
|
+
- No tragar excepciones silenciosamente.
|
|
163
|
+
- **No uses excepciones para flujos de negocio esperados** → usa el **Result Pattern** (`Result<T>`) definido en la arquitectura. Las excepciones son solo para errores inesperados / de infraestructura.
|
|
164
|
+
|
|
165
|
+
### 3.2 Async / await
|
|
166
|
+
|
|
167
|
+
- Usar `async`/`await` en cualquier I/O (BD, HTTP, archivos).
|
|
168
|
+
- `ConfigureAwait(false)` en bibliotecas reutilizables (no en endpoints ASP.NET Core).
|
|
169
|
+
- No mezclar `.Result` ni `.Wait()` con `async` (deadlocks).
|
|
170
|
+
- `CancellationToken` propagado desde el endpoint hasta el repositorio.
|
|
171
|
+
|
|
172
|
+
### 3.3 Nullability y disposable
|
|
173
|
+
|
|
174
|
+
- `<Nullable>enable</Nullable>` en el `.csproj`, respetar las anotaciones.
|
|
175
|
+
- `using` / `await using` para todo lo que implementa `IDisposable` / `IAsyncDisposable`.
|
|
176
|
+
- `sealed class` por defecto si no se necesita herencia.
|
|
177
|
+
|
|
178
|
+
### 3.4 Code smells frecuentes
|
|
179
|
+
|
|
180
|
+
- Métodos > 50 líneas → refactorizar.
|
|
181
|
+
- Complejidad ciclomática > 10 → separar.
|
|
182
|
+
- Magic numbers / strings → constantes o enums.
|
|
183
|
+
- TODO / FIXME sin issue asociado → prohibido. Si va, debe incluir referencia (`TODO(JIRA-123): ...`).
|
|
184
|
+
- Código comentado → eliminar, no comentar (para eso está Git).
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 4. Testing — cobertura ≥ 80% en código nuevo
|
|
189
|
+
|
|
190
|
+
Toda funcionalidad nueva debe incluir tests **en el mismo PR**. No "lo agrego después". **TDD es obligatorio**: escribe los tests antes del código de producción.
|
|
191
|
+
|
|
192
|
+
**Mínimo esperado por tipo:**
|
|
193
|
+
|
|
194
|
+
| Tipo de código | Tests requeridos |
|
|
195
|
+
|---|---|
|
|
196
|
+
| Validators (FluentValidation) | Happy path + cada regla de validación |
|
|
197
|
+
| Endpoints (Minimal API) | 200 OK, 400 validación, 401/403 si aplica, 404, 500 mockeado |
|
|
198
|
+
| Handlers (MediatR / similar) | Happy path + cada rama de negocio + manejo de excepciones |
|
|
199
|
+
| Servicios con dependencias externas | Mock de la dependencia, verificar contrato |
|
|
200
|
+
| Regex / parsers | Casos válidos + casos inválidos + casos borde (vacío, max length) |
|
|
201
|
+
|
|
202
|
+
Frameworks de testing (definidos en la arquitectura corporativa, NO usar otros):
|
|
203
|
+
- **Unit tests:** xUnit + EF Core InMemory.
|
|
204
|
+
- **Integration tests:** xUnit + Testcontainers.PostgreSql (base de datos real en contenedor).
|
|
205
|
+
|
|
206
|
+
Si necesitas introducir un framework adicional, primero valida contra `_siesa-agents/resources/architecture/` y justifícalo.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 5. Reglas específicas para agentes de IA
|
|
211
|
+
|
|
212
|
+
### 5.1 Antes de generar código
|
|
213
|
+
|
|
214
|
+
1. Lee este archivo completo.
|
|
215
|
+
2. Revisa las reglas relevantes al tipo de cambio que vas a hacer.
|
|
216
|
+
3. Si vas a tocar configuración, secretos, Regex, Docker o CORS → relee la sección correspondiente.
|
|
217
|
+
|
|
218
|
+
### 5.2 Durante la generación
|
|
219
|
+
|
|
220
|
+
- **Si necesitas un secreto:** genera el código que lo lee de configuración / env y **explícale al humano** dónde debe definirlo. Nunca pongas un valor real ni "de ejemplo".
|
|
221
|
+
- **Si generas Regex:** incluye `TimeSpan.FromSeconds(N)` siempre.
|
|
222
|
+
- **Si generas Dockerfile:** incluye `USER` no-root.
|
|
223
|
+
- **Si generas CORS:** lee orígenes de configuración.
|
|
224
|
+
- **Si generas un endpoint o handler:** genera también su test en el mismo turno.
|
|
225
|
+
|
|
226
|
+
### 5.3 Antes de declarar el cambio terminado
|
|
227
|
+
|
|
228
|
+
Ejecuta este self-check mentalmente:
|
|
229
|
+
|
|
230
|
+
- [ ] ¿Hay algún string que parezca una credencial real?
|
|
231
|
+
- [ ] ¿Todas las `Regex` tienen `TimeSpan` como tercer/cuarto argumento?
|
|
232
|
+
- [ ] ¿Algún `catch (Exception ex)` sin re-lanzar ni contexto?
|
|
233
|
+
- [ ] ¿Tests nuevos para todo el código nuevo?
|
|
234
|
+
- [ ] ¿`appsettings.json` modificado contiene solo valores no sensibles?
|
|
235
|
+
- [ ] ¿Si toqué Dockerfile, sigue corriendo con `USER` no-root?
|
|
236
|
+
- [ ] ¿Si toqué CORS, los orígenes son `https://` excepto `localhost`?
|
|
237
|
+
|
|
238
|
+
Si alguna respuesta es "no" o "no sé", **vuelve atrás y corrige antes de entregar**.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 6. Checklist final antes de commit
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
[ ] dotnet build sin warnings nuevos
|
|
246
|
+
[ ] dotnet test pasa al 100%
|
|
247
|
+
[ ] Cobertura de líneas nuevas ≥ 80%
|
|
248
|
+
[ ] Sin secretos en el diff (revisar con: git diff --staged | grep -iE 'password|secret|key|token')
|
|
249
|
+
[ ] sonar-scanner local sin issues nuevos de severidad >= Major
|
|
250
|
+
[ ] Pre-commit hooks ejecutados (gitleaks, dotnet format)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 7. Cuando SonarQube falla un Quality Gate
|
|
256
|
+
|
|
257
|
+
Si una corrida marca este archivo como insuficiente (apareció un patrón nuevo no cubierto aquí):
|
|
258
|
+
|
|
259
|
+
1. **No solo arregles el hallazgo** — agrega la regla a este archivo.
|
|
260
|
+
2. Si es una categoría nueva, abrir sección con ejemplo `❌ MAL` / `✅ BIEN`.
|
|
261
|
+
3. Commit del fix + commit del update de reglas en el mismo PR.
|
|
262
|
+
|
|
263
|
+
Así el siguiente código generado por IA no repetirá el patrón.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
**Origen:** consolidado a partir de hallazgos reales de análisis de SonarQube en proyectos SIESA (security hotspots, cobertura insuficiente, credenciales hardcodeadas). Mantener vivo según la sección 7.
|
package/vscode/settings.json
CHANGED