StudioDecision record

We moved every ticket into a Markdown file

On 12 October we retired our ticket tracker and put every open task in a Markdown file in each business's own git repo. It worked for just over two weeks. Then many agents writing at once pulled six of those repos apart.

406 and .md, joined by an arrow, set large on a pale grey background beside three Markdown task files, each with a status, owner and priority at the top, above the words: One file per task, in each business’s own repo
406open issues in the estate tracker when we switched it off
268stale tasks closed in two pruning passes on 14 October
6tracker repos found drifted from origin on 30 October
80 of 126new tasks filed with no owner

When we switched off Linear on 12 October, the estate plumbing project alone had 406 open issues. The company’s own admin had 58, ZooLog had 16, and every other business had a pile of its own. None of it moved to another vendor. Each live issue became a Markdown file in the git repo of the business it belonged to, and the old issues went into a read-only archive.

Two days later we wrote the standard down and built the tool around it: a small task command, one file per task, and an estate-wide overview that a background job rebuilds every five minutes from every business’s tasks/ folder. That evening we ran two pruning passes, fourteen minutes apart. The first closed 218 tasks that were stale, duplicated, or tied to a server we had already retired. The second closed 50 more.

Why a file and not a ticket

Most tasks in our trackers are now picked up, worked and closed by AI coding agents, with one of us reviewing. An agent is good at reading a file and poor at clicking through a web app. A ticket in a hosted tool needs an API key, a client library and a network round trip before the agent can even see it. A file is just there, next to the code it is about.

So the rules came out like this. One task is one file, named after its id, and an id is never reused. The frontmatter is flat key: value, so a person can read it and a script can parse it. The body says why the task exists and what “done” means. Under that sits a thread of dated, signed notes that nobody is allowed to rewrite.

---
id: ES-###
title: "websites devroot is diverged from origin (ahead 143, behind 62 on 30 Oct): every task note commit fails to push"
status: done
priority: high
area: estate
created: 2026-10-30
closed: 2026-10-30
reporter: "Claude"
---

Found 30 Oct 13:30 while adding a note to a websites task: "committed locally,
push failed (rebase conflict aborted)". It was ahead 140, behind 56 at 12:55,
so it is growing by the minute. Do not force-push.

## Notes

### 2026-10-30 13:52 · Claude

Six diverged tracker repos healed; task check --all reports 0 diverged.

That is a real file from the estate tracker, trimmed, with its id and commit hashes taken out. It happens to be the one that recorded what went wrong.

What we got for nothing: every change to a task is a git commit, so its history is git log on one file. Agents and people edit the same thing. There is no vendor to pay or to migrate away from later. And each business keeps its list in its own repo, so a session working on ZooLog never loads anything from TipBop.

The phone was the gap. The first version of the standard also retired the to-do app we had on our phones, on the theory that one list beats two. Nine days later, on 23 October, it came back as a mirror: a Cloudflare Worker that runs every minute, copies each task owned by a person into Todoist, and writes ticks, priorities and due dates back into the files. Tasks an agent will do itself stay out of it. The first run pushed 168 tasks.

Agent sessionsmany at oncetask CLIwrites, commits, pushesEach business's git repotasks/ZL-n.md, tasks/TB-n.md, tasks/WS-n.mdone Markdown file per taskEstate overviewrebuilt every 5 minutesderived, never editedTodoist synca Worker, everyminute, both waysOur phonestasks a personmust doAgents write files. Peopletick things off in Todoist.Both end up as git commits.
Files are the source of truth. The overview and the Todoist mirror are both read from them, and a tick on a phone comes back as a commit.

What broke on 30 October

By late October there were often several agent sessions working in the same business at once, each filing notes, starting tasks and closing them. Every one of those writes committed two files: the task it touched, and the business’s tasks/INDEX.md, a generated table of open work.

That index was the flaw. Any two writers changed it at the same moment, so any two writers conflicted. The push used git pull --rebase, the rebase aborted on the index, and the task commit stayed on the machine that made it. The command printed a warning and carried on, so the stranded commits piled up quietly.

On 30 October at 13:30 an agent tried to add a note to a websites task and the push failed. It checked: the websites tracker was 143 commits ahead of origin and 62 behind, up from 140 and 56 thirty-five minutes earlier. When we checked every tracker, six had drifted. By the time the repair ran that afternoon, websites stood at 157 ahead and 78 behind.

How far each tracker had drifted, 30 October

Commits the local checkout had that origin did not (ahead), and the reverse (behind). Bar length is the two added together.

TipBop8 ahead, 402 behindZooLog9 ahead, 349 behindWebsites157 ahead, 78 behindFinance4 ahead, 134 behindCompany admin1 ahead, 93 behindTyreStack8 ahead, 19 behind

Source: Estate decision log, 30 October 2026

The drift was not the only damage. Two task ids had been handed out twice, once in TipBop and once in ZooLog, because two machines each took the next free number from a checkout that could not see the other’s. The same audit turned up habits that the files had made easy:

80 of 126task new calls that named no owner
211 of 213open tasks for one of us that an agent had filed
26 of 49sessions that closed a task without ever starting it

The second one is the one that hurt. The CLI set the owner to whoever created the task, and inside a session “whoever” means the person who started the session. So agents quietly filed task after task in a director’s name, and one of us had 213 open tasks, only two of which had not come from an agent. A list like that is not a to-do list. It is noise with someone’s name on it.

The fixes, and the ones we turned down

Everything below went in the same afternoon.

The index came out of the write path. task now commits only the task files it touched. INDEX.md is regenerated separately, by task index and by the Todoist Worker, and is allowed to lag.

A rejected push now merges instead of rebasing. It takes a lock, fetches, merges origin into the checkout, and resolves task files field by field: notes from both sides are kept, the later status wins and is flagged for a person to check, and lists are merged as sets. A conflict anywhere outside tasks/ stops the merge and names the file.

We turned down two cleverer fixes. A rebase with a smarter conflict driver would have replayed 157 websites commits one at a time and met every contradiction again. A custom git merge driver for the index needs config in every clone, and nobody sets that up on a new machine. Resolving with -X theirs would have made the conflicts disappear by losing task edits without a word, which is worse than the conflicts.

task check now fails when a checkout has diverged on tasks/, and the brief every session prints at start-up warns about it. task new without --owner exits with an error. --owner none means an agent will do it. A name means it lands on that person’s phone.

The repair was dull on purpose. Each drifted repo was merged in a throwaway clone and pushed, the live checkout was fast-forwarded, and a backup ref was left in every repo. The websites merge met 19 tasks where the two sides disagreed about status. The later note decided each one, all 19 were flagged, and when we checked them by hand none needed changing.

Update, 7 November 2026

The files are still the source of truth. On 6 November we copied 2,737 tracker tasks into a native task app we are building for ourselves, as a test against real data with no cutover. Todoist and the sync Worker stay live while it proves itself.

Where the numbers come from

  1. Archive of the issues open when the old tracker was retired, 12 October 2026 (counts only).
  2. Estate git history: the task standard and the two pruning commits of 14 October 2026.
  3. Estate task standard, as revised on 30 October 2026.
  4. Todoist sync standard and estate decision log entry, 23 October 2026.
  5. Estate decision log, entries for 30 October 2026, and the task that recorded the divergence.
  6. Acceptance notes for our task app, 6 November 2026 (update only).

Mark Lovelady, Director. Builds most of our software.