@codexa/cli 9.0.14 → 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.
- package/commands/architect.test.ts +72 -0
- package/commands/architect.ts +70 -20
- package/commands/plan.test.ts +149 -1
- package/commands/plan.ts +253 -20
- package/commands/task.ts +150 -3
- package/context/agent-registry.test.ts +61 -0
- package/context/agent-registry.ts +19 -2
- package/db/schema.ts +9 -0
- package/package.json +1 -1
- package/workflow.ts +12 -2
|
@@ -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[] = [];
|
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
|
@@ -2,7 +2,144 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
3
3
|
import { resolveSpec } from "./spec-resolver";
|
|
4
4
|
import { CodexaError, ValidationError } from "../errors";
|
|
5
|
-
import { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames } from "../context/agent-registry";
|
|
5
|
+
import { resolveAgent, resolveAgentName, suggestAgent, getCanonicalAgentNames, getAgentsByDomain } from "../context/agent-registry";
|
|
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
|
+
// ═══════════════════════════════════════════════════════════════
|
|
6
143
|
|
|
7
144
|
export function generateSpecId(name: string): string {
|
|
8
145
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -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
|
|
|
@@ -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(
|
|
102
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
+
}
|
|
181
384
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
task.status === "done" ? "[x]" :
|
|
188
|
-
task.status === "running" ? "[>]" :
|
|
189
|
-
task.status === "failed" ? "[!]" : "[ ]";
|
|
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;
|
|
190
390
|
|
|
191
|
-
|
|
391
|
+
const phaseLabel = phase === currentPhase ? "(atual)" :
|
|
392
|
+
phase < currentPhase ? "(concluida)" : "(bloqueada)";
|
|
393
|
+
console.log(`\n--- Fase ${phase} ${phaseLabel} ---`);
|
|
394
|
+
|
|
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;
|
|
@@ -273,6 +498,14 @@ export function planTaskAdd(options: {
|
|
|
273
498
|
if (normalizedAgent) {
|
|
274
499
|
const entry = resolveAgent(normalizedAgent);
|
|
275
500
|
if (!entry) {
|
|
501
|
+
// Check if it's an ambiguous domain name (multiple agents)
|
|
502
|
+
const domainAgents = getAgentsByDomain(normalizedAgent.toLowerCase());
|
|
503
|
+
if (domainAgents.length > 1) {
|
|
504
|
+
const agentList = domainAgents.map(a => `"${a.canonical}"`).join(", ");
|
|
505
|
+
throw new ValidationError(
|
|
506
|
+
`"${normalizedAgent}" e um dominio com ${domainAgents.length} agentes: ${agentList}.\nEspecifique qual agente usar.`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
276
509
|
const suggestion = suggestAgent(normalizedAgent);
|
|
277
510
|
const hint = suggestion
|
|
278
511
|
? `\nVoce quis dizer: "${suggestion}"?`
|
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
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildAgentDomainMap,
|
|
8
8
|
getAllAgentNames,
|
|
9
9
|
getCanonicalAgentNames,
|
|
10
|
+
getAgentsByDomain,
|
|
10
11
|
} from "./agent-registry";
|
|
11
12
|
|
|
12
13
|
// ── AGENT_REGISTRY structure ─────────────────────────────────
|
|
@@ -90,6 +91,27 @@ describe("resolveAgent", () => {
|
|
|
90
91
|
it("returns null for empty/null-like input", () => {
|
|
91
92
|
expect(resolveAgent("")).toBeNull();
|
|
92
93
|
});
|
|
94
|
+
|
|
95
|
+
// Domain-based resolution (v9.5)
|
|
96
|
+
it("resolves unambiguous domain names (1 agent in domain)", () => {
|
|
97
|
+
expect(resolveAgent("database")?.canonical).toBe("expert-postgres-developer");
|
|
98
|
+
expect(resolveAgent("testing")?.canonical).toBe("testing-unit");
|
|
99
|
+
expect(resolveAgent("review")?.canonical).toBe("expert-code-reviewer");
|
|
100
|
+
expect(resolveAgent("security")?.canonical).toBe("security-specialist");
|
|
101
|
+
expect(resolveAgent("explore")?.canonical).toBe("deep-explore");
|
|
102
|
+
expect(resolveAgent("architecture")?.canonical).toBe("architect");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns null for ambiguous domain names (multiple agents)", () => {
|
|
106
|
+
// frontend has nextjs + flutter, backend has go + csharp + js
|
|
107
|
+
expect(resolveAgent("frontend")).toBeNull();
|
|
108
|
+
expect(resolveAgent("backend")).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("domain resolution is case-insensitive", () => {
|
|
112
|
+
expect(resolveAgent("DATABASE")?.canonical).toBe("expert-postgres-developer");
|
|
113
|
+
expect(resolveAgent("Testing")?.canonical).toBe("testing-unit");
|
|
114
|
+
});
|
|
93
115
|
});
|
|
94
116
|
|
|
95
117
|
// ── resolveAgentName ─────────────────────────────────────────
|
|
@@ -111,6 +133,16 @@ describe("resolveAgentName", () => {
|
|
|
111
133
|
expect(resolveAgentName("custom-agent")).toBe("custom-agent");
|
|
112
134
|
expect(resolveAgentName("my-special-agent")).toBe("my-special-agent");
|
|
113
135
|
});
|
|
136
|
+
|
|
137
|
+
it("resolves unambiguous domain names to canonical", () => {
|
|
138
|
+
expect(resolveAgentName("database")).toBe("expert-postgres-developer");
|
|
139
|
+
expect(resolveAgentName("testing")).toBe("testing-unit");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns ambiguous domain names unchanged (backward compat)", () => {
|
|
143
|
+
expect(resolveAgentName("frontend")).toBe("frontend");
|
|
144
|
+
expect(resolveAgentName("backend")).toBe("backend");
|
|
145
|
+
});
|
|
114
146
|
});
|
|
115
147
|
|
|
116
148
|
// ── suggestAgent ─────────────────────────────────────────────
|
|
@@ -163,6 +195,35 @@ describe("buildAgentDomainMap", () => {
|
|
|
163
195
|
});
|
|
164
196
|
});
|
|
165
197
|
|
|
198
|
+
// ── getAgentsByDomain ────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe("getAgentsByDomain", () => {
|
|
201
|
+
it("returns agents in multi-agent domains", () => {
|
|
202
|
+
const frontend = getAgentsByDomain("frontend");
|
|
203
|
+
expect(frontend).toHaveLength(2);
|
|
204
|
+
expect(frontend.map(a => a.canonical)).toContain("expert-nextjs-developer");
|
|
205
|
+
expect(frontend.map(a => a.canonical)).toContain("frontend-flutter");
|
|
206
|
+
|
|
207
|
+
const backend = getAgentsByDomain("backend");
|
|
208
|
+
expect(backend).toHaveLength(3);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns agents in single-agent domains", () => {
|
|
212
|
+
const database = getAgentsByDomain("database");
|
|
213
|
+
expect(database).toHaveLength(1);
|
|
214
|
+
expect(database[0].canonical).toBe("expert-postgres-developer");
|
|
215
|
+
|
|
216
|
+
const testing = getAgentsByDomain("testing");
|
|
217
|
+
expect(testing).toHaveLength(1);
|
|
218
|
+
expect(testing[0].canonical).toBe("testing-unit");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns empty array for unknown domains", () => {
|
|
222
|
+
expect(getAgentsByDomain("unknown")).toHaveLength(0);
|
|
223
|
+
expect(getAgentsByDomain("general")).toHaveLength(0);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
166
227
|
// ── getAllAgentNames / getCanonicalAgentNames ─────────────────
|
|
167
228
|
|
|
168
229
|
describe("getAllAgentNames", () => {
|
|
@@ -115,12 +115,21 @@ for (const entry of AGENT_REGISTRY) {
|
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Resolve any agent name variant (canonical, filename, alias) to registry entry.
|
|
118
|
-
* Case-insensitive.
|
|
118
|
+
* Case-insensitive. Falls back to domain-based resolution when exactly 1 agent
|
|
119
|
+
* exists in the matching domain (e.g., "database" → expert-postgres-developer).
|
|
120
|
+
* Returns null if no match found or if domain has multiple agents (ambiguous).
|
|
119
121
|
*/
|
|
120
122
|
export function resolveAgent(name: string): AgentEntry | null {
|
|
121
123
|
if (!name) return null;
|
|
122
124
|
const lower = name.toLowerCase();
|
|
123
|
-
|
|
125
|
+
const direct = byCanonical.get(lower) ?? byAlias.get(lower);
|
|
126
|
+
if (direct) return direct;
|
|
127
|
+
|
|
128
|
+
// Domain fallback: resolve "frontend", "backend", etc. when unambiguous
|
|
129
|
+
const domainAgents = getAgentsByDomain(lower);
|
|
130
|
+
if (domainAgents.length === 1) return domainAgents[0];
|
|
131
|
+
|
|
132
|
+
return null;
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
/**
|
|
@@ -171,6 +180,14 @@ export function suggestAgent(input: string): string | null {
|
|
|
171
180
|
return null;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Get all agents belonging to a specific domain.
|
|
185
|
+
* Used for domain-based resolution (e.g., "frontend" → agents in frontend domain).
|
|
186
|
+
*/
|
|
187
|
+
export function getAgentsByDomain(domain: string): AgentEntry[] {
|
|
188
|
+
return AGENT_REGISTRY.filter(e => e.domain === domain);
|
|
189
|
+
}
|
|
190
|
+
|
|
174
191
|
/**
|
|
175
192
|
* Derive AGENT_DOMAIN map from registry (backward compat with domains.ts).
|
|
176
193
|
*/
|
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.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
|
// ═══════════════════════════════════════════════════════════════
|