@codexa/cli 9.0.30 → 9.0.32
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 +52 -87
- package/commands/check.ts +22 -23
- package/commands/clear.ts +42 -48
- package/commands/decide.ts +49 -47
- package/commands/discover.ts +81 -94
- package/commands/integration.test.ts +262 -313
- package/commands/knowledge.test.ts +56 -61
- package/commands/knowledge.ts +126 -131
- package/commands/patterns.ts +28 -43
- package/commands/plan.ts +50 -48
- package/commands/product.ts +57 -59
- package/commands/research.ts +64 -77
- package/commands/review.ts +100 -86
- package/commands/simplify.ts +24 -35
- package/commands/spec-resolver.test.ts +52 -48
- package/commands/spec-resolver.ts +21 -23
- package/commands/standards.ts +20 -27
- package/commands/sync.ts +2 -8
- package/commands/task.ts +106 -97
- package/commands/team.test.ts +22 -83
- package/commands/team.ts +62 -50
- package/commands/utils.ts +83 -81
- package/context/assembly.ts +0 -1
- package/context/generator.ts +66 -79
- package/context/sections.ts +8 -14
- package/db/connection.ts +195 -19
- package/db/schema.test.ts +304 -298
- package/db/schema.ts +302 -392
- package/db/test-helpers.ts +18 -29
- package/gates/standards-validator.test.ts +83 -86
- package/gates/standards-validator.ts +9 -41
- package/gates/validator.test.ts +13 -22
- package/gates/validator.ts +69 -107
- package/package.json +2 -1
- package/protocol/process-return.ts +41 -57
- package/simplify/prompt-builder.test.ts +44 -42
- package/simplify/prompt-builder.ts +12 -14
- package/workflow.ts +159 -174
package/gates/validator.test.ts
CHANGED
|
@@ -588,12 +588,10 @@ class User:
|
|
|
588
588
|
// ═══════════════════════════════════════════════════════════════
|
|
589
589
|
|
|
590
590
|
describe("enforceGate", () => {
|
|
591
|
-
it("should throw GateError when gate fails", () => {
|
|
592
|
-
// "task-done" with no taskId makes "task-is-running" fail immediately
|
|
593
|
-
// without needing DB access
|
|
591
|
+
it("should throw GateError when gate fails", async () => {
|
|
594
592
|
try {
|
|
595
|
-
enforceGate("task-done", {});
|
|
596
|
-
expect(true).toBe(false);
|
|
593
|
+
await enforceGate("task-done", {});
|
|
594
|
+
expect(true).toBe(false);
|
|
597
595
|
} catch (e) {
|
|
598
596
|
expect(e instanceof GateError).toBe(true);
|
|
599
597
|
const ge = e as GateError;
|
|
@@ -603,13 +601,12 @@ describe("enforceGate", () => {
|
|
|
603
601
|
}
|
|
604
602
|
});
|
|
605
603
|
|
|
606
|
-
it("should not throw for unknown command (no gates defined)", () => {
|
|
607
|
-
|
|
608
|
-
expect(() => enforceGate("nonexistent-command")).not.toThrow();
|
|
604
|
+
it("should not throw for unknown command (no gates defined)", async () => {
|
|
605
|
+
await enforceGate("nonexistent-command");
|
|
609
606
|
});
|
|
610
607
|
|
|
611
|
-
it("should return a GateResult from validateGate", () => {
|
|
612
|
-
const result = validateGate("task-done", {});
|
|
608
|
+
it("should return a GateResult from validateGate", async () => {
|
|
609
|
+
const result = await validateGate("task-done", {});
|
|
613
610
|
expect(result.passed).toBe(false);
|
|
614
611
|
expect(result.reason).toBeDefined();
|
|
615
612
|
expect(result.resolution).toBeDefined();
|
|
@@ -621,25 +618,19 @@ describe("enforceGate", () => {
|
|
|
621
618
|
// ═══════════════════════════════════════════════════════════════
|
|
622
619
|
|
|
623
620
|
describe("Recovery Strategies (P3.3)", () => {
|
|
624
|
-
it("GateError should carry recovery suggestion", () => {
|
|
621
|
+
it("GateError should carry recovery suggestion", async () => {
|
|
625
622
|
try {
|
|
626
|
-
|
|
627
|
-
// task-is-running has no recovery strategy, so recovery should be undefined
|
|
628
|
-
enforceGate("task-done", {});
|
|
623
|
+
await enforceGate("task-done", {});
|
|
629
624
|
expect(true).toBe(false);
|
|
630
625
|
} catch (e) {
|
|
631
626
|
const ge = e as GateError;
|
|
632
627
|
expect(ge instanceof GateError).toBe(true);
|
|
633
|
-
// task-is-running has no recovery strategy
|
|
634
628
|
expect(ge.recovery).toBeUndefined();
|
|
635
629
|
}
|
|
636
630
|
});
|
|
637
631
|
|
|
638
|
-
it("validateGate should return recovery for checkpoint-filled failure", () => {
|
|
639
|
-
|
|
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)
|
|
632
|
+
it("validateGate should return recovery for checkpoint-filled failure", async () => {
|
|
633
|
+
const result = await validateGate("task-done", { taskId: null });
|
|
643
634
|
expect(result.passed).toBe(false);
|
|
644
635
|
});
|
|
645
636
|
|
|
@@ -667,8 +658,8 @@ describe("Recovery Strategies (P3.3)", () => {
|
|
|
667
658
|
expect(err.recovery).toBeUndefined();
|
|
668
659
|
});
|
|
669
660
|
|
|
670
|
-
it("validateGate for unknown command should pass with no recovery", () => {
|
|
671
|
-
const result = validateGate("nonexistent", {});
|
|
661
|
+
it("validateGate for unknown command should pass with no recovery", async () => {
|
|
662
|
+
const result = await validateGate("nonexistent", {});
|
|
672
663
|
expect(result.passed).toBe(true);
|
|
673
664
|
expect(result.recovery).toBeUndefined();
|
|
674
665
|
});
|
package/gates/validator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dbGet, dbAll, dbRun } from "../db/connection";
|
|
2
2
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
3
|
import { extname } from "path";
|
|
4
4
|
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
@@ -194,47 +194,47 @@ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggesti
|
|
|
194
194
|
}),
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
function getActiveSpec(specId?: string): any {
|
|
198
|
-
return resolveSpecOrNull(specId);
|
|
197
|
+
async function getActiveSpec(specId?: string): Promise<any> {
|
|
198
|
+
return await resolveSpecOrNull(specId);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
|
|
202
|
-
const db = getDb();
|
|
203
|
-
|
|
201
|
+
async function executeCheck(check: string, context: any): Promise<{ passed: boolean; details?: string }> {
|
|
204
202
|
switch (check) {
|
|
205
203
|
case "plan-exists": {
|
|
206
|
-
const spec = getActiveSpec(context.specId);
|
|
204
|
+
const spec = await getActiveSpec(context.specId);
|
|
207
205
|
return { passed: spec !== null };
|
|
208
206
|
}
|
|
209
207
|
|
|
210
208
|
case "has-tasks": {
|
|
211
|
-
const spec = getActiveSpec(context.specId);
|
|
209
|
+
const spec = await getActiveSpec(context.specId);
|
|
212
210
|
if (!spec) return { passed: false };
|
|
213
|
-
const count =
|
|
214
|
-
return { passed: count
|
|
211
|
+
const count = await dbGet<any>("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?", [spec.id]);
|
|
212
|
+
return { passed: count?.c > 0 };
|
|
215
213
|
}
|
|
216
214
|
|
|
217
215
|
case "phase-is-checking": {
|
|
218
|
-
const spec = getActiveSpec(context.specId);
|
|
216
|
+
const spec = await getActiveSpec(context.specId);
|
|
219
217
|
return { passed: spec?.phase === "checking" };
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
case "spec-approved": {
|
|
223
|
-
const spec = getActiveSpec(context.specId);
|
|
221
|
+
const spec = await getActiveSpec(context.specId);
|
|
224
222
|
return { passed: spec?.approved_at !== null };
|
|
225
223
|
}
|
|
226
224
|
|
|
227
225
|
case "dependencies-done": {
|
|
228
226
|
if (!context.taskId) return { passed: true };
|
|
229
|
-
const task =
|
|
227
|
+
const task = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [context.taskId]);
|
|
230
228
|
if (!task || !task.depends_on) return { passed: true };
|
|
231
229
|
|
|
232
230
|
const deps = JSON.parse(task.depends_on) as number[];
|
|
233
|
-
const pending =
|
|
234
|
-
|
|
235
|
-
const depTask =
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
const pending: number[] = [];
|
|
232
|
+
for (const depNum of deps) {
|
|
233
|
+
const depTask = await dbGet<any>("SELECT status FROM tasks WHERE spec_id = ? AND number = ?", [task.spec_id, depNum]);
|
|
234
|
+
if (depTask?.status !== "done") {
|
|
235
|
+
pending.push(depNum);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
238
|
|
|
239
239
|
return {
|
|
240
240
|
passed: pending.length === 0,
|
|
@@ -244,7 +244,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
244
244
|
|
|
245
245
|
case "task-is-running": {
|
|
246
246
|
if (!context.taskId) return { passed: false };
|
|
247
|
-
const task =
|
|
247
|
+
const task = await dbGet<any>("SELECT status FROM tasks WHERE id = ?", [context.taskId]);
|
|
248
248
|
return { passed: task?.status === "running" };
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -257,11 +257,12 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
case "all-tasks-done": {
|
|
260
|
-
const spec = getActiveSpec(context.specId);
|
|
260
|
+
const spec = await getActiveSpec(context.specId);
|
|
261
261
|
if (!spec) return { passed: false };
|
|
262
|
-
const pending =
|
|
263
|
-
"SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
|
|
264
|
-
|
|
262
|
+
const pending = await dbAll<any>(
|
|
263
|
+
"SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'",
|
|
264
|
+
[spec.id]
|
|
265
|
+
);
|
|
265
266
|
return {
|
|
266
267
|
passed: pending.length === 0,
|
|
267
268
|
details: pending.map((t) => `#${t.number}`).join(", "),
|
|
@@ -269,33 +270,27 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
case "review-exists": {
|
|
272
|
-
const spec = getActiveSpec(context.specId);
|
|
273
|
+
const spec = await getActiveSpec(context.specId);
|
|
273
274
|
if (!spec) return { passed: false };
|
|
274
|
-
const review =
|
|
275
|
+
const review = await dbGet("SELECT * FROM review WHERE spec_id = ?", [spec.id]);
|
|
275
276
|
return { passed: review !== null };
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
case "files-exist": {
|
|
279
280
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
280
281
|
|
|
281
|
-
// v8.0: Validar não apenas existência, mas conteúdo mínimo
|
|
282
|
-
// v9.2: Validar que arquivo foi modificado DURANTE a task
|
|
283
|
-
// v9.3: Tolerancia de 5s para clock skew em sandbox
|
|
284
|
-
// v9.6: Confiar no protocolo quando subagent reporta arquivos (sandbox)
|
|
285
282
|
const MTIME_TOLERANCE_MS = 5000;
|
|
286
283
|
const issues: string[] = [];
|
|
287
284
|
|
|
288
|
-
// Construir set de arquivos reportados pelo subagent (podem estar em sandbox)
|
|
289
285
|
const subagentReportedFiles = new Set<string>();
|
|
290
286
|
if (context.subagentData) {
|
|
291
287
|
for (const f of context.subagentData.files_created || []) subagentReportedFiles.add(f);
|
|
292
288
|
for (const f of context.subagentData.files_modified || []) subagentReportedFiles.add(f);
|
|
293
289
|
}
|
|
294
290
|
|
|
295
|
-
// Buscar started_at da task para comparacao temporal
|
|
296
291
|
let taskStartTime: number | null = null;
|
|
297
292
|
if (context.taskId) {
|
|
298
|
-
const taskRow =
|
|
293
|
+
const taskRow = await dbGet<any>("SELECT started_at FROM tasks WHERE id = ?", [context.taskId]);
|
|
299
294
|
if (taskRow?.started_at) {
|
|
300
295
|
taskStartTime = new Date(taskRow.started_at).getTime();
|
|
301
296
|
}
|
|
@@ -303,10 +298,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
303
298
|
|
|
304
299
|
for (const file of context.files) {
|
|
305
300
|
if (!existsSync(file)) {
|
|
306
|
-
// Arquivo nao existe no disco — verificar se veio de subagent
|
|
307
301
|
if (subagentReportedFiles.has(file)) {
|
|
308
|
-
// Subagent reportou o arquivo via protocolo → confiar
|
|
309
|
-
// (arquivo pode estar em sandbox, sera materializado no commit)
|
|
310
302
|
continue;
|
|
311
303
|
}
|
|
312
304
|
issues.push(`${file}: arquivo nao encontrado no disco`);
|
|
@@ -318,7 +310,6 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
318
310
|
issues.push(`${file}: ${validation.reason}`);
|
|
319
311
|
continue;
|
|
320
312
|
}
|
|
321
|
-
// Verificar que arquivo foi tocado durante a task (com tolerancia)
|
|
322
313
|
if (taskStartTime) {
|
|
323
314
|
try {
|
|
324
315
|
const stat = statSync(file);
|
|
@@ -328,7 +319,6 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
328
319
|
issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
|
|
329
320
|
}
|
|
330
321
|
} catch {
|
|
331
|
-
// statSync falhou — arquivo pode nao existir (ja reportado acima)
|
|
332
322
|
}
|
|
333
323
|
}
|
|
334
324
|
}
|
|
@@ -340,23 +330,18 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
340
330
|
}
|
|
341
331
|
|
|
342
332
|
case "standards-follow": {
|
|
343
|
-
// Se --force foi passado, registrar bypass e passar
|
|
344
333
|
if (context.force) {
|
|
345
|
-
logGateBypass(context.taskId, "standards-follow", context.forceReason);
|
|
334
|
+
await logGateBypass(context.taskId, "standards-follow", context.forceReason);
|
|
346
335
|
return { passed: true };
|
|
347
336
|
}
|
|
348
337
|
|
|
349
|
-
// Se nenhum arquivo, passar
|
|
350
338
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
351
339
|
|
|
352
|
-
|
|
353
|
-
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
340
|
+
const task = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [context.taskId]);
|
|
354
341
|
const agentDomain = domainToScope(getAgentDomain(task?.agent));
|
|
355
342
|
|
|
356
|
-
|
|
357
|
-
const result = validateAgainstStandards(context.files, agentDomain);
|
|
343
|
+
const result = await validateAgainstStandards(context.files, agentDomain);
|
|
358
344
|
|
|
359
|
-
// Mostrar resultado se houver violacoes
|
|
360
345
|
if (!result.passed || result.warnings.length > 0) {
|
|
361
346
|
printValidationResult(result);
|
|
362
347
|
}
|
|
@@ -370,9 +355,8 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
370
355
|
}
|
|
371
356
|
|
|
372
357
|
case "dry-check": {
|
|
373
|
-
// v8.5: Verificar se arquivos criados exportam utilities que ja existem
|
|
374
358
|
if (context.force) {
|
|
375
|
-
logGateBypass(context.taskId, "dry-check", context.forceReason);
|
|
359
|
+
await logGateBypass(context.taskId, "dry-check", context.forceReason);
|
|
376
360
|
return { passed: true };
|
|
377
361
|
}
|
|
378
362
|
|
|
@@ -384,14 +368,12 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
384
368
|
if (!existsSync(file)) continue;
|
|
385
369
|
|
|
386
370
|
const fileScope = inferScopeFromPath(file);
|
|
387
|
-
// Arquivos de teste nao conflitam com production code
|
|
388
371
|
if (fileScope === "testing") continue;
|
|
389
372
|
|
|
390
373
|
const utilities = extractUtilitiesFromFile(file);
|
|
391
374
|
|
|
392
375
|
for (const util of utilities) {
|
|
393
|
-
const existing = findDuplicateUtilities(util.name, file);
|
|
394
|
-
// Filtrar: duplicatas em scope "testing" nao contam
|
|
376
|
+
const existing = await findDuplicateUtilities(util.name, file);
|
|
395
377
|
const realDuplicates = existing.filter(
|
|
396
378
|
(e: any) => inferScopeFromPath(e.file_path) !== "testing"
|
|
397
379
|
);
|
|
@@ -404,7 +386,6 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
404
386
|
}
|
|
405
387
|
|
|
406
388
|
if (duplicates.length > 0) {
|
|
407
|
-
// Apenas function/const/class bloqueiam; interface/type so avisam
|
|
408
389
|
const blockingDupes = duplicates.filter(d =>
|
|
409
390
|
d.includes("(function)") || d.includes("(const)") || d.includes("(class)")
|
|
410
391
|
);
|
|
@@ -420,7 +401,6 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
420
401
|
details: blockingDupes.join("\n"),
|
|
421
402
|
};
|
|
422
403
|
} else {
|
|
423
|
-
// Types/interfaces: aviso nao-bloqueante
|
|
424
404
|
console.warn("\n[DRY] Aviso: nomes de tipo duplicados (nao bloqueante):");
|
|
425
405
|
for (const d of duplicates) {
|
|
426
406
|
console.warn(` - ${d}`);
|
|
@@ -434,14 +414,13 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
434
414
|
|
|
435
415
|
case "typecheck-pass": {
|
|
436
416
|
if (context.force) {
|
|
437
|
-
logGateBypass(context.taskId, "typecheck-pass", context.forceReason);
|
|
417
|
+
await logGateBypass(context.taskId, "typecheck-pass", context.forceReason);
|
|
438
418
|
return { passed: true };
|
|
439
419
|
}
|
|
440
420
|
|
|
441
421
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
442
422
|
|
|
443
|
-
|
|
444
|
-
const projectRow = db.query("SELECT stack, typecheck_command FROM project WHERE id = 'default'").get() as any;
|
|
423
|
+
const projectRow = await dbGet<any>("SELECT stack, typecheck_command FROM project WHERE id = 'default'");
|
|
445
424
|
const typecheckResult = runTypecheck(context.files, {
|
|
446
425
|
customCommand: projectRow?.typecheck_command || null,
|
|
447
426
|
stackJson: projectRow?.stack || null,
|
|
@@ -458,25 +437,28 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
458
437
|
}
|
|
459
438
|
|
|
460
439
|
case "no-critical-blockers": {
|
|
461
|
-
const spec = getActiveSpec(context.specId);
|
|
440
|
+
const spec = await getActiveSpec(context.specId);
|
|
462
441
|
if (!spec) return { passed: true };
|
|
463
442
|
|
|
464
|
-
const allKnowledge =
|
|
465
|
-
.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
const hasAck =
|
|
476
|
-
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ?"
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
443
|
+
const allKnowledge = await dbAll<any>(
|
|
444
|
+
`SELECT k.*, t.number as task_number, t.name as task_name
|
|
445
|
+
FROM knowledge k
|
|
446
|
+
LEFT JOIN tasks t ON k.task_origin = t.id
|
|
447
|
+
WHERE k.spec_id = ? AND k.severity = 'critical'
|
|
448
|
+
ORDER BY k.created_at DESC`,
|
|
449
|
+
[spec.id]
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
const unresolved: any[] = [];
|
|
453
|
+
for (const k of allKnowledge) {
|
|
454
|
+
const hasAck = await dbGet(
|
|
455
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ?",
|
|
456
|
+
[k.id]
|
|
457
|
+
);
|
|
458
|
+
if (!hasAck) {
|
|
459
|
+
unresolved.push(k);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
480
462
|
|
|
481
463
|
if (unresolved.length === 0) return { passed: true };
|
|
482
464
|
|
|
@@ -493,23 +475,21 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
493
475
|
|
|
494
476
|
case "decision-conflicts": {
|
|
495
477
|
if (context.force) {
|
|
496
|
-
logGateBypass(context.taskId, "decision-conflicts", context.forceReason);
|
|
478
|
+
await logGateBypass(context.taskId, "decision-conflicts", context.forceReason);
|
|
497
479
|
return { passed: true };
|
|
498
480
|
}
|
|
499
481
|
|
|
500
482
|
if (!context.subagentData?.decisions_made?.length) return { passed: true };
|
|
501
483
|
|
|
502
|
-
const dcTask =
|
|
484
|
+
const dcTask = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [context.taskId]);
|
|
503
485
|
if (!dcTask) return { passed: true };
|
|
504
486
|
|
|
505
|
-
const existingDecisions =
|
|
506
|
-
|
|
507
|
-
|
|
487
|
+
const existingDecisions = await dbAll<any>(
|
|
488
|
+
"SELECT * FROM decisions WHERE spec_id = ? AND status = 'active'",
|
|
489
|
+
[dcTask.spec_id]
|
|
490
|
+
);
|
|
508
491
|
if (existingDecisions.length === 0) return { passed: true };
|
|
509
492
|
|
|
510
|
-
// v10.1: Filter out decisions from the CURRENT task.
|
|
511
|
-
// processSubagentReturn() already saved them to DB before this gate runs,
|
|
512
|
-
// so without filtering, the new decisions would match against themselves.
|
|
513
493
|
const priorDecisions = existingDecisions.filter(
|
|
514
494
|
(d: any) => d.task_ref !== dcTask.number
|
|
515
495
|
);
|
|
@@ -534,18 +514,15 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
534
514
|
}
|
|
535
515
|
|
|
536
516
|
case "pattern-compliance": {
|
|
537
|
-
// v10.2: Gate 4.7 — validate code follows project patterns
|
|
538
517
|
if (context.force) {
|
|
539
|
-
logGateBypass(context.taskId, "pattern-compliance", context.forceReason);
|
|
518
|
+
await logGateBypass(context.taskId, "pattern-compliance", context.forceReason);
|
|
540
519
|
return { passed: true };
|
|
541
520
|
}
|
|
542
521
|
|
|
543
522
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
544
523
|
|
|
545
|
-
const patternResult = validatePatternCompliance(context.files);
|
|
524
|
+
const patternResult = await validatePatternCompliance(context.files);
|
|
546
525
|
if (!patternResult.passed && patternResult.violations.length > 0) {
|
|
547
|
-
// v10.2: Start in warning mode (don't block, just warn)
|
|
548
|
-
// Change to `return { passed: false, ... }` after validation in real projects
|
|
549
526
|
console.warn("\n[PATTERN] Divergencias de pattern detectadas (aviso, nao bloqueante):");
|
|
550
527
|
for (const v of patternResult.violations) {
|
|
551
528
|
console.warn(` - ${v.file}: ${v.reason} (pattern: ${v.pattern})`);
|
|
@@ -575,13 +552,12 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
575
552
|
}
|
|
576
553
|
}
|
|
577
554
|
|
|
578
|
-
function logGateBypass(taskId: number, gateName: string, reason?: string): void {
|
|
579
|
-
const
|
|
580
|
-
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
|
|
555
|
+
async function logGateBypass(taskId: number, gateName: string, reason?: string): Promise<void> {
|
|
556
|
+
const task = await dbGet<any>("SELECT * FROM tasks WHERE id = ?", [taskId]);
|
|
581
557
|
if (!task) return;
|
|
582
558
|
|
|
583
559
|
const now = new Date().toISOString();
|
|
584
|
-
|
|
560
|
+
await dbRun(
|
|
585
561
|
`INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason, created_at)
|
|
586
562
|
VALUES (?, ?, ?, ?, ?)`,
|
|
587
563
|
[task.spec_id, taskId, gateName, reason || "Nao informado", now]
|
|
@@ -592,11 +568,11 @@ function logGateBypass(taskId: number, gateName: string, reason?: string): void
|
|
|
592
568
|
console.warn(` Isso sera auditado no review.\n`);
|
|
593
569
|
}
|
|
594
570
|
|
|
595
|
-
export function validateGate(command: string, context: any = {}): GateResult {
|
|
571
|
+
export async function validateGate(command: string, context: any = {}): Promise<GateResult> {
|
|
596
572
|
const gates = GATES[command] || [];
|
|
597
573
|
|
|
598
574
|
for (const gate of gates) {
|
|
599
|
-
const result = executeCheck(gate.check, context);
|
|
575
|
+
const result = await executeCheck(gate.check, context);
|
|
600
576
|
|
|
601
577
|
if (!result.passed) {
|
|
602
578
|
const recoveryFn = RECOVERY_STRATEGIES[gate.check];
|
|
@@ -616,8 +592,8 @@ export function validateGate(command: string, context: any = {}): GateResult {
|
|
|
616
592
|
return { passed: true };
|
|
617
593
|
}
|
|
618
594
|
|
|
619
|
-
export function enforceGate(command: string, context: any = {}): void {
|
|
620
|
-
const result = validateGate(command, context);
|
|
595
|
+
export async function enforceGate(command: string, context: any = {}): Promise<void> {
|
|
596
|
+
const result = await validateGate(command, context);
|
|
621
597
|
|
|
622
598
|
if (!result.passed) {
|
|
623
599
|
throw new GateError(
|
|
@@ -629,35 +605,29 @@ export function enforceGate(command: string, context: any = {}): void {
|
|
|
629
605
|
}
|
|
630
606
|
}
|
|
631
607
|
|
|
632
|
-
// v8.0: Validar conteúdo de arquivos criados (não apenas existência)
|
|
633
608
|
export interface FileValidationResult {
|
|
634
609
|
valid: boolean;
|
|
635
610
|
reason?: string;
|
|
636
611
|
}
|
|
637
612
|
|
|
638
613
|
function validateFileContent(filePath: string): FileValidationResult {
|
|
639
|
-
// 1. Verificar existência
|
|
640
614
|
if (!existsSync(filePath)) {
|
|
641
615
|
return { valid: false, reason: "arquivo nao encontrado" };
|
|
642
616
|
}
|
|
643
617
|
|
|
644
618
|
try {
|
|
645
|
-
// 2. Verificar tamanho (arquivo vazio ou muito pequeno)
|
|
646
619
|
const stats = statSync(filePath);
|
|
647
620
|
if (stats.size === 0) {
|
|
648
621
|
return { valid: false, reason: "arquivo vazio (0 bytes)" };
|
|
649
622
|
}
|
|
650
623
|
|
|
651
|
-
// 3. Ler conteúdo e verificar estrutura mínima
|
|
652
624
|
const content = readFileSync(filePath, "utf-8");
|
|
653
625
|
const trimmed = content.trim();
|
|
654
626
|
|
|
655
|
-
// Arquivo trivialmente pequeno (< 20 caracteres não é código real)
|
|
656
627
|
if (trimmed.length < 20) {
|
|
657
628
|
return { valid: false, reason: `conteudo trivial (${trimmed.length} chars)` };
|
|
658
629
|
}
|
|
659
630
|
|
|
660
|
-
// 4. Validações específicas por extensão
|
|
661
631
|
const ext = extname(filePath).toLowerCase();
|
|
662
632
|
const validation = validateByExtension(ext, content);
|
|
663
633
|
if (!validation.valid) {
|
|
@@ -678,11 +648,9 @@ export function validateByExtension(ext: string, content: string): FileValidatio
|
|
|
678
648
|
case ".tsx":
|
|
679
649
|
case ".js":
|
|
680
650
|
case ".jsx":
|
|
681
|
-
// Arquivos JS/TS devem ter pelo menos uma declaração
|
|
682
651
|
if (!hasCodeStructure(trimmed, ["export", "import", "function", "const", "let", "var", "class", "interface", "type"])) {
|
|
683
652
|
return { valid: false, reason: "sem declaracoes validas (export, function, class, etc)" };
|
|
684
653
|
}
|
|
685
|
-
// TSX/JSX devem ter JSX ou export de componente
|
|
686
654
|
if ((ext === ".tsx" || ext === ".jsx") && !trimmed.includes("<") && !trimmed.includes("export")) {
|
|
687
655
|
return { valid: false, reason: "componente sem JSX ou export" };
|
|
688
656
|
}
|
|
@@ -691,14 +659,12 @@ export function validateByExtension(ext: string, content: string): FileValidatio
|
|
|
691
659
|
case ".css":
|
|
692
660
|
case ".scss":
|
|
693
661
|
case ".sass":
|
|
694
|
-
// CSS deve ter pelo menos um seletor e propriedade
|
|
695
662
|
if (!trimmed.includes("{") || !trimmed.includes(":")) {
|
|
696
663
|
return { valid: false, reason: "CSS sem regras validas" };
|
|
697
664
|
}
|
|
698
665
|
break;
|
|
699
666
|
|
|
700
667
|
case ".json":
|
|
701
|
-
// JSON deve ser válido
|
|
702
668
|
try {
|
|
703
669
|
JSON.parse(content);
|
|
704
670
|
} catch {
|
|
@@ -707,28 +673,24 @@ export function validateByExtension(ext: string, content: string): FileValidatio
|
|
|
707
673
|
break;
|
|
708
674
|
|
|
709
675
|
case ".sql":
|
|
710
|
-
// SQL deve ter statements
|
|
711
676
|
if (!hasCodeStructure(trimmed.toUpperCase(), ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP"])) {
|
|
712
677
|
return { valid: false, reason: "SQL sem statements validos" };
|
|
713
678
|
}
|
|
714
679
|
break;
|
|
715
680
|
|
|
716
681
|
case ".py":
|
|
717
|
-
// Python deve ter definições
|
|
718
682
|
if (!hasCodeStructure(trimmed, ["def ", "class ", "import ", "from "])) {
|
|
719
683
|
return { valid: false, reason: "Python sem definicoes (def, class, import)" };
|
|
720
684
|
}
|
|
721
685
|
break;
|
|
722
686
|
|
|
723
687
|
case ".go":
|
|
724
|
-
// Go deve ter package
|
|
725
688
|
if (!trimmed.includes("package ")) {
|
|
726
689
|
return { valid: false, reason: "Go sem declaracao package" };
|
|
727
690
|
}
|
|
728
691
|
break;
|
|
729
692
|
|
|
730
693
|
case ".md":
|
|
731
|
-
// Markdown deve ter conteúdo mínimo
|
|
732
694
|
if (trimmed.length < 50) {
|
|
733
695
|
return { valid: false, reason: "Markdown muito curto" };
|
|
734
696
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.32",
|
|
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": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"bun": ">=1.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@libsql/client": "^0.17.2",
|
|
30
31
|
"commander": "^12.0.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|