@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.
@@ -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
- mkdirSync(TEST_DIR, { recursive: true });
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(TEST_DIR);
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 with basic pattern", () => {
22
- mkdirSync(join(TEST_DIR, "todo-1-15-2025"));
23
- mkdirSync(join(TEST_DIR, "todo-12-31-2024"));
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(TEST_DIR);
32
+ const result = findArtifacts();
26
33
 
27
34
  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"));
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 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"));
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(TEST_DIR);
44
+ const result = findArtifacts();
38
45
 
39
46
  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"));
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 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
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(TEST_DIR);
58
+ const result = findArtifacts();
52
59
 
53
- expect(result).toHaveLength(1);
54
- expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
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 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");
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(TEST_DIR);
70
+ const result = findArtifacts();
63
71
 
64
72
  expect(result).toHaveLength(1);
65
- expect(result).toContain(join(TEST_DIR, "todo-1-15-2025"));
73
+ expect(result).toContain(join(BACKUPS_DIR, "core-infrastructure"));
66
74
  });
67
75
 
68
- test("findArtifacts returns empty array for non-existent directory", () => {
69
- const result = findArtifacts(join(TEST_DIR, "does-not-exist"));
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(TEST_DIR, "todo-1-15-2025"));
85
+ mkdirSync(join(BACKUPS_DIR, "core-infrastructure"));
75
86
 
76
- const result = findArtifacts(TEST_DIR);
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
- * 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.
7
+ * Finds all math artifacts (backup directories) in `.math/backups/`.
14
8
  *
15
- * Artifacts include:
16
- * - Backup directories matching pattern todo-{M}-{D}-{Y} or todo-{M}-{D}-{Y}-{N}
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
- * @param directory - The directory to search in (defaults to cwd)
19
- * @returns Array of absolute paths to artifacts
12
+ * @returns Array of absolute paths to backup directories
20
13
  */
21
- export function findArtifacts(directory: string = process.cwd()): string[] {
14
+ export function findArtifacts(): string[] {
22
15
  const artifacts: string[] = [];
16
+ const backupsDir = getBackupsDir();
23
17
 
24
18
  try {
25
- const entries = readdirSync(directory);
19
+ const entries = readdirSync(backupsDir);
26
20
 
27
21
  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.)
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 || join(process.cwd(), "todo");
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 || join(process.cwd(), "todo");
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