agentic-kdd 3.5.6 → 3.5.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "3.5.6",
3
+ "version": "3.5.8",
4
4
  "description": "Autonomous development pipeline — aa: · ag: · audit: · AST graph · Harness · Specs · Impact analysis · Decision trail · Metrics · MCP server. Works with Cursor and Claude Code.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/onboard.js ADDED
@@ -0,0 +1,312 @@
1
+ 'use strict';
2
+ /**
3
+ * Agentic KDD — Onboard v1.0
4
+ * Analiza un proyecto existente (brownfield), mapea el stack,
5
+ * pre-popula la memoria con lo que encuentra, y propone la primera tarea.
6
+ *
7
+ * Uso: akdd onboard
8
+ */
9
+
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const chalk = require('chalk');
13
+ const ora = require('ora');
14
+
15
+ async function onboard() {
16
+ const projectPath = process.cwd();
17
+ const agentic = path.join(projectPath, '.agentic');
18
+
19
+ console.log('\n' + chalk.bold.blue(' Agentic KDD') + chalk.gray(' — onboarding brownfield project\n'));
20
+
21
+ // ── Check agentic is installed ────────────────────────────────────────────
22
+ if (!fs.existsSync(path.join(agentic, 'config.md'))) {
23
+ console.log(chalk.yellow(' Agentic KDD not installed. Run: akdd init\n'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const spinner = ora({ text: 'Scanning project...', color: 'blue' }).start();
28
+
29
+ const report = {
30
+ stack: detectStack(projectPath),
31
+ modules: detectModules(projectPath),
32
+ tests: detectTests(projectPath),
33
+ patterns: detectPatterns(projectPath),
34
+ size: countFiles(projectPath),
35
+ suggested: null,
36
+ };
37
+
38
+ spinner.text = 'Analyzing architecture...';
39
+ await sleep(300);
40
+
41
+ report.suggested = suggestFirstTask(report);
42
+
43
+ spinner.succeed(chalk.green('Project analyzed!'));
44
+
45
+ // ── Print report ──────────────────────────────────────────────────────────
46
+ console.log('\n' + chalk.bold(' 📊 Project snapshot:'));
47
+ console.log(chalk.gray(` Stack: ${report.stack.join(' · ')}`));
48
+ console.log(chalk.gray(` Size: ${report.size.source} source files, ${report.size.tests} test files`));
49
+ console.log(chalk.gray(` Modules: ${report.modules.length > 0 ? report.modules.join(', ') : 'none detected'}`));
50
+ console.log(chalk.gray(` Tests: ${report.tests.framework || 'not configured'}`));
51
+
52
+ // ── Write to config.md ────────────────────────────────────────────────────
53
+ const configPath = path.join(agentic, 'config.md');
54
+ if (fs.existsSync(configPath)) {
55
+ let config = fs.readFileSync(configPath, 'utf8');
56
+
57
+ // Update stack info if blank
58
+ if (config.includes('Tipo: EXISTENTE') || config.includes('Tipo: —')) {
59
+ config = config.replace(/^Tipo:.+$/m, 'Tipo: EXISTENTE (brownfield — onboarded)');
60
+ }
61
+
62
+ // Update test command if not set
63
+ if (report.tests.command && config.match(/^\s*test:\s*(—|$)/m)) {
64
+ config = config.replace(/^(\s*test:).+$/m, `$1 ${report.tests.command}`);
65
+ console.log(chalk.green(`\n ✓ Test command set: ${report.tests.command}`));
66
+ }
67
+
68
+ fs.writeFileSync(configPath, config);
69
+ }
70
+
71
+ // ── Write patterns to memoria ─────────────────────────────────────────────
72
+ if (report.patterns.length > 0) {
73
+ const patronesPath = path.join(agentic, 'memoria', 'patrones.md');
74
+ const existing = fs.existsSync(patronesPath) ? fs.readFileSync(patronesPath, 'utf8') : '';
75
+
76
+ const newPatterns = report.patterns
77
+ .filter(p => !existing.includes(p.title))
78
+ .map(p => `\n### ${p.title}\n**confianza**: MEDIA\n**módulo**: ${p.module}\n**regla**: ${p.rule}\n**detectado por**: akdd onboard\n`)
79
+ .join('');
80
+
81
+ if (newPatterns) {
82
+ fs.appendFileSync(patronesPath, newPatterns);
83
+ console.log(chalk.green(` ✓ ${report.patterns.filter(p => !existing.includes(p.title)).length} patterns pre-populated in memoria`));
84
+ }
85
+ }
86
+
87
+ // ── Suggest first task ────────────────────────────────────────────────────
88
+ if (report.suggested) {
89
+ console.log('\n' + chalk.bold(' 💡 Suggested first task:'));
90
+ console.log(chalk.white(`\n ${report.suggested}`));
91
+ console.log(chalk.gray('\n Copy and paste as-is, or modify to fit your needs.\n'));
92
+ }
93
+ }
94
+
95
+ // ── Stack detection ───────────────────────────────────────────────────────────
96
+
97
+ function detectStack(root) {
98
+ const stack = [];
99
+
100
+ const pkg = safeReadJSON(path.join(root, 'package.json'));
101
+ if (pkg) {
102
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
103
+ if (deps['next']) stack.push('Next.js');
104
+ else if (deps['react']) stack.push('React');
105
+ else if (deps['express']) stack.push('Express');
106
+ else if (deps['fastify']) stack.push('Fastify');
107
+ else if (deps['nestjs']) stack.push('NestJS');
108
+ if (deps['typescript']) stack.push('TypeScript');
109
+ if (deps['prisma']) stack.push('Prisma');
110
+ if (deps['@supabase/supabase-js']) stack.push('Supabase');
111
+ }
112
+
113
+ if (fs.existsSync(path.join(root, 'requirements.txt')) ||
114
+ fs.existsSync(path.join(root, 'backend', 'requirements.txt'))) {
115
+ stack.push('Python');
116
+ const req = safeRead(path.join(root, 'backend', 'requirements.txt')) ||
117
+ safeRead(path.join(root, 'requirements.txt')) || '';
118
+ if (req.includes('fastapi')) stack.push('FastAPI');
119
+ if (req.includes('django')) stack.push('Django');
120
+ if (req.includes('sqlalchemy')) stack.push('SQLAlchemy');
121
+ }
122
+
123
+ if (fs.existsSync(path.join(root, 'composer.json'))) stack.push('PHP/Laravel');
124
+
125
+ if (stack.length === 0) stack.push('Unknown');
126
+ return stack;
127
+ }
128
+
129
+ // ── Module detection ──────────────────────────────────────────────────────────
130
+
131
+ function detectModules(root) {
132
+ const modules = [];
133
+ const searchDirs = ['src', 'app', 'lib', 'backend/app', 'backend/src'];
134
+
135
+ for (const dir of searchDirs) {
136
+ const full = path.join(root, dir);
137
+ if (!fs.existsSync(full)) continue;
138
+ try {
139
+ for (const entry of fs.readdirSync(full, { withFileTypes: true })) {
140
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
141
+ modules.push(entry.name);
142
+ }
143
+ }
144
+ } catch {}
145
+ }
146
+
147
+ return [...new Set(modules)].slice(0, 12);
148
+ }
149
+
150
+ // ── Test detection ────────────────────────────────────────────────────────────
151
+
152
+ function detectTests(root) {
153
+ const pkg = safeReadJSON(path.join(root, 'package.json'));
154
+ let framework = null;
155
+ let command = null;
156
+
157
+ if (pkg) {
158
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
159
+ if (deps['vitest']) { framework = 'Vitest'; command = 'npm test'; }
160
+ if (deps['jest']) { framework = 'Jest'; command = 'npm test'; }
161
+ if (pkg.scripts?.test && !pkg.scripts.test.includes('echo')) {
162
+ command = 'npm test';
163
+ }
164
+ }
165
+
166
+ const hasPytest = fs.existsSync(path.join(root, 'backend', 'requirements.txt')) &&
167
+ (safeRead(path.join(root, 'backend', 'requirements.txt')) || '').includes('pytest');
168
+ if (hasPytest) {
169
+ framework = 'pytest';
170
+ command = 'cd backend && py -3.13 -m pytest -x -v';
171
+ }
172
+
173
+ const testFiles = countTestFiles(root);
174
+ return { framework, command, count: testFiles };
175
+ }
176
+
177
+ function countTestFiles(root) {
178
+ let count = 0;
179
+ const patterns = [/\.(test|spec)\.(ts|tsx|js|jsx)$/, /test_.*\.py$/, /.*_test\.py$/];
180
+ function walk(dir, depth = 0) {
181
+ if (depth > 4) return;
182
+ try {
183
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
184
+ if (['node_modules', '.git', '__pycache__', '.next'].includes(e.name)) continue;
185
+ const full = path.join(dir, e.name);
186
+ if (e.isDirectory()) walk(full, depth + 1);
187
+ else if (patterns.some(p => p.test(e.name))) count++;
188
+ }
189
+ } catch {}
190
+ }
191
+ walk(root);
192
+ return count;
193
+ }
194
+
195
+ // ── Pattern detection ─────────────────────────────────────────────────────────
196
+
197
+ function detectPatterns(root) {
198
+ const patterns = [];
199
+ const sourceFiles = [];
200
+
201
+ function walk(dir, depth = 0) {
202
+ if (depth > 4) return;
203
+ try {
204
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
205
+ if (['node_modules', '.git', '__pycache__', '.next', 'dist'].includes(e.name)) continue;
206
+ const full = path.join(dir, e.name);
207
+ if (e.isDirectory()) walk(full, depth + 1);
208
+ else if (/\.(ts|tsx|js|py)$/.test(e.name)) sourceFiles.push(full);
209
+ }
210
+ } catch {}
211
+ }
212
+ walk(root);
213
+
214
+ const sampleFiles = sourceFiles.slice(0, 30);
215
+
216
+ let hasMultiTenant = false;
217
+ let hasSoftDelete = false;
218
+ let hasJWT = false;
219
+ let hasPrisma = false;
220
+
221
+ for (const f of sampleFiles) {
222
+ const c = safeRead(f) || '';
223
+ if (c.includes('tenant_id') || c.includes('agency_id') || c.includes('organization_id')) hasMultiTenant = true;
224
+ if (c.includes('is_active') || c.includes('deleted_at') || c.includes('soft_delete')) hasSoftDelete = true;
225
+ if (c.includes('jwt') || c.includes('access_token') || c.includes('Bearer')) hasJWT = true;
226
+ if (c.includes('prisma') || c.includes('PrismaClient')) hasPrisma = true;
227
+ }
228
+
229
+ if (hasMultiTenant) patterns.push({
230
+ title: 'Multi-tenancy: filtrar siempre por tenant_id en queries',
231
+ module: 'global',
232
+ rule: 'Cada query sobre datos de usuario DEBE incluir filtro por tenant_id/agency_id — nunca cross-tenant',
233
+ });
234
+
235
+ if (hasSoftDelete) patterns.push({
236
+ title: 'Soft delete: usar is_active=false o deleted_at en vez de DELETE',
237
+ module: 'global',
238
+ rule: 'No ejecutar DELETE hard en tablas de usuario — usar soft delete para preservar integridad referencial',
239
+ });
240
+
241
+ if (hasJWT) patterns.push({
242
+ title: 'Auth JWT: validar token antes de procesar cualquier request autenticado',
243
+ module: 'auth',
244
+ rule: 'Toda ruta protegida DEBE validar el JWT y extraer el subject antes de acceder a datos',
245
+ });
246
+
247
+ if (hasPrisma) patterns.push({
248
+ title: 'Prisma: incluir relaciones explícitamente para evitar N+1',
249
+ module: 'database',
250
+ rule: 'Usar include:{} en queries que necesiten relaciones — nunca hacer queries en loop',
251
+ });
252
+
253
+ return patterns;
254
+ }
255
+
256
+ // ── First task suggestion ─────────────────────────────────────────────────────
257
+
258
+ function suggestFirstTask(report) {
259
+ const { modules, tests, stack, size } = report;
260
+
261
+ // No tests → suggest adding tests to a detected module
262
+ if (tests.count === 0 && modules.length > 0) {
263
+ const firstModule = modules[0];
264
+ return `aa: agrega tests básicos al módulo "${firstModule}" — cubre CRUD y casos edge. No toques lógica existente, solo agrega tests.`;
265
+ }
266
+
267
+ // Has tests but no contracts → suggest running a cycle to seed contracts
268
+ if (tests.count > 0) {
269
+ return `aa: revisa el módulo más crítico del proyecto y refactoriza cualquier código que no tenga tests. Objetivo: cobertura mínima en el módulo más importante.`;
270
+ }
271
+
272
+ // Default
273
+ return `aa: analiza el estado actual del proyecto y genera un resumen de módulos implementados, tests existentes y pendientes más importantes. Solo análisis, sin cambios.`;
274
+ }
275
+
276
+ // ── Helpers ───────────────────────────────────────────────────────────────────
277
+
278
+ function countFiles(root) {
279
+ let source = 0;
280
+ let tests = 0;
281
+ const sourceExt = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.php']);
282
+ const testPattern= /\.(test|spec)\.(ts|tsx|js|jsx)$|test_.*\.py$|.*_test\.py$/;
283
+
284
+ function walk(dir, depth = 0) {
285
+ if (depth > 5) return;
286
+ try {
287
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
288
+ if (['node_modules', '.git', '__pycache__', '.next', 'dist'].includes(e.name)) continue;
289
+ const full = path.join(dir, e.name);
290
+ if (e.isDirectory()) walk(full, depth + 1);
291
+ else {
292
+ if (testPattern.test(e.name)) tests++;
293
+ else if (sourceExt.has(path.extname(e.name))) source++;
294
+ }
295
+ }
296
+ } catch {}
297
+ }
298
+ walk(root);
299
+ return { source, tests };
300
+ }
301
+
302
+ function safeRead(filePath) {
303
+ try { return require('fs').readFileSync(filePath, 'utf8'); } catch { return null; }
304
+ }
305
+
306
+ function safeReadJSON(filePath) {
307
+ try { return JSON.parse(require('fs').readFileSync(filePath, 'utf8')); } catch { return null; }
308
+ }
309
+
310
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
311
+
312
+ module.exports = { onboard };
package/src/update.js CHANGED
@@ -20,6 +20,10 @@ async function update() {
20
20
  process.exit(1);
21
21
  }
22
22
 
23
+ // ── PASO 0: Guardar estado del usuario ANTES de tocar nada ───────────────
24
+ const configPath = path.join(projectPath, '.agentic', 'config.md');
25
+ const userState = preserveUserState(projectPath, configPath);
26
+
23
27
  const spinner = ora({ text: 'Downloading latest version from GitHub...', color: 'blue' }).start();
24
28
 
25
29
  try {
@@ -36,75 +40,78 @@ async function update() {
36
40
 
37
41
  spinner.text = 'Updating system files (keeping your memory intact)...';
38
42
 
39
- // ── LO QUE SE ACTUALIZA (sistema) ────────────────────────────────────────
40
-
41
- // 1. Agentes — siempre sobreescribir
43
+ // ── 1. Agentes ──────────────────────────────────────────────────────────
42
44
  const agentsSrc = path.join(TEMP_DIR, '.agentic', 'agentes');
43
45
  const agentsDest = path.join(projectPath, '.agentic', 'agentes');
44
46
  if (fs.existsSync(agentsSrc)) {
45
47
  fs.copySync(agentsSrc, agentsDest, { overwrite: true });
46
48
  }
47
49
 
48
- // 2. Grafo — crítico: grafo.cjs + schema.sql + watch-errors.cjs
49
- const grafoSrc = path.join(TEMP_DIR, '.agentic', 'grafo');
50
+ // ── 2. Grafo ────────────────────────────────────────────────────────────
51
+ const grafoSrc = path.join(TEMP_DIR, '.agentic', 'grafo');
50
52
  const grafoDest = path.join(projectPath, '.agentic', 'grafo');
51
53
  if (fs.existsSync(grafoSrc)) {
52
54
  fs.copySync(grafoSrc, grafoDest, { overwrite: true });
53
55
  }
54
56
 
55
- // 3. Dashboard
57
+ // ── 3. Dashboard ────────────────────────────────────────────────────────
56
58
  const dashSrc = path.join(TEMP_DIR, 'dashboard.cjs');
57
59
  const dashDest = path.join(projectPath, 'dashboard.cjs');
58
60
  if (fs.existsSync(dashSrc)) {
59
61
  fs.copySync(dashSrc, dashDest, { overwrite: true });
60
62
  }
61
63
 
62
- // 4. Audit department
63
- const auditSrc = path.join(TEMP_DIR, '.audit');
64
+ // ── 4. Audit ────────────────────────────────────────────────────────────
65
+ const auditSrc = path.join(TEMP_DIR, '.audit');
64
66
  const auditDest = path.join(projectPath, '.audit');
65
67
  if (fs.existsSync(auditSrc)) {
66
68
  fs.copySync(auditSrc, auditDest, { overwrite: true });
67
69
  }
68
70
 
69
- // 5. CLAUDE.md, _LOCKS.md, cursorrules
70
- const filesToUpdate = ['CLAUDE.md', '_LOCKS.md'];
71
- for (const file of filesToUpdate) {
72
- const src = path.join(TEMP_DIR, file);
71
+ // ── 5. CLAUDE.md + cursor rules ─────────────────────────────────────────
72
+ for (const file of ['CLAUDE.md', '_LOCKS.md']) {
73
+ const src = path.join(TEMP_DIR, file);
73
74
  const dest = path.join(projectPath, file);
74
75
  if (fs.existsSync(src)) fs.copySync(src, dest, { overwrite: true });
75
76
  }
76
77
 
77
78
  const cursorSrc = path.join(TEMP_DIR, '.cursor');
78
79
  const cursorDest = path.join(projectPath, '.cursor');
79
- if (fs.existsSync(cursorSrc)) {
80
- fs.copySync(cursorSrc, cursorDest, { overwrite: true });
81
- }
80
+ if (fs.existsSync(cursorSrc)) fs.copySync(cursorSrc, cursorDest, { overwrite: true });
82
81
 
83
82
  const cursorrulesSrc = path.join(TEMP_DIR, '.cursorrules');
84
83
  const cursorrulesDest = path.join(projectPath, '.cursorrules');
85
- if (fs.existsSync(cursorrulesSrc)) {
86
- fs.copySync(cursorrulesSrc, cursorrulesDest, { overwrite: true });
87
- }
88
-
89
- // ── LO QUE NO SE TOCA (memoria del usuario) ──────────────────────────────
90
- // .agentic/memoria/ → errores.md, patrones.md, decisiones.md
91
- // .agentic/config.md → configuración del proyecto
92
- // .agentic/conocimiento/ → documentación del proyecto
93
- // .agentic/specs/ → specs generadas
94
- // .agentic/PLAN.md → plan activo
95
- // memoria.db → grafo SQLite (datos del usuario)
84
+ if (fs.existsSync(cursorrulesSrc)) fs.copySync(cursorrulesSrc, cursorrulesDest, { overwrite: true });
96
85
 
97
- // Limpiar temp
86
+ // ── Limpiar temp ────────────────────────────────────────────────────────
98
87
  fs.removeSync(TEMP_DIR);
99
88
 
100
- // Reconstruir better-sqlite3 si es necesario
101
- spinner.text = 'Checking dependencies...';
89
+ // ── PASO 1: Restaurar estado del usuario en config.md ──────────────────
90
+ // Garantiza que CONFIGURADO, nombre, stack y test command nunca se pierden
91
+ restoreUserState(configPath, userState);
92
+
93
+ // ── PASO 2: Migrar schema de memoria.db ────────────────────────────────
94
+ spinner.text = 'Migrating knowledge graph schema...';
102
95
  try {
103
- require('child_process').execSync('npm rebuild better-sqlite3', {
104
- stdio: 'pipe', cwd: projectPath
96
+ execSync(`node "${path.join(grafoDest, 'grafo.cjs')}" migrate`, {
97
+ stdio: 'pipe', cwd: projectPath, timeout: 15000
105
98
  });
99
+ } catch(e) { /* schema migration is best-effort */ }
100
+
101
+ // ── PASO 3: Reconstruir better-sqlite3 si es necesario ─────────────────
102
+ spinner.text = 'Checking dependencies...';
103
+ try {
104
+ execSync('npm rebuild better-sqlite3', { stdio: 'pipe', cwd: projectPath });
106
105
  } catch(e) {}
107
106
 
107
+ // ── PASO 4: Auto-sync para que el dashboard lea los datos actualizados ──
108
+ spinner.text = 'Syncing knowledge graph...';
109
+ try {
110
+ execSync(`node "${path.join(grafoDest, 'grafo.cjs')}" sync`, {
111
+ stdio: 'pipe', cwd: projectPath, timeout: 30000
112
+ });
113
+ } catch(e) { /* sync is best-effort */ }
114
+
108
115
  spinner.succeed(chalk.green('Updated successfully!'));
109
116
 
110
117
  console.log('\n' + chalk.bold(' What was updated:'));
@@ -119,11 +126,16 @@ async function update() {
119
126
  console.log(chalk.gray(' ✓ Your project config (.agentic/config.md)'));
120
127
  console.log(chalk.gray(' ✓ Your knowledge base (.agentic/conocimiento/)'));
121
128
  console.log(chalk.gray(' ✓ Your PLAN.md'));
122
- console.log(chalk.gray(' ✓ Your knowledge graph data (memoria.db)\n'));
129
+ console.log(chalk.gray(' ✓ Your knowledge graph data (memoria.db)'));
130
+ console.log(chalk.gray(' ✓ Your CONFIGURADO state and project settings\n'));
123
131
 
124
- console.log(chalk.dim(' Tip: run akdd sync to update the graph with latest memory.\n'));
132
+ if (userState.configured) {
133
+ console.log(chalk.green(' ✓ Project state verified: CONFIGURADO\n'));
134
+ }
125
135
 
126
136
  } catch (err) {
137
+ // Si algo falla, restaurar estado igual
138
+ try { restoreUserState(configPath, userState); } catch(e) {}
127
139
  spinner.fail(chalk.red('Update failed'));
128
140
  console.error(chalk.red('\n Error: ' + err.message));
129
141
  console.log(chalk.gray(' Check your internet connection and try again.\n'));
@@ -131,4 +143,113 @@ async function update() {
131
143
  }
132
144
  }
133
145
 
146
+ // ── preserveUserState ────────────────────────────────────────────────────────
147
+ // Lee el estado actual del usuario antes del update para restaurarlo después
148
+
149
+ function preserveUserState(projectPath, configPath) {
150
+ const state = {
151
+ configured: false,
152
+ name: null,
153
+ description: null,
154
+ stack: null,
155
+ testCommand: null,
156
+ rawSections: {}, // Secciones del usuario que no son del sistema
157
+ };
158
+
159
+ if (!fs.existsSync(configPath)) return state;
160
+
161
+ try {
162
+ const config = fs.readFileSync(configPath, 'utf8');
163
+
164
+ // CONFIGURADO
165
+ state.configured = /^CONFIGURADO:\s*SI/m.test(config);
166
+
167
+ // Nombre del proyecto
168
+ const nameMatch = config.match(/^Nombre:\s*(.+)$/m);
169
+ if (nameMatch) state.name = nameMatch[1].trim();
170
+
171
+ // Descripción
172
+ const descMatch = config.match(/^Descripción:\s*([\s\S]+?)(?=\n##|\n[A-Z])/m);
173
+ if (descMatch) state.description = descMatch[1].trim();
174
+
175
+ // Stack completo (bloque ## Stack hasta el siguiente ##)
176
+ const stackMatch = config.match(/^## Stack\n([\s\S]+?)(?=\n##|$)/m);
177
+ if (stackMatch) state.stack = stackMatch[1].trim();
178
+
179
+ // Test command
180
+ const testMatch = config.match(/^\s*test:\s*(.+)$/m);
181
+ if (testMatch && testMatch[1].trim() !== '—') {
182
+ state.testCommand = testMatch[1].trim();
183
+ }
184
+
185
+ // Secciones de módulos y reglas del proyecto (todo lo que va después de ## Reglas)
186
+ const userSections = config.match(/^## (Reglas del proyecto|Módulos|Archivos compartidos|Sinónimos)([\s\S]+?)(?=\n##|$)/gm) || [];
187
+ for (const section of userSections) {
188
+ const titleMatch = section.match(/^## (.+)/);
189
+ if (titleMatch) state.rawSections[titleMatch[1]] = section;
190
+ }
191
+
192
+ } catch(e) { /* best-effort */ }
193
+
194
+ return state;
195
+ }
196
+
197
+ // ── restoreUserState ─────────────────────────────────────────────────────────
198
+ // Restaura el estado del usuario en config.md después del update
199
+
200
+ function restoreUserState(configPath, state) {
201
+ if (!fs.existsSync(configPath)) return;
202
+
203
+ try {
204
+ let config = fs.readFileSync(configPath, 'utf8');
205
+ let changed = false;
206
+
207
+ // Restaurar CONFIGURADO: SI
208
+ if (state.configured && /^CONFIGURADO:\s*NO/m.test(config)) {
209
+ config = config.replace(/^CONFIGURADO:\s*NO/m, 'CONFIGURADO: SI');
210
+ changed = true;
211
+ }
212
+
213
+ // Restaurar nombre del proyecto
214
+ if (state.name) {
215
+ const currentName = config.match(/^Nombre:\s*(.+)$/m)?.[1]?.trim();
216
+ if (!currentName || currentName === '—' || currentName === '') {
217
+ config = config.replace(/^Nombre:\s*.*$/m, `Nombre: ${state.name}`);
218
+ changed = true;
219
+ }
220
+ }
221
+
222
+ // Restaurar test command
223
+ if (state.testCommand) {
224
+ const currentTest = config.match(/^\s*test:\s*(.+)$/m)?.[1]?.trim();
225
+ if (!currentTest || currentTest === '—') {
226
+ config = config.replace(/^(\s*test:)\s*.*$/m, `$1 ${state.testCommand}`);
227
+ changed = true;
228
+ }
229
+ }
230
+
231
+ // Restaurar stack si se perdió
232
+ if (state.stack) {
233
+ const hasStack = config.includes('## Stack') && !config.match(/^## Stack\s*\n—/m);
234
+ if (!hasStack) {
235
+ config = config.replace(/^## Stack[\s\S]*?(?=\n##)/m, `## Stack\n${state.stack}\n`);
236
+ changed = true;
237
+ }
238
+ }
239
+
240
+ // Restaurar secciones de usuario
241
+ for (const [title, section] of Object.entries(state.rawSections)) {
242
+ if (!config.includes(`## ${title}`)) {
243
+ config += `\n${section}\n`;
244
+ changed = true;
245
+ }
246
+ }
247
+
248
+ if (changed) {
249
+ fs.writeFileSync(configPath, config, 'utf8');
250
+ }
251
+
252
+ } catch(e) { /* best-effort */ }
253
+ }
254
+
134
255
  module.exports = { update };
package/tdd-gate.cjs CHANGED
@@ -234,6 +234,10 @@ function findTestFiles(projectRoot, scope = []) {
234
234
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
235
235
  /__(tests?)__\//,
236
236
  /test\/.*\.(ts|js)$/,
237
+ // Python
238
+ /test_.*\.py$/,
239
+ /.*_test\.py$/,
240
+ /tests\/.*\.py$/,
237
241
  ];
238
242
 
239
243
  const results = [];