# One task board for a person and their AI agents — spec for an AI agent

Source: https://jacobkunselman.com/tips/one-task-board/ (AI tip No. 4, 2026-10-12).
Reference implementation: the files in `reference/` (Python 3 standard library, vanilla JavaScript). Read
them, and adapt them; don't copy them blindly. Hostnames in them are placeholders.

## Goal

One to-do list that:
- the person uses on Android, Windows and Linux, including with no signal;
- every AI agent session they run can read, add to and tick off;
- pushes reminders to their phone, mirrors dated cards into Google Calendar, and feeds any dashboard.

## Components

1. **The board file**, `board/tasks.md`, in a git repo:
   - Four `## ` headings, which are the columns: `Doing`, `Todo`, `Later`, `Done`.
   - One card per line: `- [ ] Title #tag due:YYYY-MM-DD[THH:MM] every:1y store:menards until:YYYY-MM-DD ^abcd`.
     `- [x]` marks a card in Done.
   - Metadata is read only from the end of the line, so a title may contain `#` or `:`.
   - `^abcd` is a 4-character id from base 32 without l, o, 0 or 1. A line without one gets one on the next change.
   - Notes are indented lines under a card.
   - A stray line that is not a card, a note or a heading is an error. Never drop it silently.
2. **`categories.md`:** a table of `tag | name | #RRGGBB` that every surface colours by.
3. **The one way to change it.** `change(op)` takes an exclusive file lock, reads the file, applies one operation,
   writes it by rename, and commits only the board's paths. Push in the background.
   `sync(ops)` does a batch under one lock and one commit. It skips any `opid` it has seen (keep the last few
   thousand), and it drops an op whose card no longer exists.
4. **Operations:** `add`, `edit` (title, cat, due, every, store, note, notes), `move` (column, where = top,
   bottom or after a card id), `done`, `later` (until a date, or someday), `delete`, `tick`.
   - Later is kept sorted by `until`, soonest first, with someday cards last.
   - `tick` moves Later cards whose date has come to the top of Todo, and archives last month's Done to
     `done/YYYY-MM.md`. Run it hourly from a timer.
5. **Repeats.** Ticking a card with `every:` creates the next card, with the due date moved on one step:
   - as many steps as it takes to land after today, so late ticks don't stack;
   - a month step clamps to the month's last day;
   - the new card goes in Later until `due − max(1, min(14, step/4))` days.
   - Un-ticking takes that new card back.
6. **A command line** for agents: `add`, `do`, `done`, `later`, `todo`, `move`, `edit`, `note`, `rm`, `list`,
   `shop` (purchases grouped by `store:`), `tick` and `json`. On a machine without the board it sends the same
   operations to the server.
7. **A server** on the always-on machine, listening on 127.0.0.1 only:
   - `GET /`: the app
   - `GET /api/board`: `{version, board}`
   - `POST /api/sync` with `{ops, who}`: returns `{version, board, applied, dropped}`
   - Put it on the private network with HTTPS (e.g. `tailscale serve --bg --https=8443 http://127.0.0.1:8770`).
     A service worker requires HTTPS.
8. **The app:** one HTML page, one script, a service worker and a manifest (PNG icons 192, 512 and maskable).
   - It keeps the board and an **outbox of ops**, each with a unique opid, in browser storage. Every change is
     applied locally at once and sent when the server answers. The reply replaces the board, and any ops still
     waiting are re-applied on top.
   - The app's `apply()` must match the server's exactly.
   - Call `navigator.storage.persist()`.
   - Serve the app's files cache-first and refresh them in the background; never cache the API.
   - UI: columns, drag by a grip to reorder or move, a circle to tick (with undo), an editor (title, category,
     date, time, repeat, store, notes, later presets), and filter chips by category.
9. **Phone pushes** through ntfy, on a long random topic kept out of git:
   - a 7 a.m. digest of what's due and late, or nothing;
   - each timed card at its due time, with an `http` action button that POSTs a `done` op with a fixed opid,
     sent from the phone over the private network;
   - "back on the list" when a Later card wakes.
   - Remember what was sent, so nothing is sent twice.
   - Any category marked sensitive shows a generic title, since anyone who knows a topic's name can read it.
10. **Google Calendar.** Use a Desktop OAuth client with PKCE, scope
    `https://www.googleapis.com/auth/calendar.app.created`.
    - Create one calendar of its own. Each open card with a due date is an event: all-day and free for a date,
      a 30-minute event for a date and time.
    - Colour each event with the nearest Google colour to its category's. Turn reminders off; the phone pushes
      already handle them.
    - Reconcile one way: insert, update or delete by a hash of the event body, so a sync with nothing to change
      makes no API calls. If the calendar has been deleted, recreate it once.
    - Sync after every app change and every 5 minutes.

## Rules

- Never hand-edit the board file. Everything goes through `change()` or `sync()`.
- The server accepts a POST only as `application/json` and only from the app's own origin.
- Secrets live outside the repo, at mode 600: the ntfy topic, the OAuth client and the token.
- **Google OAuth:**
  - Publish the consent screen ("In production"); in Testing, refresh tokens expire after 7 days.
  - Publishing requires a home page and a privacy policy URL on a domain you own.
  - Don't upload a logo; that triggers verification.
- Tell every agent session, in the instructions they all load: everything the person must do in the real world
  is a card; add it when they mention it, tick it when they say it's done, and read the board before saying what
  is left. A repo keeps the reasoning, and the card points to it.
- To migrate: ask each session for its open items in one line format
  (`title | #tag | due | repeats | file`), merge the duplicates, and load them in one commit so a single revert
  undoes it. Then review the board with the person in batches.

## Acceptance tests

1. Round trip: parse → render → parse is identical, and a title containing `LB#4` or `Ratio: 3:1` survives.
2. A stray line under a column raises an error; it is not dropped.
3. Two batches with the same opids change the board once.
4. An `add` whose id is taken gets a new id, and later ops in the same batch follow it.
5. An op on a deleted card is reported as dropped, and the batch still succeeds.
6. Repeats: `2026-01-31 + 1m = 2026-02-28`. A monthly card ticked three months late gets the next date after
   today. Un-ticking removes the generated card.
7. `tick` on the 1st of a month archives last month's Done cards and wakes due Later cards in order.
8. Offline: block the network, make two changes, and see "2 waiting". Restore it, and both are applied once.
9. Push timing: one digest per morning (or none if nothing is due); a timed card's push fires once, up to 2 h late
   if the server was down; its Done button's request ticks the card.
10. Calendar, against a fake API: an unchanged board makes zero calls; an edit makes one update; done or a removed
    date makes one delete; a deleted calendar is recreated and repopulated.
11. A POST with `Content-Type: text/plain`, or from another origin, is refused.
