@cephalization/math 0.3.2 → 0.4.0
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/README.md +11 -2
- package/index.ts +4 -4
- package/package.json +1 -1
- package/src/commands/init.test.ts +79 -0
- package/src/commands/init.ts +7 -6
- package/src/commands/iterate.ts +33 -16
- package/src/commands/plan.ts +7 -3
- package/src/commands/status.ts +2 -1
- package/src/loop.test.ts +11 -11
- package/src/loop.ts +12 -5
- package/src/migration.test.ts +165 -0
- package/src/migration.ts +122 -0
- package/src/paths.test.ts +36 -0
- package/src/paths.ts +22 -0
- package/src/plan.ts +2 -2
- package/src/prune.test.ts +46 -35
- package/src/prune.ts +16 -25
- package/src/summary.test.ts +97 -0
- package/src/summary.ts +92 -0
- package/src/tasks.ts +3 -2
- package/src/templates.ts +4 -0
- package/src/ui/app.test.ts +0 -228
package/src/migration.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, rename } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getMathDir, getTodoDir } from "./paths";
|
|
6
|
+
|
|
7
|
+
const colors = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
cyan: "\x1b[36m",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if the legacy todo/ directory exists and contains the expected files.
|
|
17
|
+
*/
|
|
18
|
+
export function hasLegacyTodoDir(): boolean {
|
|
19
|
+
const legacyDir = join(process.cwd(), "todo");
|
|
20
|
+
|
|
21
|
+
if (!existsSync(legacyDir)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for at least one of the expected files
|
|
26
|
+
const expectedFiles = ["PROMPT.md", "TASKS.md", "LEARNINGS.md"];
|
|
27
|
+
return expectedFiles.some((file) => existsSync(join(legacyDir, file)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if we've already migrated to the new .math/todo structure.
|
|
32
|
+
*/
|
|
33
|
+
export function hasNewTodoDir(): boolean {
|
|
34
|
+
return existsSync(getTodoDir());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prompt the user to confirm migration.
|
|
39
|
+
* Returns true if user confirms, false otherwise.
|
|
40
|
+
*/
|
|
41
|
+
async function promptForMigration(): Promise<boolean> {
|
|
42
|
+
const rl = createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(
|
|
50
|
+
`${colors.yellow}${colors.bold}Migration Required${colors.reset}`
|
|
51
|
+
);
|
|
52
|
+
console.log(
|
|
53
|
+
`Found legacy ${colors.cyan}todo/${colors.reset} directory structure.`
|
|
54
|
+
);
|
|
55
|
+
console.log(
|
|
56
|
+
`This will be migrated to ${colors.cyan}.math/todo/${colors.reset}`
|
|
57
|
+
);
|
|
58
|
+
console.log();
|
|
59
|
+
|
|
60
|
+
const answer = await rl.question(
|
|
61
|
+
`${colors.cyan}Migrate now?${colors.reset} (Y/n) `
|
|
62
|
+
);
|
|
63
|
+
rl.close();
|
|
64
|
+
return answer.toLowerCase() !== "n";
|
|
65
|
+
} catch {
|
|
66
|
+
rl.close();
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Perform the migration from todo/ to .math/todo/.
|
|
73
|
+
*/
|
|
74
|
+
async function performMigration(): Promise<void> {
|
|
75
|
+
const legacyDir = join(process.cwd(), "todo");
|
|
76
|
+
const mathDir = getMathDir();
|
|
77
|
+
const newTodoDir = getTodoDir();
|
|
78
|
+
|
|
79
|
+
// Create .math directory if it doesn't exist
|
|
80
|
+
if (!existsSync(mathDir)) {
|
|
81
|
+
await mkdir(mathDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Move todo/ to .math/todo/
|
|
85
|
+
await rename(legacyDir, newTodoDir);
|
|
86
|
+
|
|
87
|
+
console.log(
|
|
88
|
+
`${colors.green}✓${colors.reset} Migrated ${colors.cyan}todo/${colors.reset} to ${colors.cyan}.math/todo/${colors.reset}`
|
|
89
|
+
);
|
|
90
|
+
console.log();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if migration is needed and perform it if the user confirms.
|
|
95
|
+
* This function is idempotent - safe to call multiple times.
|
|
96
|
+
*
|
|
97
|
+
* Returns true if migration was performed or not needed, false if user declined.
|
|
98
|
+
*/
|
|
99
|
+
export async function migrateIfNeeded(): Promise<boolean> {
|
|
100
|
+
// Already migrated - nothing to do
|
|
101
|
+
if (hasNewTodoDir()) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// No legacy directory - nothing to migrate
|
|
106
|
+
if (!hasLegacyTodoDir()) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Legacy directory exists, prompt for migration
|
|
111
|
+
const shouldMigrate = await promptForMigration();
|
|
112
|
+
|
|
113
|
+
if (!shouldMigrate) {
|
|
114
|
+
console.log(
|
|
115
|
+
`${colors.yellow}Migration skipped.${colors.reset} Some commands may not work correctly.`
|
|
116
|
+
);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await performMigration();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { getMathDir, getTodoDir, getBackupsDir } from "./paths";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
test("getMathDir returns .math in current directory", () => {
|
|
6
|
+
const result = getMathDir();
|
|
7
|
+
const expected = join(process.cwd(), ".math");
|
|
8
|
+
expect(result).toBe(expected);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("getTodoDir returns .math/todo in current directory", () => {
|
|
12
|
+
const result = getTodoDir();
|
|
13
|
+
const expected = join(process.cwd(), ".math", "todo");
|
|
14
|
+
expect(result).toBe(expected);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("getBackupsDir returns .math/backups in current directory", () => {
|
|
18
|
+
const result = getBackupsDir();
|
|
19
|
+
const expected = join(process.cwd(), ".math", "backups");
|
|
20
|
+
expect(result).toBe(expected);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("all paths are absolute", () => {
|
|
24
|
+
expect(getMathDir()).toMatch(/^\//);
|
|
25
|
+
expect(getTodoDir()).toMatch(/^\//);
|
|
26
|
+
expect(getBackupsDir()).toMatch(/^\//);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("paths have correct hierarchy", () => {
|
|
30
|
+
const mathDir = getMathDir();
|
|
31
|
+
const todoDir = getTodoDir();
|
|
32
|
+
const backupsDir = getBackupsDir();
|
|
33
|
+
|
|
34
|
+
expect(todoDir.startsWith(mathDir)).toBe(true);
|
|
35
|
+
expect(backupsDir.startsWith(mathDir)).toBe(true);
|
|
36
|
+
});
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get the root math directory path (.math)
|
|
5
|
+
*/
|
|
6
|
+
export function getMathDir(): string {
|
|
7
|
+
return join(process.cwd(), ".math");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the todo directory path (.math/todo)
|
|
12
|
+
*/
|
|
13
|
+
export function getTodoDir(): string {
|
|
14
|
+
return join(process.cwd(), ".math", "todo");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the backups directory path (.math/backups)
|
|
19
|
+
*/
|
|
20
|
+
export function getBackupsDir(): string {
|
|
21
|
+
return join(process.cwd(), ".math", "backups");
|
|
22
|
+
}
|
package/src/plan.ts
CHANGED
|
@@ -226,14 +226,14 @@ Read the attached files and update TASKS.md with a well-structured task list for
|
|
|
226
226
|
console.log();
|
|
227
227
|
console.log(`${colors.bold}Next steps:${colors.reset}`);
|
|
228
228
|
console.log(
|
|
229
|
-
` 1. Review ${colors.cyan}todo/TASKS.md${colors.reset} to verify the plan`
|
|
229
|
+
` 1. Review ${colors.cyan}.math/todo/TASKS.md${colors.reset} to verify the plan`
|
|
230
230
|
);
|
|
231
231
|
console.log(
|
|
232
232
|
` 2. Run ${colors.cyan}math run${colors.reset} to start executing tasks`
|
|
233
233
|
);
|
|
234
234
|
} else {
|
|
235
235
|
console.log(
|
|
236
|
-
`${colors.yellow}Planning completed with warnings. Check todo/TASKS.md${colors.reset}`
|
|
236
|
+
`${colors.yellow}Planning completed with warnings. Check .math/todo/TASKS.md${colors.reset}`
|
|
237
237
|
);
|
|
238
238
|
}
|
|
239
239
|
} catch (error) {
|
package/src/prune.test.ts
CHANGED
|
@@ -4,76 +4,87 @@ import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
const TEST_DIR = join(import.meta.dir, ".test-prune");
|
|
7
|
+
const BACKUPS_DIR = join(TEST_DIR, ".math", "backups");
|
|
8
|
+
|
|
9
|
+
// Store original cwd to restore after tests
|
|
10
|
+
let originalCwd: string;
|
|
7
11
|
|
|
8
12
|
beforeEach(() => {
|
|
9
|
-
|
|
13
|
+
originalCwd = process.cwd();
|
|
14
|
+
mkdirSync(BACKUPS_DIR, { recursive: true });
|
|
15
|
+
process.chdir(TEST_DIR);
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
afterEach(() => {
|
|
19
|
+
process.chdir(originalCwd);
|
|
13
20
|
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
14
21
|
});
|
|
15
22
|
|
|
16
|
-
test("findArtifacts returns empty array for empty directory", () => {
|
|
17
|
-
const result = findArtifacts(
|
|
23
|
+
test("findArtifacts returns empty array for empty .math/backups directory", () => {
|
|
24
|
+
const result = findArtifacts();
|
|
18
25
|
expect(result).toEqual([]);
|
|
19
26
|
});
|
|
20
27
|
|
|
21
|
-
test("findArtifacts finds backup directories
|
|
22
|
-
mkdirSync(join(
|
|
23
|
-
mkdirSync(join(
|
|
28
|
+
test("findArtifacts finds all backup directories in .math/backups", () => {
|
|
29
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
|
|
30
|
+
mkdirSync(join(BACKUPS_DIR, "auth-setup"));
|
|
24
31
|
|
|
25
|
-
const result = findArtifacts(
|
|
32
|
+
const result = findArtifacts();
|
|
26
33
|
|
|
27
34
|
expect(result).toHaveLength(2);
|
|
28
|
-
expect(result).toContain(join(
|
|
29
|
-
expect(result).toContain(join(
|
|
35
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure"));
|
|
36
|
+
expect(result).toContain(join(BACKUPS_DIR, "auth-setup"));
|
|
30
37
|
});
|
|
31
38
|
|
|
32
|
-
test("findArtifacts finds backup directories with
|
|
33
|
-
mkdirSync(join(
|
|
34
|
-
mkdirSync(join(
|
|
35
|
-
mkdirSync(join(
|
|
39
|
+
test("findArtifacts finds backup directories with numeric suffixes", () => {
|
|
40
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
|
|
41
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure-1"));
|
|
42
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure-42"));
|
|
36
43
|
|
|
37
|
-
const result = findArtifacts(
|
|
44
|
+
const result = findArtifacts();
|
|
38
45
|
|
|
39
46
|
expect(result).toHaveLength(3);
|
|
40
|
-
expect(result).toContain(join(
|
|
41
|
-
expect(result).toContain(join(
|
|
42
|
-
expect(result).toContain(join(
|
|
47
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure"));
|
|
48
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure-1"));
|
|
49
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure-42"));
|
|
43
50
|
});
|
|
44
51
|
|
|
45
|
-
test("findArtifacts
|
|
46
|
-
mkdirSync(join(
|
|
47
|
-
mkdirSync(join(
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
test("findArtifacts only returns directories", () => {
|
|
53
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
|
|
54
|
+
mkdirSync(join(BACKUPS_DIR, "auth-setup"));
|
|
55
|
+
// Create a file that should be ignored
|
|
56
|
+
Bun.write(join(BACKUPS_DIR, "some-file.txt"), "not a directory");
|
|
50
57
|
|
|
51
|
-
const result = findArtifacts(
|
|
58
|
+
const result = findArtifacts();
|
|
52
59
|
|
|
53
|
-
expect(result).toHaveLength(
|
|
54
|
-
expect(result).toContain(join(
|
|
60
|
+
expect(result).toHaveLength(2);
|
|
61
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure"));
|
|
62
|
+
expect(result).toContain(join(BACKUPS_DIR, "auth-setup"));
|
|
55
63
|
});
|
|
56
64
|
|
|
57
|
-
test("findArtifacts ignores files
|
|
58
|
-
mkdirSync(join(
|
|
59
|
-
// Create a file that
|
|
60
|
-
Bun.write(join(
|
|
65
|
+
test("findArtifacts ignores files in .math/backups", () => {
|
|
66
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
|
|
67
|
+
// Create a file that should be ignored
|
|
68
|
+
Bun.write(join(BACKUPS_DIR, "readme.md"), "not a directory");
|
|
61
69
|
|
|
62
|
-
const result = findArtifacts(
|
|
70
|
+
const result = findArtifacts();
|
|
63
71
|
|
|
64
72
|
expect(result).toHaveLength(1);
|
|
65
|
-
expect(result).toContain(join(
|
|
73
|
+
expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure"));
|
|
66
74
|
});
|
|
67
75
|
|
|
68
|
-
test("findArtifacts returns empty array
|
|
69
|
-
|
|
76
|
+
test("findArtifacts returns empty array when .math/backups does not exist", () => {
|
|
77
|
+
// Remove the backups directory
|
|
78
|
+
rmSync(BACKUPS_DIR, { recursive: true, force: true });
|
|
79
|
+
|
|
80
|
+
const result = findArtifacts();
|
|
70
81
|
expect(result).toEqual([]);
|
|
71
82
|
});
|
|
72
83
|
|
|
73
84
|
test("findArtifacts returns absolute paths", () => {
|
|
74
|
-
mkdirSync(join(
|
|
85
|
+
mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
|
|
75
86
|
|
|
76
|
-
const result = findArtifacts(
|
|
87
|
+
const result = findArtifacts();
|
|
77
88
|
|
|
78
89
|
expect(result).toHaveLength(1);
|
|
79
90
|
expect(result[0]).toMatch(/^\//); // Starts with / (absolute path)
|
package/src/prune.ts
CHANGED
|
@@ -1,42 +1,33 @@
|
|
|
1
1
|
import { readdirSync, statSync, rmSync } from "node:fs";
|
|
2
2
|
import { join, basename } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { getBackupsDir } from "./paths.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
* Matches: todo-{M}-{D}-{Y} or todo-{M}-{D}-{Y}-{N}
|
|
8
|
-
* Examples: todo-1-15-2025, todo-12-31-2024-1, todo-1-1-2026-42
|
|
9
|
-
*/
|
|
10
|
-
const BACKUP_DIR_PATTERN = /^todo-\d{1,2}-\d{1,2}-\d{4}(-\d+)?$/;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Finds all math artifacts in a directory.
|
|
7
|
+
* Finds all math artifacts (backup directories) in `.math/backups/`.
|
|
14
8
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* Scans the `.math/backups/` directory and returns all subdirectories
|
|
10
|
+
* as artifacts. These are created by `math iterate` with summary-based names.
|
|
17
11
|
*
|
|
18
|
-
* @
|
|
19
|
-
* @returns Array of absolute paths to artifacts
|
|
12
|
+
* @returns Array of absolute paths to backup directories
|
|
20
13
|
*/
|
|
21
|
-
export function findArtifacts(
|
|
14
|
+
export function findArtifacts(): string[] {
|
|
22
15
|
const artifacts: string[] = [];
|
|
16
|
+
const backupsDir = getBackupsDir();
|
|
23
17
|
|
|
24
18
|
try {
|
|
25
|
-
const entries = readdirSync(
|
|
19
|
+
const entries = readdirSync(backupsDir);
|
|
26
20
|
|
|
27
21
|
for (const entry of entries) {
|
|
28
|
-
const fullPath = join(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (stat.isDirectory()) {
|
|
35
|
-
artifacts.push(fullPath);
|
|
36
|
-
}
|
|
37
|
-
} catch {
|
|
38
|
-
// Skip entries we can't stat (permission issues, etc.)
|
|
22
|
+
const fullPath = join(backupsDir, entry);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const stat = statSync(fullPath);
|
|
26
|
+
if (stat.isDirectory()) {
|
|
27
|
+
artifacts.push(fullPath);
|
|
39
28
|
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Skip entries we can't stat (permission issues, etc.)
|
|
40
31
|
}
|
|
41
32
|
}
|
|
42
33
|
} catch {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { generatePlanSummary } from "./summary";
|
|
3
|
+
|
|
4
|
+
describe("generatePlanSummary", () => {
|
|
5
|
+
it("should extract summary from phase name", () => {
|
|
6
|
+
const content = `# Project Tasks
|
|
7
|
+
|
|
8
|
+
## Phase 1: Core Infrastructure
|
|
9
|
+
|
|
10
|
+
### add-paths-module
|
|
11
|
+
- content: Create paths module
|
|
12
|
+
- status: pending
|
|
13
|
+
- dependencies: none
|
|
14
|
+
`;
|
|
15
|
+
expect(generatePlanSummary(content)).toBe("core-infrastructure");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should truncate phase name to max 5 words", () => {
|
|
19
|
+
const content = `# Project Tasks
|
|
20
|
+
|
|
21
|
+
## Phase 1: Very Long Phase Name With Many Words Here
|
|
22
|
+
|
|
23
|
+
### task-1
|
|
24
|
+
- content: Some task
|
|
25
|
+
- status: pending
|
|
26
|
+
- dependencies: none
|
|
27
|
+
`;
|
|
28
|
+
expect(generatePlanSummary(content)).toBe("very-long-phase-name-with");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should fall back to task ID when no phase name", () => {
|
|
32
|
+
const content = `# Project Tasks
|
|
33
|
+
|
|
34
|
+
### auth-flow-setup
|
|
35
|
+
- content: Setup auth flow
|
|
36
|
+
- status: pending
|
|
37
|
+
- dependencies: none
|
|
38
|
+
`;
|
|
39
|
+
expect(generatePlanSummary(content)).toBe("auth-flow-setup");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle task ID with special characters", () => {
|
|
43
|
+
const content = `# Project Tasks
|
|
44
|
+
|
|
45
|
+
### add_user_auth!
|
|
46
|
+
- content: Add user auth
|
|
47
|
+
- status: pending
|
|
48
|
+
- dependencies: none
|
|
49
|
+
`;
|
|
50
|
+
expect(generatePlanSummary(content)).toBe("adduserauth");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return 'plan' as ultimate fallback", () => {
|
|
54
|
+
const content = `# Project Tasks
|
|
55
|
+
|
|
56
|
+
Just some random content without tasks or phases.
|
|
57
|
+
`;
|
|
58
|
+
expect(generatePlanSummary(content)).toBe("plan");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should handle empty content", () => {
|
|
62
|
+
expect(generatePlanSummary("")).toBe("plan");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle multiple phases and use the first one", () => {
|
|
66
|
+
const content = `# Project Tasks
|
|
67
|
+
|
|
68
|
+
## Phase 1: Setup
|
|
69
|
+
|
|
70
|
+
### task-1
|
|
71
|
+
- content: Task 1
|
|
72
|
+
- status: complete
|
|
73
|
+
- dependencies: none
|
|
74
|
+
|
|
75
|
+
## Phase 2: Implementation
|
|
76
|
+
|
|
77
|
+
### task-2
|
|
78
|
+
- content: Task 2
|
|
79
|
+
- status: pending
|
|
80
|
+
- dependencies: task-1
|
|
81
|
+
`;
|
|
82
|
+
expect(generatePlanSummary(content)).toBe("setup");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should handle phase name with numbers", () => {
|
|
86
|
+
const content = `# Project Tasks
|
|
87
|
+
|
|
88
|
+
## Phase 1: OAuth2 Integration
|
|
89
|
+
|
|
90
|
+
### oauth2-setup
|
|
91
|
+
- content: Setup OAuth2
|
|
92
|
+
- status: pending
|
|
93
|
+
- dependencies: none
|
|
94
|
+
`;
|
|
95
|
+
expect(generatePlanSummary(content)).toBe("oauth2-integration");
|
|
96
|
+
});
|
|
97
|
+
});
|
package/src/summary.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a short kebab-case summary from TASKS.md content
|
|
3
|
+
* Used for naming backup directories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract task IDs from TASKS.md content
|
|
8
|
+
*/
|
|
9
|
+
function extractTaskIds(content: string): string[] {
|
|
10
|
+
const taskIds: string[] = [];
|
|
11
|
+
const lines = content.split("\n");
|
|
12
|
+
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
// Task IDs are defined as ### task-id
|
|
15
|
+
const taskMatch = line.match(/^###\s+(.+)$/);
|
|
16
|
+
if (taskMatch && taskMatch[1]) {
|
|
17
|
+
taskIds.push(taskMatch[1].trim());
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return taskIds;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract phase names from TASKS.md content
|
|
26
|
+
*/
|
|
27
|
+
function extractPhaseNames(content: string): string[] {
|
|
28
|
+
const phases: string[] = [];
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
|
|
31
|
+
for (const line of lines) {
|
|
32
|
+
// Phase names are defined as ## Phase N: Name
|
|
33
|
+
const phaseMatch = line.match(/^##\s+Phase\s+\d+:\s*(.+)$/);
|
|
34
|
+
if (phaseMatch && phaseMatch[1]) {
|
|
35
|
+
phases.push(phaseMatch[1].trim());
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return phases;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a string to kebab-case
|
|
44
|
+
*/
|
|
45
|
+
function toKebabCase(str: string): string {
|
|
46
|
+
return str
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9\s-]/g, "") // Remove special characters
|
|
49
|
+
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
50
|
+
.replace(/-+/g, "-") // Collapse multiple hyphens
|
|
51
|
+
.replace(/^-|-$/g, ""); // Trim leading/trailing hyphens
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate a short kebab-case summary from TASKS.md content
|
|
56
|
+
* Max 5 words, e.g., "auth-flow-setup"
|
|
57
|
+
*
|
|
58
|
+
* Strategy:
|
|
59
|
+
* 1. Try to use the first phase name if available
|
|
60
|
+
* 2. Fall back to combining first few task IDs
|
|
61
|
+
* 3. Truncate to max 5 words
|
|
62
|
+
*/
|
|
63
|
+
export function generatePlanSummary(tasksContent: string): string {
|
|
64
|
+
const MAX_WORDS = 5;
|
|
65
|
+
|
|
66
|
+
// Try phase names first
|
|
67
|
+
const phases = extractPhaseNames(tasksContent);
|
|
68
|
+
if (phases.length > 0 && phases[0]) {
|
|
69
|
+
const kebab = toKebabCase(phases[0]);
|
|
70
|
+
const words = kebab.split("-").filter(Boolean);
|
|
71
|
+
if (words.length > 0) {
|
|
72
|
+
return words.slice(0, MAX_WORDS).join("-");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fall back to task IDs
|
|
77
|
+
const taskIds = extractTaskIds(tasksContent);
|
|
78
|
+
if (taskIds.length > 0) {
|
|
79
|
+
// Take the first task ID and use it as the summary
|
|
80
|
+
const firstTaskId = taskIds[0];
|
|
81
|
+
if (firstTaskId) {
|
|
82
|
+
const kebab = toKebabCase(firstTaskId);
|
|
83
|
+
const words = kebab.split("-").filter(Boolean);
|
|
84
|
+
if (words.length > 0) {
|
|
85
|
+
return words.slice(0, MAX_WORDS).join("-");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Ultimate fallback
|
|
91
|
+
return "plan";
|
|
92
|
+
}
|
package/src/tasks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { getTodoDir } from "./paths";
|
|
3
4
|
|
|
4
5
|
export interface Task {
|
|
5
6
|
id: string;
|
|
@@ -178,7 +179,7 @@ export function updateTaskStatus(
|
|
|
178
179
|
export async function readTasks(
|
|
179
180
|
todoDir?: string
|
|
180
181
|
): Promise<{ tasks: Task[]; content: string }> {
|
|
181
|
-
const dir = todoDir ||
|
|
182
|
+
const dir = todoDir || getTodoDir();
|
|
182
183
|
const tasksPath = join(dir, "TASKS.md");
|
|
183
184
|
|
|
184
185
|
if (!existsSync(tasksPath)) {
|
|
@@ -198,7 +199,7 @@ export async function writeTasks(
|
|
|
198
199
|
content: string,
|
|
199
200
|
todoDir?: string
|
|
200
201
|
): Promise<void> {
|
|
201
|
-
const dir = todoDir ||
|
|
202
|
+
const dir = todoDir || getTodoDir();
|
|
202
203
|
const tasksPath = join(dir, "TASKS.md");
|
|
203
204
|
await Bun.write(tasksPath, content);
|
|
204
205
|
}
|
package/src/templates.ts
CHANGED
|
@@ -105,6 +105,10 @@ Only commit AFTER tests pass.
|
|
|
105
105
|
| Stage all | \`git add -A\` |
|
|
106
106
|
| Commit | \`git commit -m "feat: ..."\` |
|
|
107
107
|
|
|
108
|
+
**Directory Structure:**
|
|
109
|
+
- \`.math/todo/\` - Active sprint files (PROMPT.md, TASKS.md, LEARNINGS.md)
|
|
110
|
+
- \`.math/backups/<summary>/\` - Archived sprints from \`math iterate\`
|
|
111
|
+
|
|
108
112
|
---
|
|
109
113
|
|
|
110
114
|
## Remember
|