@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/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
|
|
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
|
|
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
|
|
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
|
@@ -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
|
+
});
|
package/src/commands/init.ts
CHANGED
|
@@ -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 =
|
|
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`
|
package/src/commands/iterate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
24
|
-
const
|
|
25
|
-
const
|
|
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
|
|
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
|
|
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`
|
package/src/commands/plan.ts
CHANGED
|
@@ -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
|
-
|
|
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({
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
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
|
+
});
|