@codexa/cli 9.0.15 → 9.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/architect.test.ts +72 -0
- package/commands/architect.ts +93 -20
- package/commands/plan.test.ts +149 -1
- package/commands/plan.ts +275 -20
- package/commands/task.ts +156 -9
- package/db/schema.ts +9 -0
- package/package.json +1 -1
- package/workflow.ts +14 -4
|
@@ -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", () => {
|
package/commands/architect.ts
CHANGED
|
@@ -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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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[] = [];
|
|
@@ -594,6 +644,29 @@ export function architectSave(options: { file?: string; json?: boolean }): void
|
|
|
594
644
|
const content = readFileSync(filePath, "utf-8");
|
|
595
645
|
const parsed = parseAnalysisMd(content);
|
|
596
646
|
|
|
647
|
+
// v10.0: Validar secoes criticas antes de salvar
|
|
648
|
+
if (!parsed.babySteps || parsed.babySteps.length === 0) {
|
|
649
|
+
const msg = "Secao 'Baby Steps' esta vazia ou nao foi encontrada no .md. "
|
|
650
|
+
+ "Verifique se os headers seguem o formato '## Baby Steps' e cada step usa '### N. Nome'.";
|
|
651
|
+
if (options.json) {
|
|
652
|
+
console.log(JSON.stringify({ error: "EMPTY_BABY_STEPS", message: msg }));
|
|
653
|
+
} else {
|
|
654
|
+
console.error(`\n[ERRO] ${msg}\n`);
|
|
655
|
+
}
|
|
656
|
+
throw new CodexaError(msg);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!parsed.approach) {
|
|
660
|
+
const msg = "Secao 'Solucao Proposta' esta vazia ou nao foi encontrada no .md. "
|
|
661
|
+
+ "Verifique se o header segue o formato '## Solucao Proposta'.";
|
|
662
|
+
if (options.json) {
|
|
663
|
+
console.log(JSON.stringify({ error: "EMPTY_APPROACH", message: msg }));
|
|
664
|
+
} else {
|
|
665
|
+
console.error(`\n[ERRO] ${msg}\n`);
|
|
666
|
+
}
|
|
667
|
+
throw new CodexaError(msg);
|
|
668
|
+
}
|
|
669
|
+
|
|
597
670
|
// Popular DB com dados extraidos do .md
|
|
598
671
|
const updates: string[] = [];
|
|
599
672
|
const values: any[] = [];
|
package/commands/plan.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -47,8 +185,23 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
47
185
|
}
|
|
48
186
|
} else {
|
|
49
187
|
// v8.4: Auto-deteccao por nome
|
|
188
|
+
// v10.0: Alertar se analise aprovada existe mas --from-analysis nao foi usado
|
|
50
189
|
const match = getArchitecturalAnalysisForSpec(description);
|
|
51
|
-
if (match) {
|
|
190
|
+
if (match && match.status === "approved") {
|
|
191
|
+
const msg = `Analise arquitetural aprovada encontrada: ${match.id} (${match.name}).\n`
|
|
192
|
+
+ `Use: plan start "${description}" --from-analysis ${match.id}`;
|
|
193
|
+
if (options.json) {
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
error: "APPROVED_ANALYSIS_EXISTS",
|
|
196
|
+
analysisId: match.id,
|
|
197
|
+
analysisName: match.name,
|
|
198
|
+
suggestion: `plan start "${description}" --from-analysis ${match.id}`,
|
|
199
|
+
}));
|
|
200
|
+
} else {
|
|
201
|
+
console.error(`\n[ERRO] ${msg}\n`);
|
|
202
|
+
}
|
|
203
|
+
throw new CodexaError(msg);
|
|
204
|
+
} else if (match) {
|
|
52
205
|
analysis = match;
|
|
53
206
|
if (!options.json) {
|
|
54
207
|
console.log(`\n[INFO] Analise arquitetural encontrada: ${match.id} (${match.name})`);
|
|
@@ -80,27 +233,52 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
80
233
|
);
|
|
81
234
|
|
|
82
235
|
// v8.4: Import automatico de baby steps da analise
|
|
236
|
+
// v9.9: Com suporte a fases (phase column + auto-split)
|
|
83
237
|
let tasksCreated = 0;
|
|
238
|
+
let totalPhases = 1;
|
|
239
|
+
const maxPerPhase = options.maxTasksPerPhase || SAFE_TASKS_PER_PHASE;
|
|
240
|
+
|
|
84
241
|
if (analysis && analysis.baby_steps) {
|
|
85
242
|
try {
|
|
86
243
|
const babySteps = JSON.parse(analysis.baby_steps);
|
|
244
|
+
|
|
245
|
+
// v9.9: Determine phases
|
|
246
|
+
const hasExplicitPhases = babySteps.some((s: any) => s.phase != null);
|
|
247
|
+
let phaseMap: Map<number, number>;
|
|
248
|
+
|
|
249
|
+
if (hasExplicitPhases) {
|
|
250
|
+
// Use phases from architect markdown
|
|
251
|
+
phaseMap = new Map(babySteps.map((s: any) => [s.number, s.phase || 1]));
|
|
252
|
+
} else {
|
|
253
|
+
// Auto-assign phases based on dependency topology + budget
|
|
254
|
+
const tasksForPhasing: TaskForPhasing[] = babySteps.map((s: any) => ({
|
|
255
|
+
number: s.number,
|
|
256
|
+
dependsOn: s.dependsOn || [],
|
|
257
|
+
}));
|
|
258
|
+
phaseMap = assignPhases(tasksForPhasing, maxPerPhase);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
totalPhases = Math.max(1, ...Array.from(phaseMap.values()));
|
|
262
|
+
|
|
87
263
|
for (const step of babySteps) {
|
|
88
264
|
const files = step.files && step.files.length > 0 ? JSON.stringify(step.files) : null;
|
|
89
265
|
const dependsOn = step.dependsOn && step.dependsOn.length > 0 ? JSON.stringify(step.dependsOn) : null;
|
|
266
|
+
const phase = phaseMap.get(step.number) || 1;
|
|
90
267
|
|
|
91
268
|
const normalizedStepAgent = step.agent ? resolveAgentName(step.agent) : null;
|
|
92
269
|
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]
|
|
270
|
+
`INSERT INTO tasks (spec_id, number, name, agent, depends_on, can_parallel, files, phase)
|
|
271
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
272
|
+
[specId, step.number, step.name, normalizedStepAgent, dependsOn, 1, files, phase]
|
|
96
273
|
);
|
|
97
274
|
tasksCreated++;
|
|
98
275
|
}
|
|
99
276
|
|
|
100
|
-
// Atualizar total de tasks no contexto
|
|
101
|
-
db.run(
|
|
102
|
-
|
|
103
|
-
|
|
277
|
+
// Atualizar total de tasks e fases no contexto
|
|
278
|
+
db.run(
|
|
279
|
+
"UPDATE context SET total_tasks = ?, total_phases = ?, current_phase = 1, updated_at = ? WHERE spec_id = ?",
|
|
280
|
+
[tasksCreated, totalPhases, now, specId]
|
|
281
|
+
);
|
|
104
282
|
|
|
105
283
|
// Marcar analise como implemented
|
|
106
284
|
db.run(
|
|
@@ -112,8 +290,26 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
112
290
|
console.error("[AVISO] Falha ao parsear baby steps da analise. Adicione tasks manualmente.\n");
|
|
113
291
|
}
|
|
114
292
|
}
|
|
293
|
+
|
|
294
|
+
// v10.0: Se --from-analysis foi explicito e importou 0 tasks, e um erro
|
|
295
|
+
if (options.fromAnalysis && tasksCreated === 0) {
|
|
296
|
+
const msg = `Analise ${analysis.id} nao contem baby steps validos. `
|
|
297
|
+
+ `Verifique o .md com 'architect show --id ${analysis.id}' e corrija a secao Baby Steps.`;
|
|
298
|
+
if (options.json) {
|
|
299
|
+
console.log(JSON.stringify({ error: "ZERO_TASKS_IMPORTED", analysisId: analysis.id, message: msg }));
|
|
300
|
+
} else {
|
|
301
|
+
console.error(`\n[ERRO] ${msg}\n`);
|
|
302
|
+
}
|
|
303
|
+
// Cleanup: remover spec vazio criado
|
|
304
|
+
db.run("DELETE FROM context WHERE spec_id = ?", [specId]);
|
|
305
|
+
db.run("DELETE FROM specs WHERE id = ?", [specId]);
|
|
306
|
+
throw new CodexaError(msg);
|
|
307
|
+
}
|
|
115
308
|
}
|
|
116
309
|
|
|
310
|
+
// v9.9: Context pressure analysis
|
|
311
|
+
const pressure = tasksCreated > 0 ? estimateContextPressure(tasksCreated, maxPerPhase) : null;
|
|
312
|
+
|
|
117
313
|
if (options.json) {
|
|
118
314
|
console.log(JSON.stringify({
|
|
119
315
|
success: true,
|
|
@@ -122,6 +318,8 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
122
318
|
phase: "planning",
|
|
123
319
|
analysisId: analysis?.id || null,
|
|
124
320
|
tasksImported: tasksCreated,
|
|
321
|
+
totalPhases,
|
|
322
|
+
contextPressure: pressure,
|
|
125
323
|
}));
|
|
126
324
|
} else {
|
|
127
325
|
console.log(`\nFeature iniciada: ${description}`);
|
|
@@ -131,6 +329,32 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
131
329
|
if (tasksCreated > 0) {
|
|
132
330
|
console.log(`Analise: ${analysis.id}`);
|
|
133
331
|
console.log(`Tasks importadas: ${tasksCreated}`);
|
|
332
|
+
|
|
333
|
+
// v9.9: Show pressure warning + phase breakdown
|
|
334
|
+
if (pressure && pressure.level !== "green") {
|
|
335
|
+
const icon = pressure.level === "red" ? "[ALTO]" : "[MEDIO]";
|
|
336
|
+
console.log(`\n${icon} Pressao de contexto: ${pressure.level.toUpperCase()} (${tasksCreated} tasks)`);
|
|
337
|
+
console.log(` Apos ~${maxPerPhase} tasks, a qualidade do contexto degrada.`);
|
|
338
|
+
console.log(` Cobertura estimada: ${pressure.decisionCoverage}% decisions, ${pressure.knowledgeCoverage}% knowledge`);
|
|
339
|
+
|
|
340
|
+
if (totalPhases > 1) {
|
|
341
|
+
console.log(`\n Plano dividido em ${totalPhases} fases:`);
|
|
342
|
+
// Count tasks per phase
|
|
343
|
+
const tasksByPhase = new Map<number, number>();
|
|
344
|
+
const allTasks = db.query("SELECT phase FROM tasks WHERE spec_id = ?").all(specId) as any[];
|
|
345
|
+
for (const t of allTasks) {
|
|
346
|
+
tasksByPhase.set(t.phase || 1, (tasksByPhase.get(t.phase || 1) || 0) + 1);
|
|
347
|
+
}
|
|
348
|
+
for (let p = 1; p <= totalPhases; p++) {
|
|
349
|
+
const count = tasksByPhase.get(p) || 0;
|
|
350
|
+
console.log(` Fase ${p}: ${count} tasks`);
|
|
351
|
+
}
|
|
352
|
+
console.log(`\n Entre cada fase, o contexto sera compactado automaticamente.`);
|
|
353
|
+
}
|
|
354
|
+
} else if (totalPhases > 1) {
|
|
355
|
+
console.log(`\nFases: ${totalPhases} (definidas pela analise arquitetural)`);
|
|
356
|
+
}
|
|
357
|
+
|
|
134
358
|
console.log(`\nProximos passos:`);
|
|
135
359
|
console.log(`1. Visualize o plano: plan show`);
|
|
136
360
|
console.log(`2. Solicite aprovacao: check request\n`);
|
|
@@ -162,6 +386,9 @@ export function planShow(json: boolean = false, specId?: string): void {
|
|
|
162
386
|
return;
|
|
163
387
|
}
|
|
164
388
|
|
|
389
|
+
const currentPhase = context?.current_phase || 1;
|
|
390
|
+
const totalPhases = context?.total_phases || 1;
|
|
391
|
+
|
|
165
392
|
console.log(`\n${"=".repeat(60)}`);
|
|
166
393
|
console.log(`PLANO: ${spec.name}`);
|
|
167
394
|
console.log(`${"=".repeat(60)}`);
|
|
@@ -176,25 +403,53 @@ export function planShow(json: boolean = false, specId?: string): void {
|
|
|
176
403
|
return;
|
|
177
404
|
}
|
|
178
405
|
|
|
179
|
-
|
|
180
|
-
|
|
406
|
+
// v9.9: Show phase-aware progress
|
|
407
|
+
if (totalPhases > 1) {
|
|
408
|
+
const totalDone = tasks.filter(t => t.status === "done").length;
|
|
409
|
+
const phaseTasks = tasks.filter(t => (t.phase || 1) === currentPhase);
|
|
410
|
+
const phaseDone = phaseTasks.filter(t => t.status === "done").length;
|
|
411
|
+
console.log(`\nFase atual: ${currentPhase}/${totalPhases}`);
|
|
412
|
+
console.log(`Progresso: ${phaseDone}/${phaseTasks.length} (fase ${currentPhase}) | ${totalDone}/${tasks.length} (total)`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// v9.9: Group tasks by phase
|
|
416
|
+
if (totalPhases > 1) {
|
|
417
|
+
for (let phase = 1; phase <= totalPhases; phase++) {
|
|
418
|
+
const phaseTasks = tasks.filter(t => (t.phase || 1) === phase);
|
|
419
|
+
if (phaseTasks.length === 0) continue;
|
|
181
420
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const parallelStr = task.can_parallel ? " ||" : "";
|
|
186
|
-
const statusIcon =
|
|
187
|
-
task.status === "done" ? "[x]" :
|
|
188
|
-
task.status === "running" ? "[>]" :
|
|
189
|
-
task.status === "failed" ? "[!]" : "[ ]";
|
|
421
|
+
const phaseLabel = phase === currentPhase ? "(atual)" :
|
|
422
|
+
phase < currentPhase ? "(concluida)" : "(bloqueada)";
|
|
423
|
+
console.log(`\n--- Fase ${phase} ${phaseLabel} ---`);
|
|
190
424
|
|
|
191
|
-
|
|
425
|
+
for (const task of phaseTasks) {
|
|
426
|
+
printTaskLine(task);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
console.log(`\nTasks (${tasks.length}):`);
|
|
431
|
+
console.log(`${"─".repeat(60)}`);
|
|
432
|
+
for (const task of tasks) {
|
|
433
|
+
printTaskLine(task);
|
|
434
|
+
}
|
|
192
435
|
}
|
|
193
436
|
|
|
194
437
|
console.log(`${"─".repeat(60)}`);
|
|
195
438
|
console.log(`Legenda: [ ] pendente [>] executando [x] concluido || paralelizavel\n`);
|
|
196
439
|
}
|
|
197
440
|
|
|
441
|
+
function printTaskLine(task: any): void {
|
|
442
|
+
const deps = task.depends_on ? JSON.parse(task.depends_on) : [];
|
|
443
|
+
const depsStr = deps.length > 0 ? ` [deps: ${deps.join(",")}]` : "";
|
|
444
|
+
const parallelStr = task.can_parallel ? " ||" : "";
|
|
445
|
+
const statusIcon =
|
|
446
|
+
task.status === "done" ? "[x]" :
|
|
447
|
+
task.status === "running" ? "[>]" :
|
|
448
|
+
task.status === "failed" ? "[!]" : "[ ]";
|
|
449
|
+
|
|
450
|
+
console.log(`${statusIcon} #${task.number}: ${task.name} (${task.agent || "geral"})${depsStr}${parallelStr}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
198
453
|
export function planTaskAdd(options: {
|
|
199
454
|
name: string;
|
|
200
455
|
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
|
|
|
@@ -119,7 +146,7 @@ function showStuckWarning(stuck: any[]): void {
|
|
|
119
146
|
console.log(` Use: task done <id> --force --force-reason "timeout" para liberar\n`);
|
|
120
147
|
}
|
|
121
148
|
|
|
122
|
-
export function taskStart(ids: string, json: boolean = false,
|
|
149
|
+
export function taskStart(ids: string, json: boolean = false, minimalContext: boolean = false, specId?: string): void {
|
|
123
150
|
initSchema();
|
|
124
151
|
enforceGate("task-start");
|
|
125
152
|
|
|
@@ -182,10 +209,10 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
|
|
|
182
209
|
if (json) {
|
|
183
210
|
// NOVO: Incluir contexto COMPLETO para cada task
|
|
184
211
|
const contexts = startedTasks.map((task) => {
|
|
185
|
-
//
|
|
186
|
-
const contextText =
|
|
187
|
-
?
|
|
188
|
-
:
|
|
212
|
+
// v10.0: Contexto completo por padrao, reduzido via --minimal-context
|
|
213
|
+
const contextText = minimalContext
|
|
214
|
+
? getMinimalContextForSubagent(task.id)
|
|
215
|
+
: getContextForSubagent(task.id);
|
|
189
216
|
const unreadKnowledge = getUnreadKnowledgeForTask(spec.id, task.id);
|
|
190
217
|
|
|
191
218
|
// NOVO v7.4: Buscar implementation patterns relevantes
|
|
@@ -249,7 +276,7 @@ export function taskStart(ids: string, json: boolean = false, fullContext: boole
|
|
|
249
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.",
|
|
250
277
|
// Contexto para o subagent (NAO para o orquestrador)
|
|
251
278
|
context: contextText,
|
|
252
|
-
contextMode:
|
|
279
|
+
contextMode: minimalContext ? "minimal" : "full",
|
|
253
280
|
// Knowledge nao lido (broadcast de outras tasks)
|
|
254
281
|
unreadKnowledge: unreadKnowledge.map((k: any) => ({
|
|
255
282
|
id: k.id,
|
|
@@ -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.
|
|
3
|
+
"version": "9.0.17",
|
|
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
|
|
|
@@ -252,10 +253,10 @@ taskCmd
|
|
|
252
253
|
.command("start <ids>")
|
|
253
254
|
.description("Inicia task(s) - pode ser multiplas separadas por virgula")
|
|
254
255
|
.option("--json", "Saida em JSON")
|
|
255
|
-
.option("--
|
|
256
|
+
.option("--minimal-context", "Usar contexto reduzido (2KB) em vez do completo (16KB)")
|
|
256
257
|
.option("--spec <id>", "ID do spec (padrao: mais recente)")
|
|
257
258
|
.action(wrapAction((ids: string, options) => {
|
|
258
|
-
taskStart(ids, options.json, options.
|
|
259
|
+
taskStart(ids, options.json, options.minimalContext, options.spec);
|
|
259
260
|
}));
|
|
260
261
|
|
|
261
262
|
taskCmd
|
|
@@ -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
|
// ═══════════════════════════════════════════════════════════════
|