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/CLAUDE.md +80 -55
- package/README.md +120 -35
- package/akdd-analyze.cjs +319 -0
- package/bin/akdd.js +8 -0
- package/lock-manager.cjs +545 -0
- package/mem-curator.cjs +290 -513
- package/package.json +1 -1
- package/src/onboard.js +312 -0
- package/src/update.js +154 -33
- package/tdd-gate.cjs +4 -0
- package/templates/.agentic/DESIGN_SYSTEM.fastapi.md +109 -0
- package/templates/.agentic/DESIGN_SYSTEM.nextjs.md +91 -0
- package/templates/.agentic/grafo/akdd-analyze.cjs +319 -0
- package/templates/.agentic/grafo/grafo.cjs +696 -2
- package/templates/.agentic/grafo/lock-manager.cjs +545 -0
- package/templates/.agentic/grafo/mem-curator.cjs +361 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-kdd",
|
|
3
|
-
"version": "3.5.
|
|
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
|
-
// ──
|
|
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
|
|
49
|
-
const grafoSrc
|
|
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
|
|
63
|
-
const auditSrc
|
|
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
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
-
//
|
|
101
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 = [];
|