@codexa/cli 8.5.0 → 8.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/architect.ts +760 -896
- package/commands/check.ts +131 -131
- package/commands/clear.ts +170 -174
- package/commands/decide.ts +249 -249
- package/commands/discover.ts +82 -10
- package/commands/knowledge.ts +361 -361
- package/commands/patterns.ts +621 -621
- package/commands/plan.ts +376 -376
- package/commands/product.ts +626 -628
- package/commands/research.ts +754 -754
- package/commands/review.ts +463 -463
- package/commands/standards.ts +200 -223
- package/commands/task.ts +2 -2
- package/commands/utils.ts +1021 -1021
- package/db/connection.ts +32 -32
- package/db/schema.ts +719 -788
- package/detectors/loader.ts +0 -12
- package/gates/standards-validator.ts +204 -204
- package/gates/validator.ts +441 -441
- package/package.json +43 -43
- package/protocol/process-return.ts +450 -450
- package/protocol/subagent-protocol.ts +401 -411
- package/workflow.ts +0 -18
|
@@ -1,412 +1,402 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subagent Protocol Parser & Validator
|
|
3
|
-
*
|
|
4
|
-
* Este modulo GARANTE que todos os retornos de subagents sigam o protocolo definido.
|
|
5
|
-
* Sem validacao correta, task done NAO pode ser executado.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Tipos do protocolo
|
|
9
|
-
export interface Decision {
|
|
10
|
-
title: string;
|
|
11
|
-
decision: string;
|
|
12
|
-
rationale?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface Knowledge {
|
|
16
|
-
category: "discovery" | "decision" | "blocker" | "pattern" | "constraint";
|
|
17
|
-
content: string;
|
|
18
|
-
severity: "info" | "warning" | "critical";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// v8.0: Raciocínio do subagent
|
|
22
|
-
export interface Reasoning {
|
|
23
|
-
approach: string; // Como abordou o problema
|
|
24
|
-
challenges?: string[]; // Desafios encontrados
|
|
25
|
-
alternatives?: string[]; // Alternativas consideradas
|
|
26
|
-
recommendations?: string; // Recomendações para próximas tasks
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SubagentReturn {
|
|
30
|
-
status: "completed" | "blocked" | "needs_decision";
|
|
31
|
-
summary: string;
|
|
32
|
-
files_created: string[];
|
|
33
|
-
files_modified: string[];
|
|
34
|
-
patterns_discovered?: string[];
|
|
35
|
-
decisions_made?: Decision[];
|
|
36
|
-
blockers?: string[];
|
|
37
|
-
knowledge_to_broadcast?: Knowledge[];
|
|
38
|
-
// v8.0: Campos de raciocínio (recomendados mas não obrigatórios por compatibilidade)
|
|
39
|
-
reasoning?: Reasoning;
|
|
40
|
-
// v8.5: Utilities criadas/exportadas nesta task (DRY enforcement)
|
|
41
|
-
utilities_created?: Array<{
|
|
42
|
-
name: string;
|
|
43
|
-
file: string;
|
|
44
|
-
type?: string;
|
|
45
|
-
signature?: string;
|
|
46
|
-
description?: string;
|
|
47
|
-
}>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface ParseResult {
|
|
51
|
-
success: boolean;
|
|
52
|
-
data?: SubagentReturn;
|
|
53
|
-
errors: string[];
|
|
54
|
-
rawInput: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const VALID_STATUSES = ["completed", "blocked", "needs_decision"];
|
|
58
|
-
const VALID_KNOWLEDGE_CATEGORIES = ["discovery", "decision", "blocker", "pattern", "constraint"];
|
|
59
|
-
const VALID_SEVERITIES = ["info", "warning", "critical"];
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Extrai JSON de uma string que pode conter texto misto
|
|
63
|
-
* Subagents podem retornar texto com JSON embutido
|
|
64
|
-
*/
|
|
65
|
-
function extractJsonFromText(text: string): string | null {
|
|
66
|
-
// Tenta encontrar JSON em bloco de codigo
|
|
67
|
-
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
68
|
-
if (codeBlockMatch) {
|
|
69
|
-
return codeBlockMatch[1].trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Tenta encontrar objeto JSON direto (busca mais robusta)
|
|
73
|
-
// Procura por { ... "status" ... } capturando todo o objeto
|
|
74
|
-
let depth = 0;
|
|
75
|
-
let start = -1;
|
|
76
|
-
let inString = false;
|
|
77
|
-
let escape = false;
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < text.length; i++) {
|
|
80
|
-
const char = text[i];
|
|
81
|
-
|
|
82
|
-
if (escape) {
|
|
83
|
-
escape = false;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (char === "\\") {
|
|
88
|
-
escape = true;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (char === '"' && !escape) {
|
|
93
|
-
inString = !inString;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (inString) continue;
|
|
98
|
-
|
|
99
|
-
if (char === "{") {
|
|
100
|
-
if (depth === 0) start = i;
|
|
101
|
-
depth++;
|
|
102
|
-
} else if (char === "}") {
|
|
103
|
-
depth--;
|
|
104
|
-
if (depth === 0 && start !== -1) {
|
|
105
|
-
const candidate = text.slice(start, i + 1);
|
|
106
|
-
if (candidate.includes('"status"')) {
|
|
107
|
-
return candidate;
|
|
108
|
-
}
|
|
109
|
-
start = -1;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Se o texto inteiro parece ser JSON
|
|
115
|
-
const trimmed = text.trim();
|
|
116
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
117
|
-
return trimmed;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Valida um campo string
|
|
125
|
-
*/
|
|
126
|
-
function validateString(value: unknown, field: string, minLen = 0, maxLen = Infinity): string | null {
|
|
127
|
-
if (typeof value !== "string") {
|
|
128
|
-
return `Campo '${field}' deve ser string`;
|
|
129
|
-
}
|
|
130
|
-
if (value.length < minLen) {
|
|
131
|
-
return `Campo '${field}' deve ter pelo menos ${minLen} caracteres`;
|
|
132
|
-
}
|
|
133
|
-
if (value.length > maxLen) {
|
|
134
|
-
return `Campo '${field}' deve ter no maximo ${maxLen} caracteres`;
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Valida um array de strings
|
|
141
|
-
*/
|
|
142
|
-
function validateStringArray(value: unknown, field: string): string | null {
|
|
143
|
-
if (!Array.isArray(value)) {
|
|
144
|
-
return `Campo '${field}' deve ser um array`;
|
|
145
|
-
}
|
|
146
|
-
for (let i = 0; i < value.length; i++) {
|
|
147
|
-
if (typeof value[i] !== "string") {
|
|
148
|
-
return `Campo '${field}[${i}]' deve ser string`;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Valida uma decision
|
|
156
|
-
*/
|
|
157
|
-
function validateDecision(value: unknown, index: number): string[] {
|
|
158
|
-
const errors: string[] = [];
|
|
159
|
-
if (typeof value !== "object" || value === null) {
|
|
160
|
-
return [`decisions_made[${index}] deve ser um objeto`];
|
|
161
|
-
}
|
|
162
|
-
const dec = value as Record<string, unknown>;
|
|
163
|
-
|
|
164
|
-
if (typeof dec.title !== "string" || dec.title.length === 0) {
|
|
165
|
-
errors.push(`decisions_made[${index}].title obrigatorio`);
|
|
166
|
-
}
|
|
167
|
-
if (typeof dec.decision !== "string" || dec.decision.length === 0) {
|
|
168
|
-
errors.push(`decisions_made[${index}].decision obrigatorio`);
|
|
169
|
-
}
|
|
170
|
-
return errors;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Valida um knowledge
|
|
175
|
-
*/
|
|
176
|
-
function validateKnowledge(value: unknown, index: number): string[] {
|
|
177
|
-
const errors: string[] = [];
|
|
178
|
-
if (typeof value !== "object" || value === null) {
|
|
179
|
-
return [`knowledge_to_broadcast[${index}] deve ser um objeto`];
|
|
180
|
-
}
|
|
181
|
-
const k = value as Record<string, unknown>;
|
|
182
|
-
|
|
183
|
-
if (!VALID_KNOWLEDGE_CATEGORIES.includes(k.category as string)) {
|
|
184
|
-
errors.push(`knowledge_to_broadcast[${index}].category invalido. Use: ${VALID_KNOWLEDGE_CATEGORIES.join(", ")}`);
|
|
185
|
-
}
|
|
186
|
-
if (typeof k.content !== "string" || k.content.length === 0) {
|
|
187
|
-
errors.push(`knowledge_to_broadcast[${index}].content obrigatorio`);
|
|
188
|
-
}
|
|
189
|
-
if (!VALID_SEVERITIES.includes(k.severity as string)) {
|
|
190
|
-
errors.push(`knowledge_to_broadcast[${index}].severity invalido. Use: ${VALID_SEVERITIES.join(", ")}`);
|
|
191
|
-
}
|
|
192
|
-
return errors;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Parse e valida o retorno de um subagent
|
|
197
|
-
*/
|
|
198
|
-
export function parseSubagentReturn(input: string): ParseResult {
|
|
199
|
-
const errors: string[] = [];
|
|
200
|
-
|
|
201
|
-
// 1. Extrair JSON do input
|
|
202
|
-
const jsonStr = extractJsonFromText(input);
|
|
203
|
-
if (!jsonStr) {
|
|
204
|
-
return {
|
|
205
|
-
success: false,
|
|
206
|
-
errors: [
|
|
207
|
-
"Nenhum JSON encontrado no retorno do subagent.",
|
|
208
|
-
"O subagent DEVE retornar um objeto JSON no formato especificado em PROTOCOL.md.",
|
|
209
|
-
"Formatos aceitos: JSON puro, ou JSON em bloco ```json```",
|
|
210
|
-
],
|
|
211
|
-
rawInput: input,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// 2. Parse JSON
|
|
216
|
-
let parsed: Record<string, unknown>;
|
|
217
|
-
try {
|
|
218
|
-
parsed = JSON.parse(jsonStr);
|
|
219
|
-
} catch (e) {
|
|
220
|
-
return {
|
|
221
|
-
success: false,
|
|
222
|
-
errors: [
|
|
223
|
-
`JSON invalido: ${(e as Error).message}`,
|
|
224
|
-
"Verifique se o JSON esta bem formatado.",
|
|
225
|
-
],
|
|
226
|
-
rawInput: input,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// 3. Validar campos obrigatorios
|
|
231
|
-
|
|
232
|
-
// status
|
|
233
|
-
if (!VALID_STATUSES.includes(parsed.status as string)) {
|
|
234
|
-
errors.push(`Campo 'status' invalido. Use: ${VALID_STATUSES.join(", ")}`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// summary
|
|
238
|
-
const summaryError = validateString(parsed.summary, "summary", 10, 500);
|
|
239
|
-
if (summaryError) errors.push(summaryError);
|
|
240
|
-
|
|
241
|
-
// files_created
|
|
242
|
-
const filesCreatedError = validateStringArray(parsed.files_created, "files_created");
|
|
243
|
-
if (filesCreatedError) errors.push(filesCreatedError);
|
|
244
|
-
|
|
245
|
-
// files_modified
|
|
246
|
-
const filesModifiedError = validateStringArray(parsed.files_modified, "files_modified");
|
|
247
|
-
if (filesModifiedError) errors.push(filesModifiedError);
|
|
248
|
-
|
|
249
|
-
// 4. Validar campos opcionais se presentes
|
|
250
|
-
|
|
251
|
-
// patterns_discovered
|
|
252
|
-
if (parsed.patterns_discovered !== undefined) {
|
|
253
|
-
const patternsError = validateStringArray(parsed.patterns_discovered, "patterns_discovered");
|
|
254
|
-
if (patternsError) errors.push(patternsError);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// blockers
|
|
258
|
-
if (parsed.blockers !== undefined) {
|
|
259
|
-
const blockersError = validateStringArray(parsed.blockers, "blockers");
|
|
260
|
-
if (blockersError) errors.push(blockersError);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// decisions_made
|
|
264
|
-
if (parsed.decisions_made !== undefined) {
|
|
265
|
-
if (!Array.isArray(parsed.decisions_made)) {
|
|
266
|
-
errors.push("Campo 'decisions_made' deve ser um array");
|
|
267
|
-
} else {
|
|
268
|
-
for (let i = 0; i < parsed.decisions_made.length; i++) {
|
|
269
|
-
errors.push(...validateDecision(parsed.decisions_made[i], i));
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// knowledge_to_broadcast
|
|
275
|
-
if (parsed.knowledge_to_broadcast !== undefined) {
|
|
276
|
-
if (!Array.isArray(parsed.knowledge_to_broadcast)) {
|
|
277
|
-
errors.push("Campo 'knowledge_to_broadcast' deve ser um array");
|
|
278
|
-
} else {
|
|
279
|
-
for (let i = 0; i < parsed.knowledge_to_broadcast.length; i++) {
|
|
280
|
-
errors.push(...validateKnowledge(parsed.knowledge_to_broadcast[i], i));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// v8.0: reasoning (opcional, mas validar estrutura se presente)
|
|
286
|
-
if (parsed.reasoning !== undefined) {
|
|
287
|
-
if (typeof parsed.reasoning !== "object" || parsed.reasoning === null) {
|
|
288
|
-
errors.push("Campo 'reasoning' deve ser um objeto");
|
|
289
|
-
} else {
|
|
290
|
-
const r = parsed.reasoning as Record<string, unknown>;
|
|
291
|
-
if (r.approach !== undefined && typeof r.approach !== "string") {
|
|
292
|
-
errors.push("Campo 'reasoning.approach' deve ser string");
|
|
293
|
-
}
|
|
294
|
-
if (r.challenges !== undefined) {
|
|
295
|
-
const challengesError = validateStringArray(r.challenges, "reasoning.challenges");
|
|
296
|
-
if (challengesError) errors.push(challengesError);
|
|
297
|
-
}
|
|
298
|
-
if (r.alternatives !== undefined) {
|
|
299
|
-
const alternativesError = validateStringArray(r.alternatives, "reasoning.alternatives");
|
|
300
|
-
if (alternativesError) errors.push(alternativesError);
|
|
301
|
-
}
|
|
302
|
-
if (r.recommendations !== undefined && typeof r.recommendations !== "string") {
|
|
303
|
-
errors.push("Campo 'reasoning.recommendations' deve ser string");
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// v8.5: utilities_created (opcional, validar estrutura se presente)
|
|
309
|
-
if (parsed.utilities_created !== undefined) {
|
|
310
|
-
if (!Array.isArray(parsed.utilities_created)) {
|
|
311
|
-
errors.push("Campo 'utilities_created' deve ser um array");
|
|
312
|
-
} else {
|
|
313
|
-
for (let i = 0; i < parsed.utilities_created.length; i++) {
|
|
314
|
-
const u = parsed.utilities_created[i] as any;
|
|
315
|
-
if (!u || typeof u !== "object") {
|
|
316
|
-
errors.push(`utilities_created[${i}] deve ser um objeto`);
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
if (typeof u.name !== "string" || u.name.length === 0) {
|
|
320
|
-
errors.push(`utilities_created[${i}].name obrigatorio`);
|
|
321
|
-
}
|
|
322
|
-
if (typeof u.file !== "string" || u.file.length === 0) {
|
|
323
|
-
errors.push(`utilities_created[${i}].file obrigatorio`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 5. Validacoes semanticas
|
|
330
|
-
|
|
331
|
-
// Se blocked, deve ter blockers
|
|
332
|
-
if (parsed.status === "blocked") {
|
|
333
|
-
if (!parsed.blockers || !Array.isArray(parsed.blockers) || parsed.blockers.length === 0) {
|
|
334
|
-
errors.push("Status 'blocked' requer campo 'blockers' nao vazio");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Se needs_decision, deve ter blockers descrevendo a decisao
|
|
339
|
-
if (parsed.status === "needs_decision") {
|
|
340
|
-
if (!parsed.blockers || !Array.isArray(parsed.blockers) || parsed.blockers.length === 0) {
|
|
341
|
-
errors.push("Status 'needs_decision' requer campo 'blockers' descrevendo a decisao necessaria");
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (errors.length > 0) {
|
|
346
|
-
return {
|
|
347
|
-
success: false,
|
|
348
|
-
errors,
|
|
349
|
-
rawInput: input,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Cast para tipo correto
|
|
354
|
-
const data: SubagentReturn = {
|
|
355
|
-
status: parsed.status as SubagentReturn["status"],
|
|
356
|
-
summary: parsed.summary as string,
|
|
357
|
-
files_created: parsed.files_created as string[],
|
|
358
|
-
files_modified: parsed.files_modified as string[],
|
|
359
|
-
patterns_discovered: parsed.patterns_discovered as string[] | undefined,
|
|
360
|
-
decisions_made: parsed.decisions_made as Decision[] | undefined,
|
|
361
|
-
blockers: parsed.blockers as string[] | undefined,
|
|
362
|
-
knowledge_to_broadcast: parsed.knowledge_to_broadcast as Knowledge[] | undefined,
|
|
363
|
-
reasoning: parsed.reasoning as Reasoning | undefined,
|
|
364
|
-
utilities_created: parsed.utilities_created as SubagentReturn["utilities_created"],
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
success: true,
|
|
369
|
-
data,
|
|
370
|
-
errors: [],
|
|
371
|
-
rawInput: input,
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Formata erros de validacao para exibicao
|
|
377
|
-
*/
|
|
378
|
-
export function formatValidationErrors(result: ParseResult): string {
|
|
379
|
-
if (result.success) return "";
|
|
380
|
-
|
|
381
|
-
let output = "\n[X] ERRO: Retorno do subagent INVALIDO\n";
|
|
382
|
-
output += "─".repeat(50) + "\n\n";
|
|
383
|
-
|
|
384
|
-
for (const error of result.errors) {
|
|
385
|
-
output += ` - ${error}\n`;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
output += "\n" + "─".repeat(50) + "\n";
|
|
389
|
-
output += "O retorno deve seguir o formato:\n\n";
|
|
390
|
-
output += `{
|
|
391
|
-
"status": "completed | blocked | needs_decision",
|
|
392
|
-
"summary": "Resumo do que foi feito (10-500 chars)",
|
|
393
|
-
"files_created": ["path/to/file.ts"],
|
|
394
|
-
"files_modified": ["path/to/other.ts"],
|
|
395
|
-
"patterns_discovered": ["Pattern identificado"],
|
|
396
|
-
"decisions_made": [{"title": "...", "decision": "..."}],
|
|
397
|
-
"blockers": ["Bloqueio se status != completed"],
|
|
398
|
-
"knowledge_to_broadcast": [{"category": "...", "content": "...", "severity": "..."}]
|
|
399
|
-
}\n`;
|
|
400
|
-
|
|
401
|
-
return output;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Verifica se uma string parece conter um retorno de subagent
|
|
406
|
-
*/
|
|
407
|
-
export function looksLikeSubagentReturn(text: string): boolean {
|
|
408
|
-
return text.includes('"status"') &&
|
|
409
|
-
(text.includes('"completed"') ||
|
|
410
|
-
text.includes('"blocked"') ||
|
|
411
|
-
text.includes('"needs_decision"'));
|
|
1
|
+
/**
|
|
2
|
+
* Subagent Protocol Parser & Validator
|
|
3
|
+
*
|
|
4
|
+
* Este modulo GARANTE que todos os retornos de subagents sigam o protocolo definido.
|
|
5
|
+
* Sem validacao correta, task done NAO pode ser executado.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Tipos do protocolo
|
|
9
|
+
export interface Decision {
|
|
10
|
+
title: string;
|
|
11
|
+
decision: string;
|
|
12
|
+
rationale?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Knowledge {
|
|
16
|
+
category: "discovery" | "decision" | "blocker" | "pattern" | "constraint";
|
|
17
|
+
content: string;
|
|
18
|
+
severity: "info" | "warning" | "critical";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// v8.0: Raciocínio do subagent
|
|
22
|
+
export interface Reasoning {
|
|
23
|
+
approach: string; // Como abordou o problema
|
|
24
|
+
challenges?: string[]; // Desafios encontrados
|
|
25
|
+
alternatives?: string[]; // Alternativas consideradas
|
|
26
|
+
recommendations?: string; // Recomendações para próximas tasks
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SubagentReturn {
|
|
30
|
+
status: "completed" | "blocked" | "needs_decision";
|
|
31
|
+
summary: string;
|
|
32
|
+
files_created: string[];
|
|
33
|
+
files_modified: string[];
|
|
34
|
+
patterns_discovered?: string[];
|
|
35
|
+
decisions_made?: Decision[];
|
|
36
|
+
blockers?: string[];
|
|
37
|
+
knowledge_to_broadcast?: Knowledge[];
|
|
38
|
+
// v8.0: Campos de raciocínio (recomendados mas não obrigatórios por compatibilidade)
|
|
39
|
+
reasoning?: Reasoning;
|
|
40
|
+
// v8.5: Utilities criadas/exportadas nesta task (DRY enforcement)
|
|
41
|
+
utilities_created?: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
file: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
signature?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ParseResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
data?: SubagentReturn;
|
|
53
|
+
errors: string[];
|
|
54
|
+
rawInput: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const VALID_STATUSES = ["completed", "blocked", "needs_decision"];
|
|
58
|
+
const VALID_KNOWLEDGE_CATEGORIES = ["discovery", "decision", "blocker", "pattern", "constraint"];
|
|
59
|
+
const VALID_SEVERITIES = ["info", "warning", "critical"];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extrai JSON de uma string que pode conter texto misto
|
|
63
|
+
* Subagents podem retornar texto com JSON embutido
|
|
64
|
+
*/
|
|
65
|
+
function extractJsonFromText(text: string): string | null {
|
|
66
|
+
// Tenta encontrar JSON em bloco de codigo
|
|
67
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
68
|
+
if (codeBlockMatch) {
|
|
69
|
+
return codeBlockMatch[1].trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tenta encontrar objeto JSON direto (busca mais robusta)
|
|
73
|
+
// Procura por { ... "status" ... } capturando todo o objeto
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
let inString = false;
|
|
77
|
+
let escape = false;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < text.length; i++) {
|
|
80
|
+
const char = text[i];
|
|
81
|
+
|
|
82
|
+
if (escape) {
|
|
83
|
+
escape = false;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (char === "\\") {
|
|
88
|
+
escape = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (char === '"' && !escape) {
|
|
93
|
+
inString = !inString;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (inString) continue;
|
|
98
|
+
|
|
99
|
+
if (char === "{") {
|
|
100
|
+
if (depth === 0) start = i;
|
|
101
|
+
depth++;
|
|
102
|
+
} else if (char === "}") {
|
|
103
|
+
depth--;
|
|
104
|
+
if (depth === 0 && start !== -1) {
|
|
105
|
+
const candidate = text.slice(start, i + 1);
|
|
106
|
+
if (candidate.includes('"status"')) {
|
|
107
|
+
return candidate;
|
|
108
|
+
}
|
|
109
|
+
start = -1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Se o texto inteiro parece ser JSON
|
|
115
|
+
const trimmed = text.trim();
|
|
116
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
117
|
+
return trimmed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Valida um campo string
|
|
125
|
+
*/
|
|
126
|
+
function validateString(value: unknown, field: string, minLen = 0, maxLen = Infinity): string | null {
|
|
127
|
+
if (typeof value !== "string") {
|
|
128
|
+
return `Campo '${field}' deve ser string`;
|
|
129
|
+
}
|
|
130
|
+
if (value.length < minLen) {
|
|
131
|
+
return `Campo '${field}' deve ter pelo menos ${minLen} caracteres`;
|
|
132
|
+
}
|
|
133
|
+
if (value.length > maxLen) {
|
|
134
|
+
return `Campo '${field}' deve ter no maximo ${maxLen} caracteres`;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Valida um array de strings
|
|
141
|
+
*/
|
|
142
|
+
function validateStringArray(value: unknown, field: string): string | null {
|
|
143
|
+
if (!Array.isArray(value)) {
|
|
144
|
+
return `Campo '${field}' deve ser um array`;
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < value.length; i++) {
|
|
147
|
+
if (typeof value[i] !== "string") {
|
|
148
|
+
return `Campo '${field}[${i}]' deve ser string`;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Valida uma decision
|
|
156
|
+
*/
|
|
157
|
+
function validateDecision(value: unknown, index: number): string[] {
|
|
158
|
+
const errors: string[] = [];
|
|
159
|
+
if (typeof value !== "object" || value === null) {
|
|
160
|
+
return [`decisions_made[${index}] deve ser um objeto`];
|
|
161
|
+
}
|
|
162
|
+
const dec = value as Record<string, unknown>;
|
|
163
|
+
|
|
164
|
+
if (typeof dec.title !== "string" || dec.title.length === 0) {
|
|
165
|
+
errors.push(`decisions_made[${index}].title obrigatorio`);
|
|
166
|
+
}
|
|
167
|
+
if (typeof dec.decision !== "string" || dec.decision.length === 0) {
|
|
168
|
+
errors.push(`decisions_made[${index}].decision obrigatorio`);
|
|
169
|
+
}
|
|
170
|
+
return errors;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Valida um knowledge
|
|
175
|
+
*/
|
|
176
|
+
function validateKnowledge(value: unknown, index: number): string[] {
|
|
177
|
+
const errors: string[] = [];
|
|
178
|
+
if (typeof value !== "object" || value === null) {
|
|
179
|
+
return [`knowledge_to_broadcast[${index}] deve ser um objeto`];
|
|
180
|
+
}
|
|
181
|
+
const k = value as Record<string, unknown>;
|
|
182
|
+
|
|
183
|
+
if (!VALID_KNOWLEDGE_CATEGORIES.includes(k.category as string)) {
|
|
184
|
+
errors.push(`knowledge_to_broadcast[${index}].category invalido. Use: ${VALID_KNOWLEDGE_CATEGORIES.join(", ")}`);
|
|
185
|
+
}
|
|
186
|
+
if (typeof k.content !== "string" || k.content.length === 0) {
|
|
187
|
+
errors.push(`knowledge_to_broadcast[${index}].content obrigatorio`);
|
|
188
|
+
}
|
|
189
|
+
if (!VALID_SEVERITIES.includes(k.severity as string)) {
|
|
190
|
+
errors.push(`knowledge_to_broadcast[${index}].severity invalido. Use: ${VALID_SEVERITIES.join(", ")}`);
|
|
191
|
+
}
|
|
192
|
+
return errors;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse e valida o retorno de um subagent
|
|
197
|
+
*/
|
|
198
|
+
export function parseSubagentReturn(input: string): ParseResult {
|
|
199
|
+
const errors: string[] = [];
|
|
200
|
+
|
|
201
|
+
// 1. Extrair JSON do input
|
|
202
|
+
const jsonStr = extractJsonFromText(input);
|
|
203
|
+
if (!jsonStr) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
errors: [
|
|
207
|
+
"Nenhum JSON encontrado no retorno do subagent.",
|
|
208
|
+
"O subagent DEVE retornar um objeto JSON no formato especificado em PROTOCOL.md.",
|
|
209
|
+
"Formatos aceitos: JSON puro, ou JSON em bloco ```json```",
|
|
210
|
+
],
|
|
211
|
+
rawInput: input,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 2. Parse JSON
|
|
216
|
+
let parsed: Record<string, unknown>;
|
|
217
|
+
try {
|
|
218
|
+
parsed = JSON.parse(jsonStr);
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
errors: [
|
|
223
|
+
`JSON invalido: ${(e as Error).message}`,
|
|
224
|
+
"Verifique se o JSON esta bem formatado.",
|
|
225
|
+
],
|
|
226
|
+
rawInput: input,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 3. Validar campos obrigatorios
|
|
231
|
+
|
|
232
|
+
// status
|
|
233
|
+
if (!VALID_STATUSES.includes(parsed.status as string)) {
|
|
234
|
+
errors.push(`Campo 'status' invalido. Use: ${VALID_STATUSES.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// summary
|
|
238
|
+
const summaryError = validateString(parsed.summary, "summary", 10, 500);
|
|
239
|
+
if (summaryError) errors.push(summaryError);
|
|
240
|
+
|
|
241
|
+
// files_created
|
|
242
|
+
const filesCreatedError = validateStringArray(parsed.files_created, "files_created");
|
|
243
|
+
if (filesCreatedError) errors.push(filesCreatedError);
|
|
244
|
+
|
|
245
|
+
// files_modified
|
|
246
|
+
const filesModifiedError = validateStringArray(parsed.files_modified, "files_modified");
|
|
247
|
+
if (filesModifiedError) errors.push(filesModifiedError);
|
|
248
|
+
|
|
249
|
+
// 4. Validar campos opcionais se presentes
|
|
250
|
+
|
|
251
|
+
// patterns_discovered
|
|
252
|
+
if (parsed.patterns_discovered !== undefined) {
|
|
253
|
+
const patternsError = validateStringArray(parsed.patterns_discovered, "patterns_discovered");
|
|
254
|
+
if (patternsError) errors.push(patternsError);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// blockers
|
|
258
|
+
if (parsed.blockers !== undefined) {
|
|
259
|
+
const blockersError = validateStringArray(parsed.blockers, "blockers");
|
|
260
|
+
if (blockersError) errors.push(blockersError);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// decisions_made
|
|
264
|
+
if (parsed.decisions_made !== undefined) {
|
|
265
|
+
if (!Array.isArray(parsed.decisions_made)) {
|
|
266
|
+
errors.push("Campo 'decisions_made' deve ser um array");
|
|
267
|
+
} else {
|
|
268
|
+
for (let i = 0; i < parsed.decisions_made.length; i++) {
|
|
269
|
+
errors.push(...validateDecision(parsed.decisions_made[i], i));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// knowledge_to_broadcast
|
|
275
|
+
if (parsed.knowledge_to_broadcast !== undefined) {
|
|
276
|
+
if (!Array.isArray(parsed.knowledge_to_broadcast)) {
|
|
277
|
+
errors.push("Campo 'knowledge_to_broadcast' deve ser um array");
|
|
278
|
+
} else {
|
|
279
|
+
for (let i = 0; i < parsed.knowledge_to_broadcast.length; i++) {
|
|
280
|
+
errors.push(...validateKnowledge(parsed.knowledge_to_broadcast[i], i));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// v8.0: reasoning (opcional, mas validar estrutura se presente)
|
|
286
|
+
if (parsed.reasoning !== undefined) {
|
|
287
|
+
if (typeof parsed.reasoning !== "object" || parsed.reasoning === null) {
|
|
288
|
+
errors.push("Campo 'reasoning' deve ser um objeto");
|
|
289
|
+
} else {
|
|
290
|
+
const r = parsed.reasoning as Record<string, unknown>;
|
|
291
|
+
if (r.approach !== undefined && typeof r.approach !== "string") {
|
|
292
|
+
errors.push("Campo 'reasoning.approach' deve ser string");
|
|
293
|
+
}
|
|
294
|
+
if (r.challenges !== undefined) {
|
|
295
|
+
const challengesError = validateStringArray(r.challenges, "reasoning.challenges");
|
|
296
|
+
if (challengesError) errors.push(challengesError);
|
|
297
|
+
}
|
|
298
|
+
if (r.alternatives !== undefined) {
|
|
299
|
+
const alternativesError = validateStringArray(r.alternatives, "reasoning.alternatives");
|
|
300
|
+
if (alternativesError) errors.push(alternativesError);
|
|
301
|
+
}
|
|
302
|
+
if (r.recommendations !== undefined && typeof r.recommendations !== "string") {
|
|
303
|
+
errors.push("Campo 'reasoning.recommendations' deve ser string");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// v8.5: utilities_created (opcional, validar estrutura se presente)
|
|
309
|
+
if (parsed.utilities_created !== undefined) {
|
|
310
|
+
if (!Array.isArray(parsed.utilities_created)) {
|
|
311
|
+
errors.push("Campo 'utilities_created' deve ser um array");
|
|
312
|
+
} else {
|
|
313
|
+
for (let i = 0; i < parsed.utilities_created.length; i++) {
|
|
314
|
+
const u = parsed.utilities_created[i] as any;
|
|
315
|
+
if (!u || typeof u !== "object") {
|
|
316
|
+
errors.push(`utilities_created[${i}] deve ser um objeto`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (typeof u.name !== "string" || u.name.length === 0) {
|
|
320
|
+
errors.push(`utilities_created[${i}].name obrigatorio`);
|
|
321
|
+
}
|
|
322
|
+
if (typeof u.file !== "string" || u.file.length === 0) {
|
|
323
|
+
errors.push(`utilities_created[${i}].file obrigatorio`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 5. Validacoes semanticas
|
|
330
|
+
|
|
331
|
+
// Se blocked, deve ter blockers
|
|
332
|
+
if (parsed.status === "blocked") {
|
|
333
|
+
if (!parsed.blockers || !Array.isArray(parsed.blockers) || parsed.blockers.length === 0) {
|
|
334
|
+
errors.push("Status 'blocked' requer campo 'blockers' nao vazio");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Se needs_decision, deve ter blockers descrevendo a decisao
|
|
339
|
+
if (parsed.status === "needs_decision") {
|
|
340
|
+
if (!parsed.blockers || !Array.isArray(parsed.blockers) || parsed.blockers.length === 0) {
|
|
341
|
+
errors.push("Status 'needs_decision' requer campo 'blockers' descrevendo a decisao necessaria");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (errors.length > 0) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
errors,
|
|
349
|
+
rawInput: input,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Cast para tipo correto
|
|
354
|
+
const data: SubagentReturn = {
|
|
355
|
+
status: parsed.status as SubagentReturn["status"],
|
|
356
|
+
summary: parsed.summary as string,
|
|
357
|
+
files_created: parsed.files_created as string[],
|
|
358
|
+
files_modified: parsed.files_modified as string[],
|
|
359
|
+
patterns_discovered: parsed.patterns_discovered as string[] | undefined,
|
|
360
|
+
decisions_made: parsed.decisions_made as Decision[] | undefined,
|
|
361
|
+
blockers: parsed.blockers as string[] | undefined,
|
|
362
|
+
knowledge_to_broadcast: parsed.knowledge_to_broadcast as Knowledge[] | undefined,
|
|
363
|
+
reasoning: parsed.reasoning as Reasoning | undefined,
|
|
364
|
+
utilities_created: parsed.utilities_created as SubagentReturn["utilities_created"],
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
success: true,
|
|
369
|
+
data,
|
|
370
|
+
errors: [],
|
|
371
|
+
rawInput: input,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Formata erros de validacao para exibicao
|
|
377
|
+
*/
|
|
378
|
+
export function formatValidationErrors(result: ParseResult): string {
|
|
379
|
+
if (result.success) return "";
|
|
380
|
+
|
|
381
|
+
let output = "\n[X] ERRO: Retorno do subagent INVALIDO\n";
|
|
382
|
+
output += "─".repeat(50) + "\n\n";
|
|
383
|
+
|
|
384
|
+
for (const error of result.errors) {
|
|
385
|
+
output += ` - ${error}\n`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
output += "\n" + "─".repeat(50) + "\n";
|
|
389
|
+
output += "O retorno deve seguir o formato:\n\n";
|
|
390
|
+
output += `{
|
|
391
|
+
"status": "completed | blocked | needs_decision",
|
|
392
|
+
"summary": "Resumo do que foi feito (10-500 chars)",
|
|
393
|
+
"files_created": ["path/to/file.ts"],
|
|
394
|
+
"files_modified": ["path/to/other.ts"],
|
|
395
|
+
"patterns_discovered": ["Pattern identificado"],
|
|
396
|
+
"decisions_made": [{"title": "...", "decision": "..."}],
|
|
397
|
+
"blockers": ["Bloqueio se status != completed"],
|
|
398
|
+
"knowledge_to_broadcast": [{"category": "...", "content": "...", "severity": "..."}]
|
|
399
|
+
}\n`;
|
|
400
|
+
|
|
401
|
+
return output;
|
|
412
402
|
}
|