@codexa/cli 9.0.15 → 9.0.16

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.
@@ -237,6 +237,78 @@ describe("parseBabySteps", () => {
237
237
  const result = parseBabySteps(section);
238
238
  expect(result[0].files).toEqual(["file1.ts", "file2.ts"]);
239
239
  });
240
+
241
+ // v9.9: Phase support in baby steps
242
+ it("should parse steps with explicit phase headers", () => {
243
+ const section = `## Fase 1: Fundacao
244
+ ### 1. Create schema
245
+ **O que**: Build database schema
246
+ **Agente**: expert-database
247
+
248
+ ### 2. Setup testes
249
+ **O que**: Configure test framework
250
+ **Agente**: expert-testing
251
+
252
+ ## Fase 2: Core
253
+ ### 3. Implement API
254
+ **O que**: Create REST endpoints
255
+ **Agente**: expert-backend
256
+ **Depende de**: 1`;
257
+
258
+ const result = parseBabySteps(section);
259
+ expect(result).toHaveLength(3);
260
+ expect(result[0].phase).toBe(1);
261
+ expect(result[1].phase).toBe(1);
262
+ expect(result[2].phase).toBe(2);
263
+ });
264
+
265
+ it("should not assign phases when no phase headers exist", () => {
266
+ const section = `### 1. First step
267
+ **O que**: Do first thing
268
+
269
+ ### 2. Second step
270
+ **O que**: Do second thing`;
271
+
272
+ const result = parseBabySteps(section);
273
+ expect(result).toHaveLength(2);
274
+ expect(result[0].phase).toBeUndefined();
275
+ expect(result[1].phase).toBeUndefined();
276
+ });
277
+
278
+ it("should handle 'Phase N:' header format (English)", () => {
279
+ const section = `## Phase 1: Foundation
280
+ ### 1. Create schema
281
+ **O que**: Build database
282
+
283
+ ## Phase 2: Features
284
+ ### 2. Implement API
285
+ **O que**: Create endpoints`;
286
+
287
+ const result = parseBabySteps(section);
288
+ expect(result).toHaveLength(2);
289
+ expect(result[0].phase).toBe(1);
290
+ expect(result[1].phase).toBe(2);
291
+ });
292
+
293
+ it("should parse 3+ phases correctly", () => {
294
+ const section = `## Fase 1: Base
295
+ ### 1. Step one
296
+ **O que**: First
297
+
298
+ ## Fase 2: Core
299
+ ### 2. Step two
300
+ **O que**: Second
301
+
302
+ ## Fase 3: Polish
303
+ ### 3. Step three
304
+ **O que**: Third`;
305
+
306
+ const result = parseBabySteps(section);
307
+ expect(result).toHaveLength(3);
308
+ expect(result[0].phase).toBe(1);
309
+ expect(result[1].phase).toBe(2);
310
+ expect(result[2].phase).toBe(3);
311
+ });
240
312
  });
241
313
 
242
314
  describe("parseRisks", () => {
@@ -17,6 +17,7 @@ export interface BabyStep {
17
17
  files: string[];
18
18
  agent: string;
19
19
  dependsOn?: number[];
20
+ phase?: number;
20
21
  }
21
22
 
22
23
  export interface Risk {
@@ -180,36 +181,85 @@ export function extractSection(content: string, header: string): string {
180
181
  export function parseBabySteps(section: string): BabyStep[] {
181
182
  if (!section) return [];
182
183
  const steps: BabyStep[] = [];
183
- // Match "### N. Name" or "### Step N: Name"
184
- const stepBlocks = section.split(/^###\s+/m).filter(Boolean);
185
184
 
186
- for (const block of stepBlocks) {
187
- const headerMatch = block.match(/^(?:Step\s+)?(\d+)[.:]\s*(.+)/);
188
- if (!headerMatch) continue;
185
+ // Detect phase headers: "## Fase N:" or "## Phase N:"
186
+ let currentPhase = 1;
187
+ let hasExplicitPhases = false;
188
+ const phaseRegex = /^##\s+(?:Fase|Phase)\s+(\d+)/im;
189
+ if (phaseRegex.test(section)) {
190
+ hasExplicitPhases = true;
191
+ }
189
192
 
190
- const number = parseInt(headerMatch[1]);
191
- const name = headerMatch[2].trim();
192
- const what = block.match(/\*\*O que\*\*:\s*(.+)/)?.[1]?.trim() || "";
193
- const why = block.match(/\*\*Por que\*\*:\s*(.+)/)?.[1]?.trim() || "";
194
- const result = block.match(/\*\*Resultado\*\*:\s*(.+)/)?.[1]?.trim() || "";
195
- const filesStr = block.match(/\*\*Arquivos?\*\*:\s*(.+)/)?.[1]?.trim() || "";
196
- const agent = block.match(/\*\*Agente\*\*:\s*(.+)/)?.[1]?.trim() || "general";
197
- const depsStr = block.match(/\*\*Depende de\*\*:\s*(.+)/)?.[1]?.trim() || "";
193
+ // Split by phase headers first, then by step headers
194
+ // We process the section line-by-line to track phase context
195
+ const lines = section.split("\n");
196
+ let currentBlock = "";
197
+ let blockPhase = currentPhase;
198
198
 
199
- const files = filesStr
200
- ? filesStr.split(/[,;]/).map(f => f.trim().replace(/`/g, "")).filter(Boolean)
201
- : [];
199
+ for (const line of lines) {
200
+ const phaseMatch = line.match(/^##\s+(?:Fase|Phase)\s+(\d+)/i);
201
+ if (phaseMatch) {
202
+ // Process pending block BEFORE changing phase
203
+ if (currentBlock.trim()) {
204
+ const step = parseStepBlock(currentBlock, hasExplicitPhases ? blockPhase : undefined);
205
+ if (step) steps.push(step);
206
+ currentBlock = "";
207
+ }
208
+ currentPhase = parseInt(phaseMatch[1]);
209
+ blockPhase = currentPhase;
210
+ continue;
211
+ }
202
212
 
203
- const dependsOn = depsStr
204
- ? (depsStr.match(/\d+/g) || []).map(Number)
205
- : [];
213
+ // When we hit a new step header, process the previous block
214
+ if (line.match(/^###\s+/) && currentBlock.trim()) {
215
+ const step = parseStepBlock(currentBlock, hasExplicitPhases ? blockPhase : undefined);
216
+ if (step) steps.push(step);
217
+ currentBlock = line + "\n";
218
+ blockPhase = currentPhase;
219
+ } else {
220
+ currentBlock += line + "\n";
221
+ }
222
+ }
206
223
 
207
- steps.push({ number, name, what, why, result, files, agent, dependsOn: dependsOn.length > 0 ? dependsOn : undefined });
224
+ // Process the last block
225
+ if (currentBlock.trim()) {
226
+ const step = parseStepBlock(currentBlock, hasExplicitPhases ? blockPhase : undefined);
227
+ if (step) steps.push(step);
208
228
  }
209
229
 
210
230
  return steps;
211
231
  }
212
232
 
233
+ function parseStepBlock(block: string, phase?: number): BabyStep | null {
234
+ // Remove leading "### " prefix for matching
235
+ const content = block.replace(/^###\s+/, "");
236
+ const headerMatch = content.match(/^(?:Step\s+)?(\d+)[.:]\s*(.+)/);
237
+ if (!headerMatch) return null;
238
+
239
+ const number = parseInt(headerMatch[1]);
240
+ const name = headerMatch[2].trim();
241
+ const what = block.match(/\*\*O que\*\*:\s*(.+)/)?.[1]?.trim() || "";
242
+ const why = block.match(/\*\*Por que\*\*:\s*(.+)/)?.[1]?.trim() || "";
243
+ const result = block.match(/\*\*Resultado\*\*:\s*(.+)/)?.[1]?.trim() || "";
244
+ const filesStr = block.match(/\*\*Arquivos?\*\*:\s*(.+)/)?.[1]?.trim() || "";
245
+ const agent = block.match(/\*\*Agente\*\*:\s*(.+)/)?.[1]?.trim() || "general";
246
+ const depsStr = block.match(/\*\*Depende de\*\*:\s*(.+)/)?.[1]?.trim() || "";
247
+
248
+ const files = filesStr
249
+ ? filesStr.split(/[,;]/).map(f => f.trim().replace(/`/g, "")).filter(Boolean)
250
+ : [];
251
+
252
+ const dependsOn = depsStr
253
+ ? (depsStr.match(/\d+/g) || []).map(Number)
254
+ : [];
255
+
256
+ return {
257
+ number, name, what, why, result, files, agent,
258
+ dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
259
+ phase,
260
+ };
261
+ }
262
+
213
263
  export function parseRisks(section: string): Risk[] {
214
264
  if (!section) return [];
215
265
  const risks: Risk[] = [];
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "bun:test";
2
- import { generateSpecId } from "./plan";
2
+ import { generateSpecId, estimateContextPressure, assignPhases, SAFE_TASKS_PER_PHASE } from "./plan";
3
3
 
4
4
  describe("generateSpecId", () => {
5
5
  it("generates ID with date-slug-hash format", () => {
@@ -71,3 +71,151 @@ describe("generateSpecId", () => {
71
71
  expect(id).toContain("add-user-auth");
72
72
  });
73
73
  });
74
+
75
+ // ═══════════════════════════════════════════════════════════════
76
+ // v9.9: Context Pressure Budget Calculator
77
+ // ═══════════════════════════════════════════════════════════════
78
+
79
+ describe("estimateContextPressure", () => {
80
+ it("returns green for small task counts", () => {
81
+ const result = estimateContextPressure(5);
82
+ expect(result.level).toBe("green");
83
+ expect(result.taskCount).toBe(5);
84
+ expect(result.suggestedPhases).toBe(1);
85
+ });
86
+
87
+ it("returns green at the threshold boundary", () => {
88
+ const result = estimateContextPressure(SAFE_TASKS_PER_PHASE);
89
+ expect(result.level).toBe("green");
90
+ expect(result.suggestedPhases).toBe(1);
91
+ });
92
+
93
+ it("returns yellow for moderate task counts", () => {
94
+ const result = estimateContextPressure(18);
95
+ expect(result.level).toBe("yellow");
96
+ expect(result.suggestedPhases).toBe(2);
97
+ });
98
+
99
+ it("returns red for large task counts", () => {
100
+ const result = estimateContextPressure(30);
101
+ expect(result.level).toBe("red");
102
+ expect(result.suggestedPhases).toBe(3);
103
+ });
104
+
105
+ it("calculates decision coverage correctly", () => {
106
+ const result = estimateContextPressure(10);
107
+ // 10 tasks * 3 decisions = 30, coverage = 8/30 = 27%
108
+ expect(result.estimatedDecisions).toBe(30);
109
+ expect(result.decisionCoverage).toBe(27);
110
+ });
111
+
112
+ it("caps coverage at 100%", () => {
113
+ const result = estimateContextPressure(1);
114
+ // 1 task * 3 = 3 decisions, 8/3 = 267% -> capped at 100
115
+ expect(result.decisionCoverage).toBe(100);
116
+ expect(result.knowledgeCoverage).toBe(100);
117
+ });
118
+
119
+ it("respects custom maxPerPhase", () => {
120
+ const result = estimateContextPressure(20, 20);
121
+ expect(result.level).toBe("green");
122
+ expect(result.suggestedPhases).toBe(1);
123
+ expect(result.maxTasksPerPhase).toBe(20);
124
+ });
125
+
126
+ it("suggests correct number of phases for 38 tasks", () => {
127
+ const result = estimateContextPressure(38);
128
+ expect(result.level).toBe("red");
129
+ expect(result.suggestedPhases).toBe(4); // ceil(38/12) = 4
130
+ });
131
+ });
132
+
133
+ // ═══════════════════════════════════════════════════════════════
134
+ // v9.9: Phase Auto-Assignment (Topological Sort)
135
+ // ═══════════════════════════════════════════════════════════════
136
+
137
+ describe("assignPhases", () => {
138
+ it("assigns single phase for small task lists", () => {
139
+ const tasks = [
140
+ { number: 1, dependsOn: [] },
141
+ { number: 2, dependsOn: [1] },
142
+ { number: 3, dependsOn: [2] },
143
+ ];
144
+ const result = assignPhases(tasks, 12);
145
+ expect(result.get(1)).toBe(1);
146
+ expect(result.get(2)).toBe(1);
147
+ expect(result.get(3)).toBe(1);
148
+ });
149
+
150
+ it("splits tasks into phases based on topological depth", () => {
151
+ // Create 15 tasks: levels 0-4 with 3 tasks each
152
+ const tasks = [];
153
+ for (let i = 1; i <= 15; i++) {
154
+ const level = Math.floor((i - 1) / 3);
155
+ const deps = level > 0 ? [(level - 1) * 3 + 1] : [];
156
+ tasks.push({ number: i, dependsOn: deps });
157
+ }
158
+ const result = assignPhases(tasks, 6);
159
+
160
+ // Should split into at least 2 phases
161
+ const phases = new Set(result.values());
162
+ expect(phases.size).toBeGreaterThanOrEqual(2);
163
+ });
164
+
165
+ it("returns empty map for empty task list", () => {
166
+ const result = assignPhases([], 12);
167
+ expect(result.size).toBe(0);
168
+ });
169
+
170
+ it("handles independent tasks (no dependencies)", () => {
171
+ const tasks = Array.from({ length: 20 }, (_, i) => ({
172
+ number: i + 1,
173
+ dependsOn: [] as number[],
174
+ }));
175
+ const result = assignPhases(tasks, 10);
176
+
177
+ // All independent tasks are at level 0, but can't fit in one phase
178
+ // They should be split: 10 in phase 1, 10 in phase 2
179
+ const phases = new Set(result.values());
180
+ expect(phases.size).toBe(2);
181
+ });
182
+
183
+ it("respects dependency order across phases", () => {
184
+ const tasks = [
185
+ { number: 1, dependsOn: [] },
186
+ { number: 2, dependsOn: [] },
187
+ { number: 3, dependsOn: [1, 2] },
188
+ { number: 4, dependsOn: [3] },
189
+ ];
190
+ const result = assignPhases(tasks, 3);
191
+
192
+ // Task 3 depends on 1 and 2, so it must be in same or later phase
193
+ const phase1 = result.get(1)!;
194
+ const phase3 = result.get(3)!;
195
+ const phase4 = result.get(4)!;
196
+ expect(phase3).toBeGreaterThanOrEqual(phase1);
197
+ expect(phase4).toBeGreaterThanOrEqual(phase3);
198
+ });
199
+
200
+ it("handles a linear chain exceeding maxPerPhase", () => {
201
+ // 6 tasks in a linear chain, max 2 per phase
202
+ const tasks = [
203
+ { number: 1, dependsOn: [] },
204
+ { number: 2, dependsOn: [1] },
205
+ { number: 3, dependsOn: [2] },
206
+ { number: 4, dependsOn: [3] },
207
+ { number: 5, dependsOn: [4] },
208
+ { number: 6, dependsOn: [5] },
209
+ ];
210
+ const result = assignPhases(tasks, 2);
211
+
212
+ // Each level has 1 task, so we accumulate 2 per phase
213
+ const phases = new Set(result.values());
214
+ expect(phases.size).toBe(3);
215
+
216
+ // Verify order is preserved
217
+ for (let i = 1; i <= 5; i++) {
218
+ expect(result.get(i)!).toBeLessThanOrEqual(result.get(i + 1)!);
219
+ }
220
+ });
221
+ });
package/commands/plan.ts CHANGED
@@ -4,6 +4,143 @@ import { resolveSpec } from "./spec-resolver";
4
4
  import { CodexaError, ValidationError } from "../errors";
5
5
  import { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames, getAgentsByDomain } from "../context/agent-registry";
6
6
 
7
+ // ═══════════════════════════════════════════════════════════════
8
+ // CONTEXT PRESSURE: Budget Calculator + Phase Auto-Assignment
9
+ // ═══════════════════════════════════════════════════════════════
10
+
11
+ export const SAFE_TASKS_PER_PHASE = 12;
12
+ const DECISIONS_PER_TASK = 3;
13
+ const KNOWLEDGE_PER_TASK = 5;
14
+ const MAX_DECISIONS_IN_CONTEXT = 8;
15
+ const MAX_KNOWLEDGE_IN_CONTEXT = 30; // 20 critical + 10 info
16
+
17
+ export type PressureLevel = "green" | "yellow" | "red";
18
+
19
+ export interface ContextPressureEstimate {
20
+ level: PressureLevel;
21
+ taskCount: number;
22
+ estimatedDecisions: number;
23
+ decisionCoverage: number;
24
+ estimatedKnowledge: number;
25
+ knowledgeCoverage: number;
26
+ suggestedPhases: number;
27
+ maxTasksPerPhase: number;
28
+ }
29
+
30
+ export function estimateContextPressure(taskCount: number, maxPerPhase: number = SAFE_TASKS_PER_PHASE): ContextPressureEstimate {
31
+ const estimatedDecisions = taskCount * DECISIONS_PER_TASK;
32
+ const estimatedKnowledge = taskCount * KNOWLEDGE_PER_TASK;
33
+ const decisionCoverage = Math.min(100, Math.round((MAX_DECISIONS_IN_CONTEXT / estimatedDecisions) * 100));
34
+ const knowledgeCoverage = Math.min(100, Math.round((MAX_KNOWLEDGE_IN_CONTEXT / estimatedKnowledge) * 100));
35
+
36
+ let level: PressureLevel = "green";
37
+ if (taskCount > 25) level = "red";
38
+ else if (taskCount > maxPerPhase) level = "yellow";
39
+
40
+ const suggestedPhases = Math.max(1, Math.ceil(taskCount / maxPerPhase));
41
+
42
+ return {
43
+ level,
44
+ taskCount,
45
+ estimatedDecisions,
46
+ decisionCoverage,
47
+ estimatedKnowledge,
48
+ knowledgeCoverage,
49
+ suggestedPhases,
50
+ maxTasksPerPhase: maxPerPhase,
51
+ };
52
+ }
53
+
54
+ interface TaskForPhasing {
55
+ number: number;
56
+ dependsOn: number[];
57
+ phase?: number;
58
+ }
59
+
60
+ export function assignPhases(tasks: TaskForPhasing[], maxPerPhase: number = SAFE_TASKS_PER_PHASE): Map<number, number> {
61
+ const phaseMap = new Map<number, number>();
62
+ if (tasks.length === 0) return phaseMap;
63
+
64
+ // Build dependency graph and compute topological depth via BFS
65
+ const depthMap = new Map<number, number>();
66
+ const taskNums = new Set(tasks.map(t => t.number));
67
+ const depsOf = new Map<number, number[]>();
68
+ for (const t of tasks) {
69
+ depsOf.set(t.number, t.dependsOn.filter(d => taskNums.has(d)));
70
+ }
71
+
72
+ // Kahn's algorithm for topological levels
73
+ const inDegree = new Map<number, number>();
74
+ const children = new Map<number, number[]>();
75
+ for (const t of tasks) {
76
+ inDegree.set(t.number, 0);
77
+ children.set(t.number, []);
78
+ }
79
+ for (const t of tasks) {
80
+ for (const dep of depsOf.get(t.number) || []) {
81
+ inDegree.set(t.number, (inDegree.get(t.number) || 0) + 1);
82
+ children.get(dep)?.push(t.number);
83
+ }
84
+ }
85
+
86
+ // BFS by level
87
+ let queue = tasks.filter(t => (inDegree.get(t.number) || 0) === 0).map(t => t.number);
88
+ let level = 0;
89
+ while (queue.length > 0) {
90
+ const nextQueue: number[] = [];
91
+ for (const num of queue) {
92
+ depthMap.set(num, level);
93
+ for (const child of children.get(num) || []) {
94
+ inDegree.set(child, (inDegree.get(child) || 0) - 1);
95
+ if ((inDegree.get(child) || 0) === 0) {
96
+ nextQueue.push(child);
97
+ }
98
+ }
99
+ }
100
+ queue = nextQueue;
101
+ level++;
102
+ }
103
+
104
+ // Handle cycles (tasks not reached): assign max level
105
+ for (const t of tasks) {
106
+ if (!depthMap.has(t.number)) {
107
+ depthMap.set(t.number, level);
108
+ }
109
+ }
110
+
111
+ // Group by level, then assign phases by accumulating up to maxPerPhase
112
+ const maxLevel = Math.max(...Array.from(depthMap.values()));
113
+ let currentPhase = 1;
114
+ let tasksInCurrentPhase = 0;
115
+
116
+ for (let l = 0; l <= maxLevel; l++) {
117
+ const levelTasks = tasks.filter(t => depthMap.get(t.number) === l);
118
+ if (levelTasks.length === 0) continue;
119
+
120
+ // If adding this level would exceed max, start a new phase
121
+ if (tasksInCurrentPhase > 0 && tasksInCurrentPhase + levelTasks.length > maxPerPhase) {
122
+ currentPhase++;
123
+ tasksInCurrentPhase = 0;
124
+ }
125
+
126
+ // Handle levels with more tasks than maxPerPhase (split within level)
127
+ for (const t of levelTasks) {
128
+ if (tasksInCurrentPhase >= maxPerPhase) {
129
+ currentPhase++;
130
+ tasksInCurrentPhase = 0;
131
+ }
132
+ phaseMap.set(t.number, currentPhase);
133
+ tasksInCurrentPhase++;
134
+ }
135
+ }
136
+
137
+ return phaseMap;
138
+ }
139
+
140
+ // ═══════════════════════════════════════════════════════════════
141
+ // COMMANDS
142
+ // ═══════════════════════════════════════════════════════════════
143
+
7
144
  export function generateSpecId(name: string): string {
8
145
  const date = new Date().toISOString().split("T")[0];
9
146
  const slug = name
@@ -16,7 +153,8 @@ export function generateSpecId(name: string): string {
16
153
  }
17
154
 
18
155
  // v8.4: Suporte a --from-analysis para import automatico de baby steps
19
- export function planStart(description: string, options: { fromAnalysis?: string; json?: boolean } = {}): void {
156
+ // v9.9: Suporte a fases com budget calculator e auto-split
157
+ export function planStart(description: string, options: { fromAnalysis?: string; json?: boolean; maxTasksPerPhase?: number } = {}): void {
20
158
  initSchema();
21
159
  const db = getDb();
22
160
 
@@ -80,27 +218,52 @@ export function planStart(description: string, options: { fromAnalysis?: string;
80
218
  );
81
219
 
82
220
  // v8.4: Import automatico de baby steps da analise
221
+ // v9.9: Com suporte a fases (phase column + auto-split)
83
222
  let tasksCreated = 0;
223
+ let totalPhases = 1;
224
+ const maxPerPhase = options.maxTasksPerPhase || SAFE_TASKS_PER_PHASE;
225
+
84
226
  if (analysis && analysis.baby_steps) {
85
227
  try {
86
228
  const babySteps = JSON.parse(analysis.baby_steps);
229
+
230
+ // v9.9: Determine phases
231
+ const hasExplicitPhases = babySteps.some((s: any) => s.phase != null);
232
+ let phaseMap: Map<number, number>;
233
+
234
+ if (hasExplicitPhases) {
235
+ // Use phases from architect markdown
236
+ phaseMap = new Map(babySteps.map((s: any) => [s.number, s.phase || 1]));
237
+ } else {
238
+ // Auto-assign phases based on dependency topology + budget
239
+ const tasksForPhasing: TaskForPhasing[] = babySteps.map((s: any) => ({
240
+ number: s.number,
241
+ dependsOn: s.dependsOn || [],
242
+ }));
243
+ phaseMap = assignPhases(tasksForPhasing, maxPerPhase);
244
+ }
245
+
246
+ totalPhases = Math.max(1, ...Array.from(phaseMap.values()));
247
+
87
248
  for (const step of babySteps) {
88
249
  const files = step.files && step.files.length > 0 ? JSON.stringify(step.files) : null;
89
250
  const dependsOn = step.dependsOn && step.dependsOn.length > 0 ? JSON.stringify(step.dependsOn) : null;
251
+ const phase = phaseMap.get(step.number) || 1;
90
252
 
91
253
  const normalizedStepAgent = step.agent ? resolveAgentName(step.agent) : null;
92
254
  db.run(
93
- `INSERT INTO tasks (spec_id, number, name, agent, depends_on, can_parallel, files)
94
- VALUES (?, ?, ?, ?, ?, ?, ?)`,
95
- [specId, step.number, step.name, normalizedStepAgent, dependsOn, 1, files]
255
+ `INSERT INTO tasks (spec_id, number, name, agent, depends_on, can_parallel, files, phase)
256
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
257
+ [specId, step.number, step.name, normalizedStepAgent, dependsOn, 1, files, phase]
96
258
  );
97
259
  tasksCreated++;
98
260
  }
99
261
 
100
- // Atualizar total de tasks no contexto
101
- db.run("UPDATE context SET total_tasks = ?, updated_at = ? WHERE spec_id = ?", [
102
- tasksCreated, now, specId
103
- ]);
262
+ // Atualizar total de tasks e fases no contexto
263
+ db.run(
264
+ "UPDATE context SET total_tasks = ?, total_phases = ?, current_phase = 1, updated_at = ? WHERE spec_id = ?",
265
+ [tasksCreated, totalPhases, now, specId]
266
+ );
104
267
 
105
268
  // Marcar analise como implemented
106
269
  db.run(
@@ -114,6 +277,9 @@ export function planStart(description: string, options: { fromAnalysis?: string;
114
277
  }
115
278
  }
116
279
 
280
+ // v9.9: Context pressure analysis
281
+ const pressure = tasksCreated > 0 ? estimateContextPressure(tasksCreated, maxPerPhase) : null;
282
+
117
283
  if (options.json) {
118
284
  console.log(JSON.stringify({
119
285
  success: true,
@@ -122,6 +288,8 @@ export function planStart(description: string, options: { fromAnalysis?: string;
122
288
  phase: "planning",
123
289
  analysisId: analysis?.id || null,
124
290
  tasksImported: tasksCreated,
291
+ totalPhases,
292
+ contextPressure: pressure,
125
293
  }));
126
294
  } else {
127
295
  console.log(`\nFeature iniciada: ${description}`);
@@ -131,6 +299,32 @@ export function planStart(description: string, options: { fromAnalysis?: string;
131
299
  if (tasksCreated > 0) {
132
300
  console.log(`Analise: ${analysis.id}`);
133
301
  console.log(`Tasks importadas: ${tasksCreated}`);
302
+
303
+ // v9.9: Show pressure warning + phase breakdown
304
+ if (pressure && pressure.level !== "green") {
305
+ const icon = pressure.level === "red" ? "[ALTO]" : "[MEDIO]";
306
+ console.log(`\n${icon} Pressao de contexto: ${pressure.level.toUpperCase()} (${tasksCreated} tasks)`);
307
+ console.log(` Apos ~${maxPerPhase} tasks, a qualidade do contexto degrada.`);
308
+ console.log(` Cobertura estimada: ${pressure.decisionCoverage}% decisions, ${pressure.knowledgeCoverage}% knowledge`);
309
+
310
+ if (totalPhases > 1) {
311
+ console.log(`\n Plano dividido em ${totalPhases} fases:`);
312
+ // Count tasks per phase
313
+ const tasksByPhase = new Map<number, number>();
314
+ const allTasks = db.query("SELECT phase FROM tasks WHERE spec_id = ?").all(specId) as any[];
315
+ for (const t of allTasks) {
316
+ tasksByPhase.set(t.phase || 1, (tasksByPhase.get(t.phase || 1) || 0) + 1);
317
+ }
318
+ for (let p = 1; p <= totalPhases; p++) {
319
+ const count = tasksByPhase.get(p) || 0;
320
+ console.log(` Fase ${p}: ${count} tasks`);
321
+ }
322
+ console.log(`\n Entre cada fase, o contexto sera compactado automaticamente.`);
323
+ }
324
+ } else if (totalPhases > 1) {
325
+ console.log(`\nFases: ${totalPhases} (definidas pela analise arquitetural)`);
326
+ }
327
+
134
328
  console.log(`\nProximos passos:`);
135
329
  console.log(`1. Visualize o plano: plan show`);
136
330
  console.log(`2. Solicite aprovacao: check request\n`);
@@ -162,6 +356,9 @@ export function planShow(json: boolean = false, specId?: string): void {
162
356
  return;
163
357
  }
164
358
 
359
+ const currentPhase = context?.current_phase || 1;
360
+ const totalPhases = context?.total_phases || 1;
361
+
165
362
  console.log(`\n${"=".repeat(60)}`);
166
363
  console.log(`PLANO: ${spec.name}`);
167
364
  console.log(`${"=".repeat(60)}`);
@@ -176,25 +373,53 @@ export function planShow(json: boolean = false, specId?: string): void {
176
373
  return;
177
374
  }
178
375
 
179
- console.log(`\nTasks (${tasks.length}):`);
180
- console.log(`${"─".repeat(60)}`);
376
+ // v9.9: Show phase-aware progress
377
+ if (totalPhases > 1) {
378
+ const totalDone = tasks.filter(t => t.status === "done").length;
379
+ const phaseTasks = tasks.filter(t => (t.phase || 1) === currentPhase);
380
+ const phaseDone = phaseTasks.filter(t => t.status === "done").length;
381
+ console.log(`\nFase atual: ${currentPhase}/${totalPhases}`);
382
+ console.log(`Progresso: ${phaseDone}/${phaseTasks.length} (fase ${currentPhase}) | ${totalDone}/${tasks.length} (total)`);
383
+ }
384
+
385
+ // v9.9: Group tasks by phase
386
+ if (totalPhases > 1) {
387
+ for (let phase = 1; phase <= totalPhases; phase++) {
388
+ const phaseTasks = tasks.filter(t => (t.phase || 1) === phase);
389
+ if (phaseTasks.length === 0) continue;
181
390
 
182
- for (const task of tasks) {
183
- const deps = task.depends_on ? JSON.parse(task.depends_on) : [];
184
- const depsStr = deps.length > 0 ? ` [deps: ${deps.join(",")}]` : "";
185
- const parallelStr = task.can_parallel ? " ||" : "";
186
- const statusIcon =
187
- task.status === "done" ? "[x]" :
188
- task.status === "running" ? "[>]" :
189
- task.status === "failed" ? "[!]" : "[ ]";
391
+ const phaseLabel = phase === currentPhase ? "(atual)" :
392
+ phase < currentPhase ? "(concluida)" : "(bloqueada)";
393
+ console.log(`\n--- Fase ${phase} ${phaseLabel} ---`);
190
394
 
191
- console.log(`${statusIcon} #${task.number}: ${task.name} (${task.agent || "geral"})${depsStr}${parallelStr}`);
395
+ for (const task of phaseTasks) {
396
+ printTaskLine(task);
397
+ }
398
+ }
399
+ } else {
400
+ console.log(`\nTasks (${tasks.length}):`);
401
+ console.log(`${"─".repeat(60)}`);
402
+ for (const task of tasks) {
403
+ printTaskLine(task);
404
+ }
192
405
  }
193
406
 
194
407
  console.log(`${"─".repeat(60)}`);
195
408
  console.log(`Legenda: [ ] pendente [>] executando [x] concluido || paralelizavel\n`);
196
409
  }
197
410
 
411
+ function printTaskLine(task: any): void {
412
+ const deps = task.depends_on ? JSON.parse(task.depends_on) : [];
413
+ const depsStr = deps.length > 0 ? ` [deps: ${deps.join(",")}]` : "";
414
+ const parallelStr = task.can_parallel ? " ||" : "";
415
+ const statusIcon =
416
+ task.status === "done" ? "[x]" :
417
+ task.status === "running" ? "[>]" :
418
+ task.status === "failed" ? "[!]" : "[ ]";
419
+
420
+ console.log(`${statusIcon} #${task.number}: ${task.name} (${task.agent || "geral"})${depsStr}${parallelStr}`);
421
+ }
422
+
198
423
  export function planTaskAdd(options: {
199
424
  name: string;
200
425
  agent?: string;
package/commands/task.ts CHANGED
@@ -4,7 +4,7 @@ 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 } from "./knowledge";
7
+ import { getUnreadKnowledgeForTask, compactKnowledge } 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";
@@ -26,6 +26,11 @@ export function taskNext(json: boolean = false, specId?: string): void {
26
26
  throw new TaskStateError("Nenhuma feature em fase de implementacao.\nAprove o plano com: check approve");
27
27
  }
28
28
 
29
+ // v9.9: Get current phase info
30
+ const context = db.query("SELECT * FROM context WHERE spec_id = ?").get(spec.id) as any;
31
+ const currentPhase = context?.current_phase || 1;
32
+ const totalPhases = context?.total_phases || 1;
33
+
29
34
  // Buscar tasks pendentes cujas dependencias estao todas concluidas
30
35
  const allTasks = db
31
36
  .query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
@@ -36,6 +41,9 @@ export function taskNext(json: boolean = false, specId?: string): void {
36
41
  for (const task of allTasks) {
37
42
  if (task.status !== "pending") continue;
38
43
 
44
+ // v9.9: Only show tasks from current phase (if phases are configured)
45
+ if (totalPhases > 1 && (task.phase || 1) !== currentPhase) continue;
46
+
39
47
  // Verificar dependencias
40
48
  const deps = task.depends_on ? JSON.parse(task.depends_on) : [];
41
49
  const allDepsDone = deps.every((depNum: number) => {
@@ -49,13 +57,25 @@ export function taskNext(json: boolean = false, specId?: string): void {
49
57
  }
50
58
 
51
59
  if (json) {
52
- console.log(JSON.stringify({ available }));
60
+ console.log(JSON.stringify({ available, currentPhase, totalPhases }));
53
61
  return;
54
62
  }
55
63
 
56
64
  if (available.length === 0) {
65
+ // v9.9: Check if current phase is complete but more phases remain
66
+ if (totalPhases > 1) {
67
+ const phaseTasks = allTasks.filter((t) => (t.phase || 1) === currentPhase);
68
+ const phaseDone = phaseTasks.every((t) => t.status === "done");
69
+
70
+ if (phaseDone && currentPhase < totalPhases) {
71
+ console.log(`\nFase ${currentPhase}/${totalPhases} concluida!`);
72
+ console.log(`Avance para a proxima fase com: task phase-advance\n`);
73
+ return;
74
+ }
75
+ }
76
+
57
77
  // Verificar se todas estao concluidas
58
- const pending = allTasks.filter((t) => t.status !== "done");
78
+ const pending = allTasks.filter((t) => t.status !== "done" && t.status !== "cancelled");
59
79
  if (pending.length === 0) {
60
80
  console.log("\nTodas as tasks foram concluidas!");
61
81
  console.log("Inicie o review com: review start\n");
@@ -72,6 +92,13 @@ export function taskNext(json: boolean = false, specId?: string): void {
72
92
  return;
73
93
  }
74
94
 
95
+ // v9.9: Show phase context
96
+ if (totalPhases > 1) {
97
+ const phaseTasks = allTasks.filter((t) => (t.phase || 1) === currentPhase);
98
+ const phaseDone = phaseTasks.filter((t) => t.status === "done").length;
99
+ console.log(`\nFase ${currentPhase}/${totalPhases} (${phaseDone}/${phaseTasks.length} concluidas)`);
100
+ }
101
+
75
102
  console.log(`\nTasks disponiveis para execucao (${available.length}):`);
76
103
  console.log(`${"─".repeat(50)}`);
77
104
 
@@ -493,10 +520,48 @@ export function taskDone(id: string, options: { checkpoint: string; files?: stri
493
520
  [checkpoint, now, spec.id]
494
521
  );
495
522
 
523
+ // v9.9: Phase-aware progress
524
+ const ctx = db.query("SELECT * FROM context WHERE spec_id = ?").get(spec.id) as any;
525
+ const currentPhase = ctx?.current_phase || 1;
526
+ const totalPhases = ctx?.total_phases || 1;
527
+
496
528
  console.log(`\nTask #${task.number} concluida!`);
497
529
  console.log(`Checkpoint: ${options.checkpoint}`);
498
530
  console.log(`Progresso: ${doneCount.c}/${totalCount.c} tasks`);
499
531
 
532
+ if (totalPhases > 1) {
533
+ const phaseTasks = db
534
+ .query("SELECT * FROM tasks WHERE spec_id = ? AND phase = ?")
535
+ .all(spec.id, currentPhase) as any[];
536
+ const phaseDone = phaseTasks.filter((t: any) => t.status === "done").length;
537
+ console.log(`Fase ${currentPhase}/${totalPhases}: ${phaseDone}/${phaseTasks.length} tasks`);
538
+
539
+ if (phaseDone === phaseTasks.length && currentPhase < totalPhases) {
540
+ // Phase complete — show phase summary
541
+ const phaseDecisions = db
542
+ .query("SELECT COUNT(*) as c FROM decisions WHERE spec_id = ?")
543
+ .get(spec.id) as any;
544
+ const phaseKnowledge = db
545
+ .query("SELECT COUNT(*) as c FROM knowledge WHERE spec_id = ? AND severity != 'archived'")
546
+ .get(spec.id) as any;
547
+
548
+ const nextPhaseTasks = db
549
+ .query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND phase = ?")
550
+ .get(spec.id, currentPhase + 1) as any;
551
+
552
+ console.log(`\n${"=".repeat(55)}`);
553
+ console.log(`FASE ${currentPhase} CONCLUIDA (${phaseDone}/${phaseTasks.length} tasks)`);
554
+ console.log(`${"=".repeat(55)}`);
555
+ console.log(` Decisions acumuladas: ${phaseDecisions.c}`);
556
+ console.log(` Knowledge acumulado: ${phaseKnowledge.c}`);
557
+ console.log(`\n Proxima fase: ${currentPhase + 1}/${totalPhases} (${nextPhaseTasks?.c || 0} tasks)`);
558
+ console.log(`\n Recomendacao: Compactar contexto antes de continuar`);
559
+ console.log(` -> codexa task phase-advance [--no-compact] [--spec ${spec.id}]`);
560
+ console.log(`${"=".repeat(55)}\n`);
561
+ return;
562
+ }
563
+ }
564
+
500
565
  if (doneCount.c === totalCount.c) {
501
566
  // Mostrar resumo completo da implementacao
502
567
  const allTasks = db.query("SELECT * FROM tasks WHERE spec_id = ?").all(spec.id) as any[];
@@ -613,4 +678,86 @@ function showImplementationSummary(
613
678
  console.log(` Para pular o review: review skip`);
614
679
  console.log(`\n O agente aguarda sua decisao.`);
615
680
  console.log(`${"─".repeat(50)}\n`);
681
+ }
682
+
683
+ // v9.9: Phase advance — compact context and move to next phase
684
+ export function taskPhaseAdvance(options: { noCompact?: boolean; spec?: string } = {}): void {
685
+ initSchema();
686
+ const db = getDb();
687
+
688
+ const spec = resolveSpec(options.spec, ["implementing"]);
689
+ const ctx = db.query("SELECT * FROM context WHERE spec_id = ?").get(spec.id) as any;
690
+ const currentPhase = ctx?.current_phase || 1;
691
+ const totalPhases = ctx?.total_phases || 1;
692
+
693
+ if (totalPhases <= 1) {
694
+ console.log("\nEsta feature nao possui multiplas fases.\n");
695
+ return;
696
+ }
697
+
698
+ if (currentPhase >= totalPhases) {
699
+ console.log("\nTodas as fases ja foram concluidas!");
700
+ console.log("Inicie o review com: review start\n");
701
+ return;
702
+ }
703
+
704
+ // Verify current phase is fully complete
705
+ const phaseTasks = db
706
+ .query("SELECT * FROM tasks WHERE spec_id = ? AND phase = ?")
707
+ .all(spec.id, currentPhase) as any[];
708
+ const pendingInPhase = phaseTasks.filter((t: any) => t.status !== "done" && t.status !== "cancelled");
709
+
710
+ if (pendingInPhase.length > 0) {
711
+ console.log(`\n[ERRO] Fase ${currentPhase} nao esta completa.`);
712
+ console.log(` Tasks pendentes: ${pendingInPhase.map((t: any) => `#${t.number}`).join(", ")}`);
713
+ console.log(` Complete todas as tasks antes de avancar.\n`);
714
+ return;
715
+ }
716
+
717
+ const now = new Date().toISOString();
718
+
719
+ // Compact knowledge (unless --no-compact)
720
+ if (!options.noCompact) {
721
+ console.log(`\nCompactando contexto da fase ${currentPhase}...`);
722
+ compactKnowledge({ specId: spec.id });
723
+ }
724
+
725
+ // Create phase summary as critical knowledge
726
+ const phaseTaskNames = phaseTasks.map((t: any) => t.name).join(", ");
727
+ const decisionCount = (db.query("SELECT COUNT(*) as c FROM decisions WHERE spec_id = ?").get(spec.id) as any).c;
728
+ const artifactCount = (db.query("SELECT COUNT(*) as c FROM artifacts WHERE spec_id = ?").get(spec.id) as any).c;
729
+
730
+ const summaryContent = `Resumo Fase ${currentPhase}: ${phaseTasks.length} tasks concluidas (${phaseTaskNames.substring(0, 200)}). ${decisionCount} decisions, ${artifactCount} artefatos acumulados.`;
731
+
732
+ db.run(
733
+ `INSERT INTO knowledge (spec_id, category, content, severity, source, created_at)
734
+ VALUES (?, 'phase_summary', ?, 'critical', 'system', ?)`,
735
+ [spec.id, summaryContent, now]
736
+ );
737
+
738
+ // Advance to next phase
739
+ const nextPhase = currentPhase + 1;
740
+ db.run(
741
+ "UPDATE context SET current_phase = ?, updated_at = ? WHERE spec_id = ?",
742
+ [nextPhase, now, spec.id]
743
+ );
744
+
745
+ // Show next phase info
746
+ const nextPhaseTasks = db
747
+ .query("SELECT * FROM tasks WHERE spec_id = ? AND phase = ? ORDER BY number")
748
+ .all(spec.id, nextPhase) as any[];
749
+
750
+ console.log(`\n${"=".repeat(55)}`);
751
+ console.log(`FASE ${nextPhase}/${totalPhases} INICIADA`);
752
+ console.log(`${"=".repeat(55)}`);
753
+ console.log(` Tasks nesta fase: ${nextPhaseTasks.length}`);
754
+
755
+ for (const t of nextPhaseTasks) {
756
+ const deps = t.depends_on ? JSON.parse(t.depends_on) : [];
757
+ const depsStr = deps.length > 0 ? ` [deps: ${deps.join(",")}]` : "";
758
+ console.log(` #${t.number}: ${t.name} (${t.agent || "geral"})${depsStr}`);
759
+ }
760
+
761
+ console.log(`\n Proxima task: task next`);
762
+ console.log(`${"=".repeat(55)}\n`);
616
763
  }
package/db/schema.ts CHANGED
@@ -487,6 +487,15 @@ const MIGRATIONS: Migration[] = [
487
487
  db.exec(`ALTER TABLE project ADD COLUMN grepai_workspace TEXT`);
488
488
  },
489
489
  },
490
+ {
491
+ version: "9.9.0",
492
+ description: "Adicionar suporte a fases: phase em tasks, total_phases/current_phase em context",
493
+ up: (db) => {
494
+ db.exec(`ALTER TABLE tasks ADD COLUMN phase INTEGER DEFAULT 1`);
495
+ db.exec(`ALTER TABLE context ADD COLUMN total_phases INTEGER DEFAULT 1`);
496
+ db.exec(`ALTER TABLE context ADD COLUMN current_phase INTEGER DEFAULT 1`);
497
+ },
498
+ },
490
499
  ];
491
500
 
492
501
  export function runMigrations(): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codexa/cli",
3
- "version": "9.0.15",
3
+ "version": "9.0.16",
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": {
package/workflow.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { Command } from "commander";
4
4
  import { planStart, planShow, planTaskAdd, planCancel } from "./commands/plan";
5
5
  import { checkRequest, checkApprove, checkReject } from "./commands/check";
6
- import { taskNext, taskStart, taskDone } from "./commands/task";
6
+ import { taskNext, taskStart, taskDone, taskPhaseAdvance } from "./commands/task";
7
7
  import { decide, listDecisions, revokeDecision, supersedeDecision } from "./commands/decide";
8
8
  import { reviewStart, reviewApprove, reviewSkip } from "./commands/review";
9
9
  import { addKnowledge, listKnowledge, acknowledgeKnowledge, queryGraph, resolveKnowledge, compactKnowledge } from "./commands/knowledge";
@@ -168,8 +168,9 @@ planCmd
168
168
  .command("start <description>")
169
169
  .description("Inicia uma nova feature")
170
170
  .option("--from-analysis <id>", "Importar baby steps de analise arquitetural aprovada")
171
+ .option("--max-tasks-per-phase <n>", "Maximo de tasks por fase (padrao: 12)", parseInt)
171
172
  .option("--json", "Saida em JSON")
172
- .action((description: string, options: { fromAnalysis?: string; json?: boolean }) => {
173
+ .action((description: string, options: { fromAnalysis?: string; maxTasksPerPhase?: number; json?: boolean }) => {
173
174
  planStart(description, options);
174
175
  });
175
176
 
@@ -306,6 +307,15 @@ taskCmd
306
307
  });
307
308
  }));
308
309
 
310
+ taskCmd
311
+ .command("phase-advance")
312
+ .description("Avanca para a proxima fase (compacta contexto automaticamente)")
313
+ .option("--no-compact", "Nao compactar knowledge antes de avancar")
314
+ .option("--spec <id>", "ID do spec (padrao: mais recente)")
315
+ .action(wrapAction((options) => {
316
+ taskPhaseAdvance({ noCompact: options.compact === false, spec: options.spec });
317
+ }));
318
+
309
319
  // ═══════════════════════════════════════════════════════════════
310
320
  // DECISOES
311
321
  // ═══════════════════════════════════════════════════════════════