tasktree-manager 2.13.2__tar.gz → 2.14.0__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.13.2 → tasktree_manager-2.14.0}/.gitignore +0 -1
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/CHANGELOG.md +15 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/CONTRIBUTING.md +7 -10
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/PKG-INFO +1 -1
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/README.md +40 -16
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/configuration.md +2 -2
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/troubleshooting.md +1 -1
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/user-guide.md +6 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/pyproject.toml +1 -1
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/app.py +69 -28
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/cli.py +264 -29
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/config.py +19 -11
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/models.py +81 -5
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/task_manager.py +463 -81
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_app_review_fixes.py +97 -2
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_cli.py +88 -0
- tasktree_manager-2.14.0/tests/test_epics.py +430 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_medium_fixes.py +1 -1
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_task_manager.py +386 -24
- tasktree_manager-2.13.2/OPENSPEC.md +0 -457
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/workflows/release.yml +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/LICENSE +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/ROADMAP.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/installation.md +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/mise.toml +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/scripts/bump_version.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/commands.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/agent_sessions.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/claude_hooks.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/claude_sessions.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/forge.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/git_ops.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/themes.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/app_header.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/create_modal.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/setup_modal.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/status_panel.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/task_list.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/worktree_list.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/__init__.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/conftest.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_agent_sessions.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_agent_sessions_decode.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_app.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_bump_version.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_claude_hooks.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_claude_sessions.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_commands.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_config.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_forge.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_git_ops.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_list_selection.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_memory.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_modals.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_safety_fixes.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_status_panel.py +0 -0
- {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_worktree_badges.py +0 -0
|
@@ -15,6 +15,21 @@ 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.14.0] - 2026-10-19
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **epic**: Group tasks under epics from the CLI
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.13.3] - 2026-10-16
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Remove unused specs and refresh the README roadmap
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **archive**: Keep task-folder files when a task is finished
|
|
31
|
+
|
|
32
|
+
|
|
18
33
|
## [2.13.2] - 2026-09-28
|
|
19
34
|
|
|
20
35
|
### Changed
|
|
@@ -18,7 +18,7 @@ Be respectful, inclusive, and constructive. We're all here to build something us
|
|
|
18
18
|
|
|
19
19
|
1. **Fork and clone**
|
|
20
20
|
```bash
|
|
21
|
-
git clone https://github.com/
|
|
21
|
+
git clone https://github.com/ruslanfialkovskii/tasktree-manager.git
|
|
22
22
|
cd tasktree-manager
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -149,7 +149,7 @@ def create_task(name: str, repos: list[str]) -> Task:
|
|
|
149
149
|
- Add docstrings to all public functions/classes
|
|
150
150
|
- Use Google-style docstrings
|
|
151
151
|
- Update README.md for user-facing changes
|
|
152
|
-
- Update
|
|
152
|
+
- Update the Architecture section of CLAUDE.md for architecture changes
|
|
153
153
|
|
|
154
154
|
Example docstring:
|
|
155
155
|
```python
|
|
@@ -206,18 +206,15 @@ tasktree-manager/
|
|
|
206
206
|
│ ├── themes/ # Color themes
|
|
207
207
|
│ └── app.py # Main app
|
|
208
208
|
├── tests/ # Test suite
|
|
209
|
-
├── specs/ # Technical specs
|
|
210
209
|
└── docs/ # Documentation
|
|
211
210
|
```
|
|
212
211
|
|
|
213
212
|
### Adding New Features
|
|
214
213
|
|
|
215
|
-
1. **
|
|
216
|
-
2. **
|
|
217
|
-
3. **
|
|
218
|
-
4. **
|
|
219
|
-
5. **Document**: Update README/specs
|
|
220
|
-
6. **PR**: Submit for review
|
|
214
|
+
1. **Write tests**: Add tests first (TDD approach)
|
|
215
|
+
2. **Implement**: Write the code
|
|
216
|
+
3. **Document**: Update README.md and `docs/`
|
|
217
|
+
4. **PR**: Submit for review
|
|
221
218
|
|
|
222
219
|
## Submitting Issues
|
|
223
220
|
|
|
@@ -260,7 +257,7 @@ Include:
|
|
|
260
257
|
|
|
261
258
|
## Questions?
|
|
262
259
|
|
|
263
|
-
- Open a [Discussion](https://github.com/
|
|
260
|
+
- Open a [Discussion](https://github.com/ruslanfialkovskii/tasktree-manager/discussions)
|
|
264
261
|
- Ask in Pull Request
|
|
265
262
|
- Check existing issues
|
|
266
263
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
|
-
[](./tests/)
|
|
8
8
|
|
|
9
9
|
**tasktree-manager** is a terminal user interface (TUI) application for managing development tasks that span multiple git repositories. Create tasks, organize worktrees, and track status across your entire codebase—all from a beautiful, keyboard-driven interface inspired by [lazygit](https://github.com/jesseduffield/lazygit).
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
- 🛰️ **Agent Dashboard** - Live Claude Code session state per worktree (from `claude agents`), plus one-key background-agent dispatch — the multi-repo agent view Claude Code lacks natively
|
|
23
23
|
- ✻ **Session Recap** - The Info panel shows each task's latest Claude Code session: title, last turn duration, and Claude's own idle recap of what was done and what comes next — read straight from the transcript, no CLI call
|
|
24
24
|
- 🔀 **Forge-Aware Safety** - MR/PR and CI state via `glab`/`gh`; squash- and rebase-merged branches count as merged instead of blocking deletion forever
|
|
25
|
-
- 📦 **Finish Flow with Archives** - Ending a task saves its combined diff (committed-but-unmerged + uncommitted) to an archive before deleting
|
|
25
|
+
- 📦 **Finish Flow with Archives** - Ending a task saves its combined diff (committed-but-unmerged + uncommitted) and the task-folder files outside the worktrees (plans, ADRs, notes) to an archive before deleting
|
|
26
26
|
|
|
27
27
|
## 📊 Status Indicators
|
|
28
28
|
|
|
@@ -82,15 +82,40 @@ tasktree-manager list --json # tasks with repos and state: clean / dirty / e
|
|
|
82
82
|
tasktree-manager repos # available repos in REPOS_DIR
|
|
83
83
|
tasktree-manager add-repo DIC-1901-argocd-tls infra
|
|
84
84
|
tasktree-manager status # current task inferred from $PWD; also --json / --oneline / --forge
|
|
85
|
-
tasktree-manager finish DIC-1901-argocd-tls # safety sweep -> archive diff -> delete; --push / --no-archive / --force
|
|
86
|
-
tasktree-manager delete DIC-1901-argocd-tls # archive diff -> delete; refuses on unfinished work; --force overrides
|
|
85
|
+
tasktree-manager finish DIC-1901-argocd-tls # safety sweep -> archive diff + task files -> delete; --push / --no-archive / --force
|
|
86
|
+
tasktree-manager delete DIC-1901-argocd-tls # archive diff + task files -> delete; refuses on unfinished work; --force overrides
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
Epics group related tasks, for example a Jira epic and its child tasks. An epic is a task
|
|
90
|
+
folder with a `.tasktree_kind` marker. It can hold its own notes, Claude session and
|
|
91
|
+
worktrees. A child task names its epic in `.tasktree_epic`. Folders never move:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
tasktree-manager epic create DIC-2466-pcaps-epic # --repos is optional for an epic
|
|
95
|
+
tasktree-manager create DIC-2861-pcaps-logs --repos infra --epic DIC-2466-pcaps-epic
|
|
96
|
+
tasktree-manager epic add DIC-2466-pcaps-epic DIC-2862-pcaps-s3 # attach or move existing tasks
|
|
97
|
+
tasktree-manager epic remove DIC-2862-pcaps-s3 # detach
|
|
98
|
+
tasktree-manager epic mark DIC-2466-pcaps-epic # turn an existing task into an epic; unmark reverts
|
|
99
|
+
tasktree-manager epic list # epics and their tasks; also --json
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Epics don't nest. `list --json` and `status --json` carry `kind` (`task` or `epic`) and
|
|
103
|
+
`epic` (the parent epic or `null`). `delete` and `finish` refuse an epic that still has
|
|
104
|
+
tasks, even with `--force`.
|
|
105
|
+
|
|
89
106
|
Exit code is 0 on success, 1 on failure with the reason on stderr. `delete` and `finish` run
|
|
90
107
|
the same safety check as the TUI (uncommitted, unpushed, or unmerged work blocks deletion);
|
|
91
108
|
with `glab`/`gh` installed, squash/rebase-merged branches are recognized as merged. If the
|
|
92
109
|
diff cannot be archived for any repo, the CLI stops before deleting and prints the path of
|
|
93
|
-
the partial archive; the TUI warns and deletes anyway.
|
|
110
|
+
the partial archive; the TUI warns and deletes anyway. Files in the task folder outside the
|
|
111
|
+
worktrees are copied to `<task>-<timestamp>.files/` next to the patch:
|
|
112
|
+
|
|
113
|
+
- Skipped silently: hidden files, symlinks, `node_modules`/`vendor`/`.terraform` folders and
|
|
114
|
+
an unedited generated `CLAUDE.md`.
|
|
115
|
+
- Left out with a warning: key material (`*.pem`, `*.key`, `id_rsa`, kubeconfigs,
|
|
116
|
+
`*.tfstate`, `credentials*.json`), files over 10 MB and files past 100 MB in total.
|
|
117
|
+
- A file that cannot be read or copied stops `delete`/`finish` before anything is removed,
|
|
118
|
+
unless `--force`; the TUI warns and deletes anyway.
|
|
94
119
|
`status --oneline` emits a compact `TASK repoA✓ repoB●2↑1` summary that slots straight into
|
|
95
120
|
a Claude Code statusline or shell prompt.
|
|
96
121
|
|
|
@@ -147,7 +172,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed development setup and guidel
|
|
|
147
172
|
|
|
148
173
|
**Quick Start:**
|
|
149
174
|
```bash
|
|
150
|
-
git clone https://github.com/
|
|
175
|
+
git clone https://github.com/ruslanfialkovskii/tasktree-manager.git
|
|
151
176
|
cd tasktree-manager
|
|
152
177
|
mise install
|
|
153
178
|
mise run install
|
|
@@ -197,14 +222,13 @@ For comprehensive troubleshooting, see the [Troubleshooting Guide](docs/troubles
|
|
|
197
222
|
|
|
198
223
|
Recently shipped: agent dashboard (per-worktree Claude session badges + background dispatch),
|
|
199
224
|
forge-aware merged detection (glab/gh, squash/rebase merges), MR/CI status badges,
|
|
200
|
-
guided `finish` flow
|
|
225
|
+
guided `finish` flow that archives the diff and the task-folder files, `status` subcommand
|
|
226
|
+
for scripts and statuslines, Claude session recap in the Info panel.
|
|
201
227
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- **v0.4.0** - JIRA status column, git hooks, automation
|
|
205
|
-
- **v1.0.0** - Stable release
|
|
228
|
+
Next: epics, a layer that groups related tasks (for example a Jira epic and its child
|
|
229
|
+
tasks) in the task list.
|
|
206
230
|
|
|
207
|
-
See [
|
|
231
|
+
See [CHANGELOG.md](CHANGELOG.md) for the release history.
|
|
208
232
|
|
|
209
233
|
## 📄 License
|
|
210
234
|
|
|
@@ -218,11 +242,11 @@ MIT License - see [LICENSE](./LICENSE) for details.
|
|
|
218
242
|
|
|
219
243
|
## 💬 Support & Community
|
|
220
244
|
|
|
221
|
-
- 🐛 [Bug Reports](https://github.com/
|
|
222
|
-
- 💡 [Feature Requests](https://github.com/
|
|
223
|
-
- 💬 [Discussions](https://github.com/
|
|
245
|
+
- 🐛 [Bug Reports](https://github.com/ruslanfialkovskii/tasktree-manager/issues)
|
|
246
|
+
- 💡 [Feature Requests](https://github.com/ruslanfialkovskii/tasktree-manager/issues)
|
|
247
|
+
- 💬 [Discussions](https://github.com/ruslanfialkovskii/tasktree-manager/discussions)
|
|
224
248
|
- 📖 [Documentation](docs/)
|
|
225
249
|
|
|
226
250
|
---
|
|
227
251
|
|
|
228
|
-
**Made with ❤️ by developers, for developers** | [⭐ Star this repo](https://github.com/
|
|
252
|
+
**Made with ❤️ by developers, for developers** | [⭐ Star this repo](https://github.com/ruslanfialkovskii/tasktree-manager)
|
|
@@ -86,7 +86,7 @@ repos_dir = "/Users/username/repos"
|
|
|
86
86
|
# Created automatically if it doesn't exist
|
|
87
87
|
tasks_dir = "/Users/username/tasks"
|
|
88
88
|
|
|
89
|
-
# Directory for finished-task
|
|
89
|
+
# Directory for finished-task archives (diffs and task-folder files)
|
|
90
90
|
# Relative paths resolve under tasks_dir (never the current directory)
|
|
91
91
|
# Default: "" (uses <tasks_dir>/.archive)
|
|
92
92
|
archive_dir = ""
|
|
@@ -305,7 +305,7 @@ blocklist = [
|
|
|
305
305
|
|------------|------|-----------|---------------------|------------------------------------------------|
|
|
306
306
|
| `repos_dir` | path | `~/repos` | `REPOS_DIR` | Directory containing your git repositories |
|
|
307
307
|
| `tasks_dir` | path | `~/tasks` | `TASKS_DIR` | Directory where task worktrees will be created |
|
|
308
|
-
| `archive_dir` | path | `""` (`<tasks_dir>/.archive`) | - | Directory for finished-task
|
|
308
|
+
| `archive_dir` | path | `""` (`<tasks_dir>/.archive`) | - | Directory for finished-task archives (diffs and task-folder files) |
|
|
309
309
|
|
|
310
310
|
### Details
|
|
311
311
|
|
|
@@ -921,7 +921,7 @@ Yes! Each task creates its own worktree and branch:
|
|
|
921
921
|
|
|
922
922
|
- Worktrees are removed from `TASKS_DIR`
|
|
923
923
|
- The local task branch is deleted from the main repository (`git branch -D`);
|
|
924
|
-
the remaining diff
|
|
924
|
+
the remaining diff and the task-folder files are archived under `archive_dir` first
|
|
925
925
|
- Remote branches remain on GitHub/GitLab (if pushed)
|
|
926
926
|
|
|
927
927
|
**Manual cleanup (only for branches created outside tasktree):**
|
|
@@ -687,6 +687,12 @@ d (delete task)
|
|
|
687
687
|
- ✅ The task's remaining diff (committed-but-unmerged + uncommitted work) is archived to
|
|
688
688
|
`archive_dir` (default `<tasks_dir>/.archive/<task>-<timestamp>.patch`) before anything
|
|
689
689
|
is removed — the messages panel (`m`) shows the archive path
|
|
690
|
+
- ✅ Files in the task folder outside the worktrees (`PLAN.md`, ADRs, notes, scripts) are
|
|
691
|
+
copied to `<task>-<timestamp>.files/` next to the patch. Hidden files, symlinks,
|
|
692
|
+
`node_modules`/`vendor`/`.terraform` folders and an unedited generated `CLAUDE.md` are
|
|
693
|
+
skipped silently. Key material (`*.pem`, `*.key`, `id_rsa`, kubeconfigs, `*.tfstate`,
|
|
694
|
+
`credentials*.json`), files over 10 MB, files past 100 MB in total and files that cannot
|
|
695
|
+
be read or copied are listed in the messages panel
|
|
690
696
|
- ✅ Worktrees removed from `TASKS_DIR`
|
|
691
697
|
- ✅ Branches remain on remote (if pushed)
|
|
692
698
|
- ✅ Original repos in `REPOS_DIR` untouched
|
|
@@ -71,7 +71,7 @@ version-file = "tasktree_manager/_version.py"
|
|
|
71
71
|
# Semantic Release Configuration
|
|
72
72
|
[tool.semantic_release]
|
|
73
73
|
version_toml = ["pyproject.toml:tool.semantic_release.version"]
|
|
74
|
-
version = "2.
|
|
74
|
+
version = "2.14.0"
|
|
75
75
|
tag_format = "v{version}"
|
|
76
76
|
branch = "main"
|
|
77
77
|
commit_message = "chore(release): {version} [skip ci]"
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.14.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 14, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -24,8 +24,8 @@ from .services.claude_sessions import ClaudeRecap, read_claude_recap, transcript
|
|
|
24
24
|
from .services.config import Config, ConfigError
|
|
25
25
|
from .services.forge import Forge, ForgeStatus, get_forge_status
|
|
26
26
|
from .services.git_ops import GitOps
|
|
27
|
-
from .services.models import GitStatus, Task, TaskSafetyReport, Worktree
|
|
28
|
-
from .services.task_manager import TaskManager
|
|
27
|
+
from .services.models import ArchiveResult, GitStatus, Task, TaskSafetyReport, Worktree
|
|
28
|
+
from .services.task_manager import ARCHIVE_NOTES_SHOWN, TaskManager
|
|
29
29
|
from .themes import ALL_THEMES, THEME_CYCLE
|
|
30
30
|
from .widgets.app_header import AppHeader
|
|
31
31
|
from .widgets.create_modal import (
|
|
@@ -1300,6 +1300,16 @@ class TaskTreeApp(App):
|
|
|
1300
1300
|
if not self.current_task:
|
|
1301
1301
|
self.notify("No task selected", severity="warning")
|
|
1302
1302
|
return
|
|
1303
|
+
if self.current_task.is_epic:
|
|
1304
|
+
children = self.task_manager.epic_child_names(self.current_task.name)
|
|
1305
|
+
if children:
|
|
1306
|
+
self.notify(
|
|
1307
|
+
escape(
|
|
1308
|
+
f"Epic '{self.current_task.name}' still has tasks: {', '.join(children)}"
|
|
1309
|
+
),
|
|
1310
|
+
severity="warning",
|
|
1311
|
+
)
|
|
1312
|
+
return
|
|
1303
1313
|
|
|
1304
1314
|
# Safety checks hit the network (git fetch per worktree), so run them
|
|
1305
1315
|
# in a background thread and show the dialog when they complete
|
|
@@ -1378,14 +1388,15 @@ class TaskTreeApp(App):
|
|
|
1378
1388
|
|
|
1379
1389
|
Removing worktrees runs git per repo and rmtree over trees that can
|
|
1380
1390
|
be huge (node_modules etc.) - never block the UI thread on it.
|
|
1381
|
-
The task's remaining diff (committed-but-unmerged + uncommitted)
|
|
1382
|
-
archived first — finish_task rmtrees the
|
|
1391
|
+
The task's remaining diff (committed-but-unmerged + uncommitted) and
|
|
1392
|
+
its task-folder files are archived first — finish_task rmtrees the
|
|
1393
|
+
task directory.
|
|
1383
1394
|
"""
|
|
1384
1395
|
error: str | None = None
|
|
1385
|
-
|
|
1396
|
+
archive: ArchiveResult | None = None
|
|
1386
1397
|
try:
|
|
1387
1398
|
try:
|
|
1388
|
-
|
|
1399
|
+
archive = self.task_manager.archive_task(task)
|
|
1389
1400
|
except Exception as e:
|
|
1390
1401
|
# A failed archive must not block deletion, but it must be
|
|
1391
1402
|
# loud — the archive is the safety net for the work the
|
|
@@ -1394,6 +1405,8 @@ class TaskTreeApp(App):
|
|
|
1394
1405
|
detail = f"Archive failed for '{task.name}': {type(e).__name__}: {e}"
|
|
1395
1406
|
self.call_from_thread(self.notify, escape(detail), severity="warning", timeout=10)
|
|
1396
1407
|
self.call_from_thread(self._log_activity, detail, MessageLevel.ERROR, task.name)
|
|
1408
|
+
# Still report the files that were saved and the ones left out
|
|
1409
|
+
archive = getattr(e, "result", None)
|
|
1397
1410
|
self.task_manager.finish_task(task)
|
|
1398
1411
|
except ValueError as e:
|
|
1399
1412
|
# finish_task raises ValueError (nothing removed) when worktrees
|
|
@@ -1402,37 +1415,66 @@ class TaskTreeApp(App):
|
|
|
1402
1415
|
error = str(e)
|
|
1403
1416
|
except Exception as e:
|
|
1404
1417
|
error = f"{type(e).__name__}: {e}"
|
|
1405
|
-
self.call_from_thread(self._apply_finish_task_result, task.name, force, error,
|
|
1418
|
+
self.call_from_thread(self._apply_finish_task_result, task.name, force, error, archive)
|
|
1406
1419
|
|
|
1407
1420
|
def _apply_finish_task_result(
|
|
1408
1421
|
self,
|
|
1409
1422
|
task_name: str,
|
|
1410
1423
|
force: bool,
|
|
1411
1424
|
error: str | None,
|
|
1412
|
-
|
|
1425
|
+
archive: ArchiveResult | None = None,
|
|
1413
1426
|
) -> None:
|
|
1414
1427
|
"""Report the delete outcome and reload (runs on the main thread)."""
|
|
1415
1428
|
if error:
|
|
1416
1429
|
self.notify(escape(f"Failed to delete task: {error}"), severity="error")
|
|
1417
1430
|
self._log_activity(f"Failed to delete task: {error}", MessageLevel.ERROR, task_name)
|
|
1418
|
-
if
|
|
1431
|
+
if archive is not None:
|
|
1419
1432
|
# The archive succeeded before the delete itself failed - the
|
|
1420
1433
|
# user's work is not lost, but they should know where it is
|
|
1421
|
-
self.
|
|
1422
|
-
f"Archived diff to {archive_path} before the failed delete",
|
|
1423
|
-
MessageLevel.INFO,
|
|
1424
|
-
task_name,
|
|
1425
|
-
)
|
|
1434
|
+
self._log_archive(task_name, archive, " before the failed delete")
|
|
1426
1435
|
else:
|
|
1427
1436
|
self.notify(escape(f"Deleted task: {task_name}"))
|
|
1428
1437
|
suffix = " (force)" if force else ""
|
|
1429
1438
|
self._log_activity(
|
|
1430
1439
|
f"Task '{task_name}' deleted{suffix}", MessageLevel.SUCCESS, task_name
|
|
1431
1440
|
)
|
|
1432
|
-
if
|
|
1433
|
-
self.
|
|
1441
|
+
if archive is not None:
|
|
1442
|
+
self._log_archive(task_name, archive)
|
|
1434
1443
|
self._load_tasks()
|
|
1435
1444
|
|
|
1445
|
+
def _log_archive(self, task_name: str, archive: ArchiveResult, context: str = "") -> None:
|
|
1446
|
+
"""Log where a task's work was archived and warn about files left out.
|
|
1447
|
+
|
|
1448
|
+
The left-out notes come first and are capped, so the archive
|
|
1449
|
+
location stays among the latest entries of the bounded panel.
|
|
1450
|
+
"""
|
|
1451
|
+
notes = [f"Not archived: {item}" for item in archive.skipped]
|
|
1452
|
+
notes += [f"Could not archive: {item}" for item in archive.failed]
|
|
1453
|
+
for note in notes[:ARCHIVE_NOTES_SHOWN]:
|
|
1454
|
+
self._log_activity(note, MessageLevel.WARNING, task_name)
|
|
1455
|
+
if len(notes) > ARCHIVE_NOTES_SHOWN:
|
|
1456
|
+
self._log_activity(
|
|
1457
|
+
f"... and {len(notes) - ARCHIVE_NOTES_SHOWN} more not archived",
|
|
1458
|
+
MessageLevel.WARNING,
|
|
1459
|
+
task_name,
|
|
1460
|
+
)
|
|
1461
|
+
if archive.patch is not None:
|
|
1462
|
+
self._log_activity(
|
|
1463
|
+
f"Archived diff to {archive.patch}{context}", MessageLevel.INFO, task_name
|
|
1464
|
+
)
|
|
1465
|
+
if archive.files_dir is not None:
|
|
1466
|
+
self._log_activity(
|
|
1467
|
+
f"Archived {len(archive.files)} task file(s) to {archive.files_dir}{context}",
|
|
1468
|
+
MessageLevel.INFO,
|
|
1469
|
+
task_name,
|
|
1470
|
+
)
|
|
1471
|
+
if notes:
|
|
1472
|
+
self.notify(
|
|
1473
|
+
f"{len(notes)} task file note(s) - see messages",
|
|
1474
|
+
severity="warning",
|
|
1475
|
+
timeout=10,
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1436
1478
|
def _handle_safe_delete_action(
|
|
1437
1479
|
self, task: Task, safety_report: TaskSafetyReport, action: str | None
|
|
1438
1480
|
) -> None:
|
|
@@ -1615,10 +1657,13 @@ class TaskTreeApp(App):
|
|
|
1615
1657
|
def _remove_worktree_worker(self, task: Task, worktree: Worktree, force: bool = False) -> None:
|
|
1616
1658
|
"""Remove a single worktree in a background thread (archive, git, rmtree)."""
|
|
1617
1659
|
error: str | None = None
|
|
1618
|
-
|
|
1660
|
+
archive: ArchiveResult | None = None
|
|
1619
1661
|
try:
|
|
1620
1662
|
try:
|
|
1621
|
-
|
|
1663
|
+
# Diff only: the task folder and its files stay in place
|
|
1664
|
+
archive = self.task_manager.archive_task(
|
|
1665
|
+
self._worktree_view(task, worktree), include_files=False
|
|
1666
|
+
)
|
|
1622
1667
|
except Exception as e:
|
|
1623
1668
|
# Loud but non-blocking, same policy as task deletion
|
|
1624
1669
|
detail = f"Archive failed for '{worktree.name}': {type(e).__name__}: {e}"
|
|
@@ -1628,7 +1673,7 @@ class TaskTreeApp(App):
|
|
|
1628
1673
|
except Exception as e:
|
|
1629
1674
|
error = f"{type(e).__name__}: {e}"
|
|
1630
1675
|
self.call_from_thread(
|
|
1631
|
-
self._apply_remove_worktree_result, task.name, worktree.name, force, error,
|
|
1676
|
+
self._apply_remove_worktree_result, task.name, worktree.name, force, error, archive
|
|
1632
1677
|
)
|
|
1633
1678
|
|
|
1634
1679
|
def _apply_remove_worktree_result(
|
|
@@ -1637,7 +1682,7 @@ class TaskTreeApp(App):
|
|
|
1637
1682
|
worktree_name: str,
|
|
1638
1683
|
force: bool,
|
|
1639
1684
|
error: str | None,
|
|
1640
|
-
|
|
1685
|
+
archive: ArchiveResult | None = None,
|
|
1641
1686
|
) -> None:
|
|
1642
1687
|
"""Report the worktree removal outcome and reload (main thread)."""
|
|
1643
1688
|
if error:
|
|
@@ -1647,12 +1692,8 @@ class TaskTreeApp(App):
|
|
|
1647
1692
|
MessageLevel.ERROR,
|
|
1648
1693
|
task_name,
|
|
1649
1694
|
)
|
|
1650
|
-
if
|
|
1651
|
-
self.
|
|
1652
|
-
f"Archived diff to {archive_path} before the failed delete",
|
|
1653
|
-
MessageLevel.INFO,
|
|
1654
|
-
task_name,
|
|
1655
|
-
)
|
|
1695
|
+
if archive is not None:
|
|
1696
|
+
self._log_archive(task_name, archive, " before the failed delete")
|
|
1656
1697
|
else:
|
|
1657
1698
|
self.notify(escape(f"Deleted worktree: {worktree_name}"))
|
|
1658
1699
|
suffix = " (force)" if force else ""
|
|
@@ -1661,8 +1702,8 @@ class TaskTreeApp(App):
|
|
|
1661
1702
|
MessageLevel.SUCCESS,
|
|
1662
1703
|
task_name,
|
|
1663
1704
|
)
|
|
1664
|
-
if
|
|
1665
|
-
self.
|
|
1705
|
+
if archive is not None:
|
|
1706
|
+
self._log_archive(task_name, archive)
|
|
1666
1707
|
self._load_tasks()
|
|
1667
1708
|
|
|
1668
1709
|
def _open_lazygit_for_task(self, task: Task, safety_report: TaskSafetyReport) -> None:
|