@codexa/cli 9.0.2 → 9.0.4

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/task.ts CHANGED
@@ -1,27 +1,26 @@
1
1
  import { getDb } from "../db/connection";
2
- import { initSchema, getPatternsForFiles, getPatternsByScope, addSessionSummary, getRecentReasoning } from "../db/schema";
2
+ import { initSchema, getPatternsForFiles, getPatternsByScope, getRecentReasoning, claimTask, recordAgentPerformance } from "../db/schema";
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
6
  import { getContextForSubagent, getMinimalContextForSubagent } from "./utils";
7
7
  import { getUnreadKnowledgeForTask } from "./knowledge";
8
+ import { loadTemplate } from "../templates/loader";
9
+ import { TaskStateError, ValidationError, KnowledgeBlockError } from "../errors";
10
+ import { resolveSpec, resolveSpecOrNull } from "./spec-resolver";
8
11
 
9
- export function taskNext(json: boolean = false): void {
12
+ export function taskNext(json: boolean = false, specId?: string): void {
10
13
  initSchema();
11
14
 
12
15
  const db = getDb();
13
- const spec = db
14
- .query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
15
- .get() as any;
16
+ const spec = resolveSpecOrNull(specId, ["implementing"]);
16
17
 
17
18
  if (!spec) {
18
19
  if (json) {
19
20
  console.log(JSON.stringify({ available: [], message: "Nenhuma feature em implementacao" }));
20
- } else {
21
- console.error("\nNenhuma feature em fase de implementacao.");
22
- console.error("Aprove o plano com: check approve\n");
21
+ return;
23
22
  }
24
- process.exit(1);
23
+ throw new TaskStateError("Nenhuma feature em fase de implementacao.\nAprove o plano com: check approve");
25
24
  }
26
25
 
27
26
  // Buscar tasks pendentes cujas dependencias estao todas concluidas
@@ -101,16 +100,14 @@ export function taskNext(json: boolean = false): void {
101
100
  console.log(`Use: task start <id> ou task start <id1>,<id2>,...\n`);
102
101
  }
103
102
 
104
- export function taskStart(ids: string, json: boolean = false, fullContext: boolean = false): void {
103
+ export function taskStart(ids: string, json: boolean = false, fullContext: boolean = false, specId?: string): void {
105
104
  initSchema();
106
105
  enforceGate("task-start");
107
106
 
108
107
  const db = getDb();
109
108
  const now = new Date().toISOString();
110
109
 
111
- const spec = db
112
- .query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
113
- .get() as any;
110
+ const spec = resolveSpec(specId, ["implementing"]);
114
111
 
115
112
  const taskIds = ids.split(",").map((s) => parseInt(s.trim()));
116
113
  const startedTasks: any[] = [];
@@ -119,20 +116,19 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
119
116
  const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
120
117
 
121
118
  if (!task) {
122
- console.error(`\nTask #${taskId} nao encontrada.\n`);
123
- process.exit(2);
124
- }
125
-
126
- if (task.status !== "pending") {
127
- console.error(`\nTask #${task.number} nao esta pendente (status: ${task.status}).\n`);
128
- process.exit(2);
119
+ throw new TaskStateError(`Task #${taskId} nao encontrada.`);
129
120
  }
130
121
 
131
122
  // Validar dependencias para esta task especifica
132
123
  enforceGate("task-start", { taskId });
133
124
 
134
- // Marcar como running
135
- db.run("UPDATE tasks SET status = 'running' WHERE id = ?", [taskId]);
125
+ // Claim atomico: UPDATE ... WHERE status = 'pending'
126
+ if (!claimTask(taskId)) {
127
+ const current = db.query("SELECT status FROM tasks WHERE id = ?").get(taskId) as any;
128
+ throw new TaskStateError(
129
+ `Task #${task.number} nao pode ser iniciada (status atual: ${current?.status || "desconhecido"}).`
130
+ );
131
+ }
136
132
 
137
133
  startedTasks.push(task);
138
134
  }
@@ -233,77 +229,15 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
233
229
  // NOVO v7.4: Implementation patterns extraidos do projeto
234
230
  implementationPatterns: formattedPatterns,
235
231
  // Contexto para o SUBAGENT (o orquestrador deve passar isso via Task tool)
236
- subagentContext: `
237
- ╔══════════════════════════════════════════════════════════════════════════════╗
238
- ║ DIRETIVA CRITICA: USE Write/Edit PARA CRIAR OS ARQUIVOS ║
239
- ║ NAO descreva. NAO planeje. NAO simule. EXECUTE AGORA. ║
240
- ║ Se retornar sem usar Write/Edit, a task FALHA. ║
241
- ╚══════════════════════════════════════════════════════════════════════════════╝
242
-
243
- ARQUIVOS QUE VOCE DEVE CRIAR (use Write para cada um):
244
- ${taskFiles.map(f => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado - analise o contexto)'}
245
-
246
- ╔══════════════════════════════════════════════════════════════════════════════╗
247
- ║ POLLING OBRIGATORIO: Verifique blockers a cada 3 arquivos modificados ║
248
- ╚══════════════════════════════════════════════════════════════════════════════╝
249
-
250
- ANTES de criar o 4o, 7o, 10o arquivo (a cada 3), execute:
251
- codexa knowledge list --severity critical --unread
252
-
253
- Se retornar QUALQUER blocker:
254
- 1. PARE imediatamente
255
- 2. Retorne com status "blocked" e inclua o blocker encontrado
256
- 3. NAO continue criando arquivos apos encontrar blocker
257
-
258
- CHECKLIST OBRIGATORIO (verifique ANTES de retornar):
259
- - [ ] Usei Write ou Edit para criar/modificar arquivos?
260
- - [ ] Verifiquei blockers a cada 3 arquivos?
261
- - [ ] Os arquivos que vou listar em files_created EXISTEM no disco?
262
-
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.
271
- `.trim(),
232
+ subagentContext: loadTemplate("subagent-context", {
233
+ filesList: taskFiles.map(f => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado - analise o contexto)',
234
+ }),
272
235
  // Instrucoes de retorno para o SUBAGENT
273
- subagentReturnProtocol: `
274
- FORMATO DE RETORNO (apos criar os arquivos):
275
- {
276
- "status": "completed | blocked | needs_decision",
277
- "summary": "Resumo do que foi feito (10-500 chars)",
278
- "files_created": ["path/arquivo.ts"],
279
- "files_modified": ["path/outro.ts"],
280
- "reasoning": {
281
- "approach": "OBRIGATORIO (min 20 chars): Como voce abordou o problema e POR QUE tomou essas decisoes",
282
- "challenges": ["Desafios encontrados"],
283
- "recommendations": "Sugestoes para proximas tasks"
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"}]
289
- }
290
-
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.
294
-
295
- Se NAO conseguir criar arquivos (sem permissao, sem ferramentas), retorne:
296
- {
297
- "status": "blocked",
298
- "blockers": ["Descreva por que nao conseguiu criar os arquivos"]
299
- }
300
-
301
- Veja .claude/agents/PROTOCOL.md para detalhes completos.
302
- ${formattedPatterns.length > 0 ? `
303
- PATTERNS: Voce recebeu ${formattedPatterns.length} implementation patterns extraidos do projeto.
304
- Use os TEMPLATES fornecidos para criar codigo CONSISTENTE com o projeto existente.
305
- ` : ''}
306
- `.trim(),
236
+ subagentReturnProtocol: loadTemplate("subagent-return-protocol", {
237
+ patternsNote: formattedPatterns.length > 0
238
+ ? `\nPATTERNS: Voce recebeu ${formattedPatterns.length} implementation patterns extraidos do projeto.\nUse os TEMPLATES fornecidos para criar codigo CONSISTENTE com o projeto existente.\n`
239
+ : '',
240
+ }),
307
241
  };
308
242
  });
309
243
  console.log(JSON.stringify({ started: contexts }));
@@ -325,8 +259,7 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
325
259
  const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
326
260
 
327
261
  if (!task) {
328
- console.error(`\nTask #${taskId} nao encontrada.\n`);
329
- process.exit(2);
262
+ throw new TaskStateError(`Task #${taskId} nao encontrada.`);
330
263
  }
331
264
 
332
265
  const spec = db.query("SELECT * FROM specs WHERE id = ?").get(task.spec_id) as any;
@@ -344,10 +277,11 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
344
277
  const parseResult = parseSubagentReturn(options.output);
345
278
 
346
279
  if (!parseResult.success) {
347
- console.error(formatValidationErrors(parseResult));
348
- console.error("\nTask NAO pode ser completada sem retorno valido.");
349
- console.error("Corrija o formato do retorno do subagent e tente novamente.\n");
350
- process.exit(2);
280
+ throw new ValidationError(
281
+ formatValidationErrors(parseResult) +
282
+ "\nTask NAO pode ser completada sem retorno valido." +
283
+ "\nCorrija o formato do retorno do subagent e tente novamente."
284
+ );
351
285
  }
352
286
 
353
287
  subagentData = parseResult.data!;
@@ -411,14 +345,19 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
411
345
  .filter((k: any) => k.severity === 'critical' && k.task_origin !== taskId);
412
346
  if (unackedCritical.length > 0) {
413
347
  if (!options.force) {
414
- console.error(`\n[X] BLOQUEADO: ${unackedCritical.length} knowledge(s) critico(s) nao reconhecido(s):`);
415
- for (const k of unackedCritical) {
416
- console.error(` [X] ${k.content} (de Task #${k.task_origin})`);
417
- }
418
- console.error(`\n O subagent NAO verificou o polling obrigatorio.`);
419
- console.error(` Reconheca com: knowledge ack <id>`);
420
- console.error(` Ou force com: task done ${id} --checkpoint "..." --force --force-reason "motivo"\n`);
421
- process.exit(1);
348
+ // Task permanece em "running" (subagent output ja processado acima).
349
+ // Nao pode ser marcada "done" ate knowledge ser reconhecido.
350
+ const items = unackedCritical.map(
351
+ (k: any) => ` [X] ${k.content} (de Task #${k.task_origin})`
352
+ ).join("\n");
353
+
354
+ throw new KnowledgeBlockError(
355
+ `BLOQUEADO: ${unackedCritical.length} knowledge(s) critico(s) nao reconhecido(s):\n${items}\n\n` +
356
+ `O subagent NAO verificou o polling obrigatorio.\n` +
357
+ `Reconheca com: knowledge ack <id>\n` +
358
+ `Ou force com: task done ${id} --checkpoint "..." --force --force-reason "motivo"`,
359
+ unackedCritical
360
+ );
422
361
  } else {
423
362
  console.log(`\n[!] AVISO: ${unackedCritical.length} knowledge(s) critico(s) ignorado(s) (--force usado)`);
424
363
  for (const k of unackedCritical) {
@@ -469,6 +408,30 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
469
408
  [checkpoint, now, taskId]
470
409
  );
471
410
 
411
+ // v9.3: Registrar performance do agente
412
+ try {
413
+ const agentType = task.agent || "general-purpose";
414
+ const startedAt = task.started_at ? new Date(task.started_at).getTime() : Date.now();
415
+ const duration = Date.now() - startedAt;
416
+ const bypassCount = (db.query(
417
+ "SELECT COUNT(*) as c FROM gate_bypasses WHERE task_id = ?"
418
+ ).get(taskId) as any)?.c || 0;
419
+ const totalGates = 7;
420
+
421
+ recordAgentPerformance({
422
+ agentType,
423
+ specId: spec.id,
424
+ taskId,
425
+ gatesPassedFirstTry: Math.max(0, totalGates - bypassCount),
426
+ gatesTotal: totalGates,
427
+ bypassesUsed: bypassCount,
428
+ filesCreated: subagentData?.files_created?.length || 0,
429
+ filesModified: subagentData?.files_modified?.length || 0,
430
+ contextSizeBytes: 0,
431
+ executionDurationMs: duration,
432
+ });
433
+ } catch { /* nao-critico: nao falhar task done por tracking de performance */ }
434
+
472
435
  // Registrar artefatos se NAO veio do subagent (ja foi processado acima)
473
436
  if (!subagentData && options.files) {
474
437
  const files = options.files.split(",").map((s) => s.trim());
@@ -494,73 +457,15 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
494
457
  [checkpoint, now, spec.id]
495
458
  );
496
459
 
497
- // Criar snapshot automatico COMPLETO
498
- const allTasks = db.query("SELECT * FROM tasks WHERE spec_id = ?").all(spec.id) as any[];
499
- const allDecisions = db.query("SELECT * FROM decisions WHERE spec_id = ?").all(spec.id) as any[];
500
- const allArtifacts = db.query("SELECT * FROM artifacts WHERE spec_id = ?").all(spec.id) as any[];
501
- const context = db.query("SELECT * FROM context WHERE spec_id = ?").get(spec.id);
502
-
503
- const snapshotData = {
504
- spec,
505
- context,
506
- tasks: allTasks,
507
- decisions: allDecisions,
508
- artifacts: allArtifacts,
509
- checkpoint: options.checkpoint,
510
- taskCompleted: task.number,
511
- timestamp: now,
512
- };
513
- db.run("INSERT INTO snapshots (spec_id, data, trigger, created_at) VALUES (?, ?, 'auto', ?)", [
514
- spec.id,
515
- JSON.stringify(snapshotData),
516
- now,
517
- ]);
518
-
519
- // v8.1: Gerar session summary automaticamente
520
- try {
521
- const taskDecisions = allDecisions.filter((d) => d.task_ref === task.number);
522
- const taskArtifacts = allArtifacts.filter((a) => a.task_ref === task.number);
523
- const blockers = db.query(
524
- "SELECT content FROM knowledge WHERE spec_id = ? AND task_origin = ? AND category = 'blocker'"
525
- ).all(spec.id, taskId) as any[];
526
- const reasoning = getRecentReasoning(spec.id, 5);
527
- const nextSteps: string[] = [];
528
-
529
- // Extrair recommendations do reasoning
530
- for (const r of reasoning) {
531
- if (r.category === 'recommendation' && r.thought) {
532
- nextSteps.push(r.thought);
533
- }
534
- }
535
-
536
- // Se nao completou tudo, sugerir proximo passo
537
- if (doneCount.c < totalCount.c) {
538
- nextSteps.push(`Continuar implementacao: ${totalCount.c - doneCount.c} tasks restantes`);
539
- } else {
540
- nextSteps.push("Todas tasks concluidas. Iniciar review.");
541
- }
542
-
543
- addSessionSummary(spec.id, {
544
- startTime: task.completed_at || now, // approximation
545
- endTime: now,
546
- summary: `Task #${task.number} (${task.name}) concluida. ${checkpoint}`,
547
- decisions: taskDecisions.map((d: any) => `${d.title}: ${d.decision}`),
548
- blockers: blockers.map((b: any) => b.content),
549
- nextSteps,
550
- tasksCompleted: 1,
551
- filesCreated: taskArtifacts.filter((a: any) => a.action === 'created').length,
552
- filesModified: taskArtifacts.filter((a: any) => a.action === 'modified').length,
553
- });
554
- } catch (e) {
555
- // Session summary e best-effort, nao deve bloquear o fluxo
556
- }
557
-
558
460
  console.log(`\nTask #${task.number} concluida!`);
559
461
  console.log(`Checkpoint: ${options.checkpoint}`);
560
462
  console.log(`Progresso: ${doneCount.c}/${totalCount.c} tasks`);
561
463
 
562
464
  if (doneCount.c === totalCount.c) {
563
465
  // Mostrar resumo completo da implementacao
466
+ const allTasks = db.query("SELECT * FROM tasks WHERE spec_id = ?").all(spec.id) as any[];
467
+ const allDecisions = db.query("SELECT * FROM decisions WHERE spec_id = ?").all(spec.id) as any[];
468
+ const allArtifacts = db.query("SELECT * FROM artifacts WHERE spec_id = ?").all(spec.id) as any[];
564
469
  showImplementationSummary(spec.id, allTasks, allArtifacts, allDecisions);
565
470
  } else {
566
471
  console.log(`\nProximas tasks: task next\n`);
@@ -0,0 +1,100 @@
1
+ /**
2
+ * v9.3: Tests for AGENT_SECTIONS filtering (P3.1 — Context Intelligence)
3
+ */
4
+ import { describe, it, expect } from "bun:test";
5
+ import { AGENT_SECTIONS } from "./utils";
6
+
7
+ describe("AGENT_SECTIONS (P3.1 — Context Intelligence)", () => {
8
+ it("testing-unit gets focused sections without PRODUTO or STACK", () => {
9
+ const sections = AGENT_SECTIONS["testing-unit"];
10
+ expect(sections).toBeDefined();
11
+ expect(sections).toContain("STANDARDS");
12
+ expect(sections).toContain("DECISOES");
13
+ expect(sections).toContain("ALERTAS");
14
+ expect(sections).toContain("PATTERNS");
15
+ expect(sections).toContain("UTILITIES");
16
+ expect(sections).toContain("HINTS");
17
+ expect(sections).not.toContain("PRODUTO");
18
+ expect(sections).not.toContain("STACK");
19
+ expect(sections).not.toContain("ARQUITETURA");
20
+ });
21
+
22
+ it("deep-explore gets minimal sections", () => {
23
+ const sections = AGENT_SECTIONS["deep-explore"];
24
+ expect(sections).toBeDefined();
25
+ expect(sections).toHaveLength(2);
26
+ expect(sections).toContain("STACK");
27
+ expect(sections).toContain("ARQUITETURA");
28
+ expect(sections).not.toContain("HINTS");
29
+ });
30
+
31
+ it("security-specialist gets security-relevant sections", () => {
32
+ const sections = AGENT_SECTIONS["security-specialist"];
33
+ expect(sections).toBeDefined();
34
+ expect(sections).toContain("STANDARDS");
35
+ expect(sections).toContain("DECISOES");
36
+ expect(sections).toContain("ALERTAS");
37
+ expect(sections).toContain("STACK");
38
+ expect(sections).toContain("HINTS");
39
+ expect(sections).not.toContain("PATTERNS");
40
+ expect(sections).not.toContain("UTILITIES");
41
+ });
42
+
43
+ it("expert-code-reviewer gets review-relevant sections", () => {
44
+ const sections = AGENT_SECTIONS["expert-code-reviewer"];
45
+ expect(sections).toBeDefined();
46
+ expect(sections).toContain("STANDARDS");
47
+ expect(sections).toContain("DECISOES");
48
+ expect(sections).toContain("ARQUITETURA");
49
+ expect(sections).toContain("UTILITIES");
50
+ expect(sections).toContain("ALERTAS");
51
+ expect(sections).toContain("HINTS");
52
+ expect(sections).not.toContain("PRODUTO");
53
+ });
54
+
55
+ it("unknown agent types are not in the map (get all sections)", () => {
56
+ expect(AGENT_SECTIONS["frontend-next"]).toBeUndefined();
57
+ expect(AGENT_SECTIONS["database-postgres"]).toBeUndefined();
58
+ expect(AGENT_SECTIONS["backend-javascript"]).toBeUndefined();
59
+ expect(AGENT_SECTIONS["general-purpose"]).toBeUndefined();
60
+ });
61
+
62
+ it("filtering logic: known agent filters sections", () => {
63
+ const allSections = [
64
+ { name: "PRODUTO", content: "...", priority: 7 },
65
+ { name: "STANDARDS", content: "...", priority: 1 },
66
+ { name: "ALERTAS", content: "...", priority: 2 },
67
+ { name: "STACK", content: "...", priority: 11 },
68
+ { name: "ARQUITETURA", content: "...", priority: 3 },
69
+ { name: "DECISOES", content: "...", priority: 4 },
70
+ { name: "PATTERNS", content: "...", priority: 9 },
71
+ ];
72
+
73
+ const agentType = "deep-explore";
74
+ const allowed = AGENT_SECTIONS[agentType];
75
+ const filtered = allowed
76
+ ? allSections.filter(s => allowed.includes(s.name))
77
+ : allSections;
78
+
79
+ expect(filtered).toHaveLength(2);
80
+ expect(filtered.map(s => s.name)).toContain("STACK");
81
+ expect(filtered.map(s => s.name)).toContain("ARQUITETURA");
82
+ expect(filtered.map(s => s.name)).not.toContain("PRODUTO");
83
+ expect(filtered.map(s => s.name)).not.toContain("STANDARDS");
84
+ });
85
+
86
+ it("filtering logic: unknown agent keeps all sections", () => {
87
+ const allSections = [
88
+ { name: "PRODUTO", content: "...", priority: 7 },
89
+ { name: "STANDARDS", content: "...", priority: 1 },
90
+ ];
91
+
92
+ const agentType = "frontend-next"; // not in AGENT_SECTIONS
93
+ const allowed = AGENT_SECTIONS[agentType];
94
+ const filtered = allowed
95
+ ? allSections.filter(s => allowed.includes(s.name))
96
+ : allSections;
97
+
98
+ expect(filtered).toHaveLength(2); // All kept
99
+ });
100
+ });