btodos 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. btodos-0.1.1/.cursor/rules/use-uv.mdc +23 -0
  2. btodos-0.1.1/.github/workflows/ci.yml +27 -0
  3. btodos-0.1.1/.github/workflows/publish.yml +83 -0
  4. btodos-0.1.1/.gitignore +28 -0
  5. btodos-0.1.1/Dockerfile +21 -0
  6. btodos-0.1.1/PKG-INFO +485 -0
  7. btodos-0.1.1/README.md +477 -0
  8. btodos-0.1.1/package-lock.json +6 -0
  9. btodos-0.1.1/package.json +10 -0
  10. btodos-0.1.1/pyproject.toml +20 -0
  11. btodos-0.1.1/src/btodos/__init__.py +19 -0
  12. btodos-0.1.1/src/btodos/assignees.py +44 -0
  13. btodos-0.1.1/src/btodos/cache.py +417 -0
  14. btodos-0.1.1/src/btodos/cli.py +941 -0
  15. btodos-0.1.1/src/btodos/completion.py +316 -0
  16. btodos-0.1.1/src/btodos/config.py +1136 -0
  17. btodos-0.1.1/src/btodos/devstatus.py +167 -0
  18. btodos-0.1.1/src/btodos/dockerfiles.py +252 -0
  19. btodos-0.1.1/src/btodos/doctor.py +209 -0
  20. btodos-0.1.1/src/btodos/github.py +818 -0
  21. btodos-0.1.1/src/btodos/github_actions.py +229 -0
  22. btodos-0.1.1/src/btodos/jira.py +1304 -0
  23. btodos-0.1.1/src/btodos/jira_actions.py +555 -0
  24. btodos-0.1.1/src/btodos/markdown.py +92 -0
  25. btodos-0.1.1/src/btodos/notifications.py +315 -0
  26. btodos-0.1.1/src/btodos/ollama_host.py +142 -0
  27. btodos-0.1.1/src/btodos/progress.py +307 -0
  28. btodos-0.1.1/src/btodos/prompt_store.py +255 -0
  29. btodos-0.1.1/src/btodos/pulls.py +153 -0
  30. btodos-0.1.1/src/btodos/recap.py +664 -0
  31. btodos-0.1.1/src/btodos/redis_client.py +147 -0
  32. btodos-0.1.1/src/btodos/richtext.py +167 -0
  33. btodos-0.1.1/src/btodos/server/__init__.py +102 -0
  34. btodos-0.1.1/src/btodos/server/assets.py +185 -0
  35. btodos-0.1.1/src/btodos/server/board.py +201 -0
  36. btodos-0.1.1/src/btodos/server/http.py +812 -0
  37. btodos-0.1.1/src/btodos/server/render.py +1374 -0
  38. btodos-0.1.1/src/btodos/server/runtime.py +360 -0
  39. btodos-0.1.1/src/btodos/server/templating.py +37 -0
  40. btodos-0.1.1/src/btodos/state.py +88 -0
  41. btodos-0.1.1/src/btodos/static/alpine.min.js +5 -0
  42. btodos-0.1.1/src/btodos/static/app.css +1 -0
  43. btodos-0.1.1/src/btodos/static/board.css +1745 -0
  44. btodos-0.1.1/src/btodos/static/board.js +2735 -0
  45. btodos-0.1.1/src/btodos/static/htmx.min.js +1 -0
  46. btodos-0.1.1/src/btodos/static/loading.js +97 -0
  47. btodos-0.1.1/src/btodos/templates/board.html +74 -0
  48. btodos-0.1.1/src/btodos/templates/error.html +2 -0
  49. btodos-0.1.1/src/btodos/templates/loading.html +8 -0
  50. btodos-0.1.1/src/btodos/templates/partials/filter_select.html +36 -0
  51. btodos-0.1.1/src/btodos/templates/partials/filters.html +33 -0
  52. btodos-0.1.1/src/btodos/templates/partials/kind_filter.html +14 -0
  53. btodos-0.1.1/src/btodos/templates/partials/label_datalist.html +5 -0
  54. btodos-0.1.1/src/btodos/templates/partials/modals.html +134 -0
  55. btodos-0.1.1/src/btodos/templates/partials/review_links.html +16 -0
  56. btodos-0.1.1/src/btodos/templates/partials/review_requests.html +13 -0
  57. btodos-0.1.1/src/btodos/templates/partials/sort_filter.html +13 -0
  58. btodos-0.1.1/src/btodos/templates/partials/status_filter.html +16 -0
  59. btodos-0.1.1/src/btodos/templates/partials/toolbar.html +13 -0
  60. btodos-0.1.1/src/btodos/templates/shell.html +10 -0
  61. btodos-0.1.1/src/btodos/templates/ticket_scroll.html +11 -0
  62. btodos-0.1.1/src/btodos/tickets.py +1148 -0
  63. btodos-0.1.1/styles/input.css +20 -0
  64. btodos-0.1.1/tailwind.config.js +12 -0
  65. btodos-0.1.1/tests/support.py +76 -0
  66. btodos-0.1.1/tests/test_cli.py +535 -0
  67. btodos-0.1.1/tests/test_complexity.py +77 -0
  68. btodos-0.1.1/tests/test_config.py +268 -0
  69. btodos-0.1.1/tests/test_docker.py +263 -0
  70. btodos-0.1.1/tests/test_doctor.py +237 -0
  71. btodos-0.1.1/tests/test_enrich_criteria.py +179 -0
  72. btodos-0.1.1/tests/test_github.py +486 -0
  73. btodos-0.1.1/tests/test_github_actions.py +107 -0
  74. btodos-0.1.1/tests/test_jira.py +469 -0
  75. btodos-0.1.1/tests/test_jira_actions.py +108 -0
  76. btodos-0.1.1/tests/test_label_suggest.py +45 -0
  77. btodos-0.1.1/tests/test_notifications.py +189 -0
  78. btodos-0.1.1/tests/test_ollama_host.py +101 -0
  79. btodos-0.1.1/tests/test_output.py +994 -0
  80. btodos-0.1.1/tests/test_progress.py +62 -0
  81. btodos-0.1.1/tests/test_prompt_store.py +77 -0
  82. btodos-0.1.1/tests/test_redis.py +196 -0
  83. btodos-0.1.1/tests/test_review_requests.py +58 -0
  84. btodos-0.1.1/tests/test_richtext.py +89 -0
  85. btodos-0.1.1/tests/test_sort.py +141 -0
  86. btodos-0.1.1/tests/test_storage.py +570 -0
  87. btodos-0.1.1/tests/test_task_kind.py +117 -0
  88. btodos-0.1.1/tests/test_templating.py +85 -0
  89. btodos-0.1.1/uv.lock +111 -0
@@ -0,0 +1,23 @@
1
+ ---
2
+ description: Always use uv for Python tooling in this repo
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Always use uv
7
+
8
+ Prefer **uv** for all Python package/tooling commands. Do not use bare `pip`, `pipx`, `python -m pip`, or `venv`/`virtualenv` when uv can do the job.
9
+
10
+ ## Prefer
11
+
12
+ - `uv sync` / `uv lock` for deps
13
+ - `uv run …` to run Python, tests, scripts
14
+ - `uv build` / `uv publish` for packaging
15
+ - `uv tool install` / `uv tool run` for CLIs
16
+ - `uv pip …` only when you need pip-compatible installs inside a uv-managed env
17
+ - `astral-sh/setup-uv` in GitHub Actions (not `actions/setup-python` alone)
18
+
19
+ ## Avoid
20
+
21
+ - `pip install`
22
+ - `python -m unittest` / `python -c` in CI — use `uv run python …`
23
+ - Mixing Poetry / pipenv / conda for this project
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+
21
+ - name: Install package
22
+ run: uv sync --all-extras
23
+
24
+ - name: Run tests
25
+ env:
26
+ PYTHONPATH: tests
27
+ run: uv run python -m unittest discover -s tests -v
@@ -0,0 +1,83 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install package
22
+ run: uv sync --all-extras
23
+
24
+ - name: Run tests
25
+ env:
26
+ PYTHONPATH: tests
27
+ run: uv run python -m unittest discover -s tests -v
28
+
29
+ build:
30
+ needs: test
31
+ runs-on: ubuntu-latest
32
+ outputs:
33
+ version: ${{ steps.meta.outputs.version }}
34
+ should_publish: ${{ steps.meta.outputs.should_publish }}
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+
38
+ - uses: astral-sh/setup-uv@v5
39
+ with:
40
+ python-version: "3.12"
41
+
42
+ - name: Read version
43
+ id: meta
44
+ run: |
45
+ VERSION=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
46
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
47
+ if curl -sf "https://pypi.org/pypi/btodos/${VERSION}/json" >/dev/null; then
48
+ echo "should_publish=false" >> "$GITHUB_OUTPUT"
49
+ echo "PyPI already has btodos==${VERSION}; skipping publish."
50
+ else
51
+ echo "should_publish=true" >> "$GITHUB_OUTPUT"
52
+ echo "Will publish btodos==${VERSION}"
53
+ fi
54
+
55
+ - name: Build
56
+ if: steps.meta.outputs.should_publish == 'true'
57
+ run: uv build
58
+
59
+ - uses: actions/upload-artifact@v4
60
+ if: steps.meta.outputs.should_publish == 'true'
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+
65
+ publish:
66
+ needs: build
67
+ if: needs.build.outputs.should_publish == 'true'
68
+ runs-on: ubuntu-latest
69
+ environment:
70
+ name: pypi
71
+ url: https://pypi.org/p/btodos
72
+ permissions:
73
+ id-token: write
74
+ steps:
75
+ - uses: actions/download-artifact@v4
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+
80
+ - uses: astral-sh/setup-uv@v5
81
+
82
+ - name: Publish
83
+ run: uv publish
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ .eggs/
7
+ dist/
8
+ build/
9
+ *.egg
10
+ .venv/
11
+ venv/
12
+ .env
13
+ .env.*
14
+
15
+ # Tools / IDE
16
+ .claude/
17
+ .idea/
18
+ .vscode/
19
+ *.swp
20
+ .DS_Store
21
+
22
+ # Node (local leftovers)
23
+ node_modules/
24
+
25
+ # Local runtime / secrets
26
+ *.pem
27
+ *.key
28
+ credentials.json
@@ -0,0 +1,21 @@
1
+ # Image: Python + btodos (Jinja2 for HTML templates).
2
+ FROM python:3.13-alpine
3
+
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1 \
6
+ BTODOS_HOST=0.0.0.0
7
+
8
+ WORKDIR /app
9
+ COPY pyproject.toml README.md ./
10
+ COPY src/ ./src/
11
+
12
+ RUN pip install --no-cache-dir . \
13
+ && rm -rf /app/src /app/pyproject.toml /root/.cache \
14
+ && adduser -D -H btodos
15
+
16
+ USER btodos
17
+ WORKDIR /work
18
+ EXPOSE 4321
19
+
20
+ # The workspace (config, cache, Markdown output) is mounted at /work.
21
+ CMD ["btodos", "serve"]
btodos-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,485 @@
1
+ Metadata-Version: 2.5
2
+ Name: btodos
3
+ Version: 0.1.1
4
+ Summary: Pull your board tickets by column into a Markdown checklist or a localhost page
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: jinja2>=3.1
7
+ Description-Content-Type: text/markdown
8
+
9
+ # btodos
10
+
11
+ Pulls your assigned Jira tickets for the board columns you care about and lists them as a
12
+ checklist — either as a Markdown file in your project root (readable in Cursor) or on a
13
+ localhost webpage.
14
+
15
+ Minimal runtime dependency: Jinja2 for HTML templates (Python 3.10+).
16
+
17
+ Board UI stack (vendored in the package):
18
+ - **HTMX** — soft-refresh / swap ticket list HTML from `/board`
19
+ - **Alpine.js** — modals, filter dropdowns, focus
20
+ - **Tailwind** — utility CSS layered on `board.css` (`preflight` off)
21
+
22
+ Rebuild Tailwind after template class changes:
23
+
24
+ ```bash
25
+ pnpm install
26
+ pnpm run build:css # → src/btodos/static/app.css
27
+ ```
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install -e /Users/ahmed/Workspace/Work/MyPackages/btodos
33
+ # or: uv tool install .
34
+ # Commands: btodos and btd (same CLI)
35
+ ```
36
+
37
+ ## Configure
38
+
39
+ In your workspace root:
40
+
41
+ ```bash
42
+ btodos init
43
+ ```
44
+
45
+ That writes `.btodos/config.json`:
46
+
47
+ ```json
48
+ {
49
+ "baseUrl": "https://your-company.atlassian.net",
50
+ "projectKey": "ABC",
51
+ "columns": ["To Do", "In Progress", "In Review"],
52
+ "email": "you@your-company.com",
53
+ "assignee": ["currentUser()"],
54
+ "outputFile": "JIRA-TODOS.md",
55
+ "port": 4321
56
+ }
57
+ ```
58
+
59
+ | Key | Required | Meaning |
60
+ | ------------ | -------- | ------------------------------------------------------------ |
61
+ | `baseUrl` | yes | Your Jira Cloud site URL |
62
+ | `projectKey` | yes | Project key, e.g. `ABC` from `ABC-123` |
63
+ | `columns` | yes | Status names to include, in display order |
64
+ | `email` | yes | Atlassian account email (Basic auth user) |
65
+ | `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. |
66
+ | `outputFile` | no | Markdown output path, relative to the workspace root |
67
+ | `port` | no | Port for `serve` (default `4321`) |
68
+ | `labels` | no | Only include tickets carrying at least one of these labels |
69
+ | `models` | no | Ollama models: `{ "recap", "steps", "review" }` — each a name or fallback list (default `llama3.2:3b`; `[]` disables). `steps`/`review` default to `recap`. |
70
+ | `ollama` | no | Host Ollama limits: `{ "numParallel", "maxLoadedModels" }` (defaults `1`). Apply with `btodos ollama apply`. |
71
+ | `ollamaUrl` | no | Ollama host (default `http://localhost:11434`) |
72
+ | `redisUrl` | no | Optional Redis URL for the ticket cache (Compose sets `BTODOS_REDIS_URL`) |
73
+ | `git` | no | Pull-request lookup — see [Pull requests](#pull-requests) |
74
+ | `promptsFile` | no | Prompt templates file under `.btodos/` (default `prompts.json`; also `.md` / `.yaml`) |
75
+ | `promptsDir` | no | Directory of per-prompt Markdown files (e.g. `prompts/`). When set and present, loads instead of `promptsFile`. |
76
+ | `globalsFile` | no | Shared `{{variables}}` for config/prompts (default `globals.json`) |
77
+ | `enrich` | no | Bulk `--enrich` gates: status/label/… filters and/or LLM prompt — see below |
78
+ | `notificationsByPriority` | no | When `true`, the 🔔 inbox lists updates by ticket priority (Highest→Lowest), then newest |
79
+ | `taskKinds` | no | Closed set of task kinds for board filters, e.g. `["backend","web","mobile"]`. Empty disables. |
80
+ | `estimateField` | no | Jira custom field id for story-point / estimate (e.g. `customfield_10016`) — enables useful **order by → estimate** |
81
+ | `complexityField` | no | Optional Jira field for complexity; when filled, wins over the LLM |
82
+ | `complexities` | no | Levels for LLM complexity (default `["High","Medium","Low"]`). Empty disables LLM complexity. Edit criteria in `prompts.json` → `complexity`. |
83
+ | `orderBy` | no | Board sort modes to show (default `["priority","estimate","complexity","created"]`). Override or remove entries; `[]` hides the control. Alias: `"time created"` → `created`. |
84
+
85
+ After upgrading btodos, run **`btodos config sync`** to migrate legacy keys (e.g. top-level `recapPrompt`
86
+ or inline `prompts`) into `.btodos/prompts.json`, add any new defaults, and when needed move
87
+ `tickets.json` into `.btodos/tickets.bundle`. Existing values are never overwritten.
88
+
89
+ ## The web view
90
+
91
+ `btodos serve` is interactive:
92
+
93
+ - **Tick several tickets** and a sticky toolbar appears — pick a target column and hit **Move**.
94
+ Defaults to `In Progress`, for starting the day in one action.
95
+ - **⧉ next to each key** copies that ticket number to the clipboard; **Copy keys** in the toolbar
96
+ copies every selected key at once, space-separated.
97
+
98
+ Moves go straight to Jira through the issue transition API. A ticket that has no valid workflow
99
+ transition to the chosen column is reported back by name and left untouched; the others still move.
100
+
101
+ - **⊘ next to each ticket** hides it; a `N hidden · show hidden` control in the header brings
102
+ them back so you can unhide one at a time.
103
+ - **Column headings collapse** when clicked.
104
+ - **Filter by epic, label, pull-request state, task kind, or assignee** from the header. Epics are listed as
105
+ `ABC-100 — Checkout` so you can pick by number or by name. PR states are `has a PR`, `no PR`,
106
+ `has active PR`, `changes requested`, `approved`, `in review`, `not reviewed`, `draft`, `merged`,
107
+ `declined` — only the ones actually present are offered. Task kinds come from `taskKinds` in config
108
+ (classified by matching Jira labels, or via `btodos sync --enrich` / on-demand AI). Assignees come from the loaded tickets — set
109
+ `assignee` in config to a list of teammates if you want more than yourself. The filters combine
110
+ (epic `ABC-100` *and* `merged`), counts follow the filter, and **clear** resets them. Filtering
111
+ is a view over the tickets already loaded — no refetch, and it doesn't touch what's hidden.
112
+ **Order by** (priority / estimate / complexity / time created) reorders the board.
113
+ Estimate needs `estimateField`. Complexity uses the LLM prompt in `prompts.json` → `complexity`
114
+ (and optional `complexityField` when present). Modes shown are set by `orderBy`.
115
+ (defaults on `btodos init` / `config sync`; edit or remove freely).
116
+
117
+ Hidden tickets and collapsed columns are stored in `.btodos/view.json`, so the layout comes back
118
+ the same next time you `serve`.
119
+
120
+ Tickets are read from `.btodos/tickets.bundle` (one JSON file per ticket) rather than
121
+ refetched on every page load. Use
122
+ the header buttons:
123
+
124
+ - **fetch** pulls fresh Jira issues and linked PR state.
125
+ - **sync state** updates ticket + PR state only (keeps cached descriptions/recaps/steps; no Ollama).
126
+
127
+ Use the ✨ buttons on each card for **on-demand** description/comments/PR recaps. Optional bulk
128
+ enrichment: `btodos sync --enrich`, gated by `enrich` filters and/or `prompts.enrichCriteria`
129
+ (see [Recaps and test steps](#recaps-and-test-steps)).
130
+
131
+ ## Pull requests
132
+
133
+ Each ticket shows the pull requests linked to it, with review state and comment count:
134
+
135
+ ```
136
+ IA-142 Fix login redirect [#312 approved 2/2 💬5]
137
+ IA-149 Search is slow [#318 not reviewed]
138
+ IA-151 Audit log [#301 merged 💬3] [#305 declined 💬1]
139
+ ```
140
+
141
+ | Badge | Meaning |
142
+ | ----- | ------- |
143
+ | `merged` | the PR is merged |
144
+ | `declined` | the PR was closed without merging |
145
+ | `approved n/m` | open, with `n` of `m` reviewers approving |
146
+ | `in review` | open, has comments but no approval yet |
147
+ | `not reviewed` | open, nobody has commented or approved |
148
+
149
+ Configure where they come from under `git`:
150
+
151
+ ```json
152
+ {
153
+ "git": {
154
+ "enabled": true,
155
+ "provider": "github",
156
+ "repos": ["your-org/api", "your-org/web"],
157
+ "tokenEnv": "GITHUB_TOKEN",
158
+ "apiUrl": "https://api.github.com",
159
+ "maxPages": 3
160
+ }
161
+ }
162
+ ```
163
+
164
+ | Key | Meaning |
165
+ | --- | ------- |
166
+ | `provider` | `github` (the GitHub API) or `jira` (Jira's dev-status panel) |
167
+ | `repos` | repos to scan, `owner/name`. Required for `github` |
168
+ | `tokenEnv` | env var holding the token (default `GITHUB_TOKEN`) |
169
+ | `apiUrl` | override for GitHub Enterprise |
170
+ | `maxPages` | pages of 100 recent PRs to scan per repo (default 3) |
171
+ | `enabled` | `false` skips the lookup entirely |
172
+
173
+ With `provider: "github"`, btodos lists each repo's most recently updated pull requests and matches
174
+ them to tickets by issue key in the **PR title, branch name or body** — so `feature/ABC-123-login`
175
+ or a title starting `ABC-123` both link up. Only matched PRs cost extra requests (two each, for
176
+ comments and reviews).
177
+
178
+ The token needs `repo` scope for private repos (`public_repo` otherwise):
179
+
180
+ ```bash
181
+ export GITHUB_TOKEN="ghp_..." # https://github.com/settings/tokens
182
+ ```
183
+
184
+ `provider: "jira"` uses Jira's development-status endpoint instead — no GitHub token needed, but
185
+ that endpoint is undocumented and returns nothing on many Jira sites.
186
+
187
+ Anything missing is reported and skipped rather than failing the run: no token, no repos, a bad
188
+ token, or an unreachable repo all print a reason and leave tickets without pull requests.
189
+
190
+ Unlike recaps, PR state is refreshed by the web **fetch** / **sync state** actions, since review
191
+ counts change far more often than a ticket description.
192
+
193
+ ## Labels
194
+
195
+ Labels are shown under each ticket. Restrict the checklist to certain labels with the `labels`
196
+ config key, and edit them from the CLI:
197
+
198
+ ```bash
199
+ btodos label ABC-123 backend urgent # add
200
+ btodos label ABC-123 --remove urgent # remove
201
+ ```
202
+
203
+ This is the only command that writes to Jira.
204
+
205
+ ## Recaps and test steps
206
+
207
+ Each ticket gets a one-line recap, plus numbered steps to reproduce or verify it when the
208
+ description has enough detail (tickets shorter than ~140 characters get no steps, since the model
209
+ would have to invent them). By default both are generated locally by
210
+ [Ollama](https://ollama.com) — nothing leaves your machine. Ticket recaps/steps are **on demand** from
211
+ the board ✨ buttons (and optionally `btodos sync --enrich`).
212
+ `btodos serve` / plain `btodos sync` never bulk-run the LLM; they reuse `.btodos/tickets.bundle`.
213
+
214
+ Bulk `--enrich` only summarises tickets that pass your criteria (you control both):
215
+
216
+ 1. **Filters** in `config.json` → `enrich` (empty list = no restriction on that field):
217
+
218
+ ```json
219
+ {
220
+ "enrich": {
221
+ "statuses": ["In Progress", "In Review"],
222
+ "priorities": ["Highest", "High"],
223
+ "labels": [],
224
+ "kinds": [],
225
+ "assignees": [],
226
+ "prompt": true
227
+ }
228
+ }
229
+ ```
230
+
231
+ 2. **Prompt** in `prompts.json` → `enrichCriteria` (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`.
232
+
233
+ Shared text for config/prompts can live in `.btodos/globals.json` and be referenced as `{{name}}` (single `{title}` placeholders stay for runtime).
234
+
235
+ PR review recaps (for **changes requested**) are generated **on demand** from a button on the web page, using `reviewFeedbackPrompt`.
236
+
237
+ PR **code-change** briefs (for manual review of AI-written diffs) use **Summarise changes**. Small diffs run immediately; large ones ask you to proceed or ignore. Customise the brief with `codeDiffPrompt`.
238
+ Pull the model once:
239
+
240
+ ```bash
241
+ ollama pull llama3.2:3b
242
+ ```
243
+
244
+ Host resource limits (how many requests / loaded models Ollama keeps) live under `ollama` and are
245
+ applied to the **host** Ollama process — not the btodos container — by:
246
+
247
+ ```bash
248
+ btodos ollama apply
249
+ ```
250
+
251
+ That sets `OLLAMA_NUM_PARALLEL` / `OLLAMA_MAX_LOADED_MODELS` from config, persists them on macOS via
252
+ a LaunchAgent, and restarts the Ollama app (use `--no-restart` to skip the restart):
253
+
254
+ ```json
255
+ {
256
+ "ollama": {
257
+ "numParallel": 1,
258
+ "maxLoadedModels": 1
259
+ }
260
+ }
261
+ ```
262
+
263
+ `models.recap` takes a single name or an ordered list of fallbacks. Each model is tried in turn, and
264
+ one that isn't pulled, errors, or answers with nothing hands off to the next:
265
+
266
+ ```json
267
+ { "models": { "recap": ["qwen2.5:7b", "llama3.2:3b"] } }
268
+ ```
269
+
270
+ `models.steps` is separate and defaults to `models.recap`, so you can pair a small fast model for the
271
+ one-line recap with a stronger one for the steps — same number of calls, better fit per task:
272
+
273
+ ```json
274
+ {
275
+ "models": {
276
+ "recap": ["llama3.2:3b"],
277
+ "steps": ["qwen2.5:7b", "llama3.2:3b"]
278
+ }
279
+ }
280
+ ```
281
+
282
+ If every model fails — Ollama isn't running, none are pulled — the recap falls back to a truncated
283
+ excerpt of the ticket description and steps are omitted, so `sync` never fails because of it. Set
284
+ `"models": { "recap": [] }` to always use the excerpt and skip the LLM entirely.
285
+
286
+ Prompts live in **`.btodos/prompts.json`** by default (`promptsFile`), or as **one Markdown
287
+ file per prompt** under **`.btodos/prompts/`** when you set `"promptsDir": "prompts"`.
288
+
289
+ ```bash
290
+ btd config prompts split # prompts.json → prompts/*.md + set promptsDir
291
+ btd config prompts export # prompts/*.md → prompts.md
292
+ ```
293
+
294
+ Each file is plain Markdown (the prompt body):
295
+
296
+ ```markdown
297
+ Classify this Jira ticket as exactly one of: {kinds}.
298
+
299
+ Title: {title}
300
+ ```
301
+
302
+ Config stays readable:
303
+
304
+ ```json
305
+ {
306
+ "promptsDir": "prompts",
307
+ "promptsFile": "prompts.md"
308
+ }
309
+ ```
310
+
311
+ `promptsDir` wins when present (``.md`` preferred over legacy ``.yaml``). `promptsFile` can still be JSON, Markdown bundle, or YAML.
312
+ Each JSON value is a string or an array of lines. Most must contain `{description}`; `{title}` is optional.
313
+ `cursorWork` is the text copied by the ✨ button on each ticket for pasting into Cursor. It must
314
+ contain `{key}` and `{branch}`; optional placeholders: `{title}`, `{url}`, `{description}`, `{brief}`,
315
+ `{status}`, `{priority}`, `{assignee}`, `{reporter}`.
316
+ `taskKind` classifies tickets into `taskKinds` and must include `{kinds}` + `{description}`.
317
+ `btodos init` / `btodos config sync` write the defaults so you can edit them in place.
318
+ Print the built-in Ollama prompts template (and copy it) with:
319
+
320
+ ```bash
321
+ btodos config prompts
322
+ ```
323
+
324
+ Then paste into `.btodos/prompts.json` to replace.
325
+
326
+ ```json
327
+ {
328
+ "recap": [
329
+ "Summarise this Jira ticket in one short sentence of at most 25 words.",
330
+ "",
331
+ "Title: {title}",
332
+ "",
333
+ "Description: {description}",
334
+ "",
335
+ "Summary:"
336
+ ],
337
+ "steps": ["…"],
338
+ "reviewFeedback": ["…"],
339
+ "codeDiff": ["…"],
340
+ "reviewDraft": ["…"],
341
+ "cursorWork": ["…"],
342
+ "taskKind": ["…"]
343
+ }
344
+ ```
345
+
346
+ Legacy inline `prompts` / top-level `recapPrompt` keys still load; `btodos config sync` moves them
347
+ into the prompts file.
348
+
349
+ Recaps and steps are cached and tracked separately, so a re-sync never starts over: it keeps
350
+ everything it already has and generates only what is new. A ticket is re-summarised when its
351
+ description changes, when its model or prompt changes (recaps and steps independently), or when
352
+ the previous attempt fell back to the excerpt because Ollama was unreachable.
353
+
354
+ The API token is read from the environment, never the config file:
355
+
356
+ ```bash
357
+ export JIRA_API_TOKEN="..." # https://id.atlassian.com/manage-profile/security/api-tokens
358
+ ```
359
+
360
+ The config file is looked up in the current directory and every parent, so the commands work
361
+ from any subdirectory of the workspace.
362
+
363
+ ## Use
364
+
365
+ Run `btodos commands` (or `btd commands`) anytime for a full command table.
366
+ Official short name: **`btd`** (same CLI as `btodos`).
367
+
368
+ ```bash
369
+ # Daily
370
+ btd init # create .btodos/config.json
371
+ btd sync # fetch tickets → JIRA-TODOS.md
372
+ btd sync --enrich # also bulk-generate recaps/steps/kinds (slow)
373
+ btd sync --task-kind # reclassify kinds only
374
+ btd serve # http://127.0.0.1:4321
375
+ btodos label ABC-123 backend # add labels
376
+ btodos doctor # check Jira / GitHub / Ollama
377
+ btodos commands # show all commands as a table
378
+ btodos completion zsh # shell completion
379
+
380
+ # Config (noun + verb)
381
+ btodos config sync # migrate/add keys after an upgrade
382
+ btodos config prompts # built-in prompts.json → clipboard
383
+ btodos config sort # list orderBy modes
384
+ btodos config assignees # prompt for emails → account ids
385
+ btodos config assignees a@x.com # same, non-interactive
386
+
387
+ # Ollama host / container
388
+ btodos ollama apply # apply ollama.* limits + restart app
389
+ btodos container write # write .btodos/docker-compose.yml
390
+ btodos container up --build # start
391
+ btodos container logs # follow
392
+ btodos container down # stop
393
+ ```
394
+
395
+ Output:
396
+
397
+ ```markdown
398
+ # ABC — My Tickets
399
+
400
+ _4 ticket(s) · generated 2026-09-01 10:12 UTC by btodos._
401
+
402
+ ## To Do (2)
403
+
404
+ - [ ] [ABC-123](https://your-company.atlassian.net/browse/ABC-123) — Fix login redirect _(High · 2026-08-28)_
405
+ - [ ] [ABC-131](https://your-company.atlassian.net/browse/ABC-131) — Add audit log _(Medium · 2026-08-27)_
406
+ ```
407
+
408
+ ## Notes
409
+
410
+ - Tickets are selected with `project = <key> AND assignee IN (<assignee>) AND status IN (<columns>)`
411
+ (or `assignee = …` when only one is configured), then listed by priority (Highest first), most
412
+ recently updated first within a priority.
413
+ - Everything btodos writes lives in `.btodos/` — config, ticket cache, assignee id cache and view
414
+ state. Add it to your `.gitignore` unless you want the cache shared.
415
+ - Column names must match your Jira **status** names exactly (matching is case-insensitive).
416
+ - Checkboxes are for your own local tracking — ticking one does not write back to Jira.
417
+
418
+ ## Docker
419
+
420
+ Run the web view as a background container instead of holding a terminal:
421
+
422
+ ```bash
423
+ btodos container write # writes .btodos/docker-compose.yml
424
+ btodos container up --build # start
425
+ btodos container restart # down then up (add --build to rebuild)
426
+ btodos container logs # progress output
427
+ btodos container down # stop
428
+ ```
429
+
430
+ Or with compose directly:
431
+
432
+ ```bash
433
+ btodos container write
434
+ docker compose -f .btodos/docker-compose.yml up -d --build
435
+ open http://127.0.0.1:4321
436
+ ```
437
+
438
+ ```bash
439
+ docker compose -f .btodos/docker-compose.yml logs -f # progress output
440
+ docker compose -f .btodos/docker-compose.yml down # stop
441
+ ```
442
+
443
+ The image is `python:3.13-alpine` with the package installed and nothing else — no build
444
+ Minimal runtime dependency: Jinja2 for HTML templates. Compose image installs the package as a non-root user (~84 MB). The generated compose file:
445
+
446
+ - starts a **Redis sidecar** (`redis:7-alpine`) for the ticket cache (`BTODOS_REDIS_URL`);
447
+ the JSON file under `.btodos/` remains a fallback;
448
+ - mounts your workspace at `/work`, so config, cache, view state and the Markdown output
449
+ are the same files the CLI uses;
450
+ - sets `BTODOS_HOST=0.0.0.0` so the port mapping can reach the server;
451
+ - points `BTODOS_OLLAMA_URL` at `host.docker.internal`, since Ollama runs on the host;
452
+ - reads `JIRA_API_TOKEN` and your git token from your shell at `up` time — never stored
453
+ in the file;
454
+ - restarts unless stopped, so it comes back after a reboot.
455
+
456
+ If `btodos serve` is already using the port, give the container a different host port:
457
+
458
+ ```bash
459
+ BTODOS_PORT=4400 docker compose -f .btodos/docker-compose.yml up -d
460
+ ```
461
+
462
+ `host` in the config (or `BTODOS_HOST`) also controls what the plain `serve` command binds to;
463
+ it stays `127.0.0.1` by default.
464
+
465
+ ## Tests
466
+
467
+ ```bash
468
+ python3 -m unittest discover -s tests -t tests # from the package root
469
+ ```
470
+
471
+ 148 tests, stdlib `unittest`, no network — every remote call is stubbed. Runtime needs Jinja2.
472
+ They cover config parsing and its error messages, JQL and Jira field parsing, priority ordering,
473
+ the incremental sync/merge rules, the ticket cache and view state, GitHub PR matching and review
474
+ state, Markdown and HTML rendering, the server endpoints, and the CLI.
475
+
476
+ Tests prove the logic; they cannot prove *your* credentials work. For that:
477
+
478
+ ```bash
479
+ btodos doctor
480
+ ```
481
+
482
+ It checks, against the real services: Jira credentials and that your JQL returns tickets, that
483
+ every configured column is a real status name, the GitHub token and each repo, whether any PR
484
+ actually mentions your ticket keys, and that Ollama is up with your models pulled. It exits
485
+ non-zero if anything failed.