@codexa/cli 9.0.19 → 9.0.21
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/decide.ts +4 -0
- package/commands/knowledge.ts +64 -0
- package/commands/review.ts +7 -0
- package/commands/task.ts +139 -76
- package/context/assembly.ts +81 -28
- package/context/cache.ts +85 -0
- package/context/file-writer.ts +47 -0
- package/context/generator.ts +37 -7
- package/context/index.ts +8 -1
- package/context/model-profiles.ts +39 -0
- package/context/monitor.ts +106 -0
- package/package.json +1 -1
- package/protocol/process-return.ts +9 -0
- package/templates/subagent-prompt-lean.md +28 -0
- package/workflow.ts +43 -1
package/commands/decide.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { initSchema, getNextDecisionId } from "../db/schema";
|
|
3
3
|
import { resolveSpec } from "./spec-resolver";
|
|
4
4
|
import { CodexaError } from "../errors";
|
|
5
|
+
import { invalidateCache } from "../context/cache";
|
|
5
6
|
|
|
6
7
|
export interface ConflictAnalysis {
|
|
7
8
|
hasConflict: boolean;
|
|
@@ -193,6 +194,9 @@ export function decide(title: string, decision: string, options: { rationale?: s
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
|
|
197
|
+
// v10.0: Invalidate context cache (new decision affects context)
|
|
198
|
+
invalidateCache(spec.id);
|
|
199
|
+
|
|
196
200
|
console.log(`\nDecisao registrada: ${decisionId}`);
|
|
197
201
|
console.log(`Titulo: ${title}`);
|
|
198
202
|
console.log(`Decisao: ${decision}`);
|
package/commands/knowledge.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
|
|
3
3
|
import { resolveSpec } from "./spec-resolver";
|
|
4
4
|
import { CodexaError, ValidationError } from "../errors";
|
|
5
|
+
import { invalidateCache } from "../context/cache";
|
|
5
6
|
|
|
6
7
|
type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
|
|
7
8
|
type KnowledgeSeverity = "info" | "warning" | "critical";
|
|
@@ -48,6 +49,9 @@ export function addKnowledge(options: AddKnowledgeOptions): void {
|
|
|
48
49
|
[spec.id, currentTask.id, options.category, options.content, severity, broadcastTo, now]
|
|
49
50
|
);
|
|
50
51
|
|
|
52
|
+
// v10.0: Invalidate context cache (new knowledge affects context)
|
|
53
|
+
invalidateCache(spec.id);
|
|
54
|
+
|
|
51
55
|
const severityIcon = {
|
|
52
56
|
info: "i",
|
|
53
57
|
warning: "!",
|
|
@@ -516,4 +520,64 @@ export function resolveKnowledge(ids: string, resolution?: string, specId?: stri
|
|
|
516
520
|
}
|
|
517
521
|
|
|
518
522
|
console.log();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// v10.0: Compact reasoning_log during phase advance
|
|
526
|
+
export function compactReasoning(options: {
|
|
527
|
+
specId: string | number;
|
|
528
|
+
dryRun?: boolean;
|
|
529
|
+
}): { archived: number; kept: number } {
|
|
530
|
+
const db = getDb();
|
|
531
|
+
|
|
532
|
+
const doneTasks = db.query(
|
|
533
|
+
"SELECT id FROM tasks WHERE spec_id = ? AND status = 'done'"
|
|
534
|
+
).all(options.specId) as any[];
|
|
535
|
+
|
|
536
|
+
if (doneTasks.length === 0) return { archived: 0, kept: 0 };
|
|
537
|
+
|
|
538
|
+
const doneTaskIds = doneTasks.map((t: any) => t.id);
|
|
539
|
+
const placeholders = doneTaskIds.map(() => "?").join(",");
|
|
540
|
+
|
|
541
|
+
// Phase 1: Remove low-importance entries from completed tasks (keep recommendations always)
|
|
542
|
+
const lowImportance = db.query(
|
|
543
|
+
`SELECT id FROM reasoning_log
|
|
544
|
+
WHERE spec_id = ? AND task_id IN (${placeholders})
|
|
545
|
+
AND importance IN ('normal', 'low')
|
|
546
|
+
AND category != 'recommendation'`
|
|
547
|
+
).all(options.specId, ...doneTaskIds) as any[];
|
|
548
|
+
|
|
549
|
+
// Phase 2: Cap at 5 entries per task (keep most important)
|
|
550
|
+
const overflow: number[] = [];
|
|
551
|
+
for (const taskId of doneTaskIds) {
|
|
552
|
+
const entries = db.query(
|
|
553
|
+
`SELECT id FROM reasoning_log
|
|
554
|
+
WHERE spec_id = ? AND task_id = ?
|
|
555
|
+
ORDER BY
|
|
556
|
+
CASE importance WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'normal' THEN 3 ELSE 4 END,
|
|
557
|
+
created_at DESC`
|
|
558
|
+
).all(options.specId, taskId) as any[];
|
|
559
|
+
|
|
560
|
+
if (entries.length > 5) {
|
|
561
|
+
for (let i = 5; i < entries.length; i++) {
|
|
562
|
+
overflow.push((entries[i] as any).id);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const archiveIds = new Set([
|
|
568
|
+
...lowImportance.map((r: any) => r.id),
|
|
569
|
+
...overflow,
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
if (!options.dryRun && archiveIds.size > 0) {
|
|
573
|
+
const ids = Array.from(archiveIds);
|
|
574
|
+
const ph = ids.map(() => "?").join(",");
|
|
575
|
+
db.run(`DELETE FROM reasoning_log WHERE id IN (${ph})`, ids);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const kept = (db.query(
|
|
579
|
+
"SELECT COUNT(*) as c FROM reasoning_log WHERE spec_id = ?"
|
|
580
|
+
).get(options.specId) as any)?.c || 0;
|
|
581
|
+
|
|
582
|
+
return { archived: archiveIds.size, kept };
|
|
519
583
|
}
|
package/commands/review.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
4
|
import { resolveSpec } from "./spec-resolver";
|
|
5
5
|
import { CodexaError, GateError } from "../errors";
|
|
6
|
+
import { cleanupContextFiles } from "../context/file-writer";
|
|
6
7
|
|
|
7
8
|
// ═══════════════════════════════════════════════════════════════
|
|
8
9
|
// P1-2: Review Score — Threshold Automatico
|
|
@@ -313,6 +314,9 @@ export function reviewApprove(options?: { specId?: string; force?: boolean; forc
|
|
|
313
314
|
// Atualizar spec para completed
|
|
314
315
|
db.run("UPDATE specs SET phase = 'completed', updated_at = ? WHERE id = ?", [now, spec.id]);
|
|
315
316
|
|
|
317
|
+
// v10.0: Clean up stale context files
|
|
318
|
+
cleanupContextFiles(spec.id);
|
|
319
|
+
|
|
316
320
|
// Buscar todos os dados para snapshot e relatorio
|
|
317
321
|
const tasks = db.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number").all(spec.id) as any[];
|
|
318
322
|
const decisions = db.query("SELECT * FROM decisions WHERE spec_id = ?").all(spec.id) as any[];
|
|
@@ -379,6 +383,9 @@ export function reviewSkip(specId?: string): void {
|
|
|
379
383
|
// Atualizar spec para completed
|
|
380
384
|
db.run("UPDATE specs SET phase = 'completed', updated_at = ? WHERE id = ?", [now, spec.id]);
|
|
381
385
|
|
|
386
|
+
// v10.0: Clean up stale context files
|
|
387
|
+
cleanupContextFiles(spec.id);
|
|
388
|
+
|
|
382
389
|
// Criar snapshot final
|
|
383
390
|
const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id) as any;
|
|
384
391
|
const allData = {
|
package/commands/task.ts
CHANGED
|
@@ -4,13 +4,18 @@ 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
|
-
import { getUnreadKnowledgeForTask, compactKnowledge } from "./knowledge";
|
|
7
|
+
import { getUnreadKnowledgeForTask, compactKnowledge, compactReasoning } from "./knowledge";
|
|
8
8
|
import { loadTemplate } from "../templates/loader";
|
|
9
9
|
import { TaskStateError, ValidationError, KnowledgeBlockError } from "../errors";
|
|
10
10
|
import { resolveSpec, resolveSpecOrNull } from "./spec-resolver";
|
|
11
11
|
import { getAgentDomain, domainToScope } from "../context/domains";
|
|
12
12
|
import { resolveAgent } from "../context/agent-registry";
|
|
13
13
|
import { loadAgentExpertise, getAgentDescription } from "../context/agent-expertise";
|
|
14
|
+
import { generateContextFile } from "../context/generator";
|
|
15
|
+
import { getModelForTask } from "../context/model-profiles";
|
|
16
|
+
import { getContextBudget, formatContextWarning, estimateTokens } from "../context/monitor";
|
|
17
|
+
import { invalidateCache } from "../context/cache";
|
|
18
|
+
import { cleanupContextFiles } from "../context/file-writer";
|
|
14
19
|
|
|
15
20
|
export function taskNext(json: boolean = false, specId?: string): void {
|
|
16
21
|
initSchema();
|
|
@@ -146,7 +151,7 @@ function showStuckWarning(stuck: any[]): void {
|
|
|
146
151
|
console.log(` Use: task done <id> --force --force-reason "timeout" para liberar\n`);
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
export function taskStart(ids: string, json: boolean = false, minimalContext: boolean = false, specId?: string): void {
|
|
154
|
+
export function taskStart(ids: string, json: boolean = false, minimalContext: boolean = false, specId?: string, inlineContext: boolean = false): void {
|
|
150
155
|
initSchema();
|
|
151
156
|
enforceGate("task-start");
|
|
152
157
|
|
|
@@ -207,51 +212,10 @@ export function taskStart(ids: string, json: boolean = false, minimalContext: bo
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
if (json) {
|
|
210
|
-
// NOVO: Incluir contexto COMPLETO para cada task
|
|
211
215
|
const contexts = startedTasks.map((task) => {
|
|
212
|
-
// v10.0: Contexto completo por padrao, reduzido via --minimal-context
|
|
213
|
-
const contextText = minimalContext
|
|
214
|
-
? getMinimalContextForSubagent(task.id)
|
|
215
|
-
: getContextForSubagent(task.id);
|
|
216
|
-
const unreadKnowledge = getUnreadKnowledgeForTask(spec.id, task.id);
|
|
217
|
-
|
|
218
|
-
// NOVO v7.4: Buscar implementation patterns relevantes
|
|
219
216
|
const taskFiles = task.files ? JSON.parse(task.files) : [];
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
// Buscar patterns por escopo do agente E por arquivos esperados
|
|
223
|
-
let relevantPatterns: any[] = [];
|
|
224
|
-
|
|
225
|
-
// Primeiro: patterns que correspondem aos arquivos esperados
|
|
226
|
-
if (taskFiles.length > 0) {
|
|
227
|
-
const filePatterns = getPatternsForFiles(taskFiles);
|
|
228
|
-
relevantPatterns.push(...filePatterns);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Segundo: patterns do escopo do agente (se nao foram encontrados via arquivos)
|
|
232
|
-
if (relevantPatterns.length === 0) {
|
|
233
|
-
const scopePatterns = getPatternsByScope(agentScope);
|
|
234
|
-
relevantPatterns.push(...scopePatterns);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Formatar patterns para o contexto (sem duplicatas)
|
|
238
|
-
const patternNames = new Set<string>();
|
|
239
|
-
const formattedPatterns = relevantPatterns
|
|
240
|
-
.filter(p => {
|
|
241
|
-
if (patternNames.has(p.name)) return false;
|
|
242
|
-
patternNames.add(p.name);
|
|
243
|
-
return true;
|
|
244
|
-
})
|
|
245
|
-
.map(p => ({
|
|
246
|
-
name: p.name,
|
|
247
|
-
category: p.category,
|
|
248
|
-
applies_to: p.applies_to,
|
|
249
|
-
template: p.template,
|
|
250
|
-
structure: JSON.parse(p.structure || "{}"),
|
|
251
|
-
examples: JSON.parse(p.examples || "[]").slice(0, 3), // Top 3 exemplos
|
|
252
|
-
anti_patterns: JSON.parse(p.anti_patterns || "[]"),
|
|
253
|
-
confidence: p.confidence,
|
|
254
|
-
}));
|
|
217
|
+
const agentDomain = getAgentDomain(task.agent);
|
|
218
|
+
const agentScope = domainToScope(agentDomain);
|
|
255
219
|
|
|
256
220
|
// R1: Load condensed expertise from agent .md
|
|
257
221
|
const agentExpertise = task.agent ? loadAgentExpertise(task.agent) : "";
|
|
@@ -265,43 +229,134 @@ export function taskStart(ids: string, json: boolean = false, minimalContext: bo
|
|
|
265
229
|
// R5: Get description for orchestrator use
|
|
266
230
|
const agentDescription = task.agent ? getAgentDescription(task.agent) : null;
|
|
267
231
|
|
|
268
|
-
|
|
232
|
+
// v10.0: Model profile based on agent domain
|
|
233
|
+
const modelProfile = getModelForTask(agentDomain);
|
|
234
|
+
|
|
235
|
+
// v10.0: File-based context (default) vs inline (backward compat)
|
|
236
|
+
const useFileContext = !inlineContext && !minimalContext;
|
|
237
|
+
let contextFile: string | null = null;
|
|
238
|
+
let contextText: string | null = null;
|
|
239
|
+
let contextSummary: string;
|
|
240
|
+
|
|
241
|
+
if (useFileContext) {
|
|
242
|
+
// Write context to file, subagent reads via Read tool
|
|
243
|
+
contextFile = generateContextFile(task.id);
|
|
244
|
+
contextSummary = getMinimalContextForSubagent(task.id);
|
|
245
|
+
} else if (minimalContext) {
|
|
246
|
+
contextSummary = getMinimalContextForSubagent(task.id);
|
|
247
|
+
} else {
|
|
248
|
+
// --inline-context: backward compat
|
|
249
|
+
contextText = getContextForSubagent(task.id);
|
|
250
|
+
contextSummary = getMinimalContextForSubagent(task.id);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// v10.0: Pre-built lean prompt for subagent (orchestrator just passes it)
|
|
254
|
+
const subagentPrompt = useFileContext
|
|
255
|
+
? loadTemplate("subagent-prompt-lean", {
|
|
256
|
+
taskName: task.name,
|
|
257
|
+
filesList: taskFiles.map((f: string) => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado)',
|
|
258
|
+
taskDescription: task.checkpoint || task.name,
|
|
259
|
+
contextSummary: contextSummary!,
|
|
260
|
+
contextFile: contextFile || "",
|
|
261
|
+
agentIdentity,
|
|
262
|
+
agentExpertise: agentExpertise
|
|
263
|
+
? `\n## EXPERTISE DO AGENTE\n\n${agentExpertise}\n`
|
|
264
|
+
: '',
|
|
265
|
+
})
|
|
266
|
+
: null;
|
|
267
|
+
|
|
268
|
+
// v10.0: Context budget estimation
|
|
269
|
+
const completedTaskCount = (db.query(
|
|
270
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'"
|
|
271
|
+
).get(spec.id) as any)?.c || 0;
|
|
272
|
+
|
|
273
|
+
const promptText = subagentPrompt || contextText || contextSummary!;
|
|
274
|
+
const contextFileContent = contextFile
|
|
275
|
+
? (Bun.file(contextFile).size > 0 ? String(Bun.file(contextFile).size) : "")
|
|
276
|
+
: "";
|
|
277
|
+
const contextBudget = getContextBudget(
|
|
278
|
+
promptText,
|
|
279
|
+
contextFileContent,
|
|
280
|
+
completedTaskCount,
|
|
281
|
+
contextFile || undefined,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const contextWarning = formatContextWarning(contextBudget);
|
|
285
|
+
if (contextWarning) {
|
|
286
|
+
console.error(contextWarning);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Base output (lean by default)
|
|
290
|
+
const output: any = {
|
|
269
291
|
taskId: task.id,
|
|
270
292
|
number: task.number,
|
|
271
293
|
name: task.name,
|
|
272
294
|
agent: task.agent,
|
|
273
295
|
agentDescription,
|
|
274
296
|
files: taskFiles,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
modelProfile,
|
|
298
|
+
contextBudget,
|
|
299
|
+
_orchestratorWarning: "NAO execute esta task diretamente. Use Task tool para delegar. Use o campo 'subagentPrompt' como prompt.",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (useFileContext) {
|
|
303
|
+
// v10.0: File-based context (new default)
|
|
304
|
+
output.contextFile = contextFile;
|
|
305
|
+
output.contextSummary = contextSummary!;
|
|
306
|
+
output.contextMode = "file";
|
|
307
|
+
output.subagentPrompt = subagentPrompt;
|
|
308
|
+
} else if (inlineContext) {
|
|
309
|
+
// Backward compat: full inline
|
|
310
|
+
output.context = contextText;
|
|
311
|
+
output.contextMode = "inline";
|
|
312
|
+
|
|
313
|
+
const unreadKnowledge = getUnreadKnowledgeForTask(spec.id, task.id);
|
|
314
|
+
output.unreadKnowledge = unreadKnowledge.map((k: any) => ({
|
|
315
|
+
id: k.id, category: k.category, content: k.content,
|
|
316
|
+
severity: k.severity, origin_task: k.task_origin,
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
// Patterns (only in inline mode — in file mode they're in the context file)
|
|
320
|
+
let relevantPatterns: any[] = [];
|
|
321
|
+
if (taskFiles.length > 0) {
|
|
322
|
+
relevantPatterns.push(...getPatternsForFiles(taskFiles));
|
|
323
|
+
}
|
|
324
|
+
if (relevantPatterns.length === 0) {
|
|
325
|
+
relevantPatterns.push(...getPatternsByScope(agentScope));
|
|
326
|
+
}
|
|
327
|
+
const patternNames = new Set<string>();
|
|
328
|
+
output.implementationPatterns = relevantPatterns
|
|
329
|
+
.filter(p => {
|
|
330
|
+
if (patternNames.has(p.name)) return false;
|
|
331
|
+
patternNames.add(p.name);
|
|
332
|
+
return true;
|
|
333
|
+
})
|
|
334
|
+
.map(p => ({
|
|
335
|
+
name: p.name, category: p.category, applies_to: p.applies_to,
|
|
336
|
+
template: p.template,
|
|
337
|
+
structure: JSON.parse(p.structure || "{}"),
|
|
338
|
+
examples: JSON.parse(p.examples || "[]").slice(0, 3),
|
|
339
|
+
anti_patterns: JSON.parse(p.anti_patterns || "[]"),
|
|
340
|
+
confidence: p.confidence,
|
|
341
|
+
}));
|
|
342
|
+
|
|
343
|
+
output.subagentContext = loadTemplate("subagent-context", {
|
|
344
|
+
filesList: taskFiles.map((f: string) => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado)',
|
|
293
345
|
agentIdentity,
|
|
294
|
-
agentExpertise: agentExpertise
|
|
295
|
-
|
|
346
|
+
agentExpertise: agentExpertise ? `\n## EXPERTISE DO AGENTE\n\n${agentExpertise}\n\n` : '',
|
|
347
|
+
});
|
|
348
|
+
output.subagentReturnProtocol = loadTemplate("subagent-return-protocol", {
|
|
349
|
+
patternsNote: output.implementationPatterns.length > 0
|
|
350
|
+
? `\nPATTERNS: ${output.implementationPatterns.length} patterns extraidos do projeto.\n`
|
|
296
351
|
: '',
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
// --minimal-context
|
|
355
|
+
output.contextSummary = contextSummary!;
|
|
356
|
+
output.contextMode = "minimal";
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return output;
|
|
305
360
|
});
|
|
306
361
|
console.log(JSON.stringify({ started: contexts }));
|
|
307
362
|
return;
|
|
@@ -403,6 +458,9 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
|
|
|
403
458
|
const processResult = processSubagentReturn(spec.id, taskId, task.number, subagentData);
|
|
404
459
|
console.log(formatProcessResult(processResult));
|
|
405
460
|
|
|
461
|
+
// v10.0: Invalidate context cache (knowledge/decisions changed)
|
|
462
|
+
invalidateCache(spec.id);
|
|
463
|
+
|
|
406
464
|
// v8.3: BLOCKING check para knowledge critico nao reconhecido (substitui warning v8.2)
|
|
407
465
|
const unackedCritical = getUnreadKnowledgeForTask(spec.id, taskId)
|
|
408
466
|
.filter((k: any) => k.severity === 'critical' && k.task_origin !== taskId);
|
|
@@ -490,7 +548,7 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
|
|
|
490
548
|
bypassesUsed: bypassCount,
|
|
491
549
|
filesCreated: subagentData?.files_created?.length || 0,
|
|
492
550
|
filesModified: subagentData?.files_modified?.length || 0,
|
|
493
|
-
contextSizeBytes:
|
|
551
|
+
contextSizeBytes: estimateTokens(getContextForSubagent(task.id)) * 4,
|
|
494
552
|
executionDurationMs: duration,
|
|
495
553
|
});
|
|
496
554
|
} catch { /* nao-critico: nao falhar task done por tracking de performance */ }
|
|
@@ -716,10 +774,15 @@ export function taskPhaseAdvance(options: { noCompact?: boolean; json?: boolean;
|
|
|
716
774
|
|
|
717
775
|
const now = new Date().toISOString();
|
|
718
776
|
|
|
719
|
-
// Compact knowledge (unless --no-compact)
|
|
777
|
+
// Compact knowledge, reasoning, and clean up stale files (unless --no-compact)
|
|
720
778
|
if (!options.noCompact) {
|
|
721
779
|
console.log(`\nCompactando contexto da fase ${currentPhase}...`);
|
|
722
780
|
compactKnowledge({ specId: spec.id });
|
|
781
|
+
const reasoningResult = compactReasoning({ specId: spec.id });
|
|
782
|
+
if (reasoningResult.archived > 0) {
|
|
783
|
+
console.log(` Reasoning compactado: ${reasoningResult.archived} entradas removidas, ${reasoningResult.kept} mantidas`);
|
|
784
|
+
}
|
|
785
|
+
cleanupContextFiles(spec.id);
|
|
723
786
|
}
|
|
724
787
|
|
|
725
788
|
// Create phase summary as critical knowledge
|
package/context/assembly.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface ContextData {
|
|
|
31
31
|
discoveredPatterns: any[];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const RETURN_PROTOCOL = `
|
|
35
|
+
### RETORNO OBRIGATORIO
|
|
36
|
+
\`\`\`json
|
|
37
|
+
{"status": "completed|blocked", "summary": "...", "files_created": [], "files_modified": [], "reasoning": {"approach": "como abordou", "challenges": [], "recommendations": "para proximas tasks"}}
|
|
38
|
+
\`\`\`
|
|
39
|
+
`;
|
|
40
|
+
|
|
34
41
|
export function assembleSections(header: string, sections: ContextSection[]): string {
|
|
35
42
|
// Sort by priority (lower = higher priority, kept during truncation)
|
|
36
43
|
const sorted = [...sections].sort((a, b) => a.priority - b.priority);
|
|
@@ -40,42 +47,88 @@ export function assembleSections(header: string, sections: ContextSection[]): st
|
|
|
40
47
|
output += section.content;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
output += `
|
|
45
|
-
### RETORNO OBRIGATORIO
|
|
46
|
-
\`\`\`json
|
|
47
|
-
{"status": "completed|blocked", "summary": "...", "files_created": [], "files_modified": [], "reasoning": {"approach": "como abordou", "challenges": [], "recommendations": "para proximas tasks"}}
|
|
48
|
-
\`\`\`
|
|
49
|
-
`;
|
|
50
|
+
output += RETURN_PROTOCOL;
|
|
50
51
|
|
|
51
|
-
//
|
|
52
|
+
// v10.0: Progressive truncation — halve sections before dropping them entirely
|
|
52
53
|
if (output.length > MAX_CONTEXT_SIZE) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
return truncateWithBudget(header, sorted, MAX_CONTEXT_SIZE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return output;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function truncateWithBudget(header: string, sections: ContextSection[], maxSize: number): string {
|
|
61
|
+
const reservedSize = header.length + RETURN_PROTOCOL.length + 120;
|
|
62
|
+
const budgetForSections = maxSize - reservedSize;
|
|
63
|
+
|
|
64
|
+
// Mutable map of section content
|
|
65
|
+
const sectionContent = new Map<string, string>();
|
|
66
|
+
let totalContentSize = 0;
|
|
67
|
+
for (const s of sections) {
|
|
68
|
+
sectionContent.set(s.name, s.content);
|
|
69
|
+
totalContentSize += s.content.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (totalContentSize <= budgetForSections) {
|
|
73
|
+
let result = header;
|
|
74
|
+
for (const s of sections) result += s.content;
|
|
75
|
+
result += RETURN_PROTOCOL;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Phase 1: Halve content of lowest-priority sections iteratively
|
|
80
|
+
// Sort descending (highest priority number = least important = truncated first)
|
|
81
|
+
const byLowestPriority = [...sections].sort((a, b) => b.priority - a.priority);
|
|
82
|
+
|
|
83
|
+
for (const s of byLowestPriority) {
|
|
84
|
+
if (totalContentSize <= budgetForSections) break;
|
|
85
|
+
|
|
86
|
+
const current = sectionContent.get(s.name);
|
|
87
|
+
if (!current) continue;
|
|
88
|
+
|
|
89
|
+
const halfSize = Math.floor(current.length / 2);
|
|
90
|
+
|
|
91
|
+
if (halfSize < 80) {
|
|
92
|
+
// Too small to truncate — drop entirely
|
|
93
|
+
totalContentSize -= current.length;
|
|
94
|
+
sectionContent.delete(s.name);
|
|
95
|
+
} else {
|
|
96
|
+
const truncated = current.substring(0, halfSize) + "\n[... secao truncada]\n";
|
|
97
|
+
totalContentSize -= (current.length - truncated.length);
|
|
98
|
+
sectionContent.set(s.name, truncated);
|
|
64
99
|
}
|
|
100
|
+
}
|
|
65
101
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
// Phase 2: If still over budget, drop entire sections (fallback)
|
|
103
|
+
if (totalContentSize > budgetForSections) {
|
|
104
|
+
for (const s of byLowestPriority) {
|
|
105
|
+
if (totalContentSize <= budgetForSections) break;
|
|
106
|
+
const content = sectionContent.get(s.name);
|
|
107
|
+
if (content) {
|
|
108
|
+
totalContentSize -= content.length;
|
|
109
|
+
sectionContent.delete(s.name);
|
|
110
|
+
}
|
|
71
111
|
}
|
|
112
|
+
}
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
// Reassemble in original priority order
|
|
115
|
+
let result = header;
|
|
116
|
+
const omitted: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const s of sections) {
|
|
119
|
+
const content = sectionContent.get(s.name);
|
|
120
|
+
if (content) {
|
|
121
|
+
result += content;
|
|
122
|
+
} else {
|
|
123
|
+
omitted.push(s.name);
|
|
75
124
|
}
|
|
125
|
+
}
|
|
76
126
|
|
|
77
|
-
|
|
127
|
+
result += RETURN_PROTOCOL;
|
|
128
|
+
|
|
129
|
+
if (omitted.length > 0) {
|
|
130
|
+
result += `\n[CONTEXTO TRUNCADO: ${omitted.length} secao(oes) omitida(s) (${omitted.join(', ')}). Use: codexa context detail <secao>]`;
|
|
78
131
|
}
|
|
79
132
|
|
|
80
|
-
return
|
|
133
|
+
return result;
|
|
81
134
|
}
|
package/context/cache.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// CONTEXT CACHE (v10.0)
|
|
3
|
+
// Content-addressable cache to avoid regenerating identical context.
|
|
4
|
+
// Invalidated when knowledge, decisions, or task state changes.
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { getDb } from "../db/connection";
|
|
8
|
+
|
|
9
|
+
interface CacheEntry {
|
|
10
|
+
hash: string;
|
|
11
|
+
filePath: string;
|
|
12
|
+
specId: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cache = new Map<string, CacheEntry>();
|
|
17
|
+
|
|
18
|
+
export function computeContextHash(specId: string, taskId: number): string {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
|
|
21
|
+
const spec = db.query("SELECT updated_at FROM specs WHERE id = ?").get(specId) as any;
|
|
22
|
+
const ctx = db.query("SELECT updated_at FROM context WHERE spec_id = ?").get(specId) as any;
|
|
23
|
+
const knowledgeCount = (db.query(
|
|
24
|
+
"SELECT COUNT(*) as c FROM knowledge WHERE spec_id = ?"
|
|
25
|
+
).get(specId) as any)?.c || 0;
|
|
26
|
+
const decisionCount = (db.query(
|
|
27
|
+
"SELECT COUNT(*) as c FROM decisions WHERE spec_id = ? AND status = 'active'"
|
|
28
|
+
).get(specId) as any)?.c || 0;
|
|
29
|
+
const task = db.query("SELECT depends_on, agent, files FROM tasks WHERE id = ?").get(taskId) as any;
|
|
30
|
+
|
|
31
|
+
// Hash key components that affect context content
|
|
32
|
+
const parts = [
|
|
33
|
+
specId,
|
|
34
|
+
String(taskId),
|
|
35
|
+
spec?.updated_at || "",
|
|
36
|
+
ctx?.updated_at || "",
|
|
37
|
+
String(knowledgeCount),
|
|
38
|
+
String(decisionCount),
|
|
39
|
+
task?.depends_on || "[]",
|
|
40
|
+
task?.agent || "",
|
|
41
|
+
task?.files || "[]",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Simple hash using Bun's built-in hashing
|
|
45
|
+
const raw = parts.join("|");
|
|
46
|
+
const hasher = new Bun.CryptoHasher("md5");
|
|
47
|
+
hasher.update(raw);
|
|
48
|
+
return hasher.digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getCachedContextPath(hash: string): string | null {
|
|
52
|
+
const entry = cache.get(hash);
|
|
53
|
+
if (!entry) return null;
|
|
54
|
+
|
|
55
|
+
// Check if file still exists
|
|
56
|
+
const file = Bun.file(entry.filePath);
|
|
57
|
+
if (file.size === 0) {
|
|
58
|
+
cache.delete(hash);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return entry.filePath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function setCachedContext(hash: string, filePath: string, specId: string): void {
|
|
66
|
+
cache.set(hash, {
|
|
67
|
+
hash,
|
|
68
|
+
filePath,
|
|
69
|
+
specId,
|
|
70
|
+
createdAt: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function invalidateCache(specId: string): void {
|
|
75
|
+
// v10.0: Selective invalidation — only remove entries for the given spec
|
|
76
|
+
for (const [key, entry] of cache.entries()) {
|
|
77
|
+
if (entry.specId === specId) {
|
|
78
|
+
cache.delete(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function clearCache(): void {
|
|
84
|
+
cache.clear();
|
|
85
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// CONTEXT FILE WRITER (v10.0)
|
|
3
|
+
// Writes context to .codexa/context/task-{id}.md instead of
|
|
4
|
+
// injecting 16KB inline into the subagent prompt.
|
|
5
|
+
// Subagents read the file on-demand via Read tool.
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync } from "fs";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
const CONTEXT_DIR = ".codexa/context";
|
|
12
|
+
|
|
13
|
+
export function ensureContextDir(): string {
|
|
14
|
+
const dir = resolve(process.cwd(), CONTEXT_DIR);
|
|
15
|
+
if (!existsSync(dir)) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeContextFile(taskId: number, content: string): string {
|
|
22
|
+
const dir = ensureContextDir();
|
|
23
|
+
const filePath = join(dir, `task-${taskId}.md`);
|
|
24
|
+
Bun.write(filePath, content);
|
|
25
|
+
return resolve(filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getContextFilePath(taskId: number): string {
|
|
29
|
+
return resolve(process.cwd(), CONTEXT_DIR, `task-${taskId}.md`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cleanupContextFiles(specId: string): void {
|
|
33
|
+
const dir = resolve(process.cwd(), CONTEXT_DIR);
|
|
34
|
+
if (!existsSync(dir)) return;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { readdirSync, unlinkSync } = require("fs");
|
|
38
|
+
const files = readdirSync(dir) as string[];
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (file.startsWith("task-") && file.endsWith(".md")) {
|
|
41
|
+
unlinkSync(join(dir, file));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Non-critical: don't fail if cleanup fails
|
|
46
|
+
}
|
|
47
|
+
}
|
package/context/generator.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { ContextSection, ContextData } from "./assembly";
|
|
|
5
5
|
import { assembleSections } from "./assembly";
|
|
6
6
|
import { getAgentDomain, adjustSectionPriorities, domainToScope } from "./domains";
|
|
7
7
|
import { filterRelevantDecisions, filterRelevantStandards } from "./scoring";
|
|
8
|
+
import { computeContextHash, getCachedContextPath, setCachedContext } from "./cache";
|
|
9
|
+
import { writeContextFile } from "./file-writer";
|
|
8
10
|
import {
|
|
9
11
|
buildProductSection,
|
|
10
12
|
buildArchitectureSection,
|
|
@@ -131,7 +133,7 @@ function fetchContextData(taskId: number): ContextData | null {
|
|
|
131
133
|
|
|
132
134
|
// Decisoes relevantes (max 8, priorizando as que mencionam arquivos da task)
|
|
133
135
|
const allDecisions = db
|
|
134
|
-
.query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active' ORDER BY created_at DESC")
|
|
136
|
+
.query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 30")
|
|
135
137
|
.all(task.spec_id) as any[];
|
|
136
138
|
const decisions = filterRelevantDecisions(allDecisions, taskFiles, 8);
|
|
137
139
|
|
|
@@ -237,11 +239,9 @@ function fetchContextData(taskId: number): ContextData | null {
|
|
|
237
239
|
|
|
238
240
|
// ── Main Entry Point ──────────────────────────────────────────
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
initSchema();
|
|
242
|
-
|
|
242
|
+
function buildFullContext(taskId: number): { content: string; data: ContextData } | null {
|
|
243
243
|
const data = fetchContextData(taskId);
|
|
244
|
-
if (!data) return
|
|
244
|
+
if (!data) return null;
|
|
245
245
|
|
|
246
246
|
const header = `## CONTEXTO (Task #${data.task.number})
|
|
247
247
|
|
|
@@ -266,9 +266,39 @@ export function getContextForSubagent(taskId: number): string {
|
|
|
266
266
|
buildHintsSection(data),
|
|
267
267
|
].filter((s): s is ContextSection => s !== null);
|
|
268
268
|
|
|
269
|
-
// v9.4: Domain-based priority adjustment (replaces binary AGENT_SECTIONS)
|
|
270
269
|
const agentDomain = getAgentDomain(data.task.agent);
|
|
271
270
|
const sections = adjustSectionPriorities(allSections, agentDomain);
|
|
272
271
|
|
|
273
|
-
return assembleSections(header, sections);
|
|
272
|
+
return { content: assembleSections(header, sections), data };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function getContextForSubagent(taskId: number): string {
|
|
276
|
+
initSchema();
|
|
277
|
+
const result = buildFullContext(taskId);
|
|
278
|
+
return result?.content || "ERRO: Task nao encontrada";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── v10.0: File-Based Context ─────────────────────────────────
|
|
282
|
+
|
|
283
|
+
export function generateContextFile(taskId: number): string {
|
|
284
|
+
initSchema();
|
|
285
|
+
|
|
286
|
+
const db = getDb();
|
|
287
|
+
const task = db.query("SELECT spec_id FROM tasks WHERE id = ?").get(taskId) as any;
|
|
288
|
+
if (!task) return "";
|
|
289
|
+
|
|
290
|
+
// Check cache first
|
|
291
|
+
const hash = computeContextHash(task.spec_id, taskId);
|
|
292
|
+
const cached = getCachedContextPath(hash);
|
|
293
|
+
if (cached) return cached;
|
|
294
|
+
|
|
295
|
+
// Generate fresh context
|
|
296
|
+
const result = buildFullContext(taskId);
|
|
297
|
+
if (!result) return "";
|
|
298
|
+
|
|
299
|
+
// Write to file and cache
|
|
300
|
+
const filePath = writeContextFile(taskId, result.content);
|
|
301
|
+
setCachedContext(hash, filePath, task.spec_id);
|
|
302
|
+
|
|
303
|
+
return filePath;
|
|
274
304
|
}
|
package/context/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Re-exports for backward compatibility
|
|
2
|
-
export { getContextForSubagent, getMinimalContextForSubagent } from "./generator";
|
|
2
|
+
export { getContextForSubagent, getMinimalContextForSubagent, generateContextFile } from "./generator";
|
|
3
3
|
export { assembleSections, MAX_CONTEXT_SIZE } from "./assembly";
|
|
4
4
|
export { AGENT_DOMAIN, DOMAIN_PROFILES, getAgentDomain, adjustSectionPriorities, domainToScope } from "./domains";
|
|
5
5
|
export type { AgentDomain, Relevance, SectionName, DomainProfile } from "./domains";
|
|
@@ -25,3 +25,10 @@ export type { ReferenceFile } from "./references";
|
|
|
25
25
|
export { AGENT_REGISTRY, resolveAgent, resolveAgentName, suggestAgent, buildAgentDomainMap, getAllAgentNames, getCanonicalAgentNames } from "./agent-registry";
|
|
26
26
|
export type { AgentEntry } from "./agent-registry";
|
|
27
27
|
export { loadAgentExpertise, getAgentDescription, findAgentsDir, clearExpertiseCache } from "./agent-expertise";
|
|
28
|
+
// v10.0: Context engineering
|
|
29
|
+
export { computeContextHash, getCachedContextPath, setCachedContext, invalidateCache, clearCache } from "./cache";
|
|
30
|
+
export { writeContextFile, ensureContextDir, getContextFilePath, cleanupContextFiles } from "./file-writer";
|
|
31
|
+
export { getModelForTask, getProfileForDomain, DOMAIN_MODEL_MAP, MODEL_NAMES } from "./model-profiles";
|
|
32
|
+
export type { ModelProfile } from "./model-profiles";
|
|
33
|
+
export { estimateTokens, getContextBudget, formatContextWarning } from "./monitor";
|
|
34
|
+
export type { ContextBudget, WarningLevel, ContentType } from "./monitor";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// MODEL PROFILES (v10.0)
|
|
3
|
+
// Maps agent domains to optimal model tiers.
|
|
4
|
+
// quality = opus (complex, architecture, security)
|
|
5
|
+
// balanced = sonnet (standard implementation)
|
|
6
|
+
// budget = haiku (exploration only — does NOT use Write/Edit)
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
import type { AgentDomain } from "./domains";
|
|
10
|
+
|
|
11
|
+
export type ModelProfile = "quality" | "balanced" | "budget";
|
|
12
|
+
|
|
13
|
+
export const MODEL_NAMES: Record<ModelProfile, string> = {
|
|
14
|
+
quality: "opus",
|
|
15
|
+
balanced: "sonnet",
|
|
16
|
+
budget: "haiku",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const DOMAIN_MODEL_MAP: Record<AgentDomain, ModelProfile> = {
|
|
20
|
+
backend: "balanced",
|
|
21
|
+
frontend: "balanced",
|
|
22
|
+
database: "quality",
|
|
23
|
+
testing: "balanced",
|
|
24
|
+
review: "quality",
|
|
25
|
+
security: "quality",
|
|
26
|
+
explore: "budget",
|
|
27
|
+
architecture: "quality",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function getModelForTask(agentDomain: AgentDomain | null): string {
|
|
31
|
+
if (!agentDomain) return MODEL_NAMES.quality;
|
|
32
|
+
const profile = DOMAIN_MODEL_MAP[agentDomain];
|
|
33
|
+
return MODEL_NAMES[profile];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getProfileForDomain(agentDomain: AgentDomain | null): ModelProfile {
|
|
37
|
+
if (!agentDomain) return "quality";
|
|
38
|
+
return DOMAIN_MODEL_MAP[agentDomain];
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// CONTEXT MONITOR (v10.0)
|
|
3
|
+
// Estimates token usage and warns when context budget is high.
|
|
4
|
+
// Thresholds: 65% = caution, 75% = critical
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
export type WarningLevel = "ok" | "caution" | "critical";
|
|
8
|
+
export type ContentType = "code" | "markdown" | "mixed";
|
|
9
|
+
|
|
10
|
+
export interface ContextBudget {
|
|
11
|
+
promptTokens: number;
|
|
12
|
+
contextFileTokens: number;
|
|
13
|
+
orchestratorEstimate: number;
|
|
14
|
+
totalEstimated: number;
|
|
15
|
+
warningLevel: WarningLevel;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CAUTION_THRESHOLD = 0.65;
|
|
19
|
+
const CRITICAL_THRESHOLD = 0.75;
|
|
20
|
+
// Claude Code context window practical limit (~200k tokens, but 80% usable)
|
|
21
|
+
const MAX_USABLE_TOKENS = 160_000;
|
|
22
|
+
|
|
23
|
+
function detectContentType(text: string): ContentType {
|
|
24
|
+
if (text.length === 0) return "mixed";
|
|
25
|
+
|
|
26
|
+
// Count code indicators (braces, semicolons, operators)
|
|
27
|
+
const codeIndicators = (text.match(/[{}\[\];=()]/g) || []).length;
|
|
28
|
+
// Count markdown indicators (headers, lists, bold)
|
|
29
|
+
const markdownIndicators = (text.match(/^#{1,6}\s|^\s*[-*]\s|\*\*/gm) || []).length;
|
|
30
|
+
|
|
31
|
+
const codeRatio = codeIndicators / text.length;
|
|
32
|
+
|
|
33
|
+
if (codeRatio > 0.03) return "code";
|
|
34
|
+
if (markdownIndicators > 5 && codeRatio < 0.01) return "markdown";
|
|
35
|
+
return "mixed";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function estimateTokens(text: string, contentType?: ContentType): number {
|
|
39
|
+
const type = contentType || detectContentType(text);
|
|
40
|
+
|
|
41
|
+
switch (type) {
|
|
42
|
+
case "code":
|
|
43
|
+
// Code has more tokens per character (operators, short identifiers)
|
|
44
|
+
return Math.ceil(text.length / 3);
|
|
45
|
+
case "markdown":
|
|
46
|
+
// Portuguese markdown has longer words, fewer tokens per character
|
|
47
|
+
return Math.ceil(text.length / 5);
|
|
48
|
+
default:
|
|
49
|
+
// Mixed content — conservative estimate
|
|
50
|
+
return Math.ceil(text.length / 3.5);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getContextBudget(
|
|
55
|
+
promptText: string,
|
|
56
|
+
contextFileText: string,
|
|
57
|
+
taskCount: number,
|
|
58
|
+
contextFilePath?: string,
|
|
59
|
+
): ContextBudget {
|
|
60
|
+
const promptTokens = estimateTokens(promptText);
|
|
61
|
+
|
|
62
|
+
// Use real file size if path provided
|
|
63
|
+
let contextFileTokens: number;
|
|
64
|
+
if (contextFilePath) {
|
|
65
|
+
try {
|
|
66
|
+
const realSize = Bun.file(contextFilePath).size;
|
|
67
|
+
contextFileTokens = Math.ceil(realSize / 3.5);
|
|
68
|
+
} catch {
|
|
69
|
+
contextFileTokens = estimateTokens(contextFileText);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
contextFileTokens = estimateTokens(contextFileText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Orchestrator overhead: base + per-task accumulation (conservative)
|
|
76
|
+
const orchestratorEstimate = 2000 + (taskCount * 1200);
|
|
77
|
+
|
|
78
|
+
const totalEstimated = promptTokens + contextFileTokens + orchestratorEstimate;
|
|
79
|
+
const ratio = totalEstimated / MAX_USABLE_TOKENS;
|
|
80
|
+
|
|
81
|
+
let warningLevel: WarningLevel = "ok";
|
|
82
|
+
if (ratio >= CRITICAL_THRESHOLD) {
|
|
83
|
+
warningLevel = "critical";
|
|
84
|
+
} else if (ratio >= CAUTION_THRESHOLD) {
|
|
85
|
+
warningLevel = "caution";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
promptTokens,
|
|
90
|
+
contextFileTokens,
|
|
91
|
+
orchestratorEstimate,
|
|
92
|
+
totalEstimated,
|
|
93
|
+
warningLevel,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatContextWarning(budget: ContextBudget): string | null {
|
|
98
|
+
if (budget.warningLevel === "ok") return null;
|
|
99
|
+
|
|
100
|
+
const pct = Math.round((budget.totalEstimated / MAX_USABLE_TOKENS) * 100);
|
|
101
|
+
|
|
102
|
+
if (budget.warningLevel === "critical") {
|
|
103
|
+
return `[CRITICAL] Contexto estimado em ${pct}% do limite. Considere: --minimal-context ou knowledge compact`;
|
|
104
|
+
}
|
|
105
|
+
return `[CAUTION] Contexto estimado em ${pct}% do limite. Monitore o uso.`;
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.21",
|
|
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": {
|
|
@@ -164,6 +164,10 @@ export function processSubagentReturn(
|
|
|
164
164
|
const currentPatterns = context?.patterns ? JSON.parse(context.patterns) : [];
|
|
165
165
|
|
|
166
166
|
for (const pattern of data.patterns_discovered) {
|
|
167
|
+
// v10.0: Dedup — skip if pattern text already exists
|
|
168
|
+
if (currentPatterns.some((p: any) => p.pattern === pattern)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
167
171
|
currentPatterns.push({
|
|
168
172
|
pattern,
|
|
169
173
|
detected_at: now,
|
|
@@ -172,6 +176,11 @@ export function processSubagentReturn(
|
|
|
172
176
|
result.patternsAdded++;
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
// v10.0: Cap at 50 patterns (keep most recent)
|
|
180
|
+
if (currentPatterns.length > 50) {
|
|
181
|
+
currentPatterns.splice(0, currentPatterns.length - 50);
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
db.run(
|
|
176
185
|
"UPDATE context SET patterns = ?, updated_at = ? WHERE spec_id = ?",
|
|
177
186
|
[JSON.stringify(currentPatterns), now, specId]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{{agentIdentity}}
|
|
2
|
+
|
|
3
|
+
## TASK: {{taskName}}
|
|
4
|
+
|
|
5
|
+
**ARQUIVOS**: {{filesList}}
|
|
6
|
+
**MUDANCA**: {{taskDescription}}
|
|
7
|
+
|
|
8
|
+
{{contextSummary}}
|
|
9
|
+
|
|
10
|
+
## CONTEXTO COMPLETO
|
|
11
|
+
|
|
12
|
+
Leia o arquivo de contexto completo com Read tool (apenas secoes relevantes para sua task):
|
|
13
|
+
{{contextFile}}
|
|
14
|
+
|
|
15
|
+
## EXECUTE AGORA
|
|
16
|
+
|
|
17
|
+
1. Read o arquivo de contexto acima (secoes que precisar)
|
|
18
|
+
2. Read arquivos existentes que precisa modificar
|
|
19
|
+
3. Edit/Write para criar/modificar os arquivos listados
|
|
20
|
+
4. Retorne JSON:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{"status": "completed", "summary": "...", "files_created": [], "files_modified": [], "reasoning": {"approach": "como abordou (min 20 chars)", "challenges": [], "recommendations": "para proximas tasks"}, "knowledge_to_broadcast": [], "decisions_made": []}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**REGRA**: Se retornar sem usar Edit/Write = FALHA.
|
|
27
|
+
|
|
28
|
+
{{agentExpertise}}
|
package/workflow.ts
CHANGED
|
@@ -254,9 +254,10 @@ taskCmd
|
|
|
254
254
|
.description("Inicia task(s) - pode ser multiplas separadas por virgula")
|
|
255
255
|
.option("--json", "Saida em JSON")
|
|
256
256
|
.option("--minimal-context", "Usar contexto reduzido (2KB) em vez do completo (16KB)")
|
|
257
|
+
.option("--inline-context", "Incluir contexto inline no JSON (modo legado, 16KB)")
|
|
257
258
|
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
258
259
|
.action(wrapAction((ids: string, options) => {
|
|
259
|
-
taskStart(ids, options.json, options.minimalContext, options.spec);
|
|
260
|
+
taskStart(ids, options.json, options.minimalContext, options.spec, options.inlineContext);
|
|
260
261
|
}));
|
|
261
262
|
|
|
262
263
|
taskCmd
|
|
@@ -498,6 +499,47 @@ contextCmd
|
|
|
498
499
|
contextDetail(section, opts.json, opts.spec);
|
|
499
500
|
});
|
|
500
501
|
|
|
502
|
+
contextCmd
|
|
503
|
+
.command("budget")
|
|
504
|
+
.description("Mostra estimativa de uso de contexto para uma task")
|
|
505
|
+
.option("--task <id>", "ID da task")
|
|
506
|
+
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
507
|
+
.action(wrapAction((options: { task?: string; spec?: string }) => {
|
|
508
|
+
const { getContextForSubagent, getMinimalContextForSubagent } = require("./context/generator");
|
|
509
|
+
const { getContextBudget, formatContextWarning } = require("./context/monitor");
|
|
510
|
+
const { getDb } = require("./db/connection");
|
|
511
|
+
const { initSchema } = require("./db/schema");
|
|
512
|
+
initSchema();
|
|
513
|
+
const db = getDb();
|
|
514
|
+
|
|
515
|
+
const taskId = options.task ? parseInt(options.task) : null;
|
|
516
|
+
if (!taskId) {
|
|
517
|
+
console.log("Use --task <id> para especificar a task.");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const fullCtx = getContextForSubagent(taskId);
|
|
522
|
+
const minCtx = getMinimalContextForSubagent(taskId);
|
|
523
|
+
|
|
524
|
+
const task = db.query("SELECT spec_id FROM tasks WHERE id = ?").get(taskId) as any;
|
|
525
|
+
const completedCount = task ? (db.query(
|
|
526
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'"
|
|
527
|
+
).get(task.spec_id) as any)?.c || 0 : 0;
|
|
528
|
+
|
|
529
|
+
const budget = getContextBudget(minCtx, fullCtx, completedCount);
|
|
530
|
+
const warning = formatContextWarning(budget);
|
|
531
|
+
|
|
532
|
+
console.log(`\nContext Budget (Task #${taskId}):`);
|
|
533
|
+
console.log(`${"─".repeat(40)}`);
|
|
534
|
+
console.log(` Prompt (lean): ~${budget.promptTokens} tokens`);
|
|
535
|
+
console.log(` Context file: ~${budget.contextFileTokens} tokens`);
|
|
536
|
+
console.log(` Orchestrator est.: ~${budget.orchestratorEstimate} tokens`);
|
|
537
|
+
console.log(` Total estimado: ~${budget.totalEstimated} tokens`);
|
|
538
|
+
console.log(` Warning level: ${budget.warningLevel}`);
|
|
539
|
+
if (warning) console.log(`\n ${warning}`);
|
|
540
|
+
console.log();
|
|
541
|
+
}));
|
|
542
|
+
|
|
501
543
|
// Manter compatibilidade: context sem subcomando = export
|
|
502
544
|
program
|
|
503
545
|
.command("ctx")
|