Skip to main content

btodos

Pulls your assigned Jira tickets for the board columns you care about and lists them as a checklist — either as a Markdown file in your project root (readable in Cursor) or on a localhost webpage.

Minimal runtime dependency: Jinja2 for HTML templates (Python 3.10+).

Board UI stack (vendored in the package):

  • HTMX — soft-refresh / swap ticket list HTML from /board
  • Alpine.js — modals, filter dropdowns, review chips, PR tabs
  • Tailwind — utility CSS layered on board.css (preflight off)

Install

From PyPI (recommended):

uv tool install btodos
# or: pip install btodos

Commands: btodos and btd (same CLI).

Upgrade later with:

btodos self-update          # install latest from PyPI + migrate this workspace .btodos
btodos self-update --check  # compare versions only
btodos migrate              # refresh .btodos only (no package install)

self-update upgrades the tool, then runs btodos migrate in the current workspace so .btodos/config.json, prompts.md (purpose comments + any new prompt keys), globals.json, and the ticket cache layout match the new package. Existing values are never overwritten — only missing keys / annotations are added. Restart any running btodos serve after upgrading.

From source (development)

git clone https://github.com/dev-ahmed/btodos.git
cd btodos
uv sync
uv tool install --force --reinstall .

Rebuild Tailwind after template class changes:

pnpm install
pnpm run build:css   # → src/btodos/static/app.css

Configure

In your workspace root:

btodos init

That writes .btodos/config.json:

{
  "baseUrl": "https://your-company.atlassian.net",
  "projectKey": "ABC",
  "columns": ["To Do", "In Progress", "In Review"],
  "email": "you@your-company.com",
  "assignee": ["currentUser()"],
  "outputFile": "JIRA-TODOS.md",
  "port": 4321
}
Key Required Meaning
baseUrl yes Your Jira Cloud site URL
projectKey yes Project key, e.g. ABC from ABC-123
columns yes Status names to include, in display order
email yes Atlassian account email (Basic auth user)
assignee no One Jira user ("currentUser()") or a list of teammates. Emails are resolved to account ids via Jira user/assignable search (even when Jira hides emails in the response) and cached in .btodos/assignees.json. Account ids and currentUser() are used as-is. The web assignee filter is built from whoever appears on the loaded tickets.
outputFile no Markdown output path, relative to the workspace root
port no Port for serve (default 4321)
labels no Only include tickets carrying at least one of these labels
models no Ollama models: { "recap", "steps", "review" } — each a name or fallback list (default llama3.2:3b; [] disables). steps/review default to recap.
ollama no Host Ollama limits: { "numParallel", "maxLoadedModels" } (defaults 1). Apply with btodos ollama apply.
ollamaUrl no Ollama host (default http://localhost:11434)
redisUrl no Optional Redis URL for the ticket cache (Compose sets BTODOS_REDIS_URL)
git no Pull-request lookup — see Pull requests
promptsFile no Prompt templates file under .btodos/ (default prompts.md; also .json / .yaml)
promptsDir no Directory of per-prompt Markdown files (e.g. prompts/). When set and present, loads instead of promptsFile.
globalsFile no Shared {{variables}} for config/prompts (default globals.json)
enrich no Bulk --enrich gates: status/label/… filters and/or LLM prompt — see below
notificationsByPriority no When true, the 🔔 inbox lists updates by ticket priority (Highest→Lowest), then newest
taskKinds no Closed set of task kinds for board filters, e.g. ["backend","web","mobile"]. Empty disables.
estimateField no Jira custom field id for story-point / estimate (e.g. customfield_10016) — enables useful order by → estimate
complexityField no Optional Jira field for complexity; when filled, wins over the LLM
complexities no Rank levels for LLM complexity (default 0.01.0 in tenths). Empty disables. Edit criteria in prompts.md# complexity.
orderBy no Board sort modes to show (default ["priority","estimate","complexity","created"]). Override or remove entries; [] hides the control. Alias: "time created"created.

After upgrading btodos, btodos self-update (or btodos migrate) brings .btodos up to date: migrates legacy keys (e.g. top-level recapPrompt or inline prompts) into .btodos/prompts.md, adds any new defaults, refreshes prompt purpose comments, and when needed moves tickets.json into .btodos/tickets.bundle. Existing values are never overwritten. btodos config sync does the same config/prompts/cache migration without touching the installed package.

The web view

btodos serve is interactive:

  • Tick several tickets and a sticky toolbar appears — pick a target column and hit Move. Defaults to In Progress, for starting the day in one action.
  • ⧉ next to each key copies that ticket number to the clipboard; Copy keys in the toolbar copies every selected key at once, space-separated.

Moves go straight to Jira through the issue transition API. A ticket that has no valid workflow transition to the chosen column is reported back by name and left untouched; the others still move.

  • ⊘ next to each ticket hides it; a N hidden · show hidden control in the header brings them back so you can unhide one at a time.
  • Column headings collapse when clicked.
  • Filter by epic, label, pull-request state, task kind, assignee, or created by (reporter) from the sidebar. Epics are listed as ABC-100 — Checkout so you can pick by number or by name. PR states are has a PR, no PR, has active PR, changes requested, review outdated, approved, in review, not reviewed, draft, merged, declined — only the ones actually present are offered. An outdated review (new commits after a verdict) is marked on the card; ↻ review there or Re-request review in the ticket modal requests the previous reviewer. The same action is available after requested changes are resolved. Task kinds come from taskKinds in config (classified by matching Jira labels, or via btodos sync --enrich / on-demand AI). Assignees and reporters come from the loaded tickets. The filters combine (epic ABC-100 and merged), counts follow the filter, and clear resets them. Filtering is a view over the tickets already loaded — no refetch, and it doesn't touch what's hidden. Use save as (sidebar) to store the current search + filters + sort as a named preset in .btodos/view.json; pick it later from Saved filters… in the top chrome (right of the review chips), or delete a preset. Order by (priority / estimate / complexity / time created) reorders the board. Estimate needs estimateField. Complexity uses the LLM prompt in prompts.md# complexity (and optional complexityField when present). Modes shown are set by orderBy. (defaults on btodos init / migrate / config sync; edit or remove freely).
  • Needs your review / Waiting on team chips open the matching ticket modal only — they do not change search or filters.
  • Card face shows kind, labels, priority, complexity, and status.
  • Ticket Description has recap / ↻ recap to run the recap prompt (same job as card ✨ recap). Hidden tickets, collapsed columns, and filter presets are stored in .btodos/view.json, so the layout comes back the same next time you serve.

Tickets are read from .btodos/tickets.bundle (one JSON file per ticket) rather than refetched on every page load. Use the header buttons:

  • fetch pulls fresh Jira issues and linked PR state.
  • sync state updates ticket + PR state only (keeps cached descriptions/recaps/steps; no Ollama).

Use the ✨ buttons on each card (and ↻ recap on Description) for on-demand description/comments/PR recaps. Optional bulk enrichment: btodos sync --enrich, gated by enrich filters and/or prompts.enrichCriteria (see Recaps and test steps).

Pull requests

Each ticket shows the pull requests linked to it, with review state and comment count:

IA-142  Fix login redirect   [#312 approved 2/2 💬5]
IA-149  Search is slow       [#318 not reviewed]
IA-151  Audit log            [#301 merged 💬3] [#305 declined 💬1]
Badge Meaning
merged the PR is merged
declined the PR was closed without merging
approved n/m open, with n of m reviewers approving
in review open, has comments but no approval yet
not reviewed open, nobody has commented or approved

Configure where they come from under git:

{
  "git": {
    "enabled": true,
    "provider": "github",
    "repos": ["your-org/api", "your-org/web"],
    "tokenEnv": "GITHUB_TOKEN",
    "apiUrl": "https://api.github.com",
    "maxPages": 3
  }
}
Key Meaning
provider github (the GitHub API) or jira (Jira's dev-status panel)
repos repos to scan, owner/name. Required for github
tokenEnv env var holding the token (default GITHUB_TOKEN)
apiUrl override for GitHub Enterprise
maxPages pages of 100 recent PRs to scan per repo (default 3)
enabled false skips the lookup entirely

With provider: "github", btodos lists each repo's most recently updated pull requests and matches them to tickets by issue key in the PR title, branch name or body — so feature/ABC-123-login or a title starting ABC-123 both link up. Only matched PRs cost extra requests (two each, for comments and reviews).

The token needs repo scope for private repos (public_repo otherwise):

export GITHUB_TOKEN="ghp_..."    # https://github.com/settings/tokens

provider: "jira" uses Jira's development-status endpoint instead — no GitHub token needed, but that endpoint is undocumented and returns nothing on many Jira sites.

Anything missing is reported and skipped rather than failing the run: no token, no repos, a bad token, or an unreachable repo all print a reason and leave tickets without pull requests.

Unlike recaps, PR state is refreshed by the web fetch / sync state actions, since review counts change far more often than a ticket description.

Labels

Labels are shown under each ticket. Restrict the checklist to certain labels with the labels config key, and edit them from the CLI:

btodos label ABC-123 backend urgent      # add
btodos label ABC-123 --remove urgent     # remove

This is the only command that writes to Jira.

Recaps and test steps

Each ticket gets a one-line recap, plus numbered steps to reproduce or verify it when the description has enough detail (tickets shorter than ~140 characters get no steps, since the model would have to invent them). By default both are generated locally by Ollama — nothing leaves your machine. Ticket recaps/steps are on demand from the board ✨ buttons (and optionally btodos sync --enrich). btodos serve / plain btodos sync never bulk-run the LLM; they reuse .btodos/tickets.bundle.

Bulk --enrich only summarises tickets that pass your criteria (you control both):

  1. Filters in config.jsonenrich (empty list = no restriction on that field):
{
  "enrich": {
    "statuses": ["In Progress", "In Review"],
    "priorities": ["Highest", "High"],
    "labels": [],
    "kinds": [],
    "assignees": [],
    "prompt": true
  }
}
  1. Prompt in prompts.jsonenrichCriteria (YES/NO). Set "prompt": true to enable the LLM gate (optionally with filters); leave "prompt": false (default) for filters only, or empty filters with no prompt to summarise every ticket on --enrich.

Shared text for config/prompts can live in .btodos/globals.json and be referenced as {{name}} (single {title} placeholders stay for runtime).

PR review recaps (for changes requested) are generated on demand from a button on the web page, using prompts.md# reviewFeedback (legacy key reviewFeedbackPrompt).

PR code-change briefs use codeDiff (# codeDiff). Review drafts use reviewDraft (# reviewDraft). Small diffs run immediately; large ones ask you to proceed or ignore.

After editing on-demand prompts, restart btodos serve so the board picks them up, then click ↻ codeDiff / ↻ reviewDraft (or the first-run buttons) to regenerate.

Pull the model once:

ollama pull llama3.2:3b

Host resource limits (how many requests / loaded models Ollama keeps) live under ollama and are applied to the host Ollama process — not the btodos container — by:

btodos ollama apply

That sets OLLAMA_NUM_PARALLEL / OLLAMA_MAX_LOADED_MODELS from config, persists them on macOS via a LaunchAgent, and restarts the Ollama app (use --no-restart to skip the restart):

{
  "ollama": {
    "numParallel": 1,
    "maxLoadedModels": 1
  }
}

models.recap takes a single name or an ordered list of fallbacks. Each model is tried in turn, and one that isn't pulled, errors, or answers with nothing hands off to the next:

{ "models": { "recap": ["qwen2.5:7b", "llama3.2:3b"] } }

models.steps is separate and defaults to models.recap, so you can pair a small fast model for the one-line recap with a stronger one for the steps — same number of calls, better fit per task:

{
  "models": {
    "recap": ["llama3.2:3b"],
    "steps": ["qwen2.5:7b", "llama3.2:3b"]
  }
}

If every model fails — Ollama isn't running, none are pulled — the recap falls back to a truncated excerpt of the ticket description and steps are omitted, so sync never fails because of it. Set "models": { "recap": [] } to always use the excerpt and skip the LLM entirely.

Prompts live in .btodos/prompts.md by default (promptsFile), or as one Markdown file per prompt under .btodos/prompts/ when you set "promptsDir": "prompts".

Each # section (or prompts/<key>.md file) has an HTML comment before it naming the board CTA it powers. Those comments are editor-only — stripped on load so the model never sees them.

<!-- Used for: codeDiff tab review brief (codeDiff / ↻ codeDiff) -->
# 1. codeDiff

You are helping a human review…
Prompt key Used for
recap Description — recap / ↻ recap
steps Ticket steps (with recap / --enrich)
codeDiff PR — codeDiff / ↻ codeDiff
reviewDraft PR — reviewDraft / ↻ reviewDraft
reviewFeedback PR — reviewFeedback / ↻ reviewFeedback
cursorWork Description — AI Work
jiraPlan Ticket modal — jiraPlan
githubPlan Ticket modal — githubPlan
taskKind Kind classifier (--task-kind / --enrich)
complexity Complexity scorer (--complexity / --enrich)
enrichCriteria Bulk --enrich YES/NO gate

Re-write purpose comments into your prompts file after an upgrade (also done by btodos self-update / btodos migrate):

btd migrate                         # refresh .btodos (config + prompts annotations + cache)
btd config sync                     # same migration without upgrading the package
btd config prompts migrate          # merge → prompts.md + purpose comments (legacy cleanup)
btd config prompts split            # prompts.md → prompts/*.md + set promptsDir
btd config prompts export           # prompts/*.md → prompts.md

Each file is plain Markdown (the prompt body), optionally starting with a purpose comment:

<!-- Used for: ticket Description summary on the board (recap / ↻ recap) -->

Summarise this Jira ticket…
Title: {title}

In the combined prompts.md, sections are numbered in the heading (alphabetical key order):

<!-- Used for: … -->
# 1. codeDiff
…
<!-- Used for: … -->
# 7. recap

Config stays readable:

{
  "promptsDir": "prompts",
  "promptsFile": "prompts.md"
}

promptsDir wins when present (.md preferred over legacy .yaml). promptsFile can still be JSON, Markdown bundle, or YAML. Each JSON value is a string or an array of lines. Most must contain {description}; {title} is optional. cursorWork powers the “AI Work” button and is copied for pasting into an AI assistant. It must contain {key} and {branch}; optional placeholders: {title}, {url}, {description}, {brief}, {status}, {priority}, {assignee}, {reporter}. taskKind classifies tickets into taskKinds and must include {kinds} + {description}. btodos init / btodos config sync write the defaults so you can edit them in place. Print the built-in Ollama prompts template (and copy it) with:

btodos config prompts

Then paste into .btodos/prompts.json to replace.

{
  "recap": [
    "Summarise this Jira ticket in one short sentence of at most 25 words.",
    "",
    "Title: {title}",
    "",
    "Description: {description}",
    "",
    "Summary:"
  ],
  "steps": ["…"],
  "reviewFeedback": ["…"],
  "codeDiff": ["…"],
  "reviewDraft": ["…"],
  "cursorWork": ["…"],
  "taskKind": ["…"]
}

Legacy inline prompts / top-level recapPrompt keys still load; btodos config sync moves them into the prompts file.

Recaps and steps are cached and tracked separately, so a re-sync never starts over: it keeps everything it already has and generates only what is new. A ticket is re-summarised when its description changes, when its model or prompt changes (recaps and steps independently), or when the previous attempt fell back to the excerpt because Ollama was unreachable.

The API token is read from the environment, never the config file:

export JIRA_API_TOKEN="..."   # https://id.atlassian.com/manage-profile/security/api-tokens

The config file is looked up in the current directory and every parent, so the commands work from any subdirectory of the workspace.

Use

Run btodos commands (or btd commands) anytime for a full command table. Official short name: btd (same CLI as btodos).

# Daily
btd init                            # create .btodos/config.json
btd sync                            # fetch tickets → JIRA-TODOS.md
btd sync --enrich                   # also bulk-generate recaps/steps/kinds (slow)
btd sync --task-kind                # reclassify kinds only
btd serve                           # http://127.0.0.1:4321
btodos label ABC-123 backend        # add labels
btodos doctor                       # check Jira / GitHub / Ollama
btodos self-update                  # upgrade from PyPI + migrate .btodos
btodos self-update --check          # compare versions only
btodos migrate                      # refresh .btodos to this package version
btodos commands                     # show all commands as a table
btodos completion zsh               # shell completion

# Config (noun + verb)
btodos config sync                  # migrate/add keys (same as migrate, no package upgrade)
btodos config prompts               # built-in prompts.md → clipboard
btodos config sort                  # list orderBy modes
btodos config assignees             # prompt for emails → account ids
btodos config assignees a@x.com     # same, non-interactive

# Ollama host / container
btodos ollama apply                 # apply ollama.* limits + restart app
btodos container write              # write .btodos/docker-compose.yml
btodos container up --build         # start
btodos container logs               # follow
btodos container down               # stop

Output:

# ABC — My Tickets

_4 ticket(s) · generated 2026-09-01 10:12 UTC by btodos._

## To Do (2)

- [ ] [ABC-123](https://your-company.atlassian.net/browse/ABC-123) — Fix login redirect _(High · 2026-08-28)_
- [ ] [ABC-131](https://your-company.atlassian.net/browse/ABC-131) — Add audit log _(Medium · 2026-08-27)_

Notes

  • Tickets are selected with project = <key> AND assignee IN (<assignee>) AND status IN (<columns>) (or assignee = … when only one is configured), then listed by priority (Highest first), most recently updated first within a priority.
  • Everything btodos writes lives in .btodos/ — config, ticket cache, assignee id cache and view state. Add it to your .gitignore unless you want the cache shared.
  • Column names must match your Jira status names exactly (matching is case-insensitive).
  • Checkboxes are for your own local tracking — ticking one does not write back to Jira.

Docker

Run the web view as a background container instead of holding a terminal:

btodos container write         # writes .btodos/docker-compose.yml
btodos container up --build    # start
btodos container restart       # down then up (add --build to rebuild)
btodos container logs          # progress output
btodos container down          # stop

Or with compose directly:

btodos container write
docker compose -f .btodos/docker-compose.yml up -d --build
open http://127.0.0.1:4321
docker compose -f .btodos/docker-compose.yml logs -f   # progress output
docker compose -f .btodos/docker-compose.yml down      # stop

container up / restart pull missing images (redis:7-alpine, and python:3.13-alpine when building) before compose runs. If the registry is down but btodos:local already exists, --build falls back to starting that image instead of failing hard.

The image is python:3.13-alpine with the package installed. Compose installs as a non-root user (~84 MB). The generated compose file:

  • starts a Redis sidecar (redis:7-alpine) for the ticket cache (BTODOS_REDIS_URL); the JSON file under .btodos/ remains a fallback;
  • prefers local images (pull_policy: if_not_present, build pull: false);
  • mounts your workspace at /work, so config, cache, view state and the Markdown output are the same files the CLI uses;
  • sets BTODOS_HOST=0.0.0.0 so the port mapping can reach the server;
  • points BTODOS_OLLAMA_URL at host.docker.internal, since Ollama runs on the host;
  • reads JIRA_API_TOKEN and your git token from your shell at up time — never stored in the file;
  • restarts unless stopped, so it comes back after a reboot.

If btodos serve is already using the port, give the container a different host port:

BTODOS_PORT=4400 docker compose -f .btodos/docker-compose.yml up -d

host in the config (or BTODOS_HOST) also controls what the plain serve command binds to; it stays 127.0.0.1 by default.

Tests

uv sync
PYTHONPATH=tests uv run python -m unittest discover -s tests -v

Stdlib unittest, no network — every remote call is stubbed. Runtime needs Jinja2. They cover config parsing and its error messages, JQL and Jira field parsing, priority ordering, the incremental sync/merge rules, the ticket cache and view state, GitHub PR matching and review state, Markdown and HTML rendering, the server endpoints, and the CLI.

Tests prove the logic; they cannot prove your credentials work. For that:

btodos doctor

It checks, against the real services: Jira credentials and that your JQL returns tickets, that every configured column is a real status name, the GitHub token and each repo, whether any PR actually mentions your ticket keys, and that Ollama is up with your models pulled. It exits non-zero if anything failed.

Publishing

Pushes to main run GitHub Actions:

  1. CIuv sync + unit tests
  2. Publish to PyPI — builds with uv build and uploads with uv publish via Trusted Publishing (no API token in secrets)

To cut a release: bump version in pyproject.toml, commit, push to main. If that version is already on PyPI, publish is skipped.

Dev install of the just-published tool:

uv tool install --force btodos

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

btodos-0.1.9.tar.gz (275.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

btodos-0.1.9-py3-none-any.whl (215.1 kB view details)

Uploaded Python 3

File details

Details for the file btodos-0.1.9.tar.gz.

File metadata

  • Download URL: btodos-0.1.9.tar.gz
  • Upload date:
  • Size: 275.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for btodos-0.1.9.tar.gz
Algorithm Hash digest
SHA256 bfc6ba4233ce15d1d6962002fdf1f69eac410518644093de1fdd6bce00106a68
MD5 d8d8284409f67fc30624ab2ff4c4545d
BLAKE2b-256 12d5903bb554cefc562d5f47343a9302df8d1d2d8c3b70a816ab642e20ef7824

See more details on using hashes here.

File details

Details for the file btodos-0.1.9-py3-none-any.whl.

File metadata

  • Download URL: btodos-0.1.9-py3-none-any.whl
  • Upload date:
  • Size: 215.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.11 {"installer":{"name":"uv","version":"0.12.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for btodos-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 b1b60e37c0422b53376241e264198f4480f9bea2a4854086b5e01dd39c19d5d1
MD5 468e2209df3d4d8e466408cf1774b4b7
BLAKE2b-256 ecee17d3f3a2953e7e86aa0d6373b3892e74c57fa5cc66f316b304814abd3b31

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.12

2 files

0.1.11

2 files

0.1.10

2 files

This release

0.1.9 This release

2 files

0.1.8

2 files

0.1.6

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page