life-todo

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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:
MCLAUDE.md | 37+++++++++++++++++++++++++------------
Mscripts/setup.sh | 12++++--------
Msrc/bot.js | 17+++++++++--------
Msrc/claude.js | 44++++++++++++++++++++++++++------------------
Msrc/git.js | 2+-
Msrc/todo.js | 49++++++++++++++++++++++++++++++++++++++++++-------
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 };