TLDR;

todofiles is a file syntax (basically markdown), parser, and tool for maintaining Jira tickets in local plaintext files and syncing them to/from Jira (maybe other ticket backends later).

I don’t like navigating ticketing system Web UIs; todofiles allow me to keep my todo lists in plaintext files while syncing changes to Jira:

---
# File-level defaults applied to all tickets
labels: ["my_team", "my_project"]
board: "BackendTeamBoard"
item_type: "task"
status_map:
    x: "Done"
    in_prog: "In Progress"
    review: "In Review"
---

[ ] add json output format to products API

[in_prog] refactor frontend to use v2 API
    id: abc123
    jira: PROJ-42

[in_prog] add slack integration
    id: ghi789
    jira: PROJ-45
    description: |
        Add integration with our Slack bot using OAuth workflows.
    subtasks:
        [x] create oauth callback endpoint
            id: jkl012
            jira: PROJ-46
        [ ] register new Slack App
            id: mno345

Lines like this that don't start with [...] are free-form comments.
They are preserved in the file but never synced to any backend.
$ todofiles push ./my-proj.todo

  CREATE (5)
    + [ ] add json output format to products API
    + [in_prog] refactor frontend to use v2 API
    + [in_prog] add slack integration
    + [x] create oauth callback endpoint
    + [ ] register new Slack App

Background

I’ve always found navigating/using project management software like Jira, ClickUp, Trello, etc… to be tedious.

Creating new tickets or finding/updating existing tickets invovles menu-diving through various layers of a Web UI that attempts to load dozens of other tickets at each click.

Once I finally get to create a ticket in what seems to be in the correct backlog/board/project, the ticket disappears because I didn’t assign the correct labels/status/project/board/… - if I’m not quick enough to catch the warning popup, my newly documented ticket disappears into the ether.

When I’m in a meeting and need to scribble down a task quickly I don’t often find myself reaching for my web browser to wait for a UI to load where I can enter my ticket, nor do I want a heavy web app consuming what precious/limited memory my corporate-issue Thinkpad can spare.

Or when I’m reviewing meeting notes/next-steps, I’ve usually already got a plain-text list of TODOs - the burden of copying those into a ticketing system feels silly (and thus rarely happens…).

As a result, my Jira board is almost never reflective of what I’m actually working on and I frequently find myself apologizing to PMs for my negligence.

Plaintext .md Files for Tracking Notes/TODOs

Like many developers, I prefer to spend most of my time in my text editor, editing text files.

I’ve defaulted to keeping most of my TODOs and project-related notes in plaintext files - specifically markdown files (not that I actually ever render the markdown to html/pdf or any other, I just find the syntax highlighting nice for combining things like headers, bulleted lists, code snippets, amongst notes).

This has been nice for a few reasons:

  • I can very easily grep accross my various notes/todos (much faster than trying to search through old tickets, Confluence pages, etc…)
  • finding files is also much easier/faster (same keyboard shortcut I use to open files in development)
  • It’s super fast to pull up a new file (ctrl + shift + t, nvim ~/notes, :e TODO.md)
  • as a neovim user, I get to keep my hands on the keyboard (as opposed to in a browser/web UI requiring clicks) …etc…

So I started keeping TODOs/tasks as checkboxes in my markdown notes files:

[x] - add unit tests for new /product endpoint
[ ] - implement /product endpoints

Jira/ClickUp inevitably became very stale from my local plaintext TODO files.

todofiles

todofiles is a textfile syntax and synchronization tool that allows you to write your todos/tickets in a plaintext format roughly resembling markdown, parse that file, and sync those with tickets in Jira (maybe other ticketing systems/backends in the future…).

todofile Syntax

The syntax of a .todo file is basically markdown with a header describing the scope or default fields for all tickets the file will create in the ticketing system backend (currently just Jira):

Any line starting with [<status>] Ticket title description... will be parsed as a separate ticket (where <status> can be mapped to statuses in Jira).

---
# yaml header w/ some optional settings

# Jira board - all tickets under this file will be associated with this board
board: MYPROJ

# Any tickets created/managed by this todofile will be assigned to this user
assignee: AB32B2B2383B2B1B23412341234

# Any newly created tickets will automatically be added to this sprint
sprint: "current"

# Mapping of statuses we can place in our `[<status]` blocks to what they're called
# in our ticketing backend.
status_map:
    todo: "To Do"
    in_prog: "In Progress"
    x: "Done"
---

Any text in this document that doesn't match the header matter or ticket parser is ignored (so we can keep misc notes here too).

Once a ticket is synced w/ Jira, it receives 2 identifiers the `jira` ID for the ticket, and an internal uuid (todofiles cli will write these back to the .todo file automatically for each ticket).

[in_prog] refactor auth middleware
    id: a3f9c1b2
    jira: PROJ-84

[x] fix the flaky test
    id: 7d2e4a91
    jira: PROJ-71

This ticket isn't yet sync'd (doesn't have an id or jira id) - that's fine, we just haven't run `todofiles push ./my-todo.todo` yet.

[todo] wire up the new endpoint

Architecture

The system is a layered pipeline from todofile to the ticketin backend, with a local AST and sqlite db inbetween:

.todo file → Parser → Internal AST → SQLite (via Alembic) → Service Mapper → Jira API

Parser (todo_files/parser.py)

Reads a .todo file and produces a ParsedFile. Responsibilities:

  • Parses the YAML frontmatter block into a FileConfig object
  • Parses the body into an ordered list of Ticket objects and free-form string blocks
  • Preserves everything else in the file as is
  • Handles nested subtasks (parsed recursively at a deeper indent level)
  • Handles multiline field values using YAML block-scalar (|) syntax

Internal AST (todo_files/models.py)

The shared data model imported by every other layer:

Class Purpose
Ticket One ticket: title, status, id, remote_key, labels, description, subtasks, extra fields
FileConfig File-level defaults: board, item_type, labels, status_map, assignee, sprint
ParsedFile The full file: path + config + ordered list of Ticket | str items

Storage (todo_files/storage/)

SQLite + Alembic. Tracks every known ticket and its sync state so that push can diff local state against the last-pushed snapshot without making an API call.

Sync engine (todo_files/sync.py)

Bridges the parser and the storage/Jira layers. Responsibilities:

  • assign_ids - walks the parsed ticket tree and assigns 8-char hex UUIDs to any ticket that lacks one; returns True if the file needs to be written back
  • ticket_hash - stable SHA-256 of a ticket’s content fields (excludes id/remote_key), used to detect local changes
  • build_plan - diffs a ParsedFile against the DB and returns a SyncPlan (lists of tickets to create, update, delete, untrack, or leave clean); does not modify anything
  • execute_plan - applies the plan to SQLite; Jira calls are the CLI’s responsibility
  • mark_synced - sets sync_status=clean for tickets that were successfully pushed

Service mapper (todo_files/mappers/)

The only Jira-specific code. JiraMapper implements BaseMapper and translates between the internal AST and Jira REST API v3.

Status updates require a Jira transition (not a direct field update): the mapper fetches available transitions, matches by name, and POSTs the transition.

Adding a new backend (e.g. Linear, ClickUp) requires a new mapper class - parser, AST, and storage should remain unchanged.

CLI

todofiles push <file>             # parse → SQLite → push to Jira
todofiles push --dry-run <file>   # show what would change (no API call)
todofiles pull <file>             # fetch from Jira → update SQLite + file
todofiles pull --dry-run <file>   # show what would change without writing
todofiles import <file> <key>     # fetch an existing Jira ticket and append it to file
todofiles config set <key=val>    # set a config value
todofiles config show             # print current config (api_token redacted)
todofiles config whoami           # print your Jira account info
todofiles config status-map       # print a status_map template from your Jira project

push flow

  1. Parse file → assign missing IDs → write IDs back if any were assigned
  2. Build sync plan (DB diff, no API call)
  3. Print plan; prompt for confirmation on each deletion (delete / untrack / abort)
  4. For each CREATE: POST /rest/api/3/issue → write jira: KEY back to file and DB
  5. For each UPDATE: PUT /rest/api/3/issue/{key} + transition if status changed
  6. For each DELETE: DELETE /rest/api/3/issue/{key}
  7. Update sync_status → clean in DB

pull flow

  1. Parse file → ensure all tickets have IDs
  2. For each ticket with a remote_key, fetch from Jira and update the in-memory AST (remote wins)
  3. Write the updated file; update DB; mark pulled tickets clean

Available at github.com/jamiebeverley/todo_files.