@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/commands/architect.ts +7 -6
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -0
- package/commands/decide.ts +9 -21
- package/commands/discover.ts +11 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +190 -75
- package/commands/patterns.ts +6 -13
- package/commands/plan.ts +14 -64
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +2 -3
- package/commands/task.ts +30 -9
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -708
- package/db/schema.test.ts +475 -48
- package/db/schema.ts +136 -12
- package/gates/validator.test.ts +58 -0
- package/gates/validator.ts +83 -30
- package/package.json +1 -1
- package/workflow.ts +113 -61
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
|
|
453
|
-
// Usa
|
|
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
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
}
|
package/gates/validator.test.ts
CHANGED
|
@@ -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
|
+
});
|
package/gates/validator.ts
CHANGED
|
@@ -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
|
|
68
|
-
resolution: "Forneca --checkpoint 'resumo
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 <
|
|
170
|
-
return { passed: false, details: "Checkpoint deve ter pelo menos
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
"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": {
|