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.
@@ -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;