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.
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/workflows/release.yml +112 -64
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/CHANGELOG.md +12 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/PKG-INFO +3 -2
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/README.md +4 -2
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/configuration.md +13 -3
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/installation.md +4 -1
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/troubleshooting.md +24 -24
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/user-guide.md +18 -8
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/mise.toml +10 -7
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/pyproject.toml +2 -1
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/scripts/bump_version.py +38 -8
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/app.py +321 -83
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/cli.py +10 -2
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/claude_hooks.py +47 -19
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/config.py +97 -72
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/forge.py +12 -10
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/git_ops.py +208 -66
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/task_manager.py +170 -76
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/create_modal.py +22 -6
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/setup_modal.py +27 -21
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/task_list.py +21 -21
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/worktree_list.py +44 -28
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_app.py +3 -1
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_claude_hooks.py +5 -1
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_cli.py +10 -4
- tasktree_manager-2.12.1/tests/test_medium_fixes.py +484 -0
- tasktree_manager-2.12.1/tests/test_safety_fixes.py +338 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_task_manager.py +76 -18
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/.gitignore +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/CONTRIBUTING.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/LICENSE +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/OPENSPEC.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/ROADMAP.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/commands.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/agent_sessions.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/services/models.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/themes.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/app_header.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tasktree_manager/widgets/status_panel.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/__init__.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/conftest.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_agent_sessions.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_commands.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_config.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_forge.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_git_ops.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_memory.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_modals.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.12.0 → tasktree_manager-2.12.1}/tests/test_status_panel.py +0 -0
- {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
|
|
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
|
-
|
|
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.
|
|
73
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
66
74
|
run: |
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
81
|
-
# When no qualifying commits exist
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
echo "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: tasktree-manager
|
|
3
|
-
Version: 2.12.
|
|
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 #
|
|
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`
|
|
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
|
-
###
|
|
695
|
+
### Task branch already exists
|
|
696
696
|
|
|
697
|
-
|
|
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:
|
|
704
|
+
fatal: 'FEAT-123' is already checked out at '/Users/you/repos/repo-name'
|
|
700
705
|
```
|
|
701
706
|
|
|
702
|
-
**Cause:**
|
|
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
|
-
**
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
-
|
|
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, `.`, `_
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
- ❌
|
|
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
|
|
757
|
-
|
|
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 = "
|
|
77
|
-
run = "
|
|
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 = "
|
|
81
|
-
run = "
|
|
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 = "
|
|
85
|
-
run = "
|
|
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.
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 12,
|
|
21
|
+
__version__ = version = '2.12.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 12, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|