@codexa/cli 9.0.3 → 9.0.5

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/db/schema.ts CHANGED
@@ -408,6 +408,57 @@ const MIGRATIONS: Migration[] = [
408
408
  db.exec(`ALTER TABLE tasks ADD COLUMN started_at TEXT`);
409
409
  },
410
410
  },
411
+ {
412
+ version: "9.3.0",
413
+ description: "Criar tabela agent_performance para feedback loop",
414
+ up: (db) => {
415
+ db.exec(`
416
+ CREATE TABLE IF NOT EXISTS agent_performance (
417
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
418
+ agent_type TEXT NOT NULL,
419
+ spec_id TEXT NOT NULL,
420
+ task_id INTEGER NOT NULL,
421
+ gates_passed_first_try INTEGER DEFAULT 0,
422
+ gates_total INTEGER DEFAULT 0,
423
+ bypasses_used INTEGER DEFAULT 0,
424
+ files_created INTEGER DEFAULT 0,
425
+ files_modified INTEGER DEFAULT 0,
426
+ context_size_bytes INTEGER DEFAULT 0,
427
+ execution_duration_ms INTEGER DEFAULT 0,
428
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
429
+ )
430
+ `);
431
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_perf_type ON agent_performance(agent_type)`);
432
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_perf_created ON agent_performance(created_at)`);
433
+ },
434
+ },
435
+ {
436
+ version: "9.4.0",
437
+ description: "Migrar acknowledged_by de JSON para tabela separada",
438
+ up: (db) => {
439
+ db.exec(`
440
+ CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
441
+ knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
442
+ task_id INTEGER NOT NULL,
443
+ acknowledged_at TEXT DEFAULT CURRENT_TIMESTAMP,
444
+ PRIMARY KEY (knowledge_id, task_id)
445
+ )
446
+ `);
447
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_ka_task ON knowledge_acknowledgments(task_id)`);
448
+
449
+ // Migrar dados existentes do campo JSON
450
+ const rows = db.query("SELECT id, acknowledged_by FROM knowledge WHERE acknowledged_by IS NOT NULL").all() as any[];
451
+ const insert = db.prepare("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)");
452
+ for (const row of rows) {
453
+ try {
454
+ const taskIds = JSON.parse(row.acknowledged_by) as number[];
455
+ for (const taskId of taskIds) {
456
+ insert.run(row.id, taskId);
457
+ }
458
+ } catch { /* JSON invalido, ignorar */ }
459
+ }
460
+ },
461
+ },
411
462
  ];
412
463
 
413
464
  export function runMigrations(): void {
@@ -449,19 +500,13 @@ export function runMigrations(): void {
449
500
  // Exportar MIGRATIONS para testes
450
501
  export { MIGRATIONS };
451
502
 
452
- // Gera proximo ID de decisao para um spec (DEC-001, DEC-002, ...)
453
- // Usa MAX() atomico para evitar race condition entre tasks paralelas
503
+ // Gera proximo ID de decisao para um spec
504
+ // Usa timestamp + random hash para eliminar race conditions entre tasks paralelas
454
505
  export function getNextDecisionId(specId: string): string {
455
- const db = getDb();
456
- const result = db
457
- .query(
458
- `SELECT MAX(CAST(REPLACE(id, 'DEC-', '') AS INTEGER)) as max_num
459
- FROM decisions WHERE spec_id = ?`
460
- )
461
- .get(specId) as any;
462
-
463
- const nextNum = (result?.max_num || 0) + 1;
464
- return `DEC-${nextNum.toString().padStart(3, "0")}`;
506
+ const slug = specId.split("-").slice(1, 3).join("-");
507
+ const ts = Date.now().toString(36);
508
+ const rand = Math.random().toString(36).substring(2, 6);
509
+ return `DEC-${slug}-${ts}-${rand}`;
465
510
  }
466
511
 
467
512
  // Claim atomico de task: retorna true se task estava pending e agora esta running.
@@ -754,3 +799,82 @@ export function findDuplicateUtilities(
754
799
  "SELECT * FROM project_utilities WHERE utility_name = ?"
755
800
  ).all(utilityName) as any[];
756
801
  }
802
+
803
+ // ═══════════════════════════════════════════════════════════════
804
+ // v9.3: Agent Performance Tracking (Feedback Loop)
805
+ // ═══════════════════════════════════════════════════════════════
806
+
807
+ export interface AgentPerformanceData {
808
+ agentType: string;
809
+ specId: string;
810
+ taskId: number;
811
+ gatesPassedFirstTry: number;
812
+ gatesTotal: number;
813
+ bypassesUsed: number;
814
+ filesCreated: number;
815
+ filesModified: number;
816
+ contextSizeBytes: number;
817
+ executionDurationMs: number;
818
+ }
819
+
820
+ export function recordAgentPerformance(data: AgentPerformanceData): void {
821
+ const db = getDb();
822
+ const now = new Date().toISOString();
823
+ db.run(
824
+ `INSERT INTO agent_performance
825
+ (agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, files_created, files_modified, context_size_bytes, execution_duration_ms, created_at)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
827
+ [
828
+ data.agentType, data.specId, data.taskId,
829
+ data.gatesPassedFirstTry, data.gatesTotal, data.bypassesUsed,
830
+ data.filesCreated, data.filesModified,
831
+ data.contextSizeBytes, data.executionDurationMs, now,
832
+ ]
833
+ );
834
+ }
835
+
836
+ export function getAgentHints(agentType: string, limit: number = 5): string[] {
837
+ const db = getDb();
838
+ const hints: string[] = [];
839
+
840
+ try {
841
+ const recent = db.query(
842
+ `SELECT * FROM agent_performance
843
+ WHERE agent_type = ?
844
+ ORDER BY created_at DESC LIMIT ?`
845
+ ).all(agentType, limit) as any[];
846
+
847
+ if (recent.length === 0) return [];
848
+
849
+ const avgBypass = recent.reduce((sum: number, r: any) => sum + r.bypasses_used, 0) / recent.length;
850
+ const avgGateRate = recent.reduce((sum: number, r: any) => {
851
+ return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
852
+ }, 0) / recent.length;
853
+
854
+ if (avgBypass > 0.5) {
855
+ hints.push(`ATENCAO: Este agente usa bypasses frequentemente (media ${avgBypass.toFixed(1)}/task). Revise standards antes de iniciar.`);
856
+ }
857
+
858
+ if (avgGateRate < 0.7) {
859
+ hints.push(`ATENCAO: Gate pass rate baixo (${(avgGateRate * 100).toFixed(0)}%). Verifique standards e DRY obrigatorios.`);
860
+ }
861
+
862
+ const bypassTypes = db.query(
863
+ `SELECT gb.gate_name, COUNT(*) as cnt FROM gate_bypasses gb
864
+ JOIN tasks t ON gb.task_id = t.id
865
+ WHERE t.agent = ?
866
+ GROUP BY gb.gate_name
867
+ ORDER BY cnt DESC LIMIT 3`
868
+ ).all(agentType) as any[];
869
+
870
+ for (const bp of bypassTypes) {
871
+ if (bp.cnt >= 2) {
872
+ hints.push(`Gate '${bp.gate_name}' frequentemente ignorado (${bp.cnt}x). Preste atencao especial.`);
873
+ }
874
+ }
875
+ } catch {
876
+ // Tabela pode nao existir ainda
877
+ }
878
+
879
+ return hints;
880
+ }
@@ -615,3 +615,61 @@ describe("enforceGate", () => {
615
615
  expect(result.resolution).toBeDefined();
616
616
  });
617
617
  });
618
+
619
+ // ═══════════════════════════════════════════════════════════════
620
+ // v9.3: Recovery Strategies
621
+ // ═══════════════════════════════════════════════════════════════
622
+
623
+ describe("Recovery Strategies (P3.3)", () => {
624
+ it("GateError should carry recovery suggestion", () => {
625
+ try {
626
+ // "task-done" with no taskId fails on "task-is-running"
627
+ // task-is-running has no recovery strategy, so recovery should be undefined
628
+ enforceGate("task-done", {});
629
+ expect(true).toBe(false);
630
+ } catch (e) {
631
+ const ge = e as GateError;
632
+ expect(ge instanceof GateError).toBe(true);
633
+ // task-is-running has no recovery strategy
634
+ expect(ge.recovery).toBeUndefined();
635
+ }
636
+ });
637
+
638
+ it("validateGate should return recovery for checkpoint-filled failure", () => {
639
+ // Simulate a task-done call where task-is-running passes but checkpoint fails
640
+ // We pass taskId to skip task-is-running (it needs DB), so we test checkpoint directly
641
+ const result = validateGate("task-done", { taskId: null });
642
+ // Without taskId, task-is-running fails first (no recovery for it)
643
+ expect(result.passed).toBe(false);
644
+ });
645
+
646
+ it("GateError recovery field should have correct structure when present", () => {
647
+ const { RecoverySuggestion } = require("../errors");
648
+ const recovery = {
649
+ diagnostic: "Erros TypeScript encontrados:\nsrc/foo.ts:10 - TS2322",
650
+ steps: [
651
+ "Corrija os erros de tipo listados",
652
+ "Verifique imports e definicoes de tipo",
653
+ ],
654
+ command: "bunx tsc --noEmit",
655
+ };
656
+
657
+ const err = new GateError("test reason", "test resolution", "typecheck-pass", recovery);
658
+ expect(err.recovery).toBeDefined();
659
+ expect(err.recovery!.diagnostic).toContain("Erros TypeScript");
660
+ expect(err.recovery!.steps).toHaveLength(2);
661
+ expect(err.recovery!.steps[0]).toContain("Corrija");
662
+ expect(err.recovery!.command).toBe("bunx tsc --noEmit");
663
+ });
664
+
665
+ it("GateError without recovery should have undefined recovery", () => {
666
+ const err = new GateError("test reason", "test resolution", "unknown-gate");
667
+ expect(err.recovery).toBeUndefined();
668
+ });
669
+
670
+ it("validateGate for unknown command should pass with no recovery", () => {
671
+ const result = validateGate("nonexistent", {});
672
+ expect(result.passed).toBe(true);
673
+ expect(result.recovery).toBeUndefined();
674
+ });
675
+ });
@@ -5,12 +5,14 @@ import { validateAgainstStandards, printValidationResult } from "./standards-val
5
5
  import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
6
6
  import { extractUtilitiesFromFile } from "../commands/patterns";
7
7
  import { findDuplicateUtilities } from "../db/schema";
8
- import { GateError } from "../errors";
8
+ import { GateError, RecoverySuggestion } from "../errors";
9
+ import { resolveSpecOrNull } from "../commands/spec-resolver";
9
10
 
10
11
  export interface GateResult {
11
12
  passed: boolean;
12
13
  reason?: string;
13
14
  resolution?: string;
15
+ recovery?: RecoverySuggestion;
14
16
  }
15
17
 
16
18
  interface GateCheck {
@@ -64,8 +66,8 @@ const GATES: Record<string, GateCheck[]> = {
64
66
  },
65
67
  {
66
68
  check: "checkpoint-filled",
67
- message: "Checkpoint obrigatorio (min 30 chars, 5 palavras)",
68
- resolution: "Forneca --checkpoint 'resumo detalhado do que foi feito' (min 30 caracteres e 5 palavras)",
69
+ message: "Checkpoint obrigatorio (min 10 chars)",
70
+ resolution: "Forneca --checkpoint 'resumo do que foi feito' (min 10 caracteres)",
69
71
  },
70
72
  {
71
73
  check: "files-exist",
@@ -109,9 +111,59 @@ const GATES: Record<string, GateCheck[]> = {
109
111
  ],
110
112
  };
111
113
 
112
- function getActiveSpec(): any {
113
- const db = getDb();
114
- return db.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1").get();
114
+ // v9.3: Estrategias de recuperacao por gate — diagnostico + passos concretos
115
+ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggestion> = {
116
+ "standards-follow": (details) => ({
117
+ diagnostic: `Violacoes de standards detectadas:\n${details || "Detalhes nao disponiveis"}`,
118
+ steps: [
119
+ "Revise as violacoes listadas acima",
120
+ "Corrija o codigo para seguir os standards obrigatorios",
121
+ "Ou use --force --force-reason 'motivo' para bypass (auditado no review)",
122
+ ],
123
+ command: "codexa context detail standards",
124
+ }),
125
+ "dry-check": (details) => ({
126
+ diagnostic: `Utilities duplicadas encontradas:\n${details || "Detalhes nao disponiveis"}`,
127
+ steps: [
128
+ "Verifique as utilities existentes",
129
+ "Importe do arquivo existente em vez de recriar",
130
+ "Se intencional, use --force --force-reason 'motivo'",
131
+ ],
132
+ }),
133
+ "typecheck-pass": (details) => ({
134
+ diagnostic: `Erros TypeScript encontrados:\n${details || "Detalhes nao disponiveis"}`,
135
+ steps: [
136
+ "Corrija os erros de tipo listados",
137
+ "Verifique imports e definicoes de tipo",
138
+ "Se erros em deps externas, use --force --force-reason 'motivo'",
139
+ ],
140
+ }),
141
+ "files-exist": (details) => ({
142
+ diagnostic: `Arquivos esperados nao encontrados:\n${details || "Detalhes nao disponiveis"}`,
143
+ steps: [
144
+ "Verifique se o subagent usou Write/Edit para criar os arquivos",
145
+ "Confirme que os caminhos em --files correspondem aos arquivos reais",
146
+ "Verifique conteudo valido (nao vazio, estrutura correta)",
147
+ ],
148
+ }),
149
+ "checkpoint-filled": (details) => ({
150
+ diagnostic: `Checkpoint invalido: ${details || "muito curto ou incompleto"}`,
151
+ steps: [
152
+ "Forneca --checkpoint com resumo do que foi feito (min 10 chars)",
153
+ "Descreva O QUE foi feito, nao apenas 'feito' ou 'ok'",
154
+ ],
155
+ }),
156
+ "reasoning-provided": () => ({
157
+ diagnostic: "Subagent retornou sem reasoning.approach adequado",
158
+ steps: [
159
+ "Inclua 'reasoning.approach' no retorno JSON do subagent (min 20 chars)",
160
+ "Descreva COMO o problema foi abordado",
161
+ ],
162
+ }),
163
+ };
164
+
165
+ function getActiveSpec(specId?: string): any {
166
+ return resolveSpecOrNull(specId);
115
167
  }
116
168
 
117
169
  function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
@@ -119,24 +171,24 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
119
171
 
120
172
  switch (check) {
121
173
  case "plan-exists": {
122
- const spec = getActiveSpec();
174
+ const spec = getActiveSpec(context.specId);
123
175
  return { passed: spec !== null };
124
176
  }
125
177
 
126
178
  case "has-tasks": {
127
- const spec = getActiveSpec();
179
+ const spec = getActiveSpec(context.specId);
128
180
  if (!spec) return { passed: false };
129
181
  const count = db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(spec.id) as any;
130
182
  return { passed: count.c > 0 };
131
183
  }
132
184
 
133
185
  case "phase-is-checking": {
134
- const spec = getActiveSpec();
186
+ const spec = getActiveSpec(context.specId);
135
187
  return { passed: spec?.phase === "checking" };
136
188
  }
137
189
 
138
190
  case "spec-approved": {
139
- const spec = getActiveSpec();
191
+ const spec = getActiveSpec(context.specId);
140
192
  return { passed: spec?.approved_at !== null };
141
193
  }
142
194
 
@@ -166,18 +218,14 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
166
218
 
167
219
  case "checkpoint-filled": {
168
220
  const cp = context.checkpoint?.trim() || "";
169
- if (cp.length < 30) {
170
- return { passed: false, details: "Checkpoint deve ter pelo menos 30 caracteres" };
171
- }
172
- const wordCount = cp.split(/\s+/).filter((w: string) => w.length > 1).length;
173
- if (wordCount < 5) {
174
- return { passed: false, details: "Checkpoint deve ter pelo menos 5 palavras" };
221
+ if (cp.length < 10) {
222
+ return { passed: false, details: "Checkpoint deve ter pelo menos 10 caracteres" };
175
223
  }
176
224
  return { passed: true };
177
225
  }
178
226
 
179
227
  case "all-tasks-done": {
180
- const spec = getActiveSpec();
228
+ const spec = getActiveSpec(context.specId);
181
229
  if (!spec) return { passed: false };
182
230
  const pending = db.query(
183
231
  "SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
@@ -189,7 +237,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
189
237
  }
190
238
 
191
239
  case "review-exists": {
192
- const spec = getActiveSpec();
240
+ const spec = getActiveSpec(context.specId);
193
241
  if (!spec) return { passed: false };
194
242
  const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id);
195
243
  return { passed: review !== null };
@@ -200,6 +248,8 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
200
248
 
201
249
  // v8.0: Validar não apenas existência, mas conteúdo mínimo
202
250
  // v9.2: Validar que arquivo foi modificado DURANTE a task
251
+ // v9.3: Tolerancia de 5s para clock skew em sandbox
252
+ const MTIME_TOLERANCE_MS = 5000;
203
253
  const issues: string[] = [];
204
254
 
205
255
  // Buscar started_at da task para comparacao temporal
@@ -217,13 +267,14 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
217
267
  issues.push(`${file}: ${validation.reason}`);
218
268
  continue;
219
269
  }
220
- // Verificar que arquivo foi tocado durante a task
270
+ // Verificar que arquivo foi tocado durante a task (com tolerancia)
221
271
  if (taskStartTime) {
222
272
  try {
223
273
  const stat = statSync(file);
224
274
  const mtime = stat.mtimeMs;
225
- if (mtime < taskStartTime) {
226
- issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime anterior ao start)`);
275
+ if (mtime < (taskStartTime - MTIME_TOLERANCE_MS)) {
276
+ const diffSec = Math.round((taskStartTime - mtime) / 1000);
277
+ issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
227
278
  }
228
279
  } catch {
229
280
  // statSync falhou — arquivo pode nao existir (ja reportado por validateFileContent)
@@ -342,7 +393,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
342
393
  }
343
394
 
344
395
  case "no-critical-blockers": {
345
- const spec = getActiveSpec();
396
+ const spec = getActiveSpec(context.specId);
346
397
  if (!spec) return { passed: true };
347
398
 
348
399
  const allKnowledge = db
@@ -356,13 +407,10 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
356
407
  .all(spec.id) as any[];
357
408
 
358
409
  const unresolved = allKnowledge.filter((k: any) => {
359
- if (!k.acknowledged_by) return true;
360
- try {
361
- const acked = JSON.parse(k.acknowledged_by) as number[];
362
- return acked.length === 0;
363
- } catch {
364
- return true;
365
- }
410
+ const hasAck = db.query(
411
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ?"
412
+ ).get(k.id);
413
+ return !hasAck;
366
414
  });
367
415
 
368
416
  if (unresolved.length === 0) return { passed: true };
@@ -422,12 +470,16 @@ export function validateGate(command: string, context: any = {}): GateResult {
422
470
  const result = executeCheck(gate.check, context);
423
471
 
424
472
  if (!result.passed) {
473
+ const recoveryFn = RECOVERY_STRATEGIES[gate.check];
474
+ const recovery = recoveryFn ? recoveryFn(result.details) : undefined;
475
+
425
476
  return {
426
477
  passed: false,
427
478
  reason: result.details
428
479
  ? `${gate.message}: ${result.details}`
429
480
  : gate.message,
430
481
  resolution: gate.resolution,
482
+ recovery,
431
483
  };
432
484
  }
433
485
  }
@@ -442,7 +494,8 @@ export function enforceGate(command: string, context: any = {}): void {
442
494
  throw new GateError(
443
495
  result.reason || "Gate falhou",
444
496
  result.resolution || "Verifique o estado atual",
445
- command
497
+ command,
498
+ result.recovery
446
499
  );
447
500
  }
448
501
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.3",
3
+ "version": "9.0.5",
4
4
  "description": "Orchestrated workflow system for Claude Code - manages feature development through parallel subagents with structured phases, gates, and quality enforcement.",
5
5
  "type": "module",
6
6
  "bin": {