@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.
@@ -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}`);
@@ -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
  }
@@ -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 agentScope = domainToScope(getAgentDomain(task.agent));
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
- return {
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
- // AVISO PARA O ORQUESTRADOR - NAO EXECUTE, DELEGUE
276
- _orchestratorWarning: "NAO execute esta task diretamente. Use Task tool com subagent_type='general-purpose' para delegar. O campo 'subagentContext' abaixo e o prompt para o SUBAGENT.",
277
- // Contexto para o subagent (NAO para o orquestrador)
278
- context: contextText,
279
- contextMode: minimalContext ? "minimal" : "full",
280
- // Knowledge nao lido (broadcast de outras tasks)
281
- unreadKnowledge: unreadKnowledge.map((k: any) => ({
282
- id: k.id,
283
- category: k.category,
284
- content: k.content,
285
- severity: k.severity,
286
- origin_task: k.task_origin,
287
- })),
288
- // NOVO v7.4: Implementation patterns extraidos do projeto
289
- implementationPatterns: formattedPatterns,
290
- // Contexto para o SUBAGENT (o orquestrador deve passar isso via Task tool)
291
- subagentContext: loadTemplate("subagent-context", {
292
- filesList: taskFiles.map(f => ` - ${f}`).join('\n') || ' (nenhum arquivo especificado - analise o contexto)',
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
- ? `\n## EXPERTISE DO AGENTE\n\n${agentExpertise}\n\n`
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
- // Instrucoes de retorno para o SUBAGENT
299
- subagentReturnProtocol: loadTemplate("subagent-return-protocol", {
300
- patternsNote: formattedPatterns.length > 0
301
- ? `\nPATTERNS: Voce recebeu ${formattedPatterns.length} implementation patterns extraidos do projeto.\nUse os TEMPLATES fornecidos para criar codigo CONSISTENTE com o projeto existente.\n`
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: 0,
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
@@ -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
- // Protocolo de retorno (sempre incluido)
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
- // v8.3: Overall size cap com truncamento inteligente por secao
52
+ // v10.0: Progressive truncation halve sections before dropping them entirely
52
53
  if (output.length > MAX_CONTEXT_SIZE) {
53
- const parts = output.split('\n### ');
54
- let trimmed = parts[0]; // Sempre manter header
55
- let breakIndex = parts.length;
56
-
57
- for (let i = 1; i < parts.length; i++) {
58
- const candidate = trimmed + '\n### ' + parts[i];
59
- if (candidate.length > MAX_CONTEXT_SIZE - 200) {
60
- breakIndex = i;
61
- break;
62
- }
63
- trimmed = candidate;
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
- // Coletar nomes das secoes omitidas
67
- const omittedNames: string[] = [];
68
- for (let i = breakIndex; i < parts.length; i++) {
69
- const name = parts[i].split('\n')[0].trim();
70
- if (name) omittedNames.push(name);
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
- if (omittedNames.length > 0) {
74
- trimmed += `\n\n[CONTEXTO TRUNCADO: ${omittedNames.length} secao(oes) omitida(s) (${omittedNames.join(', ')}). Use: codexa context detail <secao>]`;
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
- output = trimmed;
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 output;
133
+ return result;
81
134
  }
@@ -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
+ }
@@ -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
- export function getContextForSubagent(taskId: number): string {
241
- initSchema();
242
-
242
+ function buildFullContext(taskId: number): { content: string; data: ContextData } | null {
243
243
  const data = fetchContextData(taskId);
244
- if (!data) return "ERRO: Task nao encontrada";
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.19",
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")