tasktree-manager 2.12.0__tar.gz → 2.12.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 (71) hide show
  1. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/workflows/release.yml +112 -64
  2. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/CHANGELOG.md +12 -0
  3. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/PKG-INFO +3 -2
  4. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/README.md +4 -2
  5. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/configuration.md +13 -3
  6. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/installation.md +4 -1
  7. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/troubleshooting.md +24 -24
  8. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/user-guide.md +18 -8
  9. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/mise.toml +10 -7
  10. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/pyproject.toml +2 -1
  11. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/scripts/bump_version.py +38 -8
  12. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/_version.py +2 -2
  13. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/app.py +321 -83
  14. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/cli.py +10 -2
  15. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/claude_hooks.py +47 -19
  16. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/config.py +97 -72
  17. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/forge.py +12 -10
  18. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/git_ops.py +208 -66
  19. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/task_manager.py +170 -76
  20. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/create_modal.py +22 -6
  21. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/setup_modal.py +27 -21
  22. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/task_list.py +21 -21
  23. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/worktree_list.py +44 -28
  24. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_app.py +3 -1
  25. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_claude_hooks.py +5 -1
  26. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_cli.py +10 -4
  27. tasktree_manager-2.12.1/tests/test_medium_fixes.py +484 -0
  28. tasktree_manager-2.12.1/tests/test_safety_fixes.py +338 -0
  29. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_task_manager.py +76 -18
  30. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/SKILL.md +0 -0
  31. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/README.md +0 -0
  32. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
  33. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
  34. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
  35. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
  36. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/layouts.md +0 -0
  37. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
  38. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/styling.md +0 -0
  39. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/widgets.md +0 -0
  40. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/templates/CHANGELOG.md.j2 +0 -0
  41. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/workflows/ci.yml +0 -0
  42. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.gitignore +0 -0
  43. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/CONTRIBUTING.md +0 -0
  44. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/LICENSE +0 -0
  45. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/OPENSPEC.md +0 -0
  46. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/ROADMAP.md +0 -0
  47. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/claude-code-integration-spec.md +0 -0
  48. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/__init__.py +0 -0
  49. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/commands.py +0 -0
  50. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/__init__.py +0 -0
  51. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/agent_sessions.py +0 -0
  52. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/models.py +0 -0
  53. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/themes.py +0 -0
  54. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/__init__.py +0 -0
  55. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/app_header.py +0 -0
  56. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/messages_panel.py +0 -0
  57. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/status_panel.py +0 -0
  58. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/__init__.py +0 -0
  59. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/conftest.py +0 -0
  60. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_agent_sessions.py +0 -0
  61. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_commands.py +0 -0
  62. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_config.py +0 -0
  63. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_forge.py +0 -0
  64. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_git_ops.py +0 -0
  65. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_integration_git.py +0 -0
  66. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_memory.py +0 -0
  67. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_messages_panel.py +0 -0
  68. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_modals.py +0 -0
  69. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_setup_modal.py +0 -0
  70. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_status_panel.py +0 -0
  71. {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_worktree_badges.py +0 -0
@@ -4,9 +4,16 @@
4
4
  # Handles version bumping, testing, building, PyPI publishing,
5
5
  # and GitHub release creation.
6
6
  #
7
+ # Order matters: the version is only *computed* up front; lint and the full
8
+ # test matrix run against the pushed commit, and only when they pass is the
9
+ # bump committed, tagged and pushed. A red release run therefore never
10
+ # leaves a ghost tag or a bumped pyproject on main.
11
+ #
7
12
  # Triggers:
8
13
  # - Push to main: semantic-release determines if a release is needed
9
- # - Manual dispatch: choose release type, pre-release, and draft options
14
+ # - Manual dispatch: choose release type, pre-release, and draft options.
15
+ # Both `draft` and `pre_release` skip the PyPI upload (the pipeline can
16
+ # only produce final X.Y.Z versions) and only shape the GitHub release.
10
17
 
11
18
  name: Release
12
19
 
@@ -26,12 +33,12 @@ on:
26
33
  - minor
27
34
  - major
28
35
  pre_release:
29
- description: "Mark as pre-release?"
36
+ description: "Mark the GitHub release as pre-release? (skips PyPI)"
30
37
  required: false
31
38
  default: false
32
39
  type: boolean
33
40
  draft:
34
- description: "Create as draft release?"
41
+ description: "Create the GitHub release as a draft? (skips PyPI)"
35
42
  required: false
36
43
  default: false
37
44
  type: boolean
@@ -49,7 +56,6 @@ jobs:
49
56
  uses: actions/checkout@v4
50
57
  with:
51
58
  fetch-depth: 0
52
- token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
53
59
 
54
60
  - name: Set up Python
55
61
  uses: actions/setup-python@v5
@@ -57,84 +63,79 @@ jobs:
57
63
  python-version: "3.12"
58
64
 
59
65
  - name: Install dependencies
60
- run: pip install python-semantic-release build hatch-vcs
66
+ # Pinned: an unpinned resolver pull once produced a PSR/GitPython
67
+ # combination that crashed on startup
68
+ run: pip install "python-semantic-release==10.6.2"
61
69
 
62
70
  - name: Determine version
63
71
  id: determine_version
64
72
  env:
65
- GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
73
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66
74
  run: |
67
- # Get current version from pyproject.toml
68
- current_version=$(python -c "
69
- import re
70
- content = open('pyproject.toml').read()
71
- match = re.search(r'^\s*version\s*=\s*\"([^\"]+)\"', content, re.MULTILINE)
72
- print(match.group(1) if match else '0.0.0')
73
- ")
75
+ # The latest v* tag is the source of truth for the current version
76
+ # (semantic-release derives the next one from it). pyproject is
77
+ # only reported: a stray local bump must not shift the release.
78
+ latest_tag=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo "v0.0.0")
79
+ current_version="${latest_tag#v}"
74
80
  echo "current_version=${current_version}" >> $GITHUB_OUTPUT
75
- echo "Current version: ${current_version}"
81
+ echo "Current version (latest tag): ${current_version}"
76
82
 
77
83
  release_type="${{ inputs.release_type || 'auto' }}"
78
84
 
79
85
  if [ "$release_type" = "auto" ]; then
80
- # Use semantic-release to determine bump type from commit history.
81
- # When no qualifying commits exist, `version --print` prints the
86
+ # Use semantic-release to determine the next version from the
87
+ # commit history. When no qualifying commits exist it prints the
82
88
  # CURRENT version — require an actual bump before releasing.
83
- VERSION=$(semantic-release version --print 2>/dev/null || echo "")
89
+ # A tool crash is an error, never "no release needed".
90
+ if ! VERSION=$(semantic-release version --print); then
91
+ echo "::error::semantic-release failed to compute the next version"
92
+ exit 1
93
+ fi
84
94
  if [ -n "$VERSION" ] && [ "$VERSION" != "$current_version" ]; then
85
- # Determine bump type by comparing versions
86
- IFS='.' read -r cur_major cur_minor cur_patch <<< "$current_version"
87
- IFS='.' read -r new_major new_minor new_patch <<< "$VERSION"
88
- if [ "$new_major" -gt "$cur_major" ]; then
89
- bump_type="major"
90
- elif [ "$new_minor" -gt "$cur_minor" ]; then
91
- bump_type="minor"
92
- else
93
- bump_type="patch"
94
- fi
95
- # Use bump_version.py to get the actual new version
96
- new_version=$(python scripts/bump_version.py "$bump_type" --dry-run 2>&1 | grep "New version:" | awk '{print $3}')
95
+ new_version="$VERSION"
97
96
  echo "new_version=${new_version}" >> $GITHUB_OUTPUT
98
97
  echo "released=true" >> $GITHUB_OUTPUT
99
- echo "bump_type=${bump_type}" >> $GITHUB_OUTPUT
100
- echo "Auto-detected bump type: ${bump_type}, version: ${new_version}"
98
+ echo "Auto-detected next version: ${new_version}"
101
99
  else
102
100
  echo "released=false" >> $GITHUB_OUTPUT
103
101
  echo "No release needed (no qualifying commits)"
104
102
  fi
105
103
  else
106
- # Manual bump using bump_version.py
107
- new_version=$(python scripts/bump_version.py "$release_type" --dry-run 2>&1 | grep "New version:" | awk '{print $3}')
108
- if [ -n "$new_version" ]; then
109
- echo "new_version=${new_version}" >> $GITHUB_OUTPUT
110
- echo "released=true" >> $GITHUB_OUTPUT
111
- echo "bump_type=${release_type}" >> $GITHUB_OUTPUT
112
- echo "Manual bump to: ${new_version}"
113
- else
114
- echo "released=false" >> $GITHUB_OUTPUT
115
- echo "Failed to determine new version"
116
- exit 1
117
- fi
104
+ # Manual bump computed from the latest tag
105
+ IFS='.' read -r major minor patch <<< "$current_version"
106
+ case "$release_type" in
107
+ major) new_version="$((major + 1)).0.0" ;;
108
+ minor) new_version="${major}.$((minor + 1)).0" ;;
109
+ patch) new_version="${major}.${minor}.$((patch + 1))" ;;
110
+ *) echo "::error::unknown release type ${release_type}"; exit 1 ;;
111
+ esac
112
+ echo "new_version=${new_version}" >> $GITHUB_OUTPUT
113
+ echo "released=true" >> $GITHUB_OUTPUT
114
+ echo "Manual bump to: ${new_version}"
118
115
  fi
119
116
 
120
- - name: Apply version bump
121
- if: steps.determine_version.outputs.released == 'true'
122
- run: |
123
- bump_type="${{ steps.determine_version.outputs.bump_type }}"
124
- python scripts/bump_version.py "$bump_type"
117
+ lint:
118
+ name: Lint
119
+ needs: prepare-release
120
+ if: needs.prepare-release.outputs.released == 'true'
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ - name: Checkout code
124
+ uses: actions/checkout@v4
125
125
 
126
- - name: Commit and tag version
127
- if: steps.determine_version.outputs.released == 'true'
128
- run: |
129
- new_version="${{ steps.determine_version.outputs.new_version }}"
130
- git config --local user.email "github-actions[bot]@users.noreply.github.com"
131
- git config --local user.name "github-actions[bot]"
132
- git add -A
133
- git diff --cached --quiet && echo "No changes to commit" || \
134
- git commit -m "chore(release): ${new_version} [skip ci]"
135
- git tag -fa "v${new_version}" -m "Release v${new_version}"
136
- git push origin main
137
- git push origin "v${new_version}" --force
126
+ - name: Set up Python
127
+ uses: actions/setup-python@v5
128
+ with:
129
+ python-version: "3.12"
130
+
131
+ - name: Install ruff
132
+ run: pip install ruff
133
+
134
+ - name: Run ruff linter
135
+ run: ruff check tasktree_manager tests
136
+
137
+ - name: Run ruff formatter check
138
+ run: ruff format --check tasktree_manager tests
138
139
 
139
140
  test:
140
141
  name: Test (Python ${{ matrix.python-version }})
@@ -144,12 +145,12 @@ jobs:
144
145
  strategy:
145
146
  fail-fast: false
146
147
  matrix:
147
- python-version: ["3.11", "3.13"]
148
+ # Same matrix as ci.yml: the release gate must not be weaker than CI
149
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
148
150
  steps:
149
151
  - name: Checkout code
150
152
  uses: actions/checkout@v4
151
153
  with:
152
- ref: "v${{ needs.prepare-release.outputs.new_version }}"
153
154
  fetch-depth: 0
154
155
 
155
156
  - name: Set up Python ${{ matrix.python-version }}
@@ -170,9 +171,47 @@ jobs:
170
171
  - name: Run tests
171
172
  run: pytest tests/ -v --tb=short
172
173
 
174
+ tag-release:
175
+ name: Commit and Tag
176
+ needs: [prepare-release, lint, test]
177
+ runs-on: ubuntu-latest
178
+ steps:
179
+ - name: Checkout code
180
+ uses: actions/checkout@v4
181
+ with:
182
+ # The exact commit that was tested. Pushing HEAD:main below fails
183
+ # (non-fast-forward) if main moved in the meantime, instead of
184
+ # tagging code the gate never saw.
185
+ ref: ${{ github.sha }}
186
+ fetch-depth: 0
187
+ token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
188
+
189
+ - name: Set up Python
190
+ uses: actions/setup-python@v5
191
+ with:
192
+ python-version: "3.12"
193
+
194
+ - name: Apply version bump
195
+ # Write the exact version semantic-release computed; deriving it
196
+ # again from pyproject would drift whenever pyproject and the tag
197
+ # disagree (phantom or skipped releases)
198
+ run: python scripts/bump_version.py --set "${{ needs.prepare-release.outputs.new_version }}"
199
+
200
+ - name: Commit and tag version
201
+ run: |
202
+ new_version="${{ needs.prepare-release.outputs.new_version }}"
203
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
204
+ git config --local user.name "github-actions[bot]"
205
+ git add -A
206
+ git diff --cached --quiet && echo "No changes to commit" || \
207
+ git commit -m "chore(release): ${new_version} [skip ci]"
208
+ git tag -a "v${new_version}" -m "Release v${new_version}"
209
+ git push origin HEAD:main
210
+ git push origin "v${new_version}"
211
+
173
212
  build:
174
213
  name: Build Package
175
- needs: [prepare-release, test]
214
+ needs: [prepare-release, tag-release]
176
215
  runs-on: ubuntu-latest
177
216
  steps:
178
217
  - name: Checkout code
@@ -210,6 +249,9 @@ jobs:
210
249
  publish-pypi:
211
250
  name: Publish to PyPI
212
251
  needs: [prepare-release, build]
252
+ # Drafts and pre-releases never reach PyPI: bump_version.py only emits
253
+ # final X.Y.Z versions, so uploading would publish a real release
254
+ if: ${{ !inputs.draft && !inputs.pre_release }}
213
255
  runs-on: ubuntu-latest
214
256
  environment: pypi
215
257
  permissions:
@@ -231,7 +273,13 @@ jobs:
231
273
 
232
274
  github-release:
233
275
  name: Create GitHub Release
234
- needs: [prepare-release, publish-pypi]
276
+ needs: [prepare-release, build, publish-pypi]
277
+ # Run after a successful build whether PyPI publishing ran or was
278
+ # skipped (draft / pre-release), but never after a failed upload
279
+ if: >-
280
+ always() &&
281
+ needs.build.result == 'success' &&
282
+ (needs.publish-pypi.result == 'success' || needs.publish-pypi.result == 'skipped')
235
283
  runs-on: ubuntu-latest
236
284
  permissions:
237
285
  contents: write
@@ -15,6 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
  - Git hooks support
16
16
  - Custom task metadata
17
17
 
18
+ ## [2.12.1] - 2026-09-03
19
+
20
+ ### Changed
21
+ - Align guides with deletion, config and platform behaviour
22
+ - **release**: Gate tag on lint and tests, derive version from tags
23
+
24
+ ### Fixed
25
+ - **tui**: Install configured keybindings, safe worktree delete, markup-safe rows
26
+ - **config**: Keep env overrides out of config.toml and fail on bad TOML
27
+ - **services**: Harden task deletion, creation and archive safety
28
+
29
+
18
30
  ## [2.12.0] - 2026-07-31
19
31
 
20
32
  ### Added
@@ -1,10 +1,11 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: tasktree-manager
3
- Version: 2.12.0
3
+ Version: 2.12.1
4
4
  Summary: TUI application for managing worktree-based tasks
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
7
7
  Requires-Dist: textual>=7.0.0
8
+ Requires-Dist: tomli>=2.0.1; python_version < '3.11'
8
9
  Provides-Extra: dev
9
10
  Requires-Dist: build>=1.0.0; extra == 'dev'
10
11
  Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
@@ -82,7 +82,7 @@ tasktree-manager repos # available repos in REPOS_DIR
82
82
  tasktree-manager add-repo DIC-1901-argocd-tls infra
83
83
  tasktree-manager status # current task inferred from $PWD; also --json / --oneline / --forge
84
84
  tasktree-manager finish DIC-1901-argocd-tls # safety sweep -> archive diff -> delete; --push / --no-archive / --force
85
- tasktree-manager delete DIC-1901-argocd-tls # low-level delete; refuses on unfinished work; --force overrides
85
+ tasktree-manager delete DIC-1901-argocd-tls # archive diff -> delete; refuses on unfinished work; --force overrides
86
86
  ```
87
87
 
88
88
  Exit code is 0 on success, 1 on failure with the reason on stderr. `delete` and `finish` run
@@ -173,7 +173,9 @@ Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
173
173
  - Python 3.10+ (3.13+ recommended)
174
174
  - Git 2.0+
175
175
  - 256-color terminal
176
- - Unix-like OS (macOS, Linux, WSL2)
176
+ - Unix-like OS (macOS, Linux, WSL2). Opening a folder, editor or Claude session in a
177
+ new terminal tab (`o`, `e`, `c`) drives Ghostty via AppleScript and is macOS-only;
178
+ elsewhere those keys show a notice
177
179
 
178
180
  See [Installation Guide](docs/installation.md) for detailed requirements and setup.
179
181
 
@@ -165,6 +165,9 @@ claude_path = "claude"
165
165
  # task's .claude/settings.local.json so every task session reads and writes one
166
166
  # shared memory pool that persists across tasks.
167
167
  # Set to "" to disable (each task keeps its own throwaway memory).
168
+ # If you relocated Claude Code's config home with $CLAUDE_CONFIG_DIR, point
169
+ # this under that directory too (the per-repo worktree memory below and
170
+ # session resume already honour the variable).
168
171
  # Default: "~/.claude/tasktree-memory"
169
172
  claude_memory_dir = "~/.claude/tasktree-memory"
170
173
 
@@ -547,7 +550,10 @@ blocklist = [
547
550
  > **Upgrading note:** if your existing `config.toml` already contains a
548
551
  > `blocklist`, it keeps overriding the defaults — the key-material patterns
549
552
  > added in newer versions do NOT apply until you add them (or delete the
550
- > `blocklist` line to inherit the defaults).
553
+ > `blocklist` line to inherit the defaults). Saving the config from the app
554
+ > (for example on a theme switch) no longer writes the default list: the key
555
+ > is only written when you customised it, so the built-in defaults keep
556
+ > applying as they evolve.
551
557
 
552
558
  ### Common Use Cases
553
559
 
@@ -687,10 +693,14 @@ Override configuration for a single session using environment variables:
687
693
  | `TASKS_DIR` | `tasks_dir` | `export TASKS_DIR=~/worktrees` |
688
694
  | `TASKTREE_THEME` | `ui.theme` | `export TASKTREE_THEME=nord` |
689
695
  | `TASKTREE_DEFAULT_BRANCH` | `git.default_base_branch` | `export TASKTREE_DEFAULT_BRANCH=develop` |
690
- | `EDITOR` | `tools.editor` | `export EDITOR=nvim` |
691
- | `SHELL` | `tools.shell` | `export SHELL=/bin/zsh` |
696
+ | `EDITOR` | `tools.editor` (fallback: used only when `tools.editor` is empty) | `export EDITOR=nvim` |
692
697
  | `XDG_CONFIG_HOME` | (config location) | `export XDG_CONFIG_HOME=~/.cfg` |
693
698
 
699
+ Environment overrides apply to the running session only: saving the config (for
700
+ example by switching themes with `t`) writes the config-file values back, never the
701
+ overridden ones. Empty values (`TASKS_DIR=`) are ignored. A config file that fails to
702
+ parse is an error at startup, not an empty config.
703
+
694
704
  ### Usage Examples
695
705
 
696
706
  **Temporary different repos directory:**
@@ -256,7 +256,10 @@ Launch tasktree-manager:
256
256
  tasktree-manager
257
257
  ```
258
258
 
259
- On first run, you'll see a configuration wizard:
259
+ On first run, you'll see a configuration wizard (it appears when the repositories
260
+ directory, `~/repos` by default, does not exist yet; with `~/repos` already present
261
+ the defaults apply and no `config.toml` is written until you change a setting, such
262
+ as the theme):
260
263
 
261
264
  #### Configuration Wizard
262
265
 
@@ -692,34 +692,33 @@ wsl --shutdown
692
692
 
693
693
  ## Git-Specific Issues
694
694
 
695
- ### "Branch already exists"
695
+ ### Task branch already exists
696
696
 
697
- **Error:**
697
+ Creating a task whose branch already exists in a repo (left behind by an earlier
698
+ task, or created by hand) is not an error: the worktree is created **on the existing
699
+ branch as-is**, keeping every commit on it. The base branch you enter is ignored for
700
+ that repo.
701
+
702
+ **Error you can still hit:**
698
703
  ```
699
- fatal: A branch named 'FEAT-123' already exists
704
+ fatal: 'FEAT-123' is already checked out at '/Users/you/repos/repo-name'
700
705
  ```
701
706
 
702
- **Cause:** Branch exists in repository from previous task.
707
+ **Cause:** the branch is checked out in the main repository (or another worktree);
708
+ git allows a branch in only one worktree at a time.
703
709
 
704
- **Solutions:**
705
-
706
- 1. **Use different task name:**
707
- ```bash
708
- # Instead of FEAT-123, use FEAT-123-v2
709
- ```
710
-
711
- 2. **Delete old branch:**
712
- ```bash
713
- cd ~/repos/repo-name
714
- git branch -d FEAT-123 # If merged
715
- git branch -D FEAT-123 # Force delete
716
- ```
710
+ **Solution:**
711
+ ```bash
712
+ cd ~/repos/repo-name
713
+ git switch main # move the main checkout off the task branch
714
+ # then retry the task creation / add-repo
715
+ ```
717
716
 
718
- 3. **Use existing branch:**
719
- ```bash
720
- # When creating task, use existing branch as base
721
- Base branch: FEAT-123
722
- ```
717
+ To start the task fresh from the base branch instead, delete the old branch first:
718
+ ```bash
719
+ git branch -d FEAT-123 # If merged
720
+ git branch -D FEAT-123 # Force delete (loses its unmerged commits)
721
+ ```
723
722
 
724
723
  ### "Detached HEAD state"
725
724
 
@@ -921,10 +920,11 @@ Yes! Each task creates its own worktree and branch:
921
920
  ### What happens to branches when I delete a task?
922
921
 
923
922
  - Worktrees are removed from `TASKS_DIR`
924
- - Local branches remain in main repository
923
+ - The local task branch is deleted from the main repository (`git branch -D`);
924
+ the remaining diff is archived under `archive_dir` first
925
925
  - Remote branches remain on GitHub/GitLab (if pushed)
926
926
 
927
- **Manual cleanup:**
927
+ **Manual cleanup (only for branches created outside tasktree):**
928
928
  ```bash
929
929
  cd ~/repos/repo-name
930
930
  git branch -d FEAT-123 # Delete local branch
@@ -310,10 +310,10 @@ what is displayed. Press `?` for the full reference at any time.
310
310
  5. Select repos with `Space` (can select multiple)
311
311
  6. Tab to "Create" button, press `Enter`
312
312
 
313
- Task names may contain letters, numbers, `.`, `_`, `/` and `-`. Because the
314
- name becomes a directory under your tasks dir (and a git branch), names with
315
- `.`/`..` path segments, empty segments, or a leading `-` are rejected; base
316
- branch names must not start with `-` either.
313
+ Task names may contain letters, numbers, `.`, `_` and `-`. The name becomes a
314
+ single directory under your tasks dir (and a git branch), so `/`, `.`, `..` and a
315
+ leading `-` are rejected; base branch names must be plain branch names too (no
316
+ leading `-`/`+`, no `:` refspecs, none of the characters git forbids in refs).
317
317
 
318
318
  **Clone task workflow:**
319
319
  Useful when you're spinning up several similar tasks that share the same set of repos.
@@ -648,6 +648,14 @@ d (delete task)
648
648
  - **Uncommitted changes**: Commit or stash first
649
649
  - **Unpushed commits**: Push or force delete
650
650
  - **Unmerged branches**: Merge PR or force delete
651
+ - **Wrong branch checked out**: a worktree that is detached or on another branch
652
+ is reported as an error — switch back to the task branch (the check and the
653
+ archive follow the task branch, because that is what deletion removes)
654
+
655
+ **Delete a single worktree:**
656
+ 1. Highlight the worktree in the right panel
657
+ 2. Press `D` — the same safety check, dialog (Push / Open Lazygit / Force Delete)
658
+ and diff archive apply, scoped to that one repo
651
659
 
652
660
  **Safety features:**
653
661
  - tasktree-manager checks for uncommitted changes
@@ -664,7 +672,9 @@ d (delete task)
664
672
  - ✅ Worktrees removed from `TASKS_DIR`
665
673
  - ✅ Branches remain on remote (if pushed)
666
674
  - ✅ Original repos in `REPOS_DIR` untouched
667
- - ❌ Local branches remain in original repos (cleanup manually if needed)
675
+ - ❌ The local task branch is deleted from each repo (`git branch -D`) — unpushed
676
+ commits survive only in the archive patch, which is why the safety check blocks
677
+ unpushed work and why a worktree must be on its task branch to count as safe
668
678
 
669
679
  **From scripts:** `tasktree-manager finish <task>` runs the same guided flow headlessly
670
680
  (safety sweep → optional `--push` → archive → delete), and `tasktree-manager status
@@ -752,9 +762,9 @@ npm i -g hunkdiff
752
762
 
753
763
  **Direct editor integration:**
754
764
  1. Highlight task or worktree
755
- 2. Press `e` to open your configured editor
756
- 3. tasktree-manager suspends while editor runs
757
- 4. Selection is preserved when you return
765
+ 2. Press `e` to open your configured editor in a new Ghostty tab (`$EDITOR .`)
766
+ 3. tasktree-manager keeps running alongside the editor (macOS + Ghostty only, see
767
+ Limitations)
758
768
 
759
769
  **Configure your editor** in `~/.config/tasktree-manager/config.toml`:
760
770
  ```toml
@@ -69,17 +69,20 @@ description = "Run tasktree-manager in dev mode with textual console"
69
69
  run = "textual run --dev tasktree_manager.app:TaskTreeApp"
70
70
 
71
71
  [tasks."release:check"]
72
- description = "Dry-run to preview what a release would do"
72
+ description = "Dry-run to preview what a patch release would do"
73
73
  run = "python scripts/bump_version.py patch --dry-run"
74
74
 
75
+ # Releases are cut by the Release workflow (tests gate the tag). Bumping
76
+ # pyproject locally desyncs it from the latest tag and makes the workflow
77
+ # skip or double-bump, so these tasks dispatch the workflow instead.
75
78
  [tasks."release:patch"]
76
- description = "Bump patch version locally"
77
- run = "python scripts/bump_version.py patch"
79
+ description = "Trigger a patch release via GitHub Actions"
80
+ run = "gh workflow run release.yml -f release_type=patch"
78
81
 
79
82
  [tasks."release:minor"]
80
- description = "Bump minor version locally"
81
- run = "python scripts/bump_version.py minor"
83
+ description = "Trigger a minor release via GitHub Actions"
84
+ run = "gh workflow run release.yml -f release_type=minor"
82
85
 
83
86
  [tasks."release:major"]
84
- description = "Bump major version locally"
85
- run = "python scripts/bump_version.py major"
87
+ description = "Trigger a major release via GitHub Actions"
88
+ run = "gh workflow run release.yml -f release_type=major"
@@ -9,6 +9,7 @@ description = "TUI application for managing worktree-based tasks"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
11
  "textual>=7.0.0",
12
+ "tomli>=2.0.1; python_version < '3.11'",
12
13
  ]
13
14
 
14
15
  [project.optional-dependencies]
@@ -70,7 +71,7 @@ version-file = "tasktree_manager/_version.py"
70
71
  # Semantic Release Configuration
71
72
  [tool.semantic_release]
72
73
  version_toml = ["pyproject.toml:tool.semantic_release.version"]
73
- version = "2.12.0"
74
+ version = "2.12.1"
74
75
  tag_format = "v{version}"
75
76
  branch = "main"
76
77
  commit_message = "chore(release): {version} [skip ci]"
@@ -22,8 +22,16 @@ def parse_args():
22
22
  parser = argparse.ArgumentParser(description="Bump version for tasktree-manager")
23
23
  parser.add_argument(
24
24
  "bump_type",
25
+ nargs="?",
25
26
  choices=["patch", "minor", "major"],
26
- help="The type of version bump to perform",
27
+ help="The type of version bump to perform (omit when using --set)",
28
+ )
29
+ parser.add_argument(
30
+ "--set",
31
+ dest="set_version",
32
+ metavar="X.Y.Z",
33
+ help="Write this exact version instead of bumping (used by the release "
34
+ "workflow, which takes the version from semantic-release)",
27
35
  )
28
36
  parser.add_argument(
29
37
  "--dry-run",
@@ -106,7 +114,9 @@ def update_pyproject_version(current_version: str, new_version: str, dry_run: bo
106
114
  elif stripped.startswith("[") and in_section:
107
115
  in_section = False
108
116
 
109
- if in_section and re.match(r'^version\s*=\s*"' + re.escape(current_version) + '"', stripped):
117
+ if in_section and re.match(
118
+ r'^version\s*=\s*"' + re.escape(current_version) + '"', stripped
119
+ ):
110
120
  line = line.replace(f'"{current_version}"', f'"{new_version}"')
111
121
  changed = True
112
122
 
@@ -135,7 +145,9 @@ def get_changelog_from_commits() -> str:
135
145
  try:
136
146
  result = subprocess.run(
137
147
  ["git", "describe", "--tags", "--abbrev=0"],
138
- capture_output=True, text=True, cwd=PROJECT_ROOT,
148
+ capture_output=True,
149
+ text=True,
150
+ cwd=PROJECT_ROOT,
139
151
  )
140
152
  last_tag = result.stdout.strip() if result.returncode == 0 else ""
141
153
  except Exception:
@@ -148,7 +160,10 @@ def get_changelog_from_commits() -> str:
148
160
 
149
161
  try:
150
162
  result = subprocess.run(
151
- git_log_cmd, capture_output=True, text=True, cwd=PROJECT_ROOT,
163
+ git_log_cmd,
164
+ capture_output=True,
165
+ text=True,
166
+ cwd=PROJECT_ROOT,
152
167
  )
153
168
  commits = result.stdout.strip().splitlines() if result.returncode == 0 else []
154
169
  except Exception:
@@ -262,7 +277,11 @@ def update_changelog(new_version: str, message: str = "", dry_run: bool = False)
262
277
  while insert_idx < len(lines) and lines[insert_idx].strip() == "":
263
278
  insert_idx += 1
264
279
  # Skip description lines
265
- while insert_idx < len(lines) and lines[insert_idx].strip() and not lines[insert_idx].startswith("##"):
280
+ while (
281
+ insert_idx < len(lines)
282
+ and lines[insert_idx].strip()
283
+ and not lines[insert_idx].startswith("##")
284
+ ):
266
285
  insert_idx += 1
267
286
  while insert_idx < len(lines) and lines[insert_idx].strip() == "":
268
287
  insert_idx += 1
@@ -288,7 +307,9 @@ def commit_changes(new_version: str, push: bool = False, create_tag: bool = Fals
288
307
  cwd=PROJECT_ROOT,
289
308
  )
290
309
 
291
- subprocess.run(["git", "add", "pyproject.toml", "CHANGELOG.md"], check=True, cwd=PROJECT_ROOT)
310
+ subprocess.run(
311
+ ["git", "add", "pyproject.toml", "CHANGELOG.md"], check=True, cwd=PROJECT_ROOT
312
+ )
292
313
 
293
314
  commit_message = f"chore(release): bump version to {new_version}"
294
315
  subprocess.run(["git", "commit", "-m", commit_message], check=True, cwd=PROJECT_ROOT)
@@ -330,7 +351,14 @@ def main():
330
351
 
331
352
  try:
332
353
  current_version = get_current_version()
333
- new_version = calculate_new_version(current_version, args.bump_type)
354
+ if args.set_version:
355
+ if not re.fullmatch(r"\d+\.\d+\.\d+", args.set_version):
356
+ raise ValueError(f"--set expects X.Y.Z, got {args.set_version!r}")
357
+ new_version = args.set_version
358
+ elif args.bump_type:
359
+ new_version = calculate_new_version(current_version, args.bump_type)
360
+ else:
361
+ raise ValueError("either a bump type (patch/minor/major) or --set X.Y.Z is required")
334
362
 
335
363
  print(f"Current version: {current_version}")
336
364
  print(f"New version: {new_version}")
@@ -357,7 +385,9 @@ def main():
357
385
  else:
358
386
  print("\nNext steps:")
359
387
  print(f"1. Edit CHANGELOG.md to document changes in version {new_version}")
360
- print(f"2. Commit changes: git commit -am 'chore(release): bump version to {new_version}'")
388
+ print(
389
+ f"2. Commit changes: git commit -am 'chore(release): bump version to {new_version}'"
390
+ )
361
391
  print(f"3. Create tag: git tag -a v{new_version} -m 'Release v{new_version}'")
362
392
  print("4. Push changes: git push origin HEAD && git push origin --tags")
363
393
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.12.0'
22
- __version_tuple__ = version_tuple = (2, 12, 0)
21
+ __version__ = version = '2.12.1'
22
+ __version_tuple__ = version_tuple = (2, 12, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None