opencode-conductor-cdd-plugin 1.0.0-beta.16 → 1.0.0-beta.18
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/dist/prompts/agent/cdd.md +16 -16
- package/dist/prompts/agent/implementer.md +5 -5
- package/dist/prompts/agent.md +7 -7
- package/dist/prompts/cdd/implement.json +1 -1
- package/dist/prompts/cdd/newTrack.json +1 -1
- package/dist/prompts/cdd/revert.json +1 -1
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/status.json +1 -1
- package/dist/test/integration/rebrand.test.js +15 -14
- package/dist/utils/agentMapping.js +2 -0
- package/dist/utils/archive-tracks.d.ts +53 -0
- package/dist/utils/archive-tracks.js +154 -0
- package/dist/utils/archive-tracks.test.d.ts +1 -0
- package/dist/utils/archive-tracks.test.js +495 -0
- package/dist/utils/metadataTracker.d.ts +39 -0
- package/dist/utils/metadataTracker.js +105 -0
- package/dist/utils/metadataTracker.test.d.ts +1 -0
- package/dist/utils/metadataTracker.test.js +265 -0
- package/dist/utils/planParser.d.ts +25 -0
- package/dist/utils/planParser.js +107 -0
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/utils/planParser.test.js +119 -0
- package/dist/utils/statusDisplay.d.ts +35 -0
- package/dist/utils/statusDisplay.js +81 -0
- package/dist/utils/statusDisplay.test.d.ts +1 -0
- package/dist/utils/statusDisplay.test.js +102 -0
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
export class MetadataTracker {
|
|
4
|
+
workDir;
|
|
5
|
+
trackId;
|
|
6
|
+
metadataPath;
|
|
7
|
+
constructor(workDir, trackId) {
|
|
8
|
+
this.workDir = workDir;
|
|
9
|
+
this.trackId = trackId;
|
|
10
|
+
this.metadataPath = join(workDir, "conductor-cdd", "tracks", trackId, "status.json");
|
|
11
|
+
}
|
|
12
|
+
ensureDirectory() {
|
|
13
|
+
const dir = join(this.metadataPath, "..");
|
|
14
|
+
if (!existsSync(dir)) {
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
initializeMetadata(metadata) {
|
|
19
|
+
this.ensureDirectory();
|
|
20
|
+
writeFileSync(this.metadataPath, JSON.stringify(metadata, null, 2));
|
|
21
|
+
}
|
|
22
|
+
readMetadata() {
|
|
23
|
+
if (!existsSync(this.metadataPath)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(this.metadataPath, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
writeMetadata(metadata) {
|
|
34
|
+
metadata.updatedAt = new Date().toISOString();
|
|
35
|
+
writeFileSync(this.metadataPath, JSON.stringify(metadata, null, 2));
|
|
36
|
+
}
|
|
37
|
+
updateTaskStatus(phaseId, taskId, status, commitSha) {
|
|
38
|
+
const metadata = this.readMetadata();
|
|
39
|
+
if (!metadata) {
|
|
40
|
+
throw new Error("Metadata file does not exist. Initialize first.");
|
|
41
|
+
}
|
|
42
|
+
const phase = metadata.phases.find(p => p.phaseId === phaseId);
|
|
43
|
+
if (!phase) {
|
|
44
|
+
throw new Error(`Phase '${phaseId}' not found`);
|
|
45
|
+
}
|
|
46
|
+
const task = phase.tasks.find(t => t.taskId === taskId);
|
|
47
|
+
if (!task) {
|
|
48
|
+
throw new Error(`Task '${taskId}' not found in phase '${phaseId}'`);
|
|
49
|
+
}
|
|
50
|
+
task.status = status;
|
|
51
|
+
if (commitSha) {
|
|
52
|
+
task.commitSha = commitSha;
|
|
53
|
+
}
|
|
54
|
+
if (status === "in_progress" && !task.startedAt) {
|
|
55
|
+
task.startedAt = new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
if (status === "completed") {
|
|
58
|
+
task.completedAt = new Date().toISOString();
|
|
59
|
+
}
|
|
60
|
+
this.writeMetadata(metadata);
|
|
61
|
+
}
|
|
62
|
+
updatePhaseStatus(phaseId, status, checkpointSha) {
|
|
63
|
+
const metadata = this.readMetadata();
|
|
64
|
+
if (!metadata) {
|
|
65
|
+
throw new Error("Metadata file does not exist. Initialize first.");
|
|
66
|
+
}
|
|
67
|
+
const phase = metadata.phases.find(p => p.phaseId === phaseId);
|
|
68
|
+
if (!phase) {
|
|
69
|
+
throw new Error(`Phase '${phaseId}' not found`);
|
|
70
|
+
}
|
|
71
|
+
phase.status = status;
|
|
72
|
+
if (checkpointSha) {
|
|
73
|
+
phase.checkpointSha = checkpointSha;
|
|
74
|
+
}
|
|
75
|
+
this.writeMetadata(metadata);
|
|
76
|
+
}
|
|
77
|
+
getActivePhase() {
|
|
78
|
+
const metadata = this.readMetadata();
|
|
79
|
+
if (!metadata) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
// First look for in_progress phase
|
|
83
|
+
const inProgressPhase = metadata.phases.find(p => p.status === "in_progress");
|
|
84
|
+
if (inProgressPhase) {
|
|
85
|
+
return inProgressPhase;
|
|
86
|
+
}
|
|
87
|
+
// Then look for first pending phase
|
|
88
|
+
const pendingPhase = metadata.phases.find(p => p.status === "pending");
|
|
89
|
+
return pendingPhase || null;
|
|
90
|
+
}
|
|
91
|
+
getActiveTask() {
|
|
92
|
+
const activePhase = this.getActivePhase();
|
|
93
|
+
if (!activePhase) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// First look for in_progress task
|
|
97
|
+
const inProgressTask = activePhase.tasks.find(t => t.status === "in_progress");
|
|
98
|
+
if (inProgressTask) {
|
|
99
|
+
return inProgressTask;
|
|
100
|
+
}
|
|
101
|
+
// Then look for first pending task
|
|
102
|
+
const pendingTask = activePhase.tasks.find(t => t.status === "pending");
|
|
103
|
+
return pendingTask || null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { MetadataTracker } from "./metadataTracker.js";
|
|
5
|
+
describe("MetadataTracker", () => {
|
|
6
|
+
const testDir = join(process.cwd(), "test-metadata-tracker");
|
|
7
|
+
const trackId = "track_test_123";
|
|
8
|
+
let tracker;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Create test directory
|
|
11
|
+
mkdirSync(testDir, { recursive: true });
|
|
12
|
+
tracker = new MetadataTracker(testDir, trackId);
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
// Clean up
|
|
16
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
describe("initializeMetadata", () => {
|
|
19
|
+
it("should create a status.json file with initial metadata", () => {
|
|
20
|
+
const metadata = {
|
|
21
|
+
trackId: trackId,
|
|
22
|
+
trackName: "Test Track",
|
|
23
|
+
phases: [
|
|
24
|
+
{
|
|
25
|
+
phaseId: "phase1",
|
|
26
|
+
phaseName: "Phase 1: Setup",
|
|
27
|
+
status: "pending",
|
|
28
|
+
tasks: [
|
|
29
|
+
{
|
|
30
|
+
taskId: "task1",
|
|
31
|
+
taskName: "Task 1",
|
|
32
|
+
status: "pending"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
createdAt: new Date().toISOString(),
|
|
38
|
+
updatedAt: new Date().toISOString()
|
|
39
|
+
};
|
|
40
|
+
tracker.initializeMetadata(metadata);
|
|
41
|
+
const metadataPath = join(testDir, "conductor-cdd", "tracks", trackId, "status.json");
|
|
42
|
+
expect(existsSync(metadataPath)).toBe(true);
|
|
43
|
+
const savedMetadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
44
|
+
expect(savedMetadata.trackId).toBe(trackId);
|
|
45
|
+
expect(savedMetadata.trackName).toBe("Test Track");
|
|
46
|
+
expect(savedMetadata.phases).toHaveLength(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe("updateTaskStatus", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
const metadata = {
|
|
52
|
+
trackId: trackId,
|
|
53
|
+
trackName: "Test Track",
|
|
54
|
+
phases: [
|
|
55
|
+
{
|
|
56
|
+
phaseId: "phase1",
|
|
57
|
+
phaseName: "Phase 1: Setup",
|
|
58
|
+
status: "pending",
|
|
59
|
+
tasks: [
|
|
60
|
+
{
|
|
61
|
+
taskId: "task1",
|
|
62
|
+
taskName: "Task 1",
|
|
63
|
+
status: "pending"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
taskId: "task2",
|
|
67
|
+
taskName: "Task 2",
|
|
68
|
+
status: "pending"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
updatedAt: new Date().toISOString()
|
|
75
|
+
};
|
|
76
|
+
tracker.initializeMetadata(metadata);
|
|
77
|
+
});
|
|
78
|
+
it("should update a task status from pending to in_progress", () => {
|
|
79
|
+
tracker.updateTaskStatus("phase1", "task1", "in_progress");
|
|
80
|
+
const metadata = tracker.readMetadata();
|
|
81
|
+
expect(metadata).not.toBeNull();
|
|
82
|
+
const task = metadata.phases[0].tasks.find((t) => t.taskId === "task1");
|
|
83
|
+
expect(task?.status).toBe("in_progress");
|
|
84
|
+
});
|
|
85
|
+
it("should update a task status from in_progress to completed with commit SHA", () => {
|
|
86
|
+
tracker.updateTaskStatus("phase1", "task1", "in_progress");
|
|
87
|
+
tracker.updateTaskStatus("phase1", "task1", "completed", "abc1234");
|
|
88
|
+
const metadata = tracker.readMetadata();
|
|
89
|
+
expect(metadata).not.toBeNull();
|
|
90
|
+
const task = metadata.phases[0].tasks.find((t) => t.taskId === "task1");
|
|
91
|
+
expect(task?.status).toBe("completed");
|
|
92
|
+
expect(task?.commitSha).toBe("abc1234");
|
|
93
|
+
});
|
|
94
|
+
it("should update the updatedAt timestamp", async () => {
|
|
95
|
+
// Wait a small amount to ensure timestamp changes
|
|
96
|
+
const beforeUpdate = tracker.readMetadata().updatedAt;
|
|
97
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
98
|
+
tracker.updateTaskStatus("phase1", "task1", "in_progress");
|
|
99
|
+
const afterUpdate = tracker.readMetadata().updatedAt;
|
|
100
|
+
expect(afterUpdate).not.toBe(beforeUpdate);
|
|
101
|
+
});
|
|
102
|
+
it("should throw error if phase does not exist", () => {
|
|
103
|
+
expect(() => {
|
|
104
|
+
tracker.updateTaskStatus("invalid_phase", "task1", "in_progress");
|
|
105
|
+
}).toThrow("Phase 'invalid_phase' not found");
|
|
106
|
+
});
|
|
107
|
+
it("should throw error if task does not exist", () => {
|
|
108
|
+
expect(() => {
|
|
109
|
+
tracker.updateTaskStatus("phase1", "invalid_task", "in_progress");
|
|
110
|
+
}).toThrow("Task 'invalid_task' not found in phase 'phase1'");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe("updatePhaseStatus", () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
const metadata = {
|
|
116
|
+
trackId: trackId,
|
|
117
|
+
trackName: "Test Track",
|
|
118
|
+
phases: [
|
|
119
|
+
{
|
|
120
|
+
phaseId: "phase1",
|
|
121
|
+
phaseName: "Phase 1: Setup",
|
|
122
|
+
status: "pending",
|
|
123
|
+
tasks: []
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
createdAt: new Date().toISOString(),
|
|
127
|
+
updatedAt: new Date().toISOString()
|
|
128
|
+
};
|
|
129
|
+
tracker.initializeMetadata(metadata);
|
|
130
|
+
});
|
|
131
|
+
it("should update a phase status to completed with checkpoint SHA", () => {
|
|
132
|
+
tracker.updatePhaseStatus("phase1", "completed", "def5678");
|
|
133
|
+
const metadata = tracker.readMetadata();
|
|
134
|
+
expect(metadata).not.toBeNull();
|
|
135
|
+
const phase = metadata.phases.find((p) => p.phaseId === "phase1");
|
|
136
|
+
expect(phase?.status).toBe("completed");
|
|
137
|
+
expect(phase?.checkpointSha).toBe("def5678");
|
|
138
|
+
});
|
|
139
|
+
it("should throw error if phase does not exist", () => {
|
|
140
|
+
expect(() => {
|
|
141
|
+
tracker.updatePhaseStatus("invalid_phase", "completed");
|
|
142
|
+
}).toThrow("Phase 'invalid_phase' not found");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("readMetadata", () => {
|
|
146
|
+
it("should return null if status.json does not exist", () => {
|
|
147
|
+
const metadata = tracker.readMetadata();
|
|
148
|
+
expect(metadata).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
it("should return parsed metadata if status.json exists", () => {
|
|
151
|
+
const initialMetadata = {
|
|
152
|
+
trackId: trackId,
|
|
153
|
+
trackName: "Test Track",
|
|
154
|
+
phases: [],
|
|
155
|
+
createdAt: new Date().toISOString(),
|
|
156
|
+
updatedAt: new Date().toISOString()
|
|
157
|
+
};
|
|
158
|
+
tracker.initializeMetadata(initialMetadata);
|
|
159
|
+
const metadata = tracker.readMetadata();
|
|
160
|
+
expect(metadata).not.toBeNull();
|
|
161
|
+
expect(metadata?.trackId).toBe(trackId);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe("getActivePhase", () => {
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
const metadata = {
|
|
167
|
+
trackId: trackId,
|
|
168
|
+
trackName: "Test Track",
|
|
169
|
+
phases: [
|
|
170
|
+
{
|
|
171
|
+
phaseId: "phase1",
|
|
172
|
+
phaseName: "Phase 1: Setup",
|
|
173
|
+
status: "completed",
|
|
174
|
+
tasks: []
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
phaseId: "phase2",
|
|
178
|
+
phaseName: "Phase 2: Implementation",
|
|
179
|
+
status: "in_progress",
|
|
180
|
+
tasks: []
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
phaseId: "phase3",
|
|
184
|
+
phaseName: "Phase 3: Testing",
|
|
185
|
+
status: "pending",
|
|
186
|
+
tasks: []
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
createdAt: new Date().toISOString(),
|
|
190
|
+
updatedAt: new Date().toISOString()
|
|
191
|
+
};
|
|
192
|
+
tracker.initializeMetadata(metadata);
|
|
193
|
+
});
|
|
194
|
+
it("should return the first in_progress phase", () => {
|
|
195
|
+
const activePhase = tracker.getActivePhase();
|
|
196
|
+
expect(activePhase?.phaseId).toBe("phase2");
|
|
197
|
+
});
|
|
198
|
+
it("should return the first pending phase if no in_progress phase exists", () => {
|
|
199
|
+
tracker.updatePhaseStatus("phase2", "completed");
|
|
200
|
+
const activePhase = tracker.getActivePhase();
|
|
201
|
+
expect(activePhase?.phaseId).toBe("phase3");
|
|
202
|
+
});
|
|
203
|
+
it("should return null if all phases are completed", () => {
|
|
204
|
+
tracker.updatePhaseStatus("phase2", "completed");
|
|
205
|
+
tracker.updatePhaseStatus("phase3", "completed");
|
|
206
|
+
const activePhase = tracker.getActivePhase();
|
|
207
|
+
expect(activePhase).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe("getActiveTask", () => {
|
|
211
|
+
beforeEach(() => {
|
|
212
|
+
const metadata = {
|
|
213
|
+
trackId: trackId,
|
|
214
|
+
trackName: "Test Track",
|
|
215
|
+
phases: [
|
|
216
|
+
{
|
|
217
|
+
phaseId: "phase1",
|
|
218
|
+
phaseName: "Phase 1: Setup",
|
|
219
|
+
status: "in_progress",
|
|
220
|
+
tasks: [
|
|
221
|
+
{
|
|
222
|
+
taskId: "task1",
|
|
223
|
+
taskName: "Task 1",
|
|
224
|
+
status: "completed"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
taskId: "task2",
|
|
228
|
+
taskName: "Task 2",
|
|
229
|
+
status: "in_progress"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
taskId: "task3",
|
|
233
|
+
taskName: "Task 3",
|
|
234
|
+
status: "pending"
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
createdAt: new Date().toISOString(),
|
|
240
|
+
updatedAt: new Date().toISOString()
|
|
241
|
+
};
|
|
242
|
+
tracker.initializeMetadata(metadata);
|
|
243
|
+
});
|
|
244
|
+
it("should return the first in_progress task in the active phase", () => {
|
|
245
|
+
const activeTask = tracker.getActiveTask();
|
|
246
|
+
expect(activeTask?.taskId).toBe("task2");
|
|
247
|
+
});
|
|
248
|
+
it("should return the first pending task if no in_progress task exists", () => {
|
|
249
|
+
tracker.updateTaskStatus("phase1", "task2", "completed");
|
|
250
|
+
const activeTask = tracker.getActiveTask();
|
|
251
|
+
expect(activeTask?.taskId).toBe("task3");
|
|
252
|
+
});
|
|
253
|
+
it("should return null if all tasks in active phase are completed", () => {
|
|
254
|
+
tracker.updateTaskStatus("phase1", "task2", "completed");
|
|
255
|
+
tracker.updateTaskStatus("phase1", "task3", "completed");
|
|
256
|
+
const activeTask = tracker.getActiveTask();
|
|
257
|
+
expect(activeTask).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
it("should return null if there is no active phase", () => {
|
|
260
|
+
tracker.updatePhaseStatus("phase1", "completed");
|
|
261
|
+
const activeTask = tracker.getActiveTask();
|
|
262
|
+
expect(activeTask).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface PlanPhase {
|
|
2
|
+
phaseId: string;
|
|
3
|
+
phaseName: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Adds machine-parsable XML-style delimiters around each phase in a plan.md file
|
|
8
|
+
* @param planContent The original plan.md content
|
|
9
|
+
* @returns The plan content with phase delimiters added
|
|
10
|
+
*/
|
|
11
|
+
export declare function addPhaseDelimiters(planContent: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Extracts the active phase from a delimited plan
|
|
14
|
+
* Active phase is the first in-progress phase, or first pending phase if none in progress
|
|
15
|
+
* @param delimitedPlan Plan content with phase delimiters
|
|
16
|
+
* @returns The active phase or null if all phases complete
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractActivePhase(delimitedPlan: string): PlanPhase | null;
|
|
19
|
+
/**
|
|
20
|
+
* Extracts a specific phase by name (case-insensitive partial match)
|
|
21
|
+
* @param delimitedPlan Plan content with phase delimiters
|
|
22
|
+
* @param phaseName Name or partial name of the phase
|
|
23
|
+
* @returns The matching phase or null
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractPhaseByName(delimitedPlan: string, phaseName: string): PlanPhase | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds machine-parsable XML-style delimiters around each phase in a plan.md file
|
|
3
|
+
* @param planContent The original plan.md content
|
|
4
|
+
* @returns The plan content with phase delimiters added
|
|
5
|
+
*/
|
|
6
|
+
export function addPhaseDelimiters(planContent) {
|
|
7
|
+
const lines = planContent.split("\n");
|
|
8
|
+
const result = [];
|
|
9
|
+
let phaseCounter = 0;
|
|
10
|
+
let inPhase = false;
|
|
11
|
+
let currentPhaseId = "";
|
|
12
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13
|
+
const line = lines[i];
|
|
14
|
+
// Detect phase header (## Phase X: ...)
|
|
15
|
+
const phaseMatch = line.match(/^##\s+Phase\s+\d+:/i);
|
|
16
|
+
if (phaseMatch) {
|
|
17
|
+
// Close previous phase if exists
|
|
18
|
+
if (inPhase) {
|
|
19
|
+
result.push(`<!-- PHASE_END:${currentPhaseId} -->`);
|
|
20
|
+
}
|
|
21
|
+
// Start new phase
|
|
22
|
+
phaseCounter++;
|
|
23
|
+
currentPhaseId = `phase${phaseCounter}`;
|
|
24
|
+
result.push(`<!-- PHASE_START:${currentPhaseId} -->`);
|
|
25
|
+
result.push(line);
|
|
26
|
+
inPhase = true;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
result.push(line);
|
|
30
|
+
// Close phase if we hit the next phase or end of file
|
|
31
|
+
if (inPhase && i === lines.length - 1) {
|
|
32
|
+
result.push(`<!-- PHASE_END:${currentPhaseId} -->`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Check if next line is a new phase header to close current phase
|
|
36
|
+
if (inPhase && i + 1 < lines.length) {
|
|
37
|
+
const nextLine = lines[i + 1];
|
|
38
|
+
const nextPhaseMatch = nextLine.match(/^##\s+Phase\s+\d+:/i);
|
|
39
|
+
if (nextPhaseMatch) {
|
|
40
|
+
result.push(`<!-- PHASE_END:${currentPhaseId} -->`);
|
|
41
|
+
inPhase = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result.join("\n");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extracts the active phase from a delimited plan
|
|
49
|
+
* Active phase is the first in-progress phase, or first pending phase if none in progress
|
|
50
|
+
* @param delimitedPlan Plan content with phase delimiters
|
|
51
|
+
* @returns The active phase or null if all phases complete
|
|
52
|
+
*/
|
|
53
|
+
export function extractActivePhase(delimitedPlan) {
|
|
54
|
+
const phases = extractAllPhases(delimitedPlan);
|
|
55
|
+
if (phases.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Find first in-progress phase (contains [~])
|
|
59
|
+
const inProgressPhase = phases.find(p => p.content.includes("[~]"));
|
|
60
|
+
if (inProgressPhase) {
|
|
61
|
+
return inProgressPhase;
|
|
62
|
+
}
|
|
63
|
+
// Find first pending phase (contains [ ] but no checkpoint marker)
|
|
64
|
+
const pendingPhase = phases.find(p => {
|
|
65
|
+
const hasIncompleteTasks = p.content.includes("[ ]");
|
|
66
|
+
const hasCheckpoint = p.content.includes("[checkpoint:");
|
|
67
|
+
return hasIncompleteTasks || !hasCheckpoint;
|
|
68
|
+
});
|
|
69
|
+
return pendingPhase || null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extracts a specific phase by name (case-insensitive partial match)
|
|
73
|
+
* @param delimitedPlan Plan content with phase delimiters
|
|
74
|
+
* @param phaseName Name or partial name of the phase
|
|
75
|
+
* @returns The matching phase or null
|
|
76
|
+
*/
|
|
77
|
+
export function extractPhaseByName(delimitedPlan, phaseName) {
|
|
78
|
+
const phases = extractAllPhases(delimitedPlan);
|
|
79
|
+
const normalizedName = phaseName.toLowerCase();
|
|
80
|
+
return (phases.find(p => p.phaseName.toLowerCase().includes(normalizedName)) || null);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Extracts all phases from a delimited plan
|
|
84
|
+
* @param delimitedPlan Plan content with phase delimiters
|
|
85
|
+
* @returns Array of all phases
|
|
86
|
+
*/
|
|
87
|
+
function extractAllPhases(delimitedPlan) {
|
|
88
|
+
const phases = [];
|
|
89
|
+
const phaseRegex = /<!-- PHASE_START:(\w+) -->[\s\S]*?<!-- PHASE_END:\1 -->/g;
|
|
90
|
+
const matches = delimitedPlan.matchAll(phaseRegex);
|
|
91
|
+
for (const match of matches) {
|
|
92
|
+
const phaseId = match[1];
|
|
93
|
+
const content = match[0];
|
|
94
|
+
// Extract phase name from the content
|
|
95
|
+
const nameMatch = content.match(/^<!-- PHASE_START:\w+ -->\s*\n##\s+(.+)$/m);
|
|
96
|
+
const phaseName = nameMatch ? nameMatch[1].trim() : phaseId;
|
|
97
|
+
phases.push({
|
|
98
|
+
phaseId,
|
|
99
|
+
phaseName,
|
|
100
|
+
content: content
|
|
101
|
+
.replace(/<!-- PHASE_START:\w+ -->\n?/, "")
|
|
102
|
+
.replace(/\n?<!-- PHASE_END:\w+ -->/, "")
|
|
103
|
+
.trim()
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return phases;
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { addPhaseDelimiters, extractActivePhase, extractPhaseByName } from "./planParser.js";
|
|
3
|
+
describe("planParser", () => {
|
|
4
|
+
const samplePlan = `# Implementation Plan: Sample Track
|
|
5
|
+
|
|
6
|
+
This is the introduction.
|
|
7
|
+
|
|
8
|
+
## Phase 1: Setup
|
|
9
|
+
*Goal: Set up the project infrastructure.*
|
|
10
|
+
|
|
11
|
+
- [x] Task: Initialize repository (abc1234)
|
|
12
|
+
- [x] Task: Configure tools (def5678)
|
|
13
|
+
- [x] Task: Orchestrator - User Manual Verification 'Phase 1: Setup' [checkpoint: aaa1111]
|
|
14
|
+
|
|
15
|
+
## Phase 2: Implementation
|
|
16
|
+
*Goal: Implement core features.*
|
|
17
|
+
|
|
18
|
+
- [~] Task: Design API
|
|
19
|
+
- [ ] Task: Implement endpoints
|
|
20
|
+
- [ ] Task: Orchestrator - User Manual Verification 'Phase 2: Implementation'
|
|
21
|
+
|
|
22
|
+
## Phase 3: Testing
|
|
23
|
+
*Goal: Write and run tests.*
|
|
24
|
+
|
|
25
|
+
- [ ] Task: Unit tests
|
|
26
|
+
- [ ] Task: Integration tests
|
|
27
|
+
- [ ] Task: Orchestrator - User Manual Verification 'Phase 3: Testing'
|
|
28
|
+
`;
|
|
29
|
+
describe("addPhaseDelimiters", () => {
|
|
30
|
+
it("should add XML-style phase delimiters to plan.md content", () => {
|
|
31
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
32
|
+
expect(delimited).toContain("<!-- PHASE_START:phase1 -->");
|
|
33
|
+
expect(delimited).toContain("<!-- PHASE_END:phase1 -->");
|
|
34
|
+
expect(delimited).toContain("<!-- PHASE_START:phase2 -->");
|
|
35
|
+
expect(delimited).toContain("<!-- PHASE_END:phase2 -->");
|
|
36
|
+
expect(delimited).toContain("<!-- PHASE_START:phase3 -->");
|
|
37
|
+
expect(delimited).toContain("<!-- PHASE_END:phase3 -->");
|
|
38
|
+
});
|
|
39
|
+
it("should preserve all original content", () => {
|
|
40
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
41
|
+
expect(delimited).toContain("# Implementation Plan: Sample Track");
|
|
42
|
+
expect(delimited).toContain("## Phase 1: Setup");
|
|
43
|
+
expect(delimited).toContain("- [x] Task: Initialize repository");
|
|
44
|
+
expect(delimited).toContain("## Phase 2: Implementation");
|
|
45
|
+
expect(delimited).toContain("## Phase 3: Testing");
|
|
46
|
+
});
|
|
47
|
+
it("should handle plan with no phases", () => {
|
|
48
|
+
const noPhasePlan = "# Plan\n\nJust some intro text.";
|
|
49
|
+
const delimited = addPhaseDelimiters(noPhasePlan);
|
|
50
|
+
expect(delimited).toBe(noPhasePlan);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("extractActivePhase", () => {
|
|
54
|
+
it("should extract the first in-progress phase", () => {
|
|
55
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
56
|
+
const activePhase = extractActivePhase(delimited);
|
|
57
|
+
expect(activePhase).not.toBeNull();
|
|
58
|
+
expect(activePhase?.phaseId).toBe("phase2");
|
|
59
|
+
expect(activePhase?.content).toContain("## Phase 2: Implementation");
|
|
60
|
+
expect(activePhase?.content).toContain("- [~] Task: Design API");
|
|
61
|
+
});
|
|
62
|
+
it("should extract the first pending phase if no in-progress phase", () => {
|
|
63
|
+
const allCompletedPlan = `# Plan
|
|
64
|
+
|
|
65
|
+
<!-- PHASE_START:phase1 -->
|
|
66
|
+
## Phase 1: Setup
|
|
67
|
+
- [x] Task: Done [checkpoint: xxx]
|
|
68
|
+
<!-- PHASE_END:phase1 -->
|
|
69
|
+
|
|
70
|
+
<!-- PHASE_START:phase2 -->
|
|
71
|
+
## Phase 2: Testing
|
|
72
|
+
- [ ] Task: Todo
|
|
73
|
+
<!-- PHASE_END:phase2 -->`;
|
|
74
|
+
const activePhase = extractActivePhase(allCompletedPlan);
|
|
75
|
+
expect(activePhase).not.toBeNull();
|
|
76
|
+
expect(activePhase?.phaseId).toBe("phase2");
|
|
77
|
+
});
|
|
78
|
+
it("should return null if all phases are completed", () => {
|
|
79
|
+
const allCompletedPlan = `# Plan
|
|
80
|
+
|
|
81
|
+
<!-- PHASE_START:phase1 -->
|
|
82
|
+
## Phase 1: Setup
|
|
83
|
+
- [x] Task: Done [checkpoint: xxx]
|
|
84
|
+
<!-- PHASE_END:phase1 -->
|
|
85
|
+
|
|
86
|
+
<!-- PHASE_START:phase2 -->
|
|
87
|
+
## Phase 2: Testing
|
|
88
|
+
- [x] Task: Done [checkpoint: yyy]
|
|
89
|
+
<!-- PHASE_END:phase2 -->`;
|
|
90
|
+
const activePhase = extractActivePhase(allCompletedPlan);
|
|
91
|
+
expect(activePhase).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
it("should return null if no delimiters found", () => {
|
|
94
|
+
const activePhase = extractActivePhase(samplePlan);
|
|
95
|
+
expect(activePhase).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("extractPhaseByName", () => {
|
|
99
|
+
it("should extract a specific phase by name", () => {
|
|
100
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
101
|
+
const phase = extractPhaseByName(delimited, "Phase 1: Setup");
|
|
102
|
+
expect(phase).not.toBeNull();
|
|
103
|
+
expect(phase?.phaseId).toBe("phase1");
|
|
104
|
+
expect(phase?.content).toContain("## Phase 1: Setup");
|
|
105
|
+
expect(phase?.content).toContain("- [x] Task: Initialize repository");
|
|
106
|
+
});
|
|
107
|
+
it("should return null if phase name not found", () => {
|
|
108
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
109
|
+
const phase = extractPhaseByName(delimited, "Phase 99: Nonexistent");
|
|
110
|
+
expect(phase).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it("should handle partial name matches", () => {
|
|
113
|
+
const delimited = addPhaseDelimiters(samplePlan);
|
|
114
|
+
const phase = extractPhaseByName(delimited, "Implementation");
|
|
115
|
+
expect(phase).not.toBeNull();
|
|
116
|
+
expect(phase?.phaseId).toBe("phase2");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface TrackStatusInfo {
|
|
2
|
+
trackId: string;
|
|
3
|
+
trackName: string;
|
|
4
|
+
totalTasks: number;
|
|
5
|
+
completedTasks: number;
|
|
6
|
+
inProgressTasks: number;
|
|
7
|
+
currentPhase: string;
|
|
8
|
+
lastUpdated: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StatusFormatOptions {
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
noColor?: boolean;
|
|
13
|
+
barLength?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calculates progress percentage
|
|
17
|
+
* @param completed Number of completed items
|
|
18
|
+
* @param total Total number of items
|
|
19
|
+
* @returns Progress percentage (0-100)
|
|
20
|
+
*/
|
|
21
|
+
export declare function calculateProgress(completed: number, total: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Generates a visual progress bar
|
|
24
|
+
* @param percentage Progress percentage (0-100)
|
|
25
|
+
* @param length Length of the progress bar (default: 20)
|
|
26
|
+
* @returns Progress bar string
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateProgressBar(percentage: number, length?: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Formats track status into a visually appealing display
|
|
31
|
+
* @param track Track status information
|
|
32
|
+
* @param options Formatting options
|
|
33
|
+
* @returns Formatted status string
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatTrackStatus(track: TrackStatusInfo, options?: StatusFormatOptions): string;
|