@cephalization/math 0.3.1 → 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 CHANGED
@@ -14,6 +14,13 @@ The primary responsibility of this harness is to **reduce context bloat** by dig
14
14
 
15
15
  The harness consists of a simple for-loop, executing a new coding agent with a mandate from `PROMPT.md` to complete a *single* task from `TASKS.md`, while reading and recording any insight gained during the work into `LEARNINGS.md`.
16
16
 
17
+ ### Directory Structure
18
+
19
+ | Path | Description |
20
+ | ---- | ----------- |
21
+ | `.math/todo/` | Active sprint files (PROMPT.md, TASKS.md, LEARNINGS.md) |
22
+ | `.math/backups/<summary>/` | Archived sprints from `math iterate`, named with AI-generated descriptions |
23
+
17
24
  ## Requirements
18
25
 
19
26
  **[Bun](https://bun.sh) is required** to run this tool. Node.js is not supported.
@@ -70,7 +77,7 @@ bun link
70
77
  math init
71
78
  ```
72
79
 
73
- Creates a `todo/` directory with template files and offers to run **planning mode** to help you break down your goal into tasks.
80
+ Creates a `.math/todo/` directory with template files and offers to run **planning mode** to help you break down your goal into tasks.
74
81
 
75
82
  Options:
76
83
 
@@ -130,12 +137,14 @@ Shows task progress with a visual progress bar and next task info.
130
137
  math iterate
131
138
  ```
132
139
 
133
- Backs up `todo/` to `todo-{M}-{D}-{Y}/` and resets for a new goal:
140
+ Backs up `.math/todo/` to `.math/backups/<summary>/` and resets for a new goal:
134
141
 
135
142
  - TASKS.md and LEARNINGS.md are reset to templates
136
143
  - PROMPT.md is preserved (keeping your accumulated "signs")
137
144
  - Offers to run planning mode for your new goal
138
145
 
146
+ The `<summary>` is a short description of the completed sprint (e.g., `add-user-auth`, `fix-api-bugs`).
147
+
139
148
  Options:
140
149
 
141
150
  - `--no-plan` - Skip the planning prompt
package/index.ts CHANGED
@@ -31,12 +31,12 @@ ${colors.bold}USAGE${colors.reset}
31
31
  math <command> [options]
32
32
 
33
33
  ${colors.bold}COMMANDS${colors.reset}
34
- ${colors.cyan}init${colors.reset} Create todo/ directory with template files
34
+ ${colors.cyan}init${colors.reset} Create .math/todo/ directory with template files
35
35
  ${colors.cyan}plan${colors.reset} Run planning mode to flesh out tasks
36
36
  ${colors.cyan}run${colors.reset} Start the agent loop until all tasks complete
37
37
  ${colors.cyan}status${colors.reset} Show current task counts
38
- ${colors.cyan}iterate${colors.reset} Backup todo/ and reset for a new sprint
39
- ${colors.cyan}prune${colors.reset} Delete backup artifacts (todo-M-D-Y directories)
38
+ ${colors.cyan}iterate${colors.reset} Backup .math/todo/ and reset for a new sprint
39
+ ${colors.cyan}prune${colors.reset} Delete backup artifacts from .math/backups/
40
40
  ${colors.cyan}help${colors.reset} Show this help message
41
41
 
42
42
  ${colors.bold}OPTIONS${colors.reset}
@@ -55,7 +55,7 @@ ${colors.bold}EXAMPLES${colors.reset}
55
55
  ${colors.dim}# Initialize without planning${colors.reset}
56
56
  math init --no-plan
57
57
 
58
- ${colors.dim}# Run planning mode on existing todo/${colors.reset}
58
+ ${colors.dim}# Run planning mode on existing .math/todo/${colors.reset}
59
59
  math plan
60
60
 
61
61
  ${colors.dim}# Quick planning without clarifying questions${colors.reset}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cephalization/math",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "author": "Tony Powell <powell.anthonyd@proton.me>",
5
5
  "access": "public",
6
6
  "repository": {
@@ -0,0 +1,79 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { rm, readFile, mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { init } from "./init";
6
+ import { getTodoDir } from "../paths";
7
+
8
+ const TEST_DIR = join(import.meta.dir, ".test-init");
9
+
10
+ // Store original cwd to restore after tests
11
+ let originalCwd: string;
12
+
13
+ beforeEach(async () => {
14
+ originalCwd = process.cwd();
15
+
16
+ // Clean up and create fresh test directory
17
+ if (existsSync(TEST_DIR)) {
18
+ await rm(TEST_DIR, { recursive: true });
19
+ }
20
+ await mkdir(TEST_DIR, { recursive: true });
21
+
22
+ // Change to test directory so getTodoDir() resolves to test location
23
+ process.chdir(TEST_DIR);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ // Restore original working directory
28
+ process.chdir(originalCwd);
29
+
30
+ // Clean up test directory
31
+ if (existsSync(TEST_DIR)) {
32
+ await rm(TEST_DIR, { recursive: true });
33
+ }
34
+ });
35
+
36
+ describe("init command", () => {
37
+ test("creates .math/todo directory structure", async () => {
38
+ // Run init with skipPlan to avoid interactive prompt
39
+ await init({ skipPlan: true });
40
+
41
+ const todoDir = getTodoDir();
42
+
43
+ // Verify directory was created
44
+ expect(existsSync(todoDir)).toBe(true);
45
+
46
+ // Verify template files were created
47
+ expect(existsSync(join(todoDir, "PROMPT.md"))).toBe(true);
48
+ expect(existsSync(join(todoDir, "TASKS.md"))).toBe(true);
49
+ expect(existsSync(join(todoDir, "LEARNINGS.md"))).toBe(true);
50
+ });
51
+
52
+ test("uses getTodoDir for path resolution", () => {
53
+ // Verify getTodoDir returns the expected .math/todo path relative to cwd
54
+ const todoDir = getTodoDir();
55
+ expect(todoDir).toContain(".math");
56
+ expect(todoDir).toContain("todo");
57
+ expect(todoDir.endsWith(".math/todo")).toBe(true);
58
+ // Should resolve relative to our test directory
59
+ expect(todoDir.startsWith(TEST_DIR)).toBe(true);
60
+ });
61
+
62
+ test("does not overwrite if directory already exists", async () => {
63
+ // First init
64
+ await init({ skipPlan: true });
65
+
66
+ const todoDir = getTodoDir();
67
+ const originalContent = await readFile(join(todoDir, "TASKS.md"), "utf-8");
68
+
69
+ // Modify a file
70
+ await Bun.write(join(todoDir, "TASKS.md"), "modified content");
71
+
72
+ // Second init should not overwrite
73
+ await init({ skipPlan: true });
74
+
75
+ // Verify content was not overwritten
76
+ const newContent = await readFile(join(todoDir, "TASKS.md"), "utf-8");
77
+ expect(newContent).toBe("modified content");
78
+ });
79
+ });
@@ -7,6 +7,7 @@ import {
7
7
  LEARNINGS_TEMPLATE,
8
8
  } from "../templates";
9
9
  import { runPlanningMode, askToRunPlanning } from "../plan";
10
+ import { getTodoDir } from "../paths";
10
11
 
11
12
  const colors = {
12
13
  reset: "\x1b[0m",
@@ -18,16 +19,16 @@ const colors = {
18
19
  export async function init(
19
20
  options: { skipPlan?: boolean; model?: string } = {}
20
21
  ) {
21
- const todoDir = join(process.cwd(), "todo");
22
+ const todoDir = getTodoDir();
22
23
 
23
24
  if (existsSync(todoDir)) {
24
25
  console.log(
25
- `${colors.yellow}todo/ directory already exists${colors.reset}`
26
+ `${colors.yellow}.math/todo/ directory already exists${colors.reset}`
26
27
  );
27
28
  return;
28
29
  }
29
30
 
30
- // Create todo directory
31
+ // Create .math/todo directory (recursive creates .math too)
31
32
  await mkdir(todoDir, { recursive: true });
32
33
 
33
34
  // Write template files
@@ -35,7 +36,7 @@ export async function init(
35
36
  await Bun.write(join(todoDir, "TASKS.md"), TASKS_TEMPLATE);
36
37
  await Bun.write(join(todoDir, "LEARNINGS.md"), LEARNINGS_TEMPLATE);
37
38
 
38
- console.log(`${colors.green}✓${colors.reset} Created todo/ directory with:`);
39
+ console.log(`${colors.green}✓${colors.reset} Created .math/todo/ directory with:`);
39
40
  console.log(
40
41
  ` ${colors.cyan}PROMPT.md${colors.reset} - System prompt with guardrails`
41
42
  );
@@ -54,10 +55,10 @@ export async function init(
54
55
  console.log();
55
56
  console.log(`Next steps:`);
56
57
  console.log(
57
- ` 1. Edit ${colors.cyan}todo/TASKS.md${colors.reset} to add your tasks`
58
+ ` 1. Edit ${colors.cyan}.math/todo/TASKS.md${colors.reset} to add your tasks`
58
59
  );
59
60
  console.log(
60
- ` 2. Customize ${colors.cyan}todo/PROMPT.md${colors.reset} for your project`
61
+ ` 2. Customize ${colors.cyan}.math/todo/PROMPT.md${colors.reset} for your project`
61
62
  );
62
63
  console.log(
63
64
  ` 3. Run ${colors.cyan}math run${colors.reset} to start the agent loop`
@@ -1,7 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { mkdir } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import { TASKS_TEMPLATE, LEARNINGS_TEMPLATE } from "../templates";
4
5
  import { runPlanningMode, askToRunPlanning } from "../plan";
6
+ import { getTodoDir, getBackupsDir } from "../paths";
7
+ import { migrateIfNeeded } from "../migration";
8
+ import { generatePlanSummary } from "../summary";
5
9
 
6
10
  const colors = {
7
11
  reset: "\x1b[0m",
@@ -14,20 +18,31 @@ const colors = {
14
18
  export async function iterate(
15
19
  options: { skipPlan?: boolean; model?: string } = {}
16
20
  ) {
17
- const todoDir = join(process.cwd(), "todo");
21
+ // Check for migration first
22
+ const migrated = await migrateIfNeeded();
23
+ if (!migrated) {
24
+ throw new Error("Migration required but was declined.");
25
+ }
26
+
27
+ const todoDir = getTodoDir();
18
28
 
19
29
  if (!existsSync(todoDir)) {
20
- throw new Error("todo/ directory not found. Run 'math init' first.");
30
+ throw new Error(".math/todo/ directory not found. Run 'math init' first.");
31
+ }
32
+
33
+ // Read current TASKS.md to generate summary for backup directory name
34
+ const tasksPath = join(todoDir, "TASKS.md");
35
+ let summary = "plan";
36
+ if (existsSync(tasksPath)) {
37
+ const tasksContent = await Bun.file(tasksPath).text();
38
+ summary = generatePlanSummary(tasksContent);
21
39
  }
22
40
 
23
- // Generate backup directory name: todo-{M}-{D}-{Y}
24
- const now = new Date();
25
- const month = now.getMonth() + 1;
26
- const day = now.getDate();
27
- const year = now.getFullYear();
28
- const backupDir = join(process.cwd(), `todo-${month}-${day}-${year}`);
41
+ // Generate backup directory in .math/backups/<summary>/
42
+ const backupsDir = getBackupsDir();
43
+ const backupDir = join(backupsDir, summary);
29
44
 
30
- // Handle existing backup for same day
45
+ // Handle existing backup with same summary
31
46
  let finalBackupDir = backupDir;
32
47
  let counter = 1;
33
48
  while (existsSync(finalBackupDir)) {
@@ -37,11 +52,15 @@ export async function iterate(
37
52
 
38
53
  console.log(`${colors.bold}Iterating to new sprint${colors.reset}\n`);
39
54
 
55
+ // Ensure .math/backups/ directory exists
56
+ if (!existsSync(backupsDir)) {
57
+ await mkdir(backupsDir, { recursive: true });
58
+ }
59
+
40
60
  // Step 1: Backup current todo directory
61
+ const backupName = finalBackupDir.split("/").pop();
41
62
  console.log(
42
- `${colors.cyan}1.${colors.reset} Backing up todo/ to ${finalBackupDir
43
- .split("/")
44
- .pop()}/`
63
+ `${colors.cyan}1.${colors.reset} Backing up .math/todo/ to .math/backups/${backupName}/`
45
64
  );
46
65
  await Bun.$`cp -r ${todoDir} ${finalBackupDir}`;
47
66
  console.log(` ${colors.green}✓${colors.reset} Backup complete\n`);
@@ -67,9 +86,7 @@ export async function iterate(
67
86
 
68
87
  console.log(`${colors.green}Done!${colors.reset} Ready for new sprint.`);
69
88
  console.log(
70
- `${colors.yellow}Previous sprint preserved at:${
71
- colors.reset
72
- } ${finalBackupDir.split("/").pop()}/`
89
+ `${colors.yellow}Previous sprint preserved at:${colors.reset} .math/backups/${backupName}/`
73
90
  );
74
91
 
75
92
  // Ask to run planning mode unless --no-plan flag
@@ -84,7 +101,7 @@ export async function iterate(
84
101
  console.log();
85
102
  console.log(`${colors.bold}Next steps:${colors.reset}`);
86
103
  console.log(
87
- ` 1. Edit ${colors.cyan}todo/TASKS.md${colors.reset} to add new tasks`
104
+ ` 1. Edit ${colors.cyan}.math/todo/TASKS.md${colors.reset} to add new tasks`
88
105
  );
89
106
  console.log(
90
107
  ` 2. Run ${colors.cyan}math run${colors.reset} to start the agent loop`
@@ -1,12 +1,16 @@
1
1
  import { existsSync } from "node:fs";
2
- import { join } from "node:path";
3
2
  import { runPlanningMode } from "../plan";
3
+ import { getTodoDir } from "../paths";
4
+ import { migrateIfNeeded } from "../migration";
4
5
 
5
6
  export async function plan(options: { model?: string; quick?: boolean } = {}) {
6
- const todoDir = join(process.cwd(), "todo");
7
+ // Check for migration from legacy todo/ to .math/todo/
8
+ await migrateIfNeeded();
9
+
10
+ const todoDir = getTodoDir();
7
11
 
8
12
  if (!existsSync(todoDir)) {
9
- throw new Error("todo/ directory not found. Run 'math init' first.");
13
+ throw new Error(".math/todo/ directory not found. Run 'math init' first.");
10
14
  }
11
15
 
12
16
  await runPlanningMode({
@@ -1,4 +1,5 @@
1
1
  import { readTasks, countTasks, findNextTask } from "../tasks";
2
+ import { getTodoDir } from "../paths";
2
3
 
3
4
  const colors = {
4
5
  reset: "\x1b[0m",
@@ -12,7 +13,7 @@ const colors = {
12
13
  };
13
14
 
14
15
  export async function status() {
15
- const { tasks } = await readTasks();
16
+ const { tasks } = await readTasks(getTodoDir());
16
17
  const counts = countTasks(tasks);
17
18
 
18
19
  console.log(`${colors.bold}Task Status${colors.reset}\n`);
package/src/loop.test.ts CHANGED
@@ -17,8 +17,8 @@ describe("runLoop dry-run mode", () => {
17
17
  originalCwd = process.cwd();
18
18
  process.chdir(testDir);
19
19
 
20
- // Create the todo directory with required files
21
- const todoDir = join(testDir, "todo");
20
+ // Create the .math/todo directory with required files (new structure)
21
+ const todoDir = join(testDir, ".math", "todo");
22
22
  await mkdir(todoDir, { recursive: true });
23
23
 
24
24
  // Create PROMPT.md
@@ -51,7 +51,7 @@ describe("runLoop dry-run mode", () => {
51
51
  test("dry-run mode uses custom mock agent", async () => {
52
52
  // Use a pending task so the agent gets invoked
53
53
  await writeFile(
54
- join(testDir, "todo", "TASKS.md"),
54
+ join(testDir, ".math", "todo", "TASKS.md"),
55
55
  `# Tasks
56
56
 
57
57
  ### test-task
@@ -113,7 +113,7 @@ describe("runLoop dry-run mode", () => {
113
113
  test("dry-run mode with pending tasks runs iteration", async () => {
114
114
  // Update TASKS.md to have a pending task
115
115
  await writeFile(
116
- join(testDir, "todo", "TASKS.md"),
116
+ join(testDir, ".math", "todo", "TASKS.md"),
117
117
  `# Tasks
118
118
 
119
119
  ### test-task
@@ -178,7 +178,7 @@ describe("runLoop dry-run mode", () => {
178
178
  test("agent option with pending task invokes agent", async () => {
179
179
  // Update TASKS.md to have a pending task
180
180
  await writeFile(
181
- join(testDir, "todo", "TASKS.md"),
181
+ join(testDir, ".math", "todo", "TASKS.md"),
182
182
  `# Tasks
183
183
 
184
184
  ### test-task
@@ -227,8 +227,8 @@ describe("runLoop stream-capture with buffer", () => {
227
227
  originalCwd = process.cwd();
228
228
  process.chdir(testDir);
229
229
 
230
- // Create the todo directory with required files
231
- const todoDir = join(testDir, "todo");
230
+ // Create the .math/todo directory with required files (new structure)
231
+ const todoDir = join(testDir, ".math", "todo");
232
232
  await mkdir(todoDir, { recursive: true });
233
233
 
234
234
  // Create PROMPT.md
@@ -318,7 +318,7 @@ describe("runLoop stream-capture with buffer", () => {
318
318
  test("agent output is captured to buffer", async () => {
319
319
  // Use a pending task so the agent gets invoked
320
320
  await writeFile(
321
- join(testDir, "todo", "TASKS.md"),
321
+ join(testDir, ".math", "todo", "TASKS.md"),
322
322
  `# Tasks
323
323
 
324
324
  ### test-task
@@ -396,7 +396,7 @@ describe("runLoop stream-capture with buffer", () => {
396
396
 
397
397
  test("buffer subscribers receive agent output in real-time", async () => {
398
398
  await writeFile(
399
- join(testDir, "todo", "TASKS.md"),
399
+ join(testDir, ".math", "todo", "TASKS.md"),
400
400
  `# Tasks
401
401
 
402
402
  ### test-task
@@ -479,8 +479,8 @@ describe("runLoop UI server integration", () => {
479
479
  originalCwd = process.cwd();
480
480
  process.chdir(testDir);
481
481
 
482
- // Create the todo directory with required files
483
- const todoDir = join(testDir, "todo");
482
+ // Create the .math/todo directory with required files (new structure)
483
+ const todoDir = join(testDir, ".math", "todo");
484
484
  await mkdir(todoDir, { recursive: true });
485
485
 
486
486
  // Create PROMPT.md
package/src/loop.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { join } from "node:path";
2
1
  import { existsSync } from "node:fs";
3
2
  import { readTasks, countTasks, updateTaskStatus, writeTasks } from "./tasks";
4
3
  import { DEFAULT_MODEL } from "./constants";
@@ -6,6 +5,8 @@ import { OpenCodeAgent, MockAgent, createLogEntry } from "./agent";
6
5
  import type { Agent, LogCategory } from "./agent";
7
6
  import { createOutputBuffer, type OutputBuffer } from "./ui/buffer";
8
7
  import { startServer, DEFAULT_PORT } from "./ui/server";
8
+ import { getTodoDir } from "./paths";
9
+ import { migrateIfNeeded } from "./migration";
9
10
 
10
11
  const colors = {
11
12
  reset: "\x1b[0m",
@@ -149,9 +150,15 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
149
150
  log(`Web UI available at http://localhost:${DEFAULT_PORT}`);
150
151
  }
151
152
 
152
- const todoDir = join(process.cwd(), "todo");
153
- const promptPath = join(todoDir, "PROMPT.md");
154
- const tasksPath = join(todoDir, "TASKS.md");
153
+ // Check for legacy todo/ directory and migrate if needed
154
+ const migrated = await migrateIfNeeded();
155
+ if (!migrated) {
156
+ throw new Error("Migration declined. Please migrate to continue.");
157
+ }
158
+
159
+ const todoDir = getTodoDir();
160
+ const promptPath = `${todoDir}/PROMPT.md`;
161
+ const tasksPath = `${todoDir}/TASKS.md`;
155
162
 
156
163
  // Check required files exist
157
164
  if (!existsSync(promptPath)) {
@@ -262,7 +269,7 @@ export async function runLoop(options: LoopOptions = {}): Promise<void> {
262
269
  try {
263
270
  const prompt =
264
271
  "Read the attached PROMPT.md and TASKS.md files. Follow the instructions in PROMPT.md to complete the next pending task.";
265
- const files = ["todo/PROMPT.md", "todo/TASKS.md"];
272
+ const files = [".math/todo/PROMPT.md", ".math/todo/TASKS.md"];
266
273
 
267
274
  const result = await agent.run({
268
275
  model,
@@ -0,0 +1,165 @@
1
+ import { test, expect, beforeEach, afterEach, mock } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { hasLegacyTodoDir, hasNewTodoDir, migrateIfNeeded } from "./migration";
6
+
7
+ // Use a temp directory for testing
8
+ const TEST_DIR = join(import.meta.dir, ".test-migration");
9
+
10
+ beforeEach(async () => {
11
+ // Clean up and create fresh test directory
12
+ if (existsSync(TEST_DIR)) {
13
+ await rm(TEST_DIR, { recursive: true });
14
+ }
15
+ await mkdir(TEST_DIR, { recursive: true });
16
+
17
+ // Change to test directory
18
+ process.chdir(TEST_DIR);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ // Go back to original directory and clean up
23
+ process.chdir(import.meta.dir);
24
+ if (existsSync(TEST_DIR)) {
25
+ await rm(TEST_DIR, { recursive: true });
26
+ }
27
+ });
28
+
29
+ test("hasLegacyTodoDir returns false when no todo/ exists", () => {
30
+ expect(hasLegacyTodoDir()).toBe(false);
31
+ });
32
+
33
+ test("hasLegacyTodoDir returns false when todo/ exists but is empty", async () => {
34
+ await mkdir(join(TEST_DIR, "todo"));
35
+ expect(hasLegacyTodoDir()).toBe(false);
36
+ });
37
+
38
+ test("hasLegacyTodoDir returns true when todo/ has TASKS.md", async () => {
39
+ await mkdir(join(TEST_DIR, "todo"));
40
+ await writeFile(join(TEST_DIR, "todo", "TASKS.md"), "# Tasks");
41
+ expect(hasLegacyTodoDir()).toBe(true);
42
+ });
43
+
44
+ test("hasLegacyTodoDir returns true when todo/ has PROMPT.md", async () => {
45
+ await mkdir(join(TEST_DIR, "todo"));
46
+ await writeFile(join(TEST_DIR, "todo", "PROMPT.md"), "# Prompt");
47
+ expect(hasLegacyTodoDir()).toBe(true);
48
+ });
49
+
50
+ test("hasLegacyTodoDir returns true when todo/ has LEARNINGS.md", async () => {
51
+ await mkdir(join(TEST_DIR, "todo"));
52
+ await writeFile(join(TEST_DIR, "todo", "LEARNINGS.md"), "# Learnings");
53
+ expect(hasLegacyTodoDir()).toBe(true);
54
+ });
55
+
56
+ test("hasNewTodoDir returns false when .math/todo/ does not exist", () => {
57
+ expect(hasNewTodoDir()).toBe(false);
58
+ });
59
+
60
+ test("hasNewTodoDir returns true when .math/todo/ exists", async () => {
61
+ await mkdir(join(TEST_DIR, ".math", "todo"), { recursive: true });
62
+ expect(hasNewTodoDir()).toBe(true);
63
+ });
64
+
65
+ test("migrateIfNeeded returns true when already migrated", async () => {
66
+ // Create new structure
67
+ await mkdir(join(TEST_DIR, ".math", "todo"), { recursive: true });
68
+
69
+ const result = await migrateIfNeeded();
70
+ expect(result).toBe(true);
71
+ });
72
+
73
+ test("migrateIfNeeded returns true when no legacy directory exists", async () => {
74
+ const result = await migrateIfNeeded();
75
+ expect(result).toBe(true);
76
+ });
77
+
78
+ // Tests for migration prompt and file moving require mocking readline
79
+ // We test the migration behavior by directly calling the internal functions
80
+ // Since promptForMigration is not exported, we test migrateIfNeeded end-to-end
81
+
82
+ test("migrateIfNeeded moves files when user confirms (simulated)", async () => {
83
+ // Create legacy structure with files
84
+ const legacyDir = join(TEST_DIR, "todo");
85
+ await mkdir(legacyDir);
86
+ await writeFile(join(legacyDir, "TASKS.md"), "# Tasks\ncontent");
87
+ await writeFile(join(legacyDir, "PROMPT.md"), "# Prompt\ncontent");
88
+ await writeFile(join(legacyDir, "LEARNINGS.md"), "# Learnings\ncontent");
89
+
90
+ // Verify legacy exists
91
+ expect(hasLegacyTodoDir()).toBe(true);
92
+ expect(hasNewTodoDir()).toBe(false);
93
+
94
+ // Since we can't easily mock readline in bun tests, we verify
95
+ // the pre-conditions and post-conditions that file moving would achieve
96
+ // by manually performing what performMigration does
97
+ const { rename } = await import("node:fs/promises");
98
+ const mathDir = join(TEST_DIR, ".math");
99
+ const newTodoDir = join(TEST_DIR, ".math", "todo");
100
+
101
+ await mkdir(mathDir, { recursive: true });
102
+ await rename(legacyDir, newTodoDir);
103
+
104
+ // Verify migration completed
105
+ expect(hasLegacyTodoDir()).toBe(false);
106
+ expect(hasNewTodoDir()).toBe(true);
107
+ expect(existsSync(join(newTodoDir, "TASKS.md"))).toBe(true);
108
+ expect(existsSync(join(newTodoDir, "PROMPT.md"))).toBe(true);
109
+ expect(existsSync(join(newTodoDir, "LEARNINGS.md"))).toBe(true);
110
+ });
111
+
112
+ test("legacy directory with multiple files is correctly detected", async () => {
113
+ const legacyDir = join(TEST_DIR, "todo");
114
+ await mkdir(legacyDir);
115
+ await writeFile(join(legacyDir, "TASKS.md"), "# Tasks");
116
+ await writeFile(join(legacyDir, "PROMPT.md"), "# Prompt");
117
+ await writeFile(join(legacyDir, "LEARNINGS.md"), "# Learnings");
118
+
119
+ expect(hasLegacyTodoDir()).toBe(true);
120
+ });
121
+
122
+ test("legacy directory with unrelated files is not detected", async () => {
123
+ const legacyDir = join(TEST_DIR, "todo");
124
+ await mkdir(legacyDir);
125
+ await writeFile(join(legacyDir, "random.txt"), "random content");
126
+
127
+ expect(hasLegacyTodoDir()).toBe(false);
128
+ });
129
+
130
+ test("new todo directory detection is independent of file contents", async () => {
131
+ // .math/todo just needs to exist, no files required
132
+ await mkdir(join(TEST_DIR, ".math", "todo"), { recursive: true });
133
+ expect(hasNewTodoDir()).toBe(true);
134
+
135
+ // Even empty, it should be detected
136
+ expect(existsSync(join(TEST_DIR, ".math", "todo", "TASKS.md"))).toBe(false);
137
+ });
138
+
139
+ test("migration preserves file contents", async () => {
140
+ const legacyDir = join(TEST_DIR, "todo");
141
+ await mkdir(legacyDir);
142
+
143
+ const tasksContent = "# Tasks\n\n## Phase 1\n\n### task-1\n- content: Test task";
144
+ const promptContent = "# Prompt\n\nCustom prompt content here";
145
+ const learningsContent = "# Learnings\n\n## task-0\n- Learned something";
146
+
147
+ await writeFile(join(legacyDir, "TASKS.md"), tasksContent);
148
+ await writeFile(join(legacyDir, "PROMPT.md"), promptContent);
149
+ await writeFile(join(legacyDir, "LEARNINGS.md"), learningsContent);
150
+
151
+ // Perform migration manually (simulating user confirmation)
152
+ const { rename, readFile } = await import("node:fs/promises");
153
+ const newTodoDir = join(TEST_DIR, ".math", "todo");
154
+ await mkdir(join(TEST_DIR, ".math"), { recursive: true });
155
+ await rename(legacyDir, newTodoDir);
156
+
157
+ // Verify file contents are preserved
158
+ const migratedTasks = await readFile(join(newTodoDir, "TASKS.md"), "utf-8");
159
+ const migratedPrompt = await readFile(join(newTodoDir, "PROMPT.md"), "utf-8");
160
+ const migratedLearnings = await readFile(join(newTodoDir, "LEARNINGS.md"), "utf-8");
161
+
162
+ expect(migratedTasks).toBe(tasksContent);
163
+ expect(migratedPrompt).toBe(promptContent);
164
+ expect(migratedLearnings).toBe(learningsContent);
165
+ });