taskledger 0.1.1__tar.gz → 0.1.2__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 (146) hide show
  1. {taskledger-0.1.1 → taskledger-0.1.2}/API.md +1 -0
  2. {taskledger-0.1.1 → taskledger-0.1.2}/CHANGELOG.md +24 -0
  3. {taskledger-0.1.1/taskledger.egg-info → taskledger-0.1.2}/PKG-INFO +13 -2
  4. {taskledger-0.1.1 → taskledger-0.1.2}/README.md +12 -1
  5. {taskledger-0.1.1 → taskledger-0.1.2}/docs/api.rst +1 -0
  6. {taskledger-0.1.1 → taskledger-0.1.2}/docs/command_contract.rst +17 -0
  7. {taskledger-0.1.1 → taskledger-0.1.2}/docs/usage.rst +6 -0
  8. {taskledger-0.1.1 → taskledger-0.1.2}/skills/taskledger/SKILL.md +9 -3
  9. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/_version.py +3 -3
  10. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/tasks.py +2 -0
  11. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli.py +35 -0
  12. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_misc.py +24 -1
  13. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/command_inventory.py +1 -0
  14. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/doctor.py +27 -5
  15. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/navigation.py +60 -11
  16. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/tasks.py +234 -29
  17. {taskledger-0.1.1 → taskledger-0.1.2/taskledger.egg-info}/PKG-INFO +13 -2
  18. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_doctor.py +25 -0
  19. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_approval_contract.py +42 -0
  20. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_plan_regeneration.py +88 -0
  21. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_release_changelog.py +6 -4
  22. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_v2_cli.py +154 -0
  23. {taskledger-0.1.1 → taskledger-0.1.2}/.codecrate.toml +0 -0
  24. {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/codecov.yml +0 -0
  25. {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/pre-commit.yml +0 -0
  26. {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/python-publish.yml +0 -0
  27. {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/tests.yml +0 -0
  28. {taskledger-0.1.1 → taskledger-0.1.2}/.gitignore +0 -0
  29. {taskledger-0.1.1 → taskledger-0.1.2}/.pre-commit-config.yaml +0 -0
  30. {taskledger-0.1.1 → taskledger-0.1.2}/.readthedocs.yaml +0 -0
  31. {taskledger-0.1.1 → taskledger-0.1.2}/.ruff.toml +0 -0
  32. {taskledger-0.1.1 → taskledger-0.1.2}/.taskledger.toml +0 -0
  33. {taskledger-0.1.1 → taskledger-0.1.2}/AGENTS.md +0 -0
  34. {taskledger-0.1.1 → taskledger-0.1.2}/LICENSE +0 -0
  35. {taskledger-0.1.1 → taskledger-0.1.2}/docs/Makefile +0 -0
  36. {taskledger-0.1.1 → taskledger-0.1.2}/docs/architecture_taskledger_split.rst +0 -0
  37. {taskledger-0.1.1 → taskledger-0.1.2}/docs/build.sh +0 -0
  38. {taskledger-0.1.1 → taskledger-0.1.2}/docs/conf.py +0 -0
  39. {taskledger-0.1.1 → taskledger-0.1.2}/docs/full_task_cycle.rst +0 -0
  40. {taskledger-0.1.1 → taskledger-0.1.2}/docs/index.rst +0 -0
  41. {taskledger-0.1.1 → taskledger-0.1.2}/docs/multi_repo.rst +0 -0
  42. {taskledger-0.1.1 → taskledger-0.1.2}/docs/public_surface.rst +0 -0
  43. {taskledger-0.1.1 → taskledger-0.1.2}/docs/requirements.txt +0 -0
  44. {taskledger-0.1.1 → taskledger-0.1.2}/pyproject.toml +0 -0
  45. {taskledger-0.1.1 → taskledger-0.1.2}/setup.cfg +0 -0
  46. {taskledger-0.1.1 → taskledger-0.1.2}/setup.py +0 -0
  47. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/__init__.py +0 -0
  48. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/__main__.py +0 -0
  49. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/__init__.py +0 -0
  50. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/handoff.py +0 -0
  51. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/introductions.py +0 -0
  52. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/locks.py +0 -0
  53. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/plans.py +0 -0
  54. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/project.py +0 -0
  55. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/questions.py +0 -0
  56. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/releases.py +0 -0
  57. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/search.py +0 -0
  58. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/task_runs.py +0 -0
  59. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_actor.py +0 -0
  60. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_common.py +0 -0
  61. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_implement.py +0 -0
  62. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_migrate.py +0 -0
  63. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_plan.py +0 -0
  64. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_question.py +0 -0
  65. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_release.py +0 -0
  66. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_task.py +0 -0
  67. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_validate.py +0 -0
  68. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/__init__.py +0 -0
  69. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/models.py +0 -0
  70. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/policies.py +0 -0
  71. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/states.py +0 -0
  72. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/errors.py +0 -0
  73. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/exchange.py +0 -0
  74. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/ids.py +0 -0
  75. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/launcher.py +0 -0
  76. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/py.typed +0 -0
  77. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/search.py +0 -0
  78. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/__init__.py +0 -0
  79. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/actors.py +0 -0
  80. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/dashboard.py +0 -0
  81. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/handoff.py +0 -0
  82. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/handoff_lifecycle.py +0 -0
  83. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/phase5_lock_transfer.py +0 -0
  84. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/plan_lint.py +0 -0
  85. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/releases.py +0 -0
  86. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/serve_read_model.py +0 -0
  87. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/validation.py +0 -0
  88. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/web_dashboard.py +0 -0
  89. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/__init__.py +0 -0
  90. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/atomic.py +0 -0
  91. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/common.py +0 -0
  92. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/events.py +0 -0
  93. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/frontmatter.py +0 -0
  94. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/indexes.py +0 -0
  95. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/init.py +0 -0
  96. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/locks.py +0 -0
  97. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/meta.py +0 -0
  98. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/migrations.py +0 -0
  99. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/paths.py +0 -0
  100. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/project_config.py +0 -0
  101. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/repos.py +0 -0
  102. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/task_store.py +0 -0
  103. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/timeutils.py +0 -0
  104. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/SOURCES.txt +0 -0
  105. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/dependency_links.txt +0 -0
  106. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/entry_points.txt +0 -0
  107. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/requires.txt +0 -0
  108. {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/top_level.txt +0 -0
  109. {taskledger-0.1.1 → taskledger-0.1.2}/tests/conftest.py +0 -0
  110. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_active_task.py +0 -0
  111. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_actor_harness_state.py +0 -0
  112. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_agent_session_protocol.py +0 -0
  113. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_cli_command_contract.py +0 -0
  114. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_cli_import_resilience.py +0 -0
  115. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_command_example_linter.py +0 -0
  116. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_command_inventory.py +0 -0
  117. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_delta_remaining_contracts.py +0 -0
  118. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_docs_and_skill.py +0 -0
  119. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_domain_policies.py +0 -0
  120. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_events.py +0 -0
  121. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_handoff_lifecycle.py +0 -0
  122. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_implementation_change_scan.py +0 -0
  123. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_json_contracts.py +0 -0
  124. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_legacy_cleanup_contracts.py +0 -0
  125. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_lifecycle_policies.py +0 -0
  126. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_locks_audit.py +0 -0
  127. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_models_v1_schema.py +0 -0
  128. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_lint.py +0 -0
  129. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_todo_materialization.py +0 -0
  130. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_project_root_config.py +0 -0
  131. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_add_many.py +0 -0
  132. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_filter_answers.py +0 -0
  133. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_search.py +0 -0
  134. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_serve_dashboard.py +0 -0
  135. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_services_dashboard.py +0 -0
  136. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_sidecar_collections.py +0 -0
  137. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_bundle_layout.py +0 -0
  138. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_common.py +0 -0
  139. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_init.py +0 -0
  140. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_migration.py +0 -0
  141. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_repos.py +0 -0
  142. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_task_events.py +0 -0
  143. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_cli_api_parity.py +0 -0
  144. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_v2_exchange.py +0 -0
  145. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_tasks_service_static.py +0 -0
  146. {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_todo_implementation_gate.py +0 -0
@@ -86,6 +86,7 @@ from taskledger.errors import (
86
86
  - `can_perform`
87
87
  - `reindex`
88
88
  - `repair_task_record`
89
+ - `repair_orphaned_planning_run`
89
90
 
90
91
  ### `taskledger.api.plans`
91
92
 
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.1.1 - 2026-04-29
4
+
5
+ ### Added
6
+
7
+ - Added `task follow-up` to create linked post-completion child tasks, preserve closure metadata, and show parent and follow-up relationships in task and handoff views.
8
+ - Added durable release records and a new `taskledger release` command group with `tag`, `list`, `show`, and `changelog`, including export/import support and public API coverage.
9
+ - Added planning helpers with `question add-many`, `plan template`, richer regeneration hints in `next-action`, and recovery commands for orphaned implementation work with `implement resume`, `task uncancel`, and `can implement-resume`.
10
+
11
+ ### Changed
12
+
13
+ - Hardened CLI startup so optional command-group import failures no longer block core commands, and launcher failures return structured diagnostics.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed recovery guidance for uncancelled tasks with orphaned implementation runs so `next-action` and `can implement` recommend `implement resume` instead of a fresh start.
18
+
19
+ ### Documentation
20
+
21
+ - Documented release tagging, changelog generation, planning helpers, follow-up task workflow, and recovery semantics across README, RST docs, API docs, and the taskledger skill.
22
+
23
+ ### Quality
24
+
25
+ - Expanded regression coverage for follow-up tasks, release workflow, CLI import resilience, planning helpers, and implementation recovery. Repo-wide pytest, ruff, and mypy passed.
26
+
3
27
  ## v0.1.0 - 2026-04-29
4
28
 
5
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskledger
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Durable project-state storage and CLI for coding workflows
5
5
  Author: Taskledger Contributors
6
6
  Maintainer: Holger Nahrstaedt
@@ -40,6 +40,11 @@ Provides-Extra: rich
40
40
  Requires-Dist: rich; extra == "rich"
41
41
  Dynamic: license-file
42
42
 
43
+ [![PyPI - Version](https://img.shields.io/pypi/v/taskledger)](https://pypi.org/project/taskledger/)
44
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/taskledger)
45
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/taskledger)
46
+ [![codecov](https://codecov.io/gh/holgern/taskledger/graph/badge.svg?token=6usFHwM5Ul)](https://codecov.io/gh/holgern/taskledger)
47
+
43
48
  # taskledger
44
49
 
45
50
  `taskledger` is a task-first durable state layer for staged coding work. It keeps
@@ -414,13 +419,19 @@ taskledger snapshot ./artifacts
414
419
 
415
420
  ## Skill packaging
416
421
 
422
+ Agent workflows work best when the `taskledger` skill is installed in the
423
+ coding harness. The CLI has a task-first lifecycle with explicit planning,
424
+ approval, implementation, validation, locks, and handoff gates; without the
425
+ skill, an agent may not know the intended command sequence or gate semantics.
426
+
417
427
  The canonical skill file lives at:
418
428
 
419
429
  ```text
420
430
  skills/taskledger/SKILL.md
421
431
  ```
422
432
 
423
- No additional `skills/taskledger/examples/` directory is required.
433
+ Keep this skill outside the Python package. No additional
434
+ `skills/taskledger/examples/` directory is required.
424
435
 
425
436
  ## Development
426
437
 
@@ -1,3 +1,8 @@
1
+ [![PyPI - Version](https://img.shields.io/pypi/v/taskledger)](https://pypi.org/project/taskledger/)
2
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/taskledger)
3
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/taskledger)
4
+ [![codecov](https://codecov.io/gh/holgern/taskledger/graph/badge.svg?token=6usFHwM5Ul)](https://codecov.io/gh/holgern/taskledger)
5
+
1
6
  # taskledger
2
7
 
3
8
  `taskledger` is a task-first durable state layer for staged coding work. It keeps
@@ -372,13 +377,19 @@ taskledger snapshot ./artifacts
372
377
 
373
378
  ## Skill packaging
374
379
 
380
+ Agent workflows work best when the `taskledger` skill is installed in the
381
+ coding harness. The CLI has a task-first lifecycle with explicit planning,
382
+ approval, implementation, validation, locks, and handoff gates; without the
383
+ skill, an agent may not know the intended command sequence or gate semantics.
384
+
375
385
  The canonical skill file lives at:
376
386
 
377
387
  ```text
378
388
  skills/taskledger/SKILL.md
379
389
  ```
380
390
 
381
- No additional `skills/taskledger/examples/` directory is required.
391
+ Keep this skill outside the Python package. No additional
392
+ `skills/taskledger/examples/` directory is required.
382
393
 
383
394
  ## Development
384
395
 
@@ -66,6 +66,7 @@ Task API
66
66
  - ``can_perform``
67
67
  - ``reindex``
68
68
  - ``repair_task_record``
69
+ - ``repair_orphaned_planning_run``
69
70
 
70
71
  Plan API
71
72
  --------
@@ -114,9 +114,26 @@ status but ``active_stage`` is missing, ``next-action`` must not report
114
114
  ``The task is cancelled.`` For an orphaned implementation with a still-running
115
115
  latest implementation run and no active lock, it should direct agents to
116
116
  ``taskledger implement resume --task TASK_REF --reason "..."``.
117
+ For an approved task with a non-implementation run still marked running,
118
+ ``next-action`` must not direct agents to ``taskledger implement start``.
119
+ It should report a repair-oriented action and point to ``taskledger doctor``.
117
120
  Truly cancelled tasks recover through ``taskledger task uncancel --reason "..."``
118
121
  to a durable non-active stage rather than directly re-entering an active stage.
119
122
 
123
+ Run and lock repair
124
+ -------------------
125
+
126
+ ``taskledger doctor`` and ``taskledger doctor locks`` report running runs without
127
+ matching active locks. Orphaned running planning runs can be finished only
128
+ through an explicit repair command with a reason:
129
+
130
+ .. code-block:: bash
131
+
132
+ taskledger repair run --task TASK_REF --run RUN_ID --reason "Planning was already completed."
133
+
134
+ The repair command refuses to finish non-planning runs, non-running runs, or
135
+ runs that still have a matching active lock.
136
+
120
137
  Post-completion follow-up deltas
121
138
  --------------------------------
122
139
 
@@ -9,6 +9,11 @@ Installation
9
9
  python -m pip install -e .
10
10
  python -m pip install -e ".[dev]"
11
11
 
12
+ Agent workflows work best when the ``taskledger`` skill is installed in the
13
+ coding harness. The CLI uses a task-first lifecycle with explicit planning,
14
+ approval, implementation, validation, locks, and handoff gates; without the
15
+ skill, an agent may not know the intended command sequence or gate semantics.
16
+
12
17
  Initialize state
13
18
  ----------------
14
19
 
@@ -125,6 +130,7 @@ over a broad generated context read:
125
130
 
126
131
  Rules for agents:
127
132
 
133
+ * Install the ``taskledger`` skill in the coding harness before relying on agent-driven workflows.
128
134
  * Prefer ``next-action`` and ``todo next`` over generated context during normal work.
129
135
  * Use the todo ``validation_hint`` before marking a todo done.
130
136
  * Record concise evidence with ``todo done``.
@@ -22,7 +22,7 @@ Use taskledger for staged coding work that needs a durable task record, reviewab
22
22
  - Do not mark validation passed without checking every mandatory acceptance criterion.
23
23
  - Do not inline large source files into taskledger records by default; use `@path` references.
24
24
  - Do not import or call `taskledger.storage.*`, `taskledger.services.*`, or `taskledger.domain.*` from ad-hoc Python during normal task work. Use CLI commands or public `taskledger.api.*` only.
25
- - Do not use repair commands (`lock break`, `repair lock`, `repair task`, `repair index`) in the normal lifecycle. Use them only after `doctor`/`lock show` proves there is stale or corrupted state.
25
+ - Do not use repair commands (`lock break`, `repair lock`, `repair run`, `repair task`, `repair index`) in the normal lifecycle. Use them only after `doctor`/`lock show` proves there is stale or corrupted state.
26
26
  - Do not pass approval escape hatches such as `--allow-empty-criteria`, `--allow-open-questions`, `--allow-empty-todos`, `--no-materialize-todos`, `--allow-lint-errors`, or `--allow-agent-approval` unless the user explicitly requested that bypass and gave a reason. All escape hatches require `--reason`.
27
27
 
28
28
  ## Fresh context entry protocol
@@ -90,8 +90,9 @@ If any `taskledger ...` command fails with a Python traceback before taskledger
90
90
  14. For diagnostic commands needed to build the plan, preserve their output in a linked artifact or use `taskledger plan command -- ...`.
91
91
  15. A proposed plan must include concrete `acceptance_criteria` and `todos` in front matter unless the user explicitly says the task is trivial.
92
92
  16. After writing the plan, do not run `taskledger lock break`; planning locks are released by plan proposal/upsert. Run `taskledger next-action`.
93
- 17. Before asking the user to approve, run `taskledger plan lint --version N` and fix lint errors. Do not ask for approval on plans with lint errors.
94
- 18. Record approval only with clear user intent such as approve, accept, go ahead, or start implementation: `taskledger plan approve --version N --actor user --note "User approved in harness: ..."` or `taskledger plan accept --version N --note "User approved in harness: ..."`.
93
+ 17. After `taskledger plan upsert --from-answers`, run `taskledger question status`. If it still reports `Plan regeneration needed: True`, do not ask for approval. Inspect `taskledger question answers`, `taskledger plan show --version N`, and `taskledger doctor`.
94
+ 18. Before asking the user to approve, run `taskledger plan lint --version N` and fix lint errors. Do not ask for approval on plans with lint errors.
95
+ 19. Record approval only with clear user intent such as approve, accept, go ahead, or start implementation: `taskledger plan approve --version N --actor user --note "User approved in harness: ..."` or `taskledger plan accept --version N --note "User approved in harness: ..."`.
95
96
 
96
97
  The plan file should use version ids like `plan-v1`, `plan-v2` in references. Do not use zero-padded forms.
97
98
 
@@ -100,6 +101,8 @@ The plan file should use version ids like `plan-v1`, `plan-v2` in references. Do
100
101
  1. `taskledger context --for implementation --format markdown`
101
102
  2. `taskledger implement start`
102
103
  - If validation already failed and the plan is still correct, prefer `taskledger implement restart --summary "Fix failed validation findings."`
104
+ - If implementation start fails because another run is already running, stop and run `taskledger doctor`, not only `taskledger doctor locks`.
105
+ - Do not edit project code until implementation has started or a valid implementation resume command succeeds.
103
106
  3. `taskledger implement checklist` - review the mandatory and optional todo checklist before starting.
104
107
  4. If no todos exist, create a concrete checklist: `taskledger todo add --text "..."`. Todo source is inferred automatically from the active lock: `implementer` during implementation, `planner` during planning, `user` otherwise.
105
108
  5. Work one todo at a time:
@@ -223,6 +226,9 @@ To receive work:
223
226
  - If breaking an implementation lock leaves a running implementation run behind, use `taskledger implement resume --reason "..."` instead of `implement start`.
224
227
  - If `task show` reports `status_stage=implementing` but `active_stage` is missing, do not run `task uncancel`; run `taskledger next-action` and resume when it reports `implement-resume`.
225
228
  - If a cancelled task is restored with `task uncancel`, run `taskledger next-action` before starting work. If the task still has a running implementation run, resume that run instead of starting a new one.
229
+ - If `taskledger next-action` recommends a command but `taskledger can <action>` rejects it, treat this as a lifecycle inconsistency. Run `taskledger doctor` and follow the repair guidance.
230
+ - Use `taskledger repair run --task TASK --run RUN --reason "..."` only when diagnostics identify an orphaned running planning run with no matching active lock.
231
+ - Never use repair to bypass approval, validation, or active implementation locks.
226
232
  - If validation fails, record the failure and return to implementation or replanning.
227
233
  - If indexes are stale, run `taskledger repair index`; `taskledger reindex` is a compatibility alias.
228
234
  - If a task is truly cancelled and the user wants to continue, use `taskledger task uncancel --reason "..." [--to STAGE]` to restore a safe durable stage before re-entering an active stage.
@@ -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 = '0.1.1'
22
- __version_tuple__ = version_tuple = (0, 1, 1)
21
+ __version__ = version = '0.1.2'
22
+ __version_tuple__ = version_tuple = (0, 1, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g5289096c8'
24
+ __commit_id__ = commit_id = 'g9eda38db5'
@@ -20,6 +20,7 @@ from taskledger.services.tasks import (
20
20
  reindex,
21
21
  remove_file_link,
22
22
  remove_requirement,
23
+ repair_orphaned_planning_run,
23
24
  repair_task_record,
24
25
  resolve_active_task,
25
26
  set_todo_done,
@@ -62,4 +63,5 @@ __all__ = [
62
63
  "can_perform",
63
64
  "reindex",
64
65
  "repair_task_record",
66
+ "repair_orphaned_planning_run",
65
67
  ]
@@ -518,6 +518,41 @@ def repair_task_command(
518
518
  emit_payload(ctx, payload, human="\n".join(human_lines))
519
519
 
520
520
 
521
+ @repair_app.command("run")
522
+ def repair_run_command(
523
+ ctx: typer.Context,
524
+ reason: Annotated[str, typer.Option("--reason")],
525
+ run_id: Annotated[str | None, typer.Option("--run")] = None,
526
+ task_ref: Annotated[
527
+ str | None,
528
+ typer.Option("--task", help="Task ref. Defaults to the active task."),
529
+ ] = None,
530
+ ) -> None:
531
+ from taskledger.api.tasks import repair_orphaned_planning_run
532
+
533
+ state = ctx.obj
534
+ assert isinstance(state, CLIState)
535
+ try:
536
+ task = resolve_cli_task(state.cwd, task_ref)
537
+ payload = repair_orphaned_planning_run(
538
+ state.cwd,
539
+ task.id,
540
+ run_id=run_id,
541
+ reason=reason,
542
+ )
543
+ except LaunchError as exc:
544
+ emit_error(ctx, exc)
545
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
546
+ emit_payload(
547
+ ctx,
548
+ payload,
549
+ human=(
550
+ f"finished orphaned {payload['run_type']} run {payload['run_id']} "
551
+ f"for {payload['task_id']}\nnext: {payload['next_command']}"
552
+ ),
553
+ )
554
+
555
+
521
556
  @repair_app.command("task-dirs")
522
557
  def repair_task_dirs_command(ctx: typer.Context) -> None:
523
558
  from taskledger.services.doctor import cleanup_orphan_slug_dirs
@@ -911,7 +911,7 @@ def emit_doctor_locks_command(ctx: typer.Context) -> None:
911
911
  emit_payload(
912
912
  ctx,
913
913
  payload,
914
- human=_expired_locks_human(payload["expired_locks"]),
914
+ human=_lock_inspection_human(payload),
915
915
  )
916
916
 
917
917
 
@@ -996,6 +996,29 @@ def _emit_handoff(
996
996
  emit_payload(ctx, payload, human=human)
997
997
 
998
998
 
999
+ def _lock_inspection_human(payload: dict[str, object]) -> str:
1000
+ expired = payload.get("expired_locks")
1001
+ mismatches = payload.get("run_lock_mismatches")
1002
+ lines = ["EXPIRED LOCKS"]
1003
+ if isinstance(expired, list) and expired:
1004
+ for item in expired:
1005
+ if isinstance(item, dict):
1006
+ lines.append(str(item.get("task_id")))
1007
+ else:
1008
+ lines.append("(empty)")
1009
+ lines.append("RUN/LOCK MISMATCHES")
1010
+ if isinstance(mismatches, list) and mismatches:
1011
+ for item in mismatches:
1012
+ if isinstance(item, dict):
1013
+ lines.append(
1014
+ f"{item.get('task_id')} {item.get('run_type')} "
1015
+ f"{item.get('run_id')} next: {item.get('next_command')}"
1016
+ )
1017
+ else:
1018
+ lines.append("(empty)")
1019
+ return "\n".join(lines)
1020
+
1021
+
999
1022
  def _expired_locks_human(payload: object) -> str:
1000
1023
  if not isinstance(payload, list) or not payload:
1001
1024
  return "EXPIRED LOCKS\n(empty)"
@@ -126,6 +126,7 @@ COMMAND_METADATA: dict[str, tuple[str, str]] = {
126
126
  "doctor indexes": (REPAIR, "safe_read_only"),
127
127
  "repair index": (REPAIR, "ledger_mutation"),
128
128
  "repair lock": (REPAIR, "ledger_mutation"),
129
+ "repair run": (REPAIR, "ledger_mutation"),
129
130
  "repair task": (REPAIR, "ledger_mutation"),
130
131
  "repair task-dirs": (REPAIR, "ledger_mutation"),
131
132
  "migrate status": (STABLE_FOR_AGENTS, "safe_read_only"),
@@ -39,6 +39,7 @@ def inspect_v2_project(workspace_root: Path) -> dict[str, object]: # noqa: C901
39
39
  errors: list[str] = []
40
40
  warnings: list[str] = []
41
41
  repair_hints: list[str] = []
42
+ run_lock_mismatches: list[dict[str, object]] = []
42
43
  config_candidates = [
43
44
  resolved_paths.workspace_root / filename
44
45
  for filename in PROJECT_CONFIG_FILENAMES
@@ -175,6 +176,24 @@ def inspect_v2_project(workspace_root: Path) -> dict[str, object]: # noqa: C901
175
176
  errors.append(
176
177
  f"Task {task.id} has a running run without a matching active lock."
177
178
  )
179
+ for run in running_runs:
180
+ next_command = "taskledger doctor"
181
+ if run.run_type == "planning":
182
+ next_command = (
183
+ "taskledger repair run "
184
+ f"--task {task.id} --run {run.run_id} "
185
+ '--reason "Finish orphaned planning run."'
186
+ )
187
+ run_lock_mismatches.append(
188
+ {
189
+ "kind": "running_run_without_matching_lock",
190
+ "task_id": task.id,
191
+ "run_id": run.run_id,
192
+ "run_type": run.run_type,
193
+ "status": run.status,
194
+ "next_command": next_command,
195
+ }
196
+ )
178
197
  running_implementation = next(
179
198
  (
180
199
  run
@@ -198,7 +217,7 @@ def inspect_v2_project(workspace_root: Path) -> dict[str, object]: # noqa: C901
198
217
  )
199
218
  else:
200
219
  repair_hints.append(
201
- "Inspect the run/lock pair and either repair it or break the lock "
220
+ "Inspect the run/lock pair and repair the orphaned run "
202
221
  f"for task {task.id} explicitly."
203
222
  )
204
223
  if active_lock is not None and active_stage is None and not running_runs:
@@ -211,13 +230,13 @@ def inspect_v2_project(workspace_root: Path) -> dict[str, object]: # noqa: C901
211
230
  )
212
231
 
213
232
  for change in list_changes(workspace_root, task.id):
214
- run = run_map.get((task.id, change.implementation_run))
215
- if run is None:
233
+ change_run = run_map.get((task.id, change.implementation_run))
234
+ if change_run is None:
216
235
  errors.append(
217
236
  f"Change {change.change_id} references missing "
218
237
  f"implementation run {change.implementation_run}."
219
238
  )
220
- elif run.run_type != "implementation":
239
+ elif change_run.run_type != "implementation":
221
240
  errors.append(
222
241
  f"Change {change.change_id} references "
223
242
  f"non-implementation run {change.implementation_run}."
@@ -346,16 +365,19 @@ def inspect_v2_project(workspace_root: Path) -> dict[str, object]: # noqa: C901
346
365
  "repair_hints": repair_hints,
347
366
  "broken_links": broken_links,
348
367
  "expired_locks": expired_locks,
368
+ "run_lock_mismatches": run_lock_mismatches,
349
369
  }
350
370
 
351
371
 
352
372
  def inspect_v2_locks(workspace_root: Path) -> dict[str, object]:
353
373
  payload = inspect_v2_project(workspace_root)
354
374
  expired_locks = list(cast(list[object], payload["expired_locks"]))
375
+ run_lock_mismatches = list(cast(list[object], payload["run_lock_mismatches"]))
355
376
  return {
356
377
  "kind": "taskledger_lock_inspection",
357
- "healthy": not expired_locks,
378
+ "healthy": not expired_locks and not run_lock_mismatches,
358
379
  "expired_locks": expired_locks,
380
+ "run_lock_mismatches": run_lock_mismatches,
359
381
  }
360
382
 
361
383
 
@@ -26,6 +26,8 @@ from taskledger.services.tasks import (
26
26
  _optional_run,
27
27
  _planning_template_hints,
28
28
  _resumable_implementation_run,
29
+ _running_run_details,
30
+ _running_runs,
29
31
  _task_active_stage,
30
32
  _task_with_sidecars,
31
33
  _todo_command_hints,
@@ -322,6 +324,38 @@ def _inactive_status_next_action(
322
324
  )
323
325
  if task.status_stage == "approved":
324
326
  next_item = _task_next_item(task)
327
+ running_runs = _running_runs(workspace_root, task)
328
+ non_resumable_runs = [
329
+ run
330
+ for run in running_runs
331
+ if not (
332
+ run.run_type == "implementation"
333
+ and run.run_id == task.latest_implementation_run
334
+ and lock is None
335
+ )
336
+ ]
337
+ if non_resumable_runs:
338
+ run = non_resumable_runs[0]
339
+ blockers.append(
340
+ {
341
+ "kind": "running_run",
342
+ "message": (
343
+ f"Task has running {run.run_type} run {run.run_id}; "
344
+ "run `taskledger doctor`."
345
+ ),
346
+ **_running_run_details(task, run, lock),
347
+ }
348
+ )
349
+ return (
350
+ "repair-run-state",
351
+ (
352
+ f"Task is approved, but {run.run_type} run {run.run_id} "
353
+ "is still marked running."
354
+ ),
355
+ next_item,
356
+ blockers,
357
+ progress,
358
+ )
325
359
  resumable_run = _resumable_implementation_run(
326
360
  workspace_root,
327
361
  task,
@@ -416,7 +450,7 @@ def can_perform(workspace_root: Path, task_ref: str, action: str) -> dict[str, o
416
450
  active_stage = _task_active_stage(workspace_root, task, lock=lock)
417
451
  ok = False
418
452
  reason = ""
419
- blocking: list[dict[str, str]] = []
453
+ blocking: list[dict[str, object]] = []
420
454
  if action == "plan":
421
455
  ok = task.status_stage in {"draft", "plan_review"} and lock is None
422
456
  reason = (
@@ -437,11 +471,7 @@ def can_perform(workspace_root: Path, task_ref: str, action: str) -> dict[str, o
437
471
  }
438
472
  )
439
473
  elif action == "implement":
440
- running_runs = [
441
- item
442
- for item in list_runs(workspace_root, task.id)
443
- if item.status == "running"
444
- ]
474
+ running_runs = _running_runs(workspace_root, task)
445
475
  ok = (
446
476
  task.status_stage in IMPLEMENTABLE_TASK_STAGES
447
477
  and task.accepted_plan_version is not None
@@ -462,17 +492,33 @@ def can_perform(workspace_root: Path, task_ref: str, action: str) -> dict[str, o
462
492
  blocking.append(
463
493
  {"kind": "approval", "message": "No accepted plan version."}
464
494
  )
465
- blocking.extend(_dependency_blockers(workspace_root, task))
495
+ blocking.extend(
496
+ cast(list[dict[str, object]], _dependency_blockers(workspace_root, task))
497
+ )
466
498
  if running_runs:
467
499
  running_run = running_runs[0]
500
+ can_resume = (
501
+ running_run.run_type == "implementation"
502
+ and running_run.run_id == task.latest_implementation_run
503
+ )
468
504
  blocking.append(
469
505
  {
470
506
  "kind": "implementation",
471
507
  "message": (
472
508
  f"Task already has running {running_run.run_type} run "
473
- f"{running_run.run_id}; use taskledger implement resume."
509
+ f"{running_run.run_id}; "
510
+ + (
511
+ "use taskledger implement resume."
512
+ if can_resume
513
+ else "run taskledger doctor."
514
+ )
515
+ ),
516
+ "command_hint": (
517
+ _implement_resume_command(task.id)
518
+ if can_resume
519
+ else "taskledger doctor"
474
520
  ),
475
- "command_hint": _implement_resume_command(task.id),
521
+ **_running_run_details(task, running_run, lock),
476
522
  }
477
523
  )
478
524
  if lock is not None:
@@ -534,7 +580,7 @@ def can_perform(workspace_root: Path, task_ref: str, action: str) -> dict[str, o
534
580
  "message": "No running implementation run is available to resume.",
535
581
  }
536
582
  )
537
- blocking.extend(dependency_blockers)
583
+ blocking.extend(cast(list[dict[str, object]], dependency_blockers))
538
584
  if lock is not None:
539
585
  blocking.append(
540
586
  {
@@ -599,7 +645,9 @@ def can_perform(workspace_root: Path, task_ref: str, action: str) -> dict[str, o
599
645
  "message": "No previous implementation run is available.",
600
646
  }
601
647
  )
602
- blocking.extend(_dependency_blockers(workspace_root, task))
648
+ blocking.extend(
649
+ cast(list[dict[str, object]], _dependency_blockers(workspace_root, task))
650
+ )
603
651
  if lock is not None:
604
652
  blocking.append(
605
653
  {
@@ -952,6 +1000,7 @@ def _next_action_command(action: str) -> str | None:
952
1000
  "taskledger validate finish --result passed --summary SUMMARY"
953
1001
  ),
954
1002
  "repair-lock": "taskledger lock show",
1003
+ "repair-run-state": "taskledger doctor",
955
1004
  }.get(action)
956
1005
 
957
1006