@cephalization/math 0.2.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 +190 -0
- package/index.ts +155 -0
- package/package.json +50 -0
- package/src/agent.test.ts +179 -0
- package/src/agent.ts +275 -0
- package/src/commands/init.ts +65 -0
- package/src/commands/iterate.ts +92 -0
- package/src/commands/plan.ts +16 -0
- package/src/commands/prune.ts +63 -0
- package/src/commands/run.test.ts +27 -0
- package/src/commands/run.ts +16 -0
- package/src/commands/status.ts +55 -0
- package/src/constants.ts +1 -0
- package/src/loop.test.ts +537 -0
- package/src/loop.ts +325 -0
- package/src/plan.ts +263 -0
- package/src/prune.test.ts +174 -0
- package/src/prune.ts +146 -0
- package/src/tasks.ts +204 -0
- package/src/templates.ts +172 -0
- package/src/ui/app.test.ts +228 -0
- package/src/ui/buffer.test.ts +222 -0
- package/src/ui/buffer.ts +131 -0
- package/src/ui/server.test.ts +271 -0
- package/src/ui/server.ts +124 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { findArtifacts, deleteArtifacts, confirmPrune } from "./prune";
|
|
3
|
+
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = join(import.meta.dir, ".test-prune");
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("findArtifacts returns empty array for empty directory", () => {
|
|
17
|
+
const result = findArtifacts(TEST_DIR);
|
|
18
|
+
expect(result).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("findArtifacts finds backup directories with basic pattern", () => {
|
|
22
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
|
|
23
|
+
mkdirSync(join(TEST_DIR, "todo-12-31-2024"));
|
|
24
|
+
|
|
25
|
+
const result = findArtifacts(TEST_DIR);
|
|
26
|
+
|
|
27
|
+
expect(result).toHaveLength(2);
|
|
28
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
|
|
29
|
+
expect(result).toContain(join(TEST_DIR, "todo-12-31-2024"));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("findArtifacts finds backup directories with counter suffix", () => {
|
|
33
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
|
|
34
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025-1"));
|
|
35
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025-42"));
|
|
36
|
+
|
|
37
|
+
const result = findArtifacts(TEST_DIR);
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveLength(3);
|
|
40
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
|
|
41
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025-1"));
|
|
42
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025-42"));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("findArtifacts ignores non-matching directories", () => {
|
|
46
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
|
|
47
|
+
mkdirSync(join(TEST_DIR, "todo")); // Not a backup
|
|
48
|
+
mkdirSync(join(TEST_DIR, "node_modules")); // Not a backup
|
|
49
|
+
mkdirSync(join(TEST_DIR, "todo-invalid")); // Invalid pattern
|
|
50
|
+
|
|
51
|
+
const result = findArtifacts(TEST_DIR);
|
|
52
|
+
|
|
53
|
+
expect(result).toHaveLength(1);
|
|
54
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("findArtifacts ignores files matching pattern", () => {
|
|
58
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
|
|
59
|
+
// Create a file that matches the pattern (should be ignored)
|
|
60
|
+
Bun.write(join(TEST_DIR, "todo-2-20-2025"), "not a directory");
|
|
61
|
+
|
|
62
|
+
const result = findArtifacts(TEST_DIR);
|
|
63
|
+
|
|
64
|
+
expect(result).toHaveLength(1);
|
|
65
|
+
expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("findArtifacts returns empty array for non-existent directory", () => {
|
|
69
|
+
const result = findArtifacts(join(TEST_DIR, "does-not-exist"));
|
|
70
|
+
expect(result).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("findArtifacts returns absolute paths", () => {
|
|
74
|
+
mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
|
|
75
|
+
|
|
76
|
+
const result = findArtifacts(TEST_DIR);
|
|
77
|
+
|
|
78
|
+
expect(result).toHaveLength(1);
|
|
79
|
+
expect(result[0]).toMatch(/^\//); // Starts with / (absolute path)
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// deleteArtifacts tests
|
|
83
|
+
|
|
84
|
+
test("deleteArtifacts deletes directories successfully", () => {
|
|
85
|
+
const dir1 = join(TEST_DIR, "todo-1-15-2025");
|
|
86
|
+
const dir2 = join(TEST_DIR, "todo-2-20-2025");
|
|
87
|
+
mkdirSync(dir1);
|
|
88
|
+
mkdirSync(dir2);
|
|
89
|
+
|
|
90
|
+
const result = deleteArtifacts([dir1, dir2]);
|
|
91
|
+
|
|
92
|
+
expect(result.deleted).toHaveLength(2);
|
|
93
|
+
expect(result.deleted).toContain(dir1);
|
|
94
|
+
expect(result.deleted).toContain(dir2);
|
|
95
|
+
expect(result.failed).toHaveLength(0);
|
|
96
|
+
expect(existsSync(dir1)).toBe(false);
|
|
97
|
+
expect(existsSync(dir2)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("deleteArtifacts deletes directories with contents", () => {
|
|
101
|
+
const dir = join(TEST_DIR, "todo-1-15-2025");
|
|
102
|
+
mkdirSync(dir);
|
|
103
|
+
Bun.write(join(dir, "file.txt"), "content");
|
|
104
|
+
mkdirSync(join(dir, "subdir"));
|
|
105
|
+
Bun.write(join(dir, "subdir", "nested.txt"), "nested content");
|
|
106
|
+
|
|
107
|
+
const result = deleteArtifacts([dir]);
|
|
108
|
+
|
|
109
|
+
expect(result.deleted).toHaveLength(1);
|
|
110
|
+
expect(result.failed).toHaveLength(0);
|
|
111
|
+
expect(existsSync(dir)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("deleteArtifacts returns empty arrays for empty input", () => {
|
|
115
|
+
const result = deleteArtifacts([]);
|
|
116
|
+
|
|
117
|
+
expect(result.deleted).toHaveLength(0);
|
|
118
|
+
expect(result.failed).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("deleteArtifacts handles non-existent paths gracefully", () => {
|
|
122
|
+
const nonExistent = join(TEST_DIR, "does-not-exist");
|
|
123
|
+
|
|
124
|
+
const result = deleteArtifacts([nonExistent]);
|
|
125
|
+
|
|
126
|
+
// rmSync with force: true doesn't throw for non-existent paths
|
|
127
|
+
expect(result.deleted).toHaveLength(1);
|
|
128
|
+
expect(result.failed).toHaveLength(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("deleteArtifacts continues after a failure", () => {
|
|
132
|
+
const dir1 = join(TEST_DIR, "todo-1-15-2025");
|
|
133
|
+
const dir2 = join(TEST_DIR, "todo-2-20-2025");
|
|
134
|
+
mkdirSync(dir1);
|
|
135
|
+
mkdirSync(dir2);
|
|
136
|
+
|
|
137
|
+
// Delete first one manually to prove second still gets processed
|
|
138
|
+
rmSync(dir1, { recursive: true });
|
|
139
|
+
|
|
140
|
+
const result = deleteArtifacts([dir1, dir2]);
|
|
141
|
+
|
|
142
|
+
// Both should succeed since rmSync with force:true handles non-existent
|
|
143
|
+
expect(result.deleted).toHaveLength(2);
|
|
144
|
+
expect(result.failed).toHaveLength(0);
|
|
145
|
+
expect(existsSync(dir2)).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// confirmPrune tests
|
|
149
|
+
|
|
150
|
+
test("confirmPrune returns confirmed: true with force flag", async () => {
|
|
151
|
+
const paths = ["/some/path/todo-1-15-2025", "/some/path/todo-2-20-2025"];
|
|
152
|
+
|
|
153
|
+
const result = await confirmPrune(paths, { force: true });
|
|
154
|
+
|
|
155
|
+
expect(result.confirmed).toBe(true);
|
|
156
|
+
expect(result.paths).toEqual(paths);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("confirmPrune returns confirmed: true with empty paths", async () => {
|
|
160
|
+
const result = await confirmPrune([]);
|
|
161
|
+
|
|
162
|
+
expect(result.confirmed).toBe(true);
|
|
163
|
+
expect(result.paths).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("confirmPrune returns paths even with force flag", async () => {
|
|
167
|
+
const paths = ["/a/todo-1-1-2025", "/b/todo-2-2-2025"];
|
|
168
|
+
|
|
169
|
+
const result = await confirmPrune(paths, { force: true });
|
|
170
|
+
|
|
171
|
+
expect(result.paths).toHaveLength(2);
|
|
172
|
+
expect(result.paths).toContain("/a/todo-1-1-2025");
|
|
173
|
+
expect(result.paths).toContain("/b/todo-2-2-2025");
|
|
174
|
+
});
|
package/src/prune.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readdirSync, statSync, rmSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pattern for backup directories created by `math iterate`
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* Artifacts include:
|
|
16
|
+
* - Backup directories matching pattern todo-{M}-{D}-{Y} or todo-{M}-{D}-{Y}-{N}
|
|
17
|
+
*
|
|
18
|
+
* @param directory - The directory to search in (defaults to cwd)
|
|
19
|
+
* @returns Array of absolute paths to artifacts
|
|
20
|
+
*/
|
|
21
|
+
export function findArtifacts(directory: string = process.cwd()): string[] {
|
|
22
|
+
const artifacts: string[] = [];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const entries = readdirSync(directory);
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = join(directory, entry);
|
|
29
|
+
|
|
30
|
+
// Check if it's a backup directory
|
|
31
|
+
if (BACKUP_DIR_PATTERN.test(entry)) {
|
|
32
|
+
try {
|
|
33
|
+
const stat = statSync(fullPath);
|
|
34
|
+
if (stat.isDirectory()) {
|
|
35
|
+
artifacts.push(fullPath);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip entries we can't stat (permission issues, etc.)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// If we can't read the directory, return empty array
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return artifacts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Result of a delete operation
|
|
51
|
+
*/
|
|
52
|
+
export interface DeleteResult {
|
|
53
|
+
/** Paths that were successfully deleted */
|
|
54
|
+
deleted: string[];
|
|
55
|
+
/** Paths that failed to delete with their error messages */
|
|
56
|
+
failed: { path: string; error: string }[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Deletes the provided artifact paths.
|
|
61
|
+
*
|
|
62
|
+
* Handles errors gracefully - if a path fails to delete (e.g., permission denied),
|
|
63
|
+
* it continues with the remaining paths and reports the failure.
|
|
64
|
+
*
|
|
65
|
+
* @param paths - Array of absolute paths to delete
|
|
66
|
+
* @returns Summary of deleted paths and any failures
|
|
67
|
+
*/
|
|
68
|
+
export function deleteArtifacts(paths: string[]): DeleteResult {
|
|
69
|
+
const result: DeleteResult = {
|
|
70
|
+
deleted: [],
|
|
71
|
+
failed: [],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (const path of paths) {
|
|
75
|
+
try {
|
|
76
|
+
rmSync(path, { recursive: true, force: true });
|
|
77
|
+
result.deleted.push(path);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
const errorMessage =
|
|
80
|
+
err instanceof Error ? err.message : "Unknown error";
|
|
81
|
+
result.failed.push({ path, error: errorMessage });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Result of a confirmation prompt
|
|
90
|
+
*/
|
|
91
|
+
export interface ConfirmationResult {
|
|
92
|
+
/** Whether the user confirmed the action */
|
|
93
|
+
confirmed: boolean;
|
|
94
|
+
/** The paths that were shown to the user */
|
|
95
|
+
paths: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Shows an interactive confirmation prompt for pruning artifacts.
|
|
100
|
+
*
|
|
101
|
+
* Lists all artifacts that will be deleted and asks for user confirmation.
|
|
102
|
+
* If `force` is true, skips the prompt and returns confirmed: true.
|
|
103
|
+
*
|
|
104
|
+
* @param paths - Array of absolute paths to be deleted
|
|
105
|
+
* @param options - Configuration options
|
|
106
|
+
* @param options.force - If true, skip confirmation and return confirmed: true
|
|
107
|
+
* @returns Result indicating whether user confirmed and what paths were shown
|
|
108
|
+
*/
|
|
109
|
+
export async function confirmPrune(
|
|
110
|
+
paths: string[],
|
|
111
|
+
options: { force?: boolean } = {}
|
|
112
|
+
): Promise<ConfirmationResult> {
|
|
113
|
+
// If force flag is set, skip confirmation
|
|
114
|
+
if (options.force) {
|
|
115
|
+
return { confirmed: true, paths };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If no paths, nothing to confirm
|
|
119
|
+
if (paths.length === 0) {
|
|
120
|
+
return { confirmed: true, paths };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Show what will be deleted
|
|
124
|
+
console.log("\nThe following artifacts will be deleted:\n");
|
|
125
|
+
for (const path of paths) {
|
|
126
|
+
console.log(` - ${basename(path)}/`);
|
|
127
|
+
}
|
|
128
|
+
console.log();
|
|
129
|
+
|
|
130
|
+
// Ask for confirmation
|
|
131
|
+
const rl = createInterface({
|
|
132
|
+
input: process.stdin,
|
|
133
|
+
output: process.stdout,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const answer = await rl.question("Delete these artifacts? (y/N) ");
|
|
138
|
+
rl.close();
|
|
139
|
+
|
|
140
|
+
const confirmed = answer.toLowerCase() === "y";
|
|
141
|
+
return { confirmed, paths };
|
|
142
|
+
} catch {
|
|
143
|
+
rl.close();
|
|
144
|
+
return { confirmed: false, paths };
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/tasks.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export interface Task {
|
|
5
|
+
id: string;
|
|
6
|
+
content: string;
|
|
7
|
+
status: "pending" | "in_progress" | "complete";
|
|
8
|
+
dependencies: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TaskCounts {
|
|
12
|
+
pending: number;
|
|
13
|
+
in_progress: number;
|
|
14
|
+
complete: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse TASKS.md file and extract all tasks
|
|
20
|
+
*
|
|
21
|
+
* Expected format:
|
|
22
|
+
* ### task-id
|
|
23
|
+
* - content: Description of the task
|
|
24
|
+
* - status: pending | in_progress | complete
|
|
25
|
+
* - dependencies: task-1, task-2
|
|
26
|
+
*/
|
|
27
|
+
export function parseTasks(content: string): Task[] {
|
|
28
|
+
const tasks: Task[] = [];
|
|
29
|
+
const lines = content.split("\n");
|
|
30
|
+
|
|
31
|
+
let currentTask: Partial<Task> | null = null;
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
// New task starts with ### task-id
|
|
35
|
+
const taskMatch = line.match(/^###\s+(.+)$/);
|
|
36
|
+
if (taskMatch && taskMatch[1]) {
|
|
37
|
+
// Save previous task if exists
|
|
38
|
+
if (currentTask?.id) {
|
|
39
|
+
tasks.push({
|
|
40
|
+
id: currentTask.id,
|
|
41
|
+
content: currentTask.content || "",
|
|
42
|
+
status: currentTask.status || "pending",
|
|
43
|
+
dependencies: currentTask.dependencies || [],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
currentTask = { id: taskMatch[1].trim() };
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!currentTask) continue;
|
|
51
|
+
|
|
52
|
+
// Parse content line
|
|
53
|
+
const contentMatch = line.match(/^-\s+content:\s*(.+)$/);
|
|
54
|
+
if (contentMatch && contentMatch[1]) {
|
|
55
|
+
currentTask.content = contentMatch[1].trim();
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Parse status line
|
|
60
|
+
const statusMatch = line.match(
|
|
61
|
+
/^-\s+status:\s*(pending|in_progress|complete)$/
|
|
62
|
+
);
|
|
63
|
+
if (statusMatch && statusMatch[1]) {
|
|
64
|
+
currentTask.status = statusMatch[1] as Task["status"];
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Parse dependencies line
|
|
69
|
+
const depsMatch = line.match(/^-\s+dependencies:\s*(.*)$/);
|
|
70
|
+
if (depsMatch && depsMatch[1]) {
|
|
71
|
+
const deps = depsMatch[1].trim();
|
|
72
|
+
if (deps && deps.toLowerCase() !== "none") {
|
|
73
|
+
currentTask.dependencies = deps
|
|
74
|
+
.split(",")
|
|
75
|
+
.map((d) => d.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
} else {
|
|
78
|
+
currentTask.dependencies = [];
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Don't forget the last task
|
|
85
|
+
if (currentTask?.id) {
|
|
86
|
+
tasks.push({
|
|
87
|
+
id: currentTask.id,
|
|
88
|
+
content: currentTask.content || "",
|
|
89
|
+
status: currentTask.status || "pending",
|
|
90
|
+
dependencies: currentTask.dependencies || [],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return tasks;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Count tasks by status
|
|
99
|
+
*/
|
|
100
|
+
export function countTasks(tasks: Task[]): TaskCounts {
|
|
101
|
+
const counts: TaskCounts = {
|
|
102
|
+
pending: 0,
|
|
103
|
+
in_progress: 0,
|
|
104
|
+
complete: 0,
|
|
105
|
+
total: tasks.length,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const task of tasks) {
|
|
109
|
+
counts[task.status]++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return counts;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Find the next task to work on:
|
|
117
|
+
* - Status must be "pending"
|
|
118
|
+
* - All dependencies must be "complete"
|
|
119
|
+
*/
|
|
120
|
+
export function findNextTask(tasks: Task[]): Task | null {
|
|
121
|
+
const completedIds = new Set(
|
|
122
|
+
tasks.filter((t) => t.status === "complete").map((t) => t.id)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
for (const task of tasks) {
|
|
126
|
+
if (task.status !== "pending") continue;
|
|
127
|
+
|
|
128
|
+
// Check if all dependencies are complete
|
|
129
|
+
const depsComplete = task.dependencies.every((dep) =>
|
|
130
|
+
completedIds.has(dep)
|
|
131
|
+
);
|
|
132
|
+
if (depsComplete) {
|
|
133
|
+
return task;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update a task's status in the TASKS.md content
|
|
142
|
+
*/
|
|
143
|
+
export function updateTaskStatus(
|
|
144
|
+
content: string,
|
|
145
|
+
taskId: string,
|
|
146
|
+
newStatus: Task["status"]
|
|
147
|
+
): string {
|
|
148
|
+
const lines = content.split("\n");
|
|
149
|
+
const result: string[] = [];
|
|
150
|
+
let inTargetTask = false;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < lines.length; i++) {
|
|
153
|
+
const line = lines[i] ?? "";
|
|
154
|
+
|
|
155
|
+
// Check if we're entering a task section
|
|
156
|
+
const taskMatch = line.match(/^###\s+(.+)$/);
|
|
157
|
+
if (taskMatch && taskMatch[1]) {
|
|
158
|
+
inTargetTask = taskMatch[1].trim() === taskId;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If we're in the target task and this is a status line, replace it
|
|
162
|
+
if (
|
|
163
|
+
inTargetTask &&
|
|
164
|
+
line.match(/^-\s+status:\s*(pending|in_progress|complete)$/)
|
|
165
|
+
) {
|
|
166
|
+
result.push(`- status: ${newStatus}`);
|
|
167
|
+
} else {
|
|
168
|
+
result.push(line);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read and parse tasks from the todo directory
|
|
177
|
+
*/
|
|
178
|
+
export async function readTasks(
|
|
179
|
+
todoDir?: string
|
|
180
|
+
): Promise<{ tasks: Task[]; content: string }> {
|
|
181
|
+
const dir = todoDir || join(process.cwd(), "todo");
|
|
182
|
+
const tasksPath = join(dir, "TASKS.md");
|
|
183
|
+
|
|
184
|
+
if (!existsSync(tasksPath)) {
|
|
185
|
+
throw new Error(`TASKS.md not found at ${tasksPath}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const content = await Bun.file(tasksPath).text();
|
|
189
|
+
const tasks = parseTasks(content);
|
|
190
|
+
|
|
191
|
+
return { tasks, content };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Write updated content to TASKS.md
|
|
196
|
+
*/
|
|
197
|
+
export async function writeTasks(
|
|
198
|
+
content: string,
|
|
199
|
+
todoDir?: string
|
|
200
|
+
): Promise<void> {
|
|
201
|
+
const dir = todoDir || join(process.cwd(), "todo");
|
|
202
|
+
const tasksPath = join(dir, "TASKS.md");
|
|
203
|
+
await Bun.write(tasksPath, content);
|
|
204
|
+
}
|
package/src/templates.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
export const PROMPT_TEMPLATE = `# Agent Task Prompt
|
|
2
|
+
|
|
3
|
+
You are a coding agent implementing tasks one at a time.
|
|
4
|
+
|
|
5
|
+
## Your Mission
|
|
6
|
+
|
|
7
|
+
Implement ONE task from TASKS.md, test it, commit it, log your learnings, then EXIT.
|
|
8
|
+
|
|
9
|
+
## The Loop
|
|
10
|
+
|
|
11
|
+
1. **Read TASKS.md** - Find the first task with \`status: pending\` where ALL dependencies have \`status: complete\`
|
|
12
|
+
2. **Mark in_progress** - Update the task's status to \`in_progress\` in TASKS.md
|
|
13
|
+
3. **Implement** - Write the code following the project's patterns
|
|
14
|
+
4. **Write tests** - For behavioral code changes, create unit tests in the appropriate directory. Skip for documentation-only tasks.
|
|
15
|
+
5. **Run tests** - Execute tests from the package directory (ensures existing tests still pass)
|
|
16
|
+
6. **Fix failures** - If tests fail, debug and fix. DO NOT PROCEED WITH FAILING TESTS.
|
|
17
|
+
7. **Mark complete** - Update the task's status to \`complete\` in TASKS.md
|
|
18
|
+
8. **Log learnings** - Append insights to LEARNINGS.md
|
|
19
|
+
9. **Commit** - Stage and commit: \`git add -A && git commit -m "feat: <task-id> - <description>"\`
|
|
20
|
+
10. **EXIT** - Stop. The loop will reinvoke you for the next task.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Signs
|
|
25
|
+
|
|
26
|
+
READ THESE CAREFULLY. They are guardrails that prevent common mistakes.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### SIGN: One Task Only
|
|
31
|
+
|
|
32
|
+
- You implement **EXACTLY ONE** task per invocation
|
|
33
|
+
- After your commit, you **STOP**
|
|
34
|
+
- Do NOT continue to the next task
|
|
35
|
+
- Do NOT "while you're here" other improvements
|
|
36
|
+
- The loop will reinvoke you for the next task
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### SIGN: Dependencies Matter
|
|
41
|
+
|
|
42
|
+
Before starting a task, verify ALL its dependencies have \`status: complete\`.
|
|
43
|
+
|
|
44
|
+
\`\`\`
|
|
45
|
+
❌ WRONG: Start task with pending dependencies
|
|
46
|
+
✅ RIGHT: Check deps, proceed only if all complete
|
|
47
|
+
✅ RIGHT: If deps not complete, EXIT with clear error message
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
Do NOT skip ahead. Do NOT work on tasks out of order.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### SIGN: Learnings are Required
|
|
55
|
+
|
|
56
|
+
Before exiting, append to \`LEARNINGS.md\`:
|
|
57
|
+
|
|
58
|
+
\`\`\`markdown
|
|
59
|
+
## <task-id>
|
|
60
|
+
|
|
61
|
+
- Key insight or decision made
|
|
62
|
+
- Gotcha or pitfall discovered
|
|
63
|
+
- Pattern that worked well
|
|
64
|
+
- Anything the next agent should know
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Be specific. Be helpful. Future agents will thank you.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### SIGN: Commit Format
|
|
72
|
+
|
|
73
|
+
One commit per task. Format:
|
|
74
|
+
|
|
75
|
+
\`\`\`
|
|
76
|
+
feat: <task-id> - <short description>
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
Only commit AFTER tests pass.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### SIGN: Don't Over-Engineer
|
|
84
|
+
|
|
85
|
+
- Implement what the task specifies, nothing more
|
|
86
|
+
- Don't add features "while you're here"
|
|
87
|
+
- Don't refactor unrelated code
|
|
88
|
+
- Don't add abstractions for "future flexibility"
|
|
89
|
+
- Don't make perfect mocks in tests - use simple stubs instead
|
|
90
|
+
- Don't use complex test setups - keep tests simple and focused
|
|
91
|
+
- YAGNI: You Ain't Gonna Need It
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick Reference
|
|
96
|
+
|
|
97
|
+
<!-- This table should be customized for your project's tooling -->
|
|
98
|
+
<!-- Run 'math plan' to auto-detect and populate these commands -->
|
|
99
|
+
|
|
100
|
+
| Action | Command |
|
|
101
|
+
|--------|---------|
|
|
102
|
+
| Run tests | \`<your-test-command>\` |
|
|
103
|
+
| Build | \`<your-build-command>\` |
|
|
104
|
+
| Lint | \`<your-lint-command>\` |
|
|
105
|
+
| Stage all | \`git add -A\` |
|
|
106
|
+
| Commit | \`git commit -m "feat: ..."\` |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Remember
|
|
111
|
+
|
|
112
|
+
You do one thing. You do it well. You learn. You exit.
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
export const TASKS_TEMPLATE = `# Project Tasks
|
|
116
|
+
|
|
117
|
+
Task tracker for multi-agent development.
|
|
118
|
+
Each agent picks the next pending task, implements it, and marks it complete.
|
|
119
|
+
|
|
120
|
+
## How to Use
|
|
121
|
+
|
|
122
|
+
1. Find the first task with \`status: pending\` where ALL dependencies have \`status: complete\`
|
|
123
|
+
2. Change that task's status to \`in_progress\`
|
|
124
|
+
3. Implement the task
|
|
125
|
+
4. Write and run tests
|
|
126
|
+
5. Change the task's status to \`complete\`
|
|
127
|
+
6. Append learnings to LEARNINGS.md
|
|
128
|
+
7. Commit with message: \`feat: <task-id> - <description>\`
|
|
129
|
+
8. EXIT
|
|
130
|
+
|
|
131
|
+
## Task Statuses
|
|
132
|
+
|
|
133
|
+
- \`pending\` - Not started
|
|
134
|
+
- \`in_progress\` - Currently being worked on
|
|
135
|
+
- \`complete\` - Done and committed
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Phase 1: Setup
|
|
140
|
+
|
|
141
|
+
### example-task
|
|
142
|
+
|
|
143
|
+
- content: Replace this with your first task description
|
|
144
|
+
- status: pending
|
|
145
|
+
- dependencies: none
|
|
146
|
+
|
|
147
|
+
### another-task
|
|
148
|
+
|
|
149
|
+
- content: This task depends on example-task
|
|
150
|
+
- status: pending
|
|
151
|
+
- dependencies: example-task
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
export const LEARNINGS_TEMPLATE = `# Project Learnings Log
|
|
155
|
+
|
|
156
|
+
This file is appended by each agent after completing a task.
|
|
157
|
+
Key insights, gotchas, and patterns discovered during implementation.
|
|
158
|
+
|
|
159
|
+
Use this knowledge to avoid repeating mistakes and build on what works.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
<!-- Agents: Append your learnings below this line -->
|
|
164
|
+
<!-- Format:
|
|
165
|
+
## <task-id>
|
|
166
|
+
|
|
167
|
+
- Key insight or decision made
|
|
168
|
+
- Gotcha or pitfall discovered
|
|
169
|
+
- Pattern that worked well
|
|
170
|
+
- Anything the next agent should know
|
|
171
|
+
-->
|
|
172
|
+
`;
|