@codexa/cli 8.6.15 → 9.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/knowledge.ts +51 -0
- package/commands/patterns.ts +410 -28
- package/commands/task.ts +64 -13
- package/commands/utils.ts +244 -0
- package/gates/typecheck-validator.ts +174 -0
- package/gates/validator.ts +88 -0
- package/package.json +1 -1
- package/protocol/subagent-protocol.ts +31 -4
- package/workflow.ts +62 -3
package/commands/task.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { initSchema, getPatternsForFiles, getPatternsByScope, addSessionSummary,
|
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
4
|
import { parseSubagentReturn, formatValidationErrors } from "../protocol/subagent-protocol";
|
|
5
5
|
import { processSubagentReturn, formatProcessResult } from "../protocol/process-return";
|
|
6
|
-
import { getContextForSubagent } from "./utils";
|
|
6
|
+
import { getContextForSubagent, getMinimalContextForSubagent } from "./utils";
|
|
7
7
|
import { getUnreadKnowledgeForTask } from "./knowledge";
|
|
8
8
|
|
|
9
9
|
export function taskNext(json: boolean = false): void {
|
|
@@ -101,7 +101,7 @@ export function taskNext(json: boolean = false): void {
|
|
|
101
101
|
console.log(`Use: task start <id> ou task start <id1>,<id2>,...\n`);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function taskStart(ids: string, json: boolean = false): void {
|
|
104
|
+
export function taskStart(ids: string, json: boolean = false, fullContext: boolean = false): void {
|
|
105
105
|
initSchema();
|
|
106
106
|
enforceGate("task-start");
|
|
107
107
|
|
|
@@ -144,10 +144,33 @@ export function taskStart(ids: string, json: boolean = false): void {
|
|
|
144
144
|
spec.id,
|
|
145
145
|
]);
|
|
146
146
|
|
|
147
|
+
// v9.0: Mostrar resumo de knowledge relevante antes de iniciar
|
|
148
|
+
const allKnowledge = db.query(
|
|
149
|
+
`SELECT * FROM knowledge
|
|
150
|
+
WHERE spec_id = ? AND severity IN ('critical', 'warning')
|
|
151
|
+
ORDER BY severity DESC, created_at DESC
|
|
152
|
+
LIMIT 10`
|
|
153
|
+
).all(spec.id) as any[];
|
|
154
|
+
|
|
155
|
+
if (allKnowledge.length > 0 && !json) {
|
|
156
|
+
console.log(`\n[i] Knowledge ativo (${allKnowledge.length}):`);
|
|
157
|
+
for (const k of allKnowledge.slice(0, 5)) {
|
|
158
|
+
const icon = k.severity === 'critical' ? '[X]' : '[!]';
|
|
159
|
+
console.log(` ${icon} ${k.content}`);
|
|
160
|
+
}
|
|
161
|
+
if (allKnowledge.length > 5) {
|
|
162
|
+
console.log(` ... +${allKnowledge.length - 5} mais`);
|
|
163
|
+
}
|
|
164
|
+
console.log();
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
if (json) {
|
|
148
168
|
// NOVO: Incluir contexto COMPLETO para cada task
|
|
149
169
|
const contexts = startedTasks.map((task) => {
|
|
150
|
-
|
|
170
|
+
// v9.0: Usar contexto minimo por padrao, expandido via --full-context
|
|
171
|
+
const contextText = fullContext
|
|
172
|
+
? getContextForSubagent(task.id)
|
|
173
|
+
: getMinimalContextForSubagent(task.id);
|
|
151
174
|
const unreadKnowledge = getUnreadKnowledgeForTask(spec.id, task.id);
|
|
152
175
|
|
|
153
176
|
// NOVO v7.4: Buscar implementation patterns relevantes
|
|
@@ -196,8 +219,9 @@ export function taskStart(ids: string, json: boolean = false): void {
|
|
|
196
219
|
files: taskFiles,
|
|
197
220
|
// AVISO PARA O ORQUESTRADOR - NAO EXECUTE, DELEGUE
|
|
198
221
|
_orchestratorWarning: "NAO execute esta task diretamente. Use Task tool com subagent_type='general-purpose' para delegar. O campo 'subagentContext' abaixo e o prompt para o SUBAGENT.",
|
|
199
|
-
// Contexto
|
|
222
|
+
// Contexto para o subagent (NAO para o orquestrador)
|
|
200
223
|
context: contextText,
|
|
224
|
+
contextMode: fullContext ? "full" : "minimal",
|
|
201
225
|
// Knowledge nao lido (broadcast de outras tasks)
|
|
202
226
|
unreadKnowledge: unreadKnowledge.map((k: any) => ({
|
|
203
227
|
id: k.id,
|
|
@@ -237,6 +261,13 @@ CHECKLIST OBRIGATORIO (verifique ANTES de retornar):
|
|
|
237
261
|
- [ ] Os arquivos que vou listar em files_created EXISTEM no disco?
|
|
238
262
|
|
|
239
263
|
Se nao marcou todos os items, PARE e corrija AGORA.
|
|
264
|
+
|
|
265
|
+
CONTEXTO ON-DEMAND: Se precisar de mais contexto alem do fornecido, execute:
|
|
266
|
+
codexa context detail standards # Regras do projeto
|
|
267
|
+
codexa context detail decisions # Decisoes tomadas
|
|
268
|
+
codexa context detail patterns # Patterns de codigo
|
|
269
|
+
codexa context detail knowledge # Discoveries de outras tasks
|
|
270
|
+
NAO execute todos - apenas o que for NECESSARIO para sua task.
|
|
240
271
|
`.trim(),
|
|
241
272
|
// Instrucoes de retorno para o SUBAGENT
|
|
242
273
|
subagentReturnProtocol: `
|
|
@@ -246,19 +277,20 @@ FORMATO DE RETORNO (apos criar os arquivos):
|
|
|
246
277
|
"summary": "Resumo do que foi feito (10-500 chars)",
|
|
247
278
|
"files_created": ["path/arquivo.ts"],
|
|
248
279
|
"files_modified": ["path/outro.ts"],
|
|
249
|
-
"patterns_discovered": ["Pattern identificado"],
|
|
250
|
-
"decisions_made": [{"title": "...", "decision": "..."}],
|
|
251
|
-
"blockers": ["Se status != completed"],
|
|
252
|
-
"knowledge_to_broadcast": [{"category": "discovery|pattern|constraint", "content": "...", "severity": "info|warning|critical"}],
|
|
253
280
|
"reasoning": {
|
|
254
|
-
"approach": "Como voce abordou o problema
|
|
281
|
+
"approach": "OBRIGATORIO (min 20 chars): Como voce abordou o problema e POR QUE tomou essas decisoes",
|
|
255
282
|
"challenges": ["Desafios encontrados"],
|
|
256
283
|
"recommendations": "Sugestoes para proximas tasks"
|
|
257
|
-
}
|
|
284
|
+
},
|
|
285
|
+
"patterns_discovered": ["Pattern identificado"],
|
|
286
|
+
"decisions_made": [{"title": "...", "decision": "..."}],
|
|
287
|
+
"blockers": ["Se status != completed"],
|
|
288
|
+
"knowledge_to_broadcast": [{"category": "discovery|pattern|constraint", "content": "...", "severity": "info|warning|critical"}]
|
|
258
289
|
}
|
|
259
290
|
|
|
260
|
-
|
|
261
|
-
|
|
291
|
+
ATENCAO: O campo "reasoning.approach" e OBRIGATORIO para status "completed".
|
|
292
|
+
Se retornar sem ele, Gate 4.4 BLOQUEIA a finalizacao da task.
|
|
293
|
+
Descreva COMO abordou o problema, nao apenas O QUE fez.
|
|
262
294
|
|
|
263
295
|
Se NAO conseguir criar arquivos (sem permissao, sem ferramentas), retorne:
|
|
264
296
|
{
|
|
@@ -401,17 +433,36 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
|
|
|
401
433
|
: [];
|
|
402
434
|
}
|
|
403
435
|
|
|
404
|
-
// Validar gates incluindo files-exist
|
|
436
|
+
// Validar gates incluindo files-exist, standards-follow e reasoning (v9.0)
|
|
405
437
|
enforceGate("task-done", {
|
|
406
438
|
taskId,
|
|
407
439
|
checkpoint,
|
|
408
440
|
files: expectedFiles,
|
|
409
441
|
force: options.force,
|
|
410
442
|
forceReason: options.forceReason,
|
|
443
|
+
subagentData: subagentData,
|
|
411
444
|
});
|
|
412
445
|
|
|
413
446
|
// Nota: Validacao de standards agora e feita pelo Gate 4.2 (standards-follow) em enforceGate
|
|
414
447
|
|
|
448
|
+
// v9.0: Atualizar patterns incrementalmente com arquivos da task
|
|
449
|
+
try {
|
|
450
|
+
if (subagentData && subagentData.status === "completed") {
|
|
451
|
+
const allFiles = [
|
|
452
|
+
...(subagentData.files_created || []),
|
|
453
|
+
...(subagentData.files_modified || []),
|
|
454
|
+
];
|
|
455
|
+
const codeFiles = allFiles.filter((f: string) => /\.(ts|tsx|js|jsx)$/.test(f));
|
|
456
|
+
|
|
457
|
+
if (codeFiles.length > 0) {
|
|
458
|
+
const { updatePatternsIncremental } = require("./patterns");
|
|
459
|
+
updatePatternsIncremental(codeFiles, task.number);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} catch {
|
|
463
|
+
// Nao-critico: nao falhar task done por atualizacao de patterns
|
|
464
|
+
}
|
|
465
|
+
|
|
415
466
|
// Marcar como done
|
|
416
467
|
db.run(
|
|
417
468
|
"UPDATE tasks SET status = 'done', checkpoint = ?, completed_at = ? WHERE id = ?",
|
package/commands/utils.ts
CHANGED
|
@@ -345,6 +345,102 @@ export function contextExport(options: { task?: string; json?: boolean; }): void
|
|
|
345
345
|
console.log(JSON.stringify(exportData, null, 2));
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
// v9.0: Contexto minimo para subagent (max ~2KB)
|
|
349
|
+
const MAX_MINIMAL_CONTEXT = 2048;
|
|
350
|
+
|
|
351
|
+
export function getMinimalContextForSubagent(taskId: number): string {
|
|
352
|
+
initSchema();
|
|
353
|
+
const db = getDb();
|
|
354
|
+
|
|
355
|
+
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
|
|
356
|
+
if (!task) return "ERRO: Task nao encontrada";
|
|
357
|
+
|
|
358
|
+
const spec = db.query("SELECT * FROM specs WHERE id = ?").get(task.spec_id) as any;
|
|
359
|
+
const context = db.query("SELECT * FROM context WHERE spec_id = ?").get(task.spec_id) as any;
|
|
360
|
+
|
|
361
|
+
const taskFiles = task.files ? JSON.parse(task.files) : [];
|
|
362
|
+
const domain = task.agent?.split("-")[0] || "all";
|
|
363
|
+
|
|
364
|
+
// 1. Standards REQUIRED apenas (nao recommended)
|
|
365
|
+
const requiredStandards = db.query(
|
|
366
|
+
`SELECT rule FROM standards
|
|
367
|
+
WHERE enforcement = 'required' AND (scope = 'all' OR scope = ?)
|
|
368
|
+
LIMIT 10`
|
|
369
|
+
).all(domain) as any[];
|
|
370
|
+
|
|
371
|
+
// 2. Blockers CRITICAL apenas
|
|
372
|
+
const criticalBlockers = db.query(
|
|
373
|
+
`SELECT content FROM knowledge
|
|
374
|
+
WHERE spec_id = ? AND severity = 'critical'
|
|
375
|
+
ORDER BY created_at DESC LIMIT 5`
|
|
376
|
+
).all(task.spec_id) as any[];
|
|
377
|
+
|
|
378
|
+
// 3. Contradicoes (se existir)
|
|
379
|
+
let contradictions: any[] = [];
|
|
380
|
+
try {
|
|
381
|
+
contradictions = findContradictions(task.spec_id);
|
|
382
|
+
} catch { /* ignore */ }
|
|
383
|
+
|
|
384
|
+
// 4. Decisoes da task anterior (dependency direta)
|
|
385
|
+
const dependsOn = task.depends_on ? JSON.parse(task.depends_on) : [];
|
|
386
|
+
let depDecisions: any[] = [];
|
|
387
|
+
if (dependsOn.length > 0) {
|
|
388
|
+
const placeholders = dependsOn.map(() => '?').join(',');
|
|
389
|
+
const depTasks = db.query(
|
|
390
|
+
`SELECT id FROM tasks WHERE spec_id = ? AND number IN (${placeholders})`
|
|
391
|
+
).all(task.spec_id, ...dependsOn) as any[];
|
|
392
|
+
|
|
393
|
+
for (const dt of depTasks) {
|
|
394
|
+
const reasoning = db.query(
|
|
395
|
+
`SELECT thought FROM reasoning_log
|
|
396
|
+
WHERE spec_id = ? AND task_id = ? AND category = 'recommendation'
|
|
397
|
+
ORDER BY created_at DESC LIMIT 2`
|
|
398
|
+
).all(task.spec_id, dt.id) as any[];
|
|
399
|
+
depDecisions.push(...reasoning);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Montar output minimo
|
|
404
|
+
let output = `## CONTEXTO MINIMO (Task #${task.number})
|
|
405
|
+
|
|
406
|
+
**Feature:** ${spec.name}
|
|
407
|
+
**Objetivo:** ${context?.objective || "N/A"}
|
|
408
|
+
**Arquivos:** ${taskFiles.join(", ") || "Nenhum especificado"}
|
|
409
|
+
`;
|
|
410
|
+
|
|
411
|
+
if (requiredStandards.length > 0) {
|
|
412
|
+
output += `\n**Standards (OBRIGATORIOS):**\n${requiredStandards.map((s: any) => `- ${s.rule}`).join("\n")}\n`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (criticalBlockers.length > 0) {
|
|
416
|
+
output += `\n**BLOCKERS CRITICOS:**\n${criticalBlockers.map((b: any) => `[X] ${b.content}`).join("\n")}\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (contradictions.length > 0) {
|
|
420
|
+
output += `\n**CONTRADICOES:**\n${contradictions.map((c: any) => `[!] "${c.decision1}" <-> "${c.decision2}"`).join("\n")}\n`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (depDecisions.length > 0) {
|
|
424
|
+
output += `\n**Recomendacoes de tasks anteriores:**\n${depDecisions.map((d: any) => `- ${d.thought}`).join("\n")}\n`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
output += `
|
|
428
|
+
**Contexto expandido**: Se precisar de mais contexto, execute:
|
|
429
|
+
codexa context detail standards # Todos os standards
|
|
430
|
+
codexa context detail decisions # Todas as decisoes
|
|
431
|
+
codexa context detail patterns # Patterns do projeto
|
|
432
|
+
codexa context detail knowledge # Knowledge completo
|
|
433
|
+
codexa context detail architecture # Analise arquitetural
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
// Truncar se exceder limite
|
|
437
|
+
if (output.length > MAX_MINIMAL_CONTEXT) {
|
|
438
|
+
output = output.substring(0, MAX_MINIMAL_CONTEXT - 100) + "\n\n[CONTEXTO TRUNCADO - use: codexa context detail <secao>]\n";
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return output;
|
|
442
|
+
}
|
|
443
|
+
|
|
348
444
|
// Funcao helper para formatar contexto para subagent (uso interno)
|
|
349
445
|
// v8.1: Contexto COMPLETO mas ESTRUTURADO - todas as fontes relevantes incluidas
|
|
350
446
|
export function getContextForSubagent(taskId: number): string {
|
|
@@ -873,6 +969,154 @@ function filterRelevantStandards(standards: any[], taskFiles: string[]): any[] {
|
|
|
873
969
|
});
|
|
874
970
|
}
|
|
875
971
|
|
|
972
|
+
export function contextDetail(section: string, json: boolean = false): void {
|
|
973
|
+
initSchema();
|
|
974
|
+
const db = getDb();
|
|
975
|
+
|
|
976
|
+
const spec = db
|
|
977
|
+
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
978
|
+
.get() as any;
|
|
979
|
+
|
|
980
|
+
if (!spec) {
|
|
981
|
+
console.error("\nNenhuma feature ativa.\n");
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Encontrar task running (para contexto relevante)
|
|
986
|
+
const runningTask = db.query(
|
|
987
|
+
"SELECT * FROM tasks WHERE spec_id = ? AND status = 'running' LIMIT 1"
|
|
988
|
+
).get(spec.id) as any;
|
|
989
|
+
|
|
990
|
+
const domain = runningTask?.agent?.split("-")[0] || "all";
|
|
991
|
+
|
|
992
|
+
let output = "";
|
|
993
|
+
|
|
994
|
+
switch (section) {
|
|
995
|
+
case "standards": {
|
|
996
|
+
const standards = db.query(
|
|
997
|
+
"SELECT * FROM standards WHERE scope = 'all' OR scope = ? ORDER BY enforcement DESC, category"
|
|
998
|
+
).all(domain) as any[];
|
|
999
|
+
|
|
1000
|
+
if (json) { console.log(JSON.stringify({ standards })); return; }
|
|
1001
|
+
|
|
1002
|
+
output = `Standards (${standards.length}):\n`;
|
|
1003
|
+
for (const s of standards) {
|
|
1004
|
+
output += `\n[${s.enforcement.toUpperCase()}] ${s.category}: ${s.rule}`;
|
|
1005
|
+
if (s.examples) {
|
|
1006
|
+
try {
|
|
1007
|
+
const examples = JSON.parse(s.examples);
|
|
1008
|
+
if (examples.length > 0) output += `\n Exemplos: ${examples.slice(0, 2).join(", ")}`;
|
|
1009
|
+
} catch { /* ignore */ }
|
|
1010
|
+
}
|
|
1011
|
+
if (s.anti_examples) {
|
|
1012
|
+
try {
|
|
1013
|
+
const anti = JSON.parse(s.anti_examples);
|
|
1014
|
+
if (anti.length > 0) output += `\n Anti-exemplos: ${anti.slice(0, 2).join(", ")}`;
|
|
1015
|
+
} catch { /* ignore */ }
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
case "decisions": {
|
|
1022
|
+
const decisions = db.query(
|
|
1023
|
+
"SELECT * FROM decisions WHERE spec_id = ? AND status = 'active' ORDER BY created_at DESC"
|
|
1024
|
+
).all(spec.id) as any[];
|
|
1025
|
+
|
|
1026
|
+
if (json) { console.log(JSON.stringify({ decisions })); return; }
|
|
1027
|
+
|
|
1028
|
+
output = `Decisoes ativas (${decisions.length}):\n`;
|
|
1029
|
+
for (const d of decisions) {
|
|
1030
|
+
output += `\n[${d.id}] ${d.title}`;
|
|
1031
|
+
output += `\n Decisao: ${d.decision}`;
|
|
1032
|
+
if (d.rationale) output += `\n Racional: ${d.rationale}`;
|
|
1033
|
+
output += `\n Task: #${d.task_ref} | ${d.created_at}`;
|
|
1034
|
+
}
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
case "patterns": {
|
|
1039
|
+
const patterns = db.query(
|
|
1040
|
+
"SELECT * FROM implementation_patterns ORDER BY category, name"
|
|
1041
|
+
).all() as any[];
|
|
1042
|
+
|
|
1043
|
+
if (json) { console.log(JSON.stringify({ patterns })); return; }
|
|
1044
|
+
|
|
1045
|
+
output = `Patterns (${patterns.length}):\n`;
|
|
1046
|
+
for (const p of patterns) {
|
|
1047
|
+
output += `\n[${p.category}] ${p.name} (${p.scope})`;
|
|
1048
|
+
output += `\n Applies to: ${p.applies_to}`;
|
|
1049
|
+
output += `\n Confidence: ${(p.confidence * 100).toFixed(0)}%`;
|
|
1050
|
+
if (p.template) {
|
|
1051
|
+
const preview = p.template.length > 300 ? p.template.substring(0, 300) + "..." : p.template;
|
|
1052
|
+
output += `\n Template:\n${preview}`;
|
|
1053
|
+
}
|
|
1054
|
+
const anti = p.anti_patterns ? JSON.parse(p.anti_patterns) : [];
|
|
1055
|
+
if (anti.length > 0) {
|
|
1056
|
+
output += `\n Anti-patterns: ${anti.join("; ")}`;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
case "knowledge": {
|
|
1063
|
+
const knowledge = db.query(
|
|
1064
|
+
"SELECT k.*, t.number as task_number FROM knowledge k LEFT JOIN tasks t ON k.task_origin = t.id WHERE k.spec_id = ? ORDER BY k.severity DESC, k.created_at DESC LIMIT 50"
|
|
1065
|
+
).all(spec.id) as any[];
|
|
1066
|
+
|
|
1067
|
+
if (json) { console.log(JSON.stringify({ knowledge })); return; }
|
|
1068
|
+
|
|
1069
|
+
output = `Knowledge (${knowledge.length}):\n`;
|
|
1070
|
+
for (const k of knowledge) {
|
|
1071
|
+
const icon = k.severity === 'critical' ? '[X]' : k.severity === 'warning' ? '[!]' : '[i]';
|
|
1072
|
+
output += `\n${icon} #${k.id} (${k.category}) Task #${k.task_number || '?'}`;
|
|
1073
|
+
output += `\n ${k.content}`;
|
|
1074
|
+
}
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case "architecture": {
|
|
1079
|
+
const analysis = getArchitecturalAnalysisForSpec(spec.name, spec.id);
|
|
1080
|
+
if (!analysis) {
|
|
1081
|
+
output = "Nenhuma analise arquitetural encontrada.";
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (json) { console.log(JSON.stringify({ analysis })); return; }
|
|
1086
|
+
|
|
1087
|
+
output = `Analise Arquitetural: ${analysis.id}\n`;
|
|
1088
|
+
output += `Status: ${analysis.status}\n`;
|
|
1089
|
+
if (analysis.approach) output += `\nAbordagem:\n${analysis.approach}\n`;
|
|
1090
|
+
if (analysis.risks) {
|
|
1091
|
+
try {
|
|
1092
|
+
const risks = JSON.parse(analysis.risks);
|
|
1093
|
+
output += `\nRiscos (${risks.length}):\n`;
|
|
1094
|
+
for (const r of risks) {
|
|
1095
|
+
output += ` - ${r.description} (${r.probability}/${r.impact}) -> ${r.mitigation}\n`;
|
|
1096
|
+
}
|
|
1097
|
+
} catch { /* ignore */ }
|
|
1098
|
+
}
|
|
1099
|
+
if (analysis.decisions) {
|
|
1100
|
+
try {
|
|
1101
|
+
const decisions = JSON.parse(analysis.decisions);
|
|
1102
|
+
output += `\nDecisoes (${decisions.length}):\n`;
|
|
1103
|
+
for (const d of decisions) {
|
|
1104
|
+
output += ` - ${d.decision}: ${d.rationale || ''}\n`;
|
|
1105
|
+
}
|
|
1106
|
+
} catch { /* ignore */ }
|
|
1107
|
+
}
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
default:
|
|
1112
|
+
console.error(`\nSecao invalida: ${section}`);
|
|
1113
|
+
console.error("Secoes validas: standards, decisions, patterns, knowledge, architecture\n");
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
console.log(output || "\nNenhum dado encontrado.\n");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
876
1120
|
export function recover(options: {
|
|
877
1121
|
list?: boolean;
|
|
878
1122
|
snapshot?: string;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate 4.5: Typecheck Validator
|
|
3
|
+
*
|
|
4
|
+
* Executa verificacao de tipos nos arquivos criados/modificados pelo subagent.
|
|
5
|
+
* Requer tsconfig.json no projeto.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { spawnSync } from "child_process";
|
|
10
|
+
import { resolve, dirname } from "path";
|
|
11
|
+
|
|
12
|
+
export interface TypecheckResult {
|
|
13
|
+
passed: boolean;
|
|
14
|
+
errors: TypecheckError[];
|
|
15
|
+
skipped: boolean;
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TypecheckError {
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
column: number;
|
|
23
|
+
message: string;
|
|
24
|
+
code: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Encontra o tsconfig.json mais proximo subindo diretorios
|
|
29
|
+
*/
|
|
30
|
+
function findTsConfig(startDir: string): string | null {
|
|
31
|
+
let dir = startDir;
|
|
32
|
+
const root = resolve("/");
|
|
33
|
+
|
|
34
|
+
while (dir !== root) {
|
|
35
|
+
const candidate = resolve(dir, "tsconfig.json");
|
|
36
|
+
if (existsSync(candidate)) return candidate;
|
|
37
|
+
dir = dirname(dir);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detecta o runtime do projeto para escolher o comando tsc
|
|
44
|
+
*/
|
|
45
|
+
function detectTscCommand(): string[] {
|
|
46
|
+
const bunCheck = spawnSync("bun", ["--version"], { encoding: "utf-8", timeout: 3000 });
|
|
47
|
+
if (!bunCheck.error) return ["bunx", "tsc"];
|
|
48
|
+
|
|
49
|
+
const npxCheck = spawnSync("npx", ["--version"], { encoding: "utf-8", timeout: 3000 });
|
|
50
|
+
if (!npxCheck.error) return ["npx", "tsc"];
|
|
51
|
+
|
|
52
|
+
const tscCheck = spawnSync("tsc", ["--version"], { encoding: "utf-8", timeout: 3000 });
|
|
53
|
+
if (!tscCheck.error) return ["tsc"];
|
|
54
|
+
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parseia output do tsc em erros estruturados
|
|
60
|
+
*/
|
|
61
|
+
function parseTscOutput(output: string, relevantFiles: string[]): TypecheckError[] {
|
|
62
|
+
const errors: TypecheckError[] = [];
|
|
63
|
+
const lines = output.split(/\r?\n/);
|
|
64
|
+
|
|
65
|
+
// Formato: file(line,col): error TSxxxx: message
|
|
66
|
+
const errorRegex = /^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const match = line.match(errorRegex);
|
|
70
|
+
if (!match) continue;
|
|
71
|
+
|
|
72
|
+
const [, file, lineNum, col, code, message] = match;
|
|
73
|
+
const normalizedFile = file.replace(/\\/g, "/");
|
|
74
|
+
|
|
75
|
+
// Filtrar: apenas erros nos arquivos relevantes
|
|
76
|
+
const isRelevant = relevantFiles.some(rf => {
|
|
77
|
+
const normalizedRf = rf.replace(/\\/g, "/");
|
|
78
|
+
return (
|
|
79
|
+
normalizedFile.endsWith(normalizedRf) ||
|
|
80
|
+
normalizedFile.includes(normalizedRf) ||
|
|
81
|
+
normalizedRf.endsWith(normalizedFile) ||
|
|
82
|
+
normalizedRf.includes(normalizedFile)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (isRelevant) {
|
|
87
|
+
errors.push({
|
|
88
|
+
file: normalizedFile,
|
|
89
|
+
line: parseInt(lineNum),
|
|
90
|
+
column: parseInt(col),
|
|
91
|
+
message,
|
|
92
|
+
code,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return errors;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Executa typecheck nos arquivos especificados
|
|
102
|
+
*/
|
|
103
|
+
export function runTypecheck(files: string[]): TypecheckResult {
|
|
104
|
+
// Filtrar apenas arquivos TS/TSX
|
|
105
|
+
const tsFiles = files.filter(f => /\.(ts|tsx)$/.test(f) && existsSync(f));
|
|
106
|
+
|
|
107
|
+
if (tsFiles.length === 0) {
|
|
108
|
+
return { passed: true, errors: [], skipped: true, reason: "Nenhum arquivo TypeScript" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Encontrar tsconfig.json (primeiro no cwd, depois no diretorio dos arquivos)
|
|
112
|
+
let tsconfig = findTsConfig(process.cwd());
|
|
113
|
+
if (!tsconfig) {
|
|
114
|
+
const firstFileDir = dirname(resolve(tsFiles[0]));
|
|
115
|
+
tsconfig = findTsConfig(firstFileDir);
|
|
116
|
+
}
|
|
117
|
+
if (!tsconfig) {
|
|
118
|
+
return { passed: true, errors: [], skipped: true, reason: "tsconfig.json nao encontrado" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Detectar comando tsc
|
|
122
|
+
const tscCmd = detectTscCommand();
|
|
123
|
+
if (tscCmd.length === 0) {
|
|
124
|
+
return { passed: true, errors: [], skipped: true, reason: "tsc nao encontrado (instale typescript)" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Executar tsc --noEmit
|
|
128
|
+
const result = spawnSync(
|
|
129
|
+
tscCmd[0],
|
|
130
|
+
[...tscCmd.slice(1), "--noEmit", "--project", tsconfig],
|
|
131
|
+
{
|
|
132
|
+
encoding: "utf-8",
|
|
133
|
+
timeout: 60000,
|
|
134
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
135
|
+
cwd: dirname(tsconfig),
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (result.error) {
|
|
140
|
+
return { passed: true, errors: [], skipped: true, reason: `Erro ao executar tsc: ${result.error.message}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Parsear erros, filtrando apenas os dos arquivos relevantes
|
|
144
|
+
const output = (result.stdout || "") + (result.stderr || "");
|
|
145
|
+
const errors = parseTscOutput(output, tsFiles);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
passed: errors.length === 0,
|
|
149
|
+
errors,
|
|
150
|
+
skipped: false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Formata resultado para exibicao no CLI
|
|
156
|
+
*/
|
|
157
|
+
export function printTypecheckResult(result: TypecheckResult): void {
|
|
158
|
+
if (result.skipped) {
|
|
159
|
+
console.log(`\n[i] Typecheck pulado: ${result.reason}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.passed) {
|
|
164
|
+
console.log(`\n[ok] Typecheck: sem erros nos arquivos da task`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.error(`\n[X] TYPECHECK: ${result.errors.length} erro(s) encontrado(s)\n`);
|
|
169
|
+
for (const err of result.errors) {
|
|
170
|
+
console.error(` ${err.file}:${err.line}:${err.column}`);
|
|
171
|
+
console.error(` ${err.code}: ${err.message}`);
|
|
172
|
+
console.error();
|
|
173
|
+
}
|
|
174
|
+
}
|
package/gates/validator.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { extname } from "path";
|
|
4
4
|
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
5
|
+
import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
|
|
5
6
|
import { extractUtilitiesFromFile } from "../commands/patterns";
|
|
6
7
|
import { findDuplicateUtilities } from "../db/schema";
|
|
7
8
|
|
|
@@ -48,6 +49,11 @@ const GATES: Record<string, GateCheck[]> = {
|
|
|
48
49
|
message: "Dependencias pendentes",
|
|
49
50
|
resolution: "Complete as tasks dependentes primeiro",
|
|
50
51
|
},
|
|
52
|
+
{
|
|
53
|
+
check: "no-critical-blockers",
|
|
54
|
+
message: "Knowledge critico nao resolvido",
|
|
55
|
+
resolution: "Verifique os blockers com: knowledge list --severity critical. Reconheca com: knowledge resolve <id>",
|
|
56
|
+
},
|
|
51
57
|
],
|
|
52
58
|
"task-done": [
|
|
53
59
|
{
|
|
@@ -75,6 +81,16 @@ const GATES: Record<string, GateCheck[]> = {
|
|
|
75
81
|
message: "Duplicacao de utilities detectada (DRY)",
|
|
76
82
|
resolution: "Importe do arquivo existente ou use --force --force-reason para bypass",
|
|
77
83
|
},
|
|
84
|
+
{
|
|
85
|
+
check: "reasoning-provided",
|
|
86
|
+
message: "Reasoning obrigatorio ausente no retorno do subagent",
|
|
87
|
+
resolution: "O subagent deve incluir 'reasoning.approach' (min 20 chars) no retorno JSON",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
check: "typecheck-pass",
|
|
91
|
+
message: "Erros de tipo encontrados nos arquivos da task",
|
|
92
|
+
resolution: "Corrija os erros TypeScript ou use --force --force-reason para bypass",
|
|
93
|
+
},
|
|
78
94
|
],
|
|
79
95
|
"review-start": [
|
|
80
96
|
{
|
|
@@ -275,6 +291,78 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
275
291
|
return { passed: true };
|
|
276
292
|
}
|
|
277
293
|
|
|
294
|
+
case "typecheck-pass": {
|
|
295
|
+
if (context.force) {
|
|
296
|
+
logGateBypass(context.taskId, "typecheck-pass", context.forceReason);
|
|
297
|
+
return { passed: true };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!context.files || context.files.length === 0) return { passed: true };
|
|
301
|
+
|
|
302
|
+
const typecheckResult = runTypecheck(context.files);
|
|
303
|
+
|
|
304
|
+
printTypecheckResult(typecheckResult);
|
|
305
|
+
|
|
306
|
+
if (typecheckResult.skipped) return { passed: true };
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
passed: typecheckResult.passed,
|
|
310
|
+
details: typecheckResult.errors.map(e => `${e.file}:${e.line} ${e.code}: ${e.message}`).join("\n"),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case "no-critical-blockers": {
|
|
315
|
+
const spec = getActiveSpec();
|
|
316
|
+
if (!spec) return { passed: true };
|
|
317
|
+
|
|
318
|
+
const allKnowledge = db
|
|
319
|
+
.query(
|
|
320
|
+
`SELECT k.*, t.number as task_number, t.name as task_name
|
|
321
|
+
FROM knowledge k
|
|
322
|
+
LEFT JOIN tasks t ON k.task_origin = t.id
|
|
323
|
+
WHERE k.spec_id = ? AND k.severity = 'critical'
|
|
324
|
+
ORDER BY k.created_at DESC`
|
|
325
|
+
)
|
|
326
|
+
.all(spec.id) as any[];
|
|
327
|
+
|
|
328
|
+
const unresolved = allKnowledge.filter((k: any) => {
|
|
329
|
+
if (!k.acknowledged_by) return true;
|
|
330
|
+
try {
|
|
331
|
+
const acked = JSON.parse(k.acknowledged_by) as number[];
|
|
332
|
+
return acked.length === 0;
|
|
333
|
+
} catch {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (unresolved.length === 0) return { passed: true };
|
|
339
|
+
|
|
340
|
+
const details = unresolved.map((k: any) => {
|
|
341
|
+
const origin = k.task_name ? `Task #${k.task_number} (${k.task_name})` : `Task ID ${k.task_origin}`;
|
|
342
|
+
return `[CRITICAL] ${k.content} (origem: ${origin})`;
|
|
343
|
+
}).join("\n");
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
passed: false,
|
|
347
|
+
details: `${unresolved.length} blocker(s) critico(s) nao resolvido(s):\n${details}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
case "reasoning-provided": {
|
|
352
|
+
if (!context.subagentData) return { passed: true };
|
|
353
|
+
|
|
354
|
+
const data = context.subagentData;
|
|
355
|
+
if (data.status !== "completed") return { passed: true };
|
|
356
|
+
|
|
357
|
+
if (!data.reasoning?.approach || data.reasoning.approach.length < 20) {
|
|
358
|
+
return {
|
|
359
|
+
passed: false,
|
|
360
|
+
details: "O subagent retornou status 'completed' sem reasoning.approach (min 20 chars)",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return { passed: true };
|
|
364
|
+
}
|
|
365
|
+
|
|
278
366
|
default:
|
|
279
367
|
return { passed: true };
|
|
280
368
|
}
|