Philo includes a powerful task management system that automatically rolls over incomplete tasks from previous days to keep your to-do list current.
Tasks use standard Markdown checkbox syntax:
- [ ] Unchecked task
- [x] Checked task
- [ ] Nested subtask
- [x] Completed subtask
Task Patterns
Philo recognizes two task patterns:
// Unchecked tasks
const UNCHECKED_TASK = /^(\s*)[-*] \[ \] (.+)$/;
// Checked tasks
const CHECKED_TASK = /^(\s*)[-*] \[x\] (.+)$/i;
Both - and * are supported as list markers, and the check is case-insensitive.
Task Rollover
The rolloverTasks() function automatically moves incomplete tasks from past days to today’s note.
How It Works
Scan past notes
Checks the last 30 days (configurable) for unchecked tasks
Extract tasks
Removes unchecked tasks from source notes while preserving indentation
Check for recurring tasks
Identifies completed tasks with recurrence tags (e.g., #daily, #weekly)
Deduplicate
Prevents duplicate tasks from appearing in today’s note
Prepend to today
Adds all rolled-over tasks to the top of today’s note
Implementation
export async function rolloverTasks(days: number = 30): Promise<boolean> {
const today = getToday();
const taskMap = new Map<string, TaskLine>();
const modifiedNotes: DailyNote[] = [];
// Scan past days
for (let i = 1; i <= days; i++) {
const date = getDaysAgo(i);
const note = await loadDailyNote(date);
if (!note || !note.content.trim()) continue;
const markdown = json2md(parseJsonContent(note.content));
const { tasks, cleaned } = extractUncheckedTasks(markdown);
// Collect unchecked tasks
if (tasks.length > 0) {
tasks.forEach((t) => {
if (!taskMap.has(t.text)) {
taskMap.set(t.text, t);
}
});
modifiedNotes.push({ ...note, content: JSON.stringify(md2json(cleaned)) });
}
// Check for recurring tasks
const checkedRecurring = extractCheckedRecurringTasks(markdown);
for (const taskText of checkedRecurring) {
const recurrence = parseRecurrence(taskText)!;
const nextDue = addDaysToDate(date, recurrence.intervalDays);
if (nextDue <= today && !taskMap.has(taskText)) {
taskMap.set(taskText, { indent: "", text: taskText });
}
}
}
if (taskMap.size === 0) return false;
// Save cleaned source notes
await Promise.all(modifiedNotes.map((note) => saveDailyNote(note)));
// Update today's note
const todayNote = await loadDailyNote(today);
const todayMarkdown = todayNote ? json2md(parseJsonContent(todayNote.content)) : "";
const existingTasks = extractAllTaskTexts(todayMarkdown);
const newTasks = [...taskMap.values()].filter((t) => !existingTasks.has(t.text));
if (newTasks.length === 0) return false;
const updated = prependTasks(todayMarkdown, newTasks);
const updatedJson = JSON.stringify(md2json(updated));
await saveDailyNote({ date: today, content: updatedJson, city: todayNote?.city });
return true;
}
Nested Tasks
Philo preserves task hierarchy during rollover:
interface TaskLine {
indent: string; // Preserved whitespace for nesting
text: string; // Task text including any tags
}
function extractUncheckedTasks(content: string): {
tasks: TaskLine[];
cleaned: string;
} {
const lines = content.split("\n");
const tasks: TaskLine[] = [];
const kept: string[] = [];
for (const line of lines) {
const match = line.match(UNCHECKED_TASK);
if (match) {
tasks.push({ indent: match[1], text: match[2] });
} else {
kept.push(line);
}
}
let cleaned = kept.join("\n");
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
return { tasks, cleaned };
}
Example
- [ ] Complete project proposal
- [ ] Draft outline
- [x] Research competitors
- [ ] Create budget
- [x] Email team
Completed subtasks are not rolled over, maintaining a clean task list.
Task Deduplication
To prevent duplicates, Philo checks existing tasks in today’s note:
function extractAllTaskTexts(content: string): Set<string> {
const lines = content.split("\n");
const texts = new Set<string>();
for (const line of lines) {
const unchecked = line.match(UNCHECKED_TASK);
if (unchecked) texts.add(unchecked[2]);
const checked = line.match(CHECKED_TASK);
if (checked) texts.add(checked[2]);
}
return texts;
}
Tasks are matched by their full text, including any tags or metadata.
Prepending Tasks
Rolled-over tasks are added to the beginning of today’s note:
function prependTasks(content: string, tasks: TaskLine[]): string {
if (tasks.length === 0) return content;
const taskLines = tasks.map((t) => `${t.indent}- [ ] ${t.text}`).join("\n");
const trimmed = content.trim();
if (!trimmed) return `\n\n\n${taskLines}`;
return `${taskLines}\n\n${trimmed}`;
}
Best Practices
Use consistent syntax
Stick with either - or * for list markers to maintain consistency
Add context
Include enough detail in task text so it’s meaningful when rolled over
Nest related tasks
Group subtasks under parent tasks to maintain logical structure
Use recurring tasks
Add recurrence tags like #daily for habits and routine tasks
Run task rollover when you first open Philo each day to start with a clean, updated task list.