opencode-conductor-cdd-plugin 1.0.0-beta.17 → 1.0.0-beta.19
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/revert.json +1 -1
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/setup.test.js +40 -118
- package/dist/prompts/cdd/setup.test.ts +40 -143
- package/dist/test/integration/rebrand.test.js +15 -14
- package/dist/utils/agentMapping.js +2 -0
- package/dist/utils/archive-tracks.d.ts +28 -0
- package/dist/utils/archive-tracks.js +154 -1
- package/dist/utils/archive-tracks.test.d.ts +1 -0
- package/dist/utils/archive-tracks.test.js +495 -0
- package/dist/utils/codebaseAnalysis.d.ts +61 -0
- package/dist/utils/codebaseAnalysis.js +429 -0
- package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
- package/dist/utils/codebaseAnalysis.test.js +556 -0
- package/dist/utils/documentGeneration.d.ts +97 -0
- package/dist/utils/documentGeneration.js +301 -0
- package/dist/utils/documentGeneration.test.d.ts +1 -0
- package/dist/utils/documentGeneration.test.js +380 -0
- package/dist/utils/interactiveMenu.d.ts +56 -0
- package/dist/utils/interactiveMenu.js +144 -0
- package/dist/utils/interactiveMenu.test.d.ts +1 -0
- package/dist/utils/interactiveMenu.test.js +231 -0
- package/dist/utils/interactiveSetup.d.ts +43 -0
- package/dist/utils/interactiveSetup.js +131 -0
- package/dist/utils/interactiveSetup.test.d.ts +1 -0
- package/dist/utils/interactiveSetup.test.js +124 -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/projectMaturity.d.ts +53 -0
- package/dist/utils/projectMaturity.js +179 -0
- package/dist/utils/projectMaturity.test.d.ts +1 -0
- package/dist/utils/projectMaturity.test.js +298 -0
- package/dist/utils/questionGenerator.d.ts +51 -0
- package/dist/utils/questionGenerator.js +535 -0
- package/dist/utils/questionGenerator.test.d.ts +1 -0
- package/dist/utils/questionGenerator.test.js +328 -0
- package/dist/utils/setupIntegration.d.ts +72 -0
- package/dist/utils/setupIntegration.js +179 -0
- package/dist/utils/setupIntegration.test.d.ts +1 -0
- package/dist/utils/setupIntegration.test.js +344 -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,124 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { executeInteractiveSetup, generateSetupSummary, } from './interactiveSetup.js';
|
|
6
|
+
describe('interactiveSetup', () => {
|
|
7
|
+
let testOutputDir;
|
|
8
|
+
let testProjectDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
testOutputDir = path.join(os.tmpdir(), 'cdd-interactive-setup-test');
|
|
11
|
+
testProjectDir = path.join(testOutputDir, 'test-project');
|
|
12
|
+
if (fs.existsSync(testOutputDir)) {
|
|
13
|
+
fs.rmSync(testOutputDir, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
fs.mkdirSync(testProjectDir, { recursive: true });
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (fs.existsSync(testOutputDir)) {
|
|
19
|
+
fs.rmSync(testOutputDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
describe('executeInteractiveSetup', () => {
|
|
23
|
+
it('should return success result when setup completes', async () => {
|
|
24
|
+
// Mock runFullSetup to return success
|
|
25
|
+
const { executeInteractiveSetup } = await import('./interactiveSetup.js');
|
|
26
|
+
const config = {
|
|
27
|
+
projectPath: testProjectDir,
|
|
28
|
+
};
|
|
29
|
+
const result = await executeInteractiveSetup(config);
|
|
30
|
+
// Note: This will use default responder which returns empty arrays
|
|
31
|
+
// In actual implementation, we need to mock the responder properly
|
|
32
|
+
expect(result).toHaveProperty('success');
|
|
33
|
+
expect(result).toHaveProperty('message');
|
|
34
|
+
expect(result).toHaveProperty('documentsCreated');
|
|
35
|
+
});
|
|
36
|
+
it('should use custom outputDir when provided', async () => {
|
|
37
|
+
const customOutputDir = path.join(testProjectDir, 'custom-cdd');
|
|
38
|
+
const config = {
|
|
39
|
+
projectPath: testProjectDir,
|
|
40
|
+
outputDir: customOutputDir,
|
|
41
|
+
};
|
|
42
|
+
await executeInteractiveSetup(config);
|
|
43
|
+
// Verify outputDir was used (will be created by setupIntegration)
|
|
44
|
+
// This is just a smoke test - full integration tested in setupIntegration.test.ts
|
|
45
|
+
expect(true).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('should handle resume flag', async () => {
|
|
48
|
+
const config = {
|
|
49
|
+
projectPath: testProjectDir,
|
|
50
|
+
resume: true,
|
|
51
|
+
};
|
|
52
|
+
const result = await executeInteractiveSetup(config);
|
|
53
|
+
expect(result).toHaveProperty('resumed');
|
|
54
|
+
});
|
|
55
|
+
it('should handle errors gracefully', async () => {
|
|
56
|
+
// Pass invalid projectPath to trigger error
|
|
57
|
+
const config = {
|
|
58
|
+
projectPath: '/nonexistent/path/that/does/not/exist',
|
|
59
|
+
};
|
|
60
|
+
const result = await executeInteractiveSetup(config);
|
|
61
|
+
expect(result.success).toBe(false);
|
|
62
|
+
expect(result.message).toBeTruthy();
|
|
63
|
+
expect(result.errors).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('generateSetupSummary', () => {
|
|
67
|
+
it('should generate success summary with documents', () => {
|
|
68
|
+
const result = {
|
|
69
|
+
success: true,
|
|
70
|
+
message: 'Setup complete',
|
|
71
|
+
documentsCreated: [
|
|
72
|
+
'/path/to/product.md',
|
|
73
|
+
'/path/to/guidelines.md',
|
|
74
|
+
'/path/to/tech-stack.md',
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
const summary = generateSetupSummary(result);
|
|
78
|
+
expect(summary).toContain('✅ CDD Setup Complete!');
|
|
79
|
+
expect(summary).toContain('product.md');
|
|
80
|
+
expect(summary).toContain('guidelines.md');
|
|
81
|
+
expect(summary).toContain('tech-stack.md');
|
|
82
|
+
expect(summary).toContain('Next steps:');
|
|
83
|
+
});
|
|
84
|
+
it('should generate success summary with resume info', () => {
|
|
85
|
+
const result = {
|
|
86
|
+
success: true,
|
|
87
|
+
message: 'Setup complete',
|
|
88
|
+
documentsCreated: ['/path/to/guidelines.md'],
|
|
89
|
+
resumed: true,
|
|
90
|
+
resumedFrom: '2.1_product_guide',
|
|
91
|
+
};
|
|
92
|
+
const summary = generateSetupSummary(result);
|
|
93
|
+
expect(summary).toContain('Resumed from checkpoint');
|
|
94
|
+
expect(summary).toContain('2.1_product_guide');
|
|
95
|
+
});
|
|
96
|
+
it('should generate failure summary with partial completion', () => {
|
|
97
|
+
const result = {
|
|
98
|
+
success: false,
|
|
99
|
+
message: 'Setup failed at guidelines',
|
|
100
|
+
documentsCreated: ['/path/to/product.md'],
|
|
101
|
+
errors: ['Approval rejected 3 times'],
|
|
102
|
+
};
|
|
103
|
+
const summary = generateSetupSummary(result);
|
|
104
|
+
expect(summary).toContain('❌ CDD Setup Failed');
|
|
105
|
+
expect(summary).toContain('Setup failed at guidelines');
|
|
106
|
+
expect(summary).toContain('Partially completed');
|
|
107
|
+
expect(summary).toContain('product.md');
|
|
108
|
+
expect(summary).toContain('resume setup');
|
|
109
|
+
expect(summary).toContain('Approval rejected 3 times');
|
|
110
|
+
});
|
|
111
|
+
it('should generate failure summary with no documents', () => {
|
|
112
|
+
const result = {
|
|
113
|
+
success: false,
|
|
114
|
+
message: 'Project path does not exist',
|
|
115
|
+
documentsCreated: [],
|
|
116
|
+
errors: ['ENOENT: no such file or directory'],
|
|
117
|
+
};
|
|
118
|
+
const summary = generateSetupSummary(result);
|
|
119
|
+
expect(summary).toContain('❌ CDD Setup Failed');
|
|
120
|
+
expect(summary).toContain('Project path does not exist');
|
|
121
|
+
expect(summary).toContain('ENOENT');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type TaskStatus = "pending" | "in_progress" | "completed" | "cancelled";
|
|
2
|
+
export type PhaseStatus = "pending" | "in_progress" | "completed";
|
|
3
|
+
export interface TaskMetadata {
|
|
4
|
+
taskId: string;
|
|
5
|
+
taskName: string;
|
|
6
|
+
status: TaskStatus;
|
|
7
|
+
commitSha?: string;
|
|
8
|
+
startedAt?: string;
|
|
9
|
+
completedAt?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PhaseMetadata {
|
|
12
|
+
phaseId: string;
|
|
13
|
+
phaseName: string;
|
|
14
|
+
status: PhaseStatus;
|
|
15
|
+
checkpointSha?: string;
|
|
16
|
+
tasks: TaskMetadata[];
|
|
17
|
+
}
|
|
18
|
+
export interface TrackMetadata {
|
|
19
|
+
trackId: string;
|
|
20
|
+
trackName: string;
|
|
21
|
+
phases: PhaseMetadata[];
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
completedAt?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class MetadataTracker {
|
|
27
|
+
private workDir;
|
|
28
|
+
private trackId;
|
|
29
|
+
private metadataPath;
|
|
30
|
+
constructor(workDir: string, trackId: string);
|
|
31
|
+
private ensureDirectory;
|
|
32
|
+
initializeMetadata(metadata: TrackMetadata): void;
|
|
33
|
+
readMetadata(): TrackMetadata | null;
|
|
34
|
+
private writeMetadata;
|
|
35
|
+
updateTaskStatus(phaseId: string, taskId: string, status: TaskStatus, commitSha?: string): void;
|
|
36
|
+
updatePhaseStatus(phaseId: string, status: PhaseStatus, checkpointSha?: string): void;
|
|
37
|
+
getActivePhase(): PhaseMetadata | null;
|
|
38
|
+
getActiveTask(): TaskMetadata | null;
|
|
39
|
+
}
|
|
@@ -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 {};
|