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.
Files changed (78) hide show
  1. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.gitignore +0 -1
  2. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/CHANGELOG.md +15 -0
  3. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/CONTRIBUTING.md +7 -10
  4. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/PKG-INFO +1 -1
  5. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/README.md +40 -16
  6. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/configuration.md +2 -2
  7. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/troubleshooting.md +1 -1
  8. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/user-guide.md +6 -0
  9. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/pyproject.toml +1 -1
  10. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/_version.py +2 -2
  11. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/app.py +69 -28
  12. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/cli.py +264 -29
  13. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/config.py +19 -11
  14. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/models.py +81 -5
  15. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/task_manager.py +463 -81
  16. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_app_review_fixes.py +97 -2
  17. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_cli.py +88 -0
  18. tasktree_manager-2.14.0/tests/test_epics.py +430 -0
  19. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_medium_fixes.py +1 -1
  20. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_task_manager.py +386 -24
  21. tasktree_manager-2.13.2/OPENSPEC.md +0 -457
  22. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/SKILL.md +0 -0
  23. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/README.md +0 -0
  24. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
  25. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
  26. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
  27. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
  28. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
  29. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
  30. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/styling.md +0 -0
  31. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
  32. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/templates/CHANGELOG.md.j2 +0 -0
  33. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/workflows/ci.yml +0 -0
  34. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/.github/workflows/release.yml +0 -0
  35. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/LICENSE +0 -0
  36. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/ROADMAP.md +0 -0
  37. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/claude-code-integration-spec.md +0 -0
  38. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/docs/installation.md +0 -0
  39. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/mise.toml +0 -0
  40. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/scripts/bump_version.py +0 -0
  41. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/__init__.py +0 -0
  42. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/commands.py +0 -0
  43. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/__init__.py +0 -0
  44. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/agent_sessions.py +0 -0
  45. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/claude_hooks.py +0 -0
  46. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/claude_sessions.py +0 -0
  47. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/forge.py +0 -0
  48. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/services/git_ops.py +0 -0
  49. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/themes.py +0 -0
  50. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/__init__.py +0 -0
  51. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/app_header.py +0 -0
  52. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/create_modal.py +0 -0
  53. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/messages_panel.py +0 -0
  54. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/setup_modal.py +0 -0
  55. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/status_panel.py +0 -0
  56. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/task_list.py +0 -0
  57. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tasktree_manager/widgets/worktree_list.py +0 -0
  58. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/__init__.py +0 -0
  59. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/conftest.py +0 -0
  60. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_agent_sessions.py +0 -0
  61. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_agent_sessions_decode.py +0 -0
  62. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_app.py +0 -0
  63. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_bump_version.py +0 -0
  64. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_claude_hooks.py +0 -0
  65. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_claude_sessions.py +0 -0
  66. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_commands.py +0 -0
  67. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_config.py +0 -0
  68. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_forge.py +0 -0
  69. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_git_ops.py +0 -0
  70. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_integration_git.py +0 -0
  71. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_list_selection.py +0 -0
  72. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_memory.py +0 -0
  73. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_messages_panel.py +0 -0
  74. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_modals.py +0 -0
  75. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_safety_fixes.py +0 -0
  76. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_setup_modal.py +0 -0
  77. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_status_panel.py +0 -0
  78. {tasktree_manager-2.13.2 → tasktree_manager-2.14.0}/tests/test_worktree_badges.py +0 -0
@@ -53,7 +53,6 @@ tasktree_manager/_version.py
53
53
 
54
54
  # Documentation (tracked separately)
55
55
  PRD.md
56
- specs/
57
56
  CLAUDE.md
58
57
 
59
58
  # uv project-mode lockfile — tooling installs via `uv pip`/pip from pyproject.toml,
@@ -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/yourusername/tasktree-manager.git
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 OPENSPEC.md for architecture changes
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. **Check specs**: Review existing specs in `specs/`
216
- 2. **Create spec**: Write a new spec (SPEC-XXX) if needed
217
- 3. **Write tests**: Add tests first (TDD approach)
218
- 4. **Implement**: Write the code
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/yourusername/tasktree-manager/discussions)
260
+ - Open a [Discussion](https://github.com/ruslanfialkovskii/tasktree-manager/discussions)
264
261
  - Ask in Pull Request
265
262
  - Check existing issues
266
263
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tasktree-manager
3
- Version: 2.13.2
3
+ Version: 2.14.0
4
4
  Summary: TUI application for managing worktree-based tasks
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
- [![Tests](https://img.shields.io/badge/tests-228%20passed-green.svg)](./tests/)
7
+ [![Tests](https://img.shields.io/badge/tests-pytest-green.svg)](./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/yourusername/tasktree-manager.git
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 with diff archiving, `status` subcommand for scripts and statuslines.
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
- Upcoming:
203
- - **v0.3.0** - Search/filter, task templates
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 [ROADMAP.md](ROADMAP.md) and [CHANGELOG.md](CHANGELOG.md) for details.
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/yourusername/tasktree-manager/issues)
222
- - 💡 [Feature Requests](https://github.com/yourusername/tasktree-manager/issues)
223
- - 💬 [Discussions](https://github.com/yourusername/tasktree-manager/discussions)
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/yourusername/tasktree-manager)
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 diff archives
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 diff archives |
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 is archived under `archive_dir` first
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.13.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.13.2'
22
- __version_tuple__ = version_tuple = (2, 13, 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) is
1382
- archived first — finish_task rmtrees the task directory.
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
- archive_path: Path | None = None
1396
+ archive: ArchiveResult | None = None
1386
1397
  try:
1387
1398
  try:
1388
- archive_path = self.task_manager.archive_task(task)
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, archive_path)
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
- archive_path: Path | None = None,
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 archive_path is not None:
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._log_activity(
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 archive_path is not None:
1433
- self._log_activity(f"Archived diff to {archive_path}", MessageLevel.INFO, task_name)
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
- archive_path: Path | None = None
1660
+ archive: ArchiveResult | None = None
1619
1661
  try:
1620
1662
  try:
1621
- archive_path = self.task_manager.archive_task(self._worktree_view(task, worktree))
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, archive_path
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
- archive_path: Path | None = None,
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 archive_path is not None:
1651
- self._log_activity(
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 archive_path is not None:
1665
- self._log_activity(f"Archived diff to {archive_path}", MessageLevel.INFO, task_name)
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: