commit afc9928967d6383cbfbc4e96700c511e861511c1
parent 2bbe9402334affa62178aa94c47f1fa550d31484
Author: Michael Percival <m@michaelpercival.xyz>
Date: Wed, 15 Apr 2026 14:18:26 +0100
Redesign todo: daily today.md + persistent backlog.md with archiving
Each day gets a fresh today.md; stale files are archived to
archive/YYYY-MM-DD.md using the date from the file header.
backlog.md holds persistent items (Urgent/Someday). Claude manages
both files simultaneously and is notified on daily rotation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat:
6 files changed, 107 insertions(+), 54 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
@@ -2,26 +2,39 @@
You are a personal assistant managing a life todo list for the user via Telegram.
+## Files
+
+You manage two files:
+
+- **`today.md`** — today's task list. Has a date header (`# Todo — YYYY-MM-DD`). Resets each day.
+- **`backlog.md`** — persistent items with no fixed day, organised by priority.
+
## Your Role
- Accept new todo items in any format — natural language, lists, brain dumps, half-formed thoughts
-- Format and categorise items into the appropriate section of the todo list
+- Decide whether new items belong in today or the backlog; ask if genuinely unclear
- Coalesce duplicate or very similar items rather than creating redundant entries
-- Occasionally ask about priority or urgency when it's genuinely unclear (at most one question per message)
-- Suggest breaking vague items into actionable ones when it would genuinely help
-- Mark items as done when the user says so, noting the completion date
-- Nudge (gently, occasionally) on items that have been sitting in a section for a long time
+- Mark items done when the user says so, noting the completion date
+- When the user says "pull X from backlog" or "add X to today", move it: remove from backlog, add to today
+- When the user says "defer X" or "push X to backlog", move it: remove from today, add to backlog
+- Nudge (gently, occasionally) on items that have been sitting for a long time
-## Categories
+## Daily Reset
-Items belong in one of these sections, in this order:
+When a new day is detected (indicated by a note in the system prompt), today.md has already been
+reset to a fresh file with today's date. Briefly acknowledge the new day and suggest pulling any
+urgent or time-sensitive backlog items into today — but don't move them automatically, just mention them.
+## Sections
+
+**today.md** (in this order):
+- `# Todo — YYYY-MM-DD` — date header, always first line
+- **Tasks** — things to do today
+- **Done** — completed today (kept until next reset)
+
+**backlog.md** (in this order):
- **Urgent** — needs to happen within days
-- **This Week** — target this week, no hard deadline
- **Someday** — no deadline, low pressure, nice to have
-- **Done** — completed items (keep the 10 most recent, oldest at bottom)
-
-If you're unsure where something belongs, default to **This Week** and mention it.
## Tone
@@ -36,4 +49,4 @@ If you're unsure where something belongs, default to **This Week** and mention i
- Completed items: `- [x] item text _(YYYY-MM-DD)_`
- Section headers: `## SectionName`
- No sub-bullets unless a task genuinely has distinct steps the user mentioned
-- Keep the file clean — no trailing whitespace, blank line between sections
+- Keep files clean — no trailing whitespace, blank line between sections
diff --git a/scripts/setup.sh b/scripts/setup.sh
@@ -42,16 +42,12 @@ echo "✓ data/tmp directory ready"
DATA_REPO="$REPO_ROOT/data/repo"
if [ ! -d "$DATA_REPO/.git" ]; then
echo "Initializing data repo..."
- mkdir -p "$DATA_REPO/conversations"
+ mkdir -p "$DATA_REPO/conversations" "$DATA_REPO/archive"
git -C "$DATA_REPO" init
git -C "$DATA_REPO" remote add origin git@michaelpercival.com:/var/www/gitcom/todo-list.git
- # Migrate existing todo.md from project root if present
- if [ -f "$REPO_ROOT/todo.md" ]; then
- mv "$REPO_ROOT/todo.md" "$DATA_REPO/todo.md"
- echo " Migrated todo.md from project root"
- else
- touch "$DATA_REPO/todo.md"
- fi
+ TODAY=$(date +%Y-%m-%d)
+ printf "# Todo — %snn## Tasksnn## Donen" "$TODAY" > "$DATA_REPO/today.md"
+ printf "## Urgentnn## Somedayn" > "$DATA_REPO/backlog.md"
git -C "$DATA_REPO" add .
git -C "$DATA_REPO" commit -m "Initialize todo list repo"
git -C "$DATA_REPO" push -u origin master || echo " WARNING: push failed — check remote access"
diff --git a/src/bot.js b/src/bot.js
@@ -39,22 +39,23 @@ function createBot(token) {
console.log(`[user:${userId}] ${text}`);
}
+ const rotated = todo.rotateTodayIfStale();
+ const currentToday = todo.readToday();
+ const currentBacklog = todo.readBacklog();
const history = state.load(userId);
- const currentTodo = todo.read();
- const result = await claude.chat(history, text, currentTodo);
+ const result = await claude.chat(history, text, currentToday, currentBacklog, rotated);
console.log(`[claude] ${result.reply}`);
- if (result.updatedTodo !== currentTodo) {
- todo.write(result.updatedTodo);
- }
+ if (result.updatedToday !== currentToday) todo.writeToday(result.updatedToday);
+ if (result.updatedBacklog !== currentBacklog) todo.writeBacklog(result.updatedBacklog);
- // Persist both sides of the exchange
state.append(userId, 'user', text);
state.append(userId, 'assistant', result.reply);
- // Always commit — todo + conversation history together
- const commitMessage = result.updatedTodo !== currentTodo && result.commitMessage
+ const todayChanged = result.updatedToday !== currentToday;
+ const backlogChanged = result.updatedBacklog !== currentBacklog;
+ const commitMessage = (todayChanged || backlogChanged) && result.commitMessage
? result.commitMessage
: 'Update conversation history';
try {
diff --git a/src/claude.js b/src/claude.js
@@ -10,17 +10,18 @@ const RESPONSE_FORMAT_INSTRUCTIONS = `
## Response Format
You MUST respond with a raw JSON object — no markdown fences, no extra text before or after.
-The object must have exactly these three fields:
+The object must have exactly these four fields:
{
"reply": "<your conversational reply to send to the user via Telegram>",
- "updatedTodo": "<the full updated todo.md content as a plain string>",
- "commitMessage": "<a short git commit message, e.g. 'add: book dentist (This Week)'>"
+ "updatedToday": "<the full updated today.md content as a plain string>",
+ "updatedBacklog": "<the full updated backlog.md content as a plain string>",
+ "commitMessage": "<a short git commit message, e.g. 'add: book dentist (backlog)'>"
}
Rules:
-- If the todo list did not change, return the original content unchanged in updatedTodo.
-- If you're asking a clarifying question, still return the current todo unchanged.
+- If a file did not change, return its original content unchanged.
+- If you're asking a clarifying question, return both files unchanged.
- Always return valid JSON. Never wrap it in a code block.
`;
@@ -29,9 +30,15 @@ function getClaudeMd() {
return fs.readFileSync(CLAUDE_MD_PATH, 'utf8');
}
-function buildSystemBlocks(todoContent) {
- // The static part (CLAUDE.md + format instructions) is marked for prompt caching.
- // The dynamic part (current todo) is a separate block and not cached since it changes.
+function buildSystemBlocks(todayContent, backlogContent, rotated) {
+ const parts = [
+ `## today.mdnn```markdownn${todayContent || '(empty)'}n````,
+ `## backlog.mdnn```markdownn${backlogContent || '(empty)'}n````,
+ ];
+ if (rotated) {
+ parts.unshift('> **New day.** today.md has been reset. Briefly suggest pulling any urgent backlog items into today — but don't do it automatically.');
+ }
+
return [
{
type: 'text',
@@ -40,12 +47,12 @@ function buildSystemBlocks(todoContent) {
},
{
type: 'text',
- text: `## Current Todo Listnn```markdownn${todoContent || '(empty — no items yet)'}n````,
+ text: parts.join('nn'),
},
];
}
-async function chat(history, userMessage, todoContent) {
+async function chat(history, userMessage, todayContent, backlogContent, rotated = false) {
const messages = [
...history,
{ role: 'user', content: userMessage },
@@ -54,7 +61,7 @@ async function chat(history, userMessage, todoContent) {
const response = await client.messages.create({
model: 'claude-sonnet-4-6',
max_tokens: 2048,
- system: buildSystemBlocks(todoContent),
+ system: buildSystemBlocks(todayContent, backlogContent, rotated),
messages,
});
@@ -63,17 +70,18 @@ async function chat(history, userMessage, todoContent) {
try {
const parsed = JSON.parse(raw);
return {
- reply: parsed.reply ?? '(no reply)',
- updatedTodo: parsed.updatedTodo ?? todoContent,
- commitMessage: parsed.commitMessage ?? 'update: todo list',
+ reply: parsed.reply ?? '(no reply)',
+ updatedToday: parsed.updatedToday ?? todayContent,
+ updatedBacklog: parsed.updatedBacklog ?? backlogContent,
+ commitMessage: parsed.commitMessage ?? 'update: todo',
};
} catch {
- // Parsing failed — return the raw text as the reply, leave todo unchanged.
console.error('Failed to parse Claude response as JSON:', raw);
return {
- reply: raw,
- updatedTodo: todoContent,
- commitMessage: null,
+ reply: raw,
+ updatedToday: todayContent,
+ updatedBacklog: backlogContent,
+ commitMessage: null,
};
}
}
diff --git a/src/git.js b/src/git.js
@@ -5,7 +5,7 @@ const DATA_REPO = path.join(__dirname, '..', 'data', 'repo');
const git = simpleGit(DATA_REPO);
async function commitAndPush(message) {
- await git.add(['todo.md', 'conversations']);
+ await git.add(['today.md', 'backlog.md', 'archive', 'conversations']);
const status = await git.status();
if (status.staged.length === 0) {
diff --git a/src/todo.js b/src/todo.js
@@ -1,15 +1,50 @@
const fs = require('fs');
const path = require('path');
-const TODO_PATH = path.join(__dirname, '..', 'data', 'repo', 'todo.md');
+const DATA_REPO = path.join(__dirname, '..', 'data', 'repo');
+const TODAY_PATH = path.join(DATA_REPO, 'today.md');
+const BACKLOG_PATH = path.join(DATA_REPO, 'backlog.md');
+const ARCHIVE_DIR = path.join(DATA_REPO, 'archive');
-function read() {
- if (!fs.existsSync(TODO_PATH)) return '';
- return fs.readFileSync(TODO_PATH, 'utf8');
+function todayDate() {
+ return new Date().toISOString().slice(0, 10);
}
-function write(content) {
- fs.writeFileSync(TODO_PATH, content, 'utf8');
+function readToday() {
+ if (!fs.existsSync(TODAY_PATH)) return '';
+ return fs.readFileSync(TODAY_PATH, 'utf8');
}
-module.exports = { read, write, TODO_PATH };
+function readBacklog() {
+ if (!fs.existsSync(BACKLOG_PATH)) return '';
+ return fs.readFileSync(BACKLOG_PATH, 'utf8');
+}
+
+function writeToday(content) {
+ fs.writeFileSync(TODAY_PATH, content, 'utf8');
+}
+
+function writeBacklog(content) {
+ fs.writeFileSync(BACKLOG_PATH, content, 'utf8');
+}
+
+// Archives today.md under archive/YYYY-MM-DD.md and resets it for today.
+// Returns true if a rotation occurred, false if today.md is already current.
+function rotateTodayIfStale() {
+ if (!fs.existsSync(TODAY_PATH)) return false;
+ const content = fs.readFileSync(TODAY_PATH, 'utf8');
+ const match = content.match(/^# Todo — (d{4}-d{2}-d{2})/);
+ if (!match) return false;
+
+ const fileDate = match[1];
+ const today = todayDate();
+ if (fileDate === today) return false;
+
+ fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
+ fs.writeFileSync(path.join(ARCHIVE_DIR, `${fileDate}.md`), content, 'utf8');
+ fs.writeFileSync(TODAY_PATH, `# Todo — ${today}nn## Tasksnn## Donen`, 'utf8');
+
+ return true;
+}
+
+module.exports = { readToday, readBacklog, writeToday, writeBacklog, rotateTodayIfStale };