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.
- {taskledger-0.1.1 → taskledger-0.1.2}/API.md +1 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/CHANGELOG.md +24 -0
- {taskledger-0.1.1/taskledger.egg-info → taskledger-0.1.2}/PKG-INFO +13 -2
- {taskledger-0.1.1 → taskledger-0.1.2}/README.md +12 -1
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/api.rst +1 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/command_contract.rst +17 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/usage.rst +6 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/skills/taskledger/SKILL.md +9 -3
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/_version.py +3 -3
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/tasks.py +2 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli.py +35 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_misc.py +24 -1
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/command_inventory.py +1 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/doctor.py +27 -5
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/navigation.py +60 -11
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/tasks.py +234 -29
- {taskledger-0.1.1 → taskledger-0.1.2/taskledger.egg-info}/PKG-INFO +13 -2
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_doctor.py +25 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_approval_contract.py +42 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_plan_regeneration.py +88 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_release_changelog.py +6 -4
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_v2_cli.py +154 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.codecrate.toml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/codecov.yml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/pre-commit.yml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/python-publish.yml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.github/workflows/tests.yml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.gitignore +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.pre-commit-config.yaml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.readthedocs.yaml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.ruff.toml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/.taskledger.toml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/AGENTS.md +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/LICENSE +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/Makefile +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/architecture_taskledger_split.rst +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/build.sh +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/conf.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/full_task_cycle.rst +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/index.rst +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/multi_repo.rst +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/public_surface.rst +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/docs/requirements.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/pyproject.toml +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/setup.cfg +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/setup.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/__init__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/__main__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/__init__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/handoff.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/introductions.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/locks.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/plans.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/project.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/questions.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/releases.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/search.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/api/task_runs.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_actor.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_common.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_implement.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_migrate.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_plan.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_question.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_release.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_task.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/cli_validate.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/__init__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/models.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/policies.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/domain/states.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/errors.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/exchange.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/ids.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/launcher.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/py.typed +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/search.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/__init__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/actors.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/dashboard.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/handoff.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/handoff_lifecycle.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/phase5_lock_transfer.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/plan_lint.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/releases.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/serve_read_model.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/validation.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/services/web_dashboard.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/__init__.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/atomic.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/common.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/events.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/frontmatter.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/indexes.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/init.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/locks.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/meta.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/migrations.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/paths.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/project_config.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/repos.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/storage/task_store.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger/timeutils.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/SOURCES.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/dependency_links.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/entry_points.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/requires.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/taskledger.egg-info/top_level.txt +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/conftest.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_active_task.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_actor_harness_state.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_agent_session_protocol.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_cli_command_contract.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_cli_import_resilience.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_command_example_linter.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_command_inventory.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_delta_remaining_contracts.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_docs_and_skill.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_domain_policies.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_events.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_handoff_lifecycle.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_implementation_change_scan.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_json_contracts.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_legacy_cleanup_contracts.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_lifecycle_policies.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_locks_audit.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_models_v1_schema.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_lint.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_plan_todo_materialization.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_project_root_config.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_add_many.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_question_filter_answers.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_search.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_serve_dashboard.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_services_dashboard.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_sidecar_collections.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_bundle_layout.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_common.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_init.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_migration.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_storage_repos.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_task_events.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_cli_api_parity.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_taskledger_v2_exchange.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_tasks_service_static.py +0 -0
- {taskledger-0.1.1 → taskledger-0.1.2}/tests/test_todo_implementation_gate.py +0 -0
|
@@ -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.
|
|
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
|
+
[](https://pypi.org/project/taskledger/)
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+
[](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
|
-
|
|
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
|
+
[](https://pypi.org/project/taskledger/)
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
[](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
|
-
|
|
391
|
+
Keep this skill outside the Python package. No additional
|
|
392
|
+
`skills/taskledger/examples/` directory is required.
|
|
382
393
|
|
|
383
394
|
## Development
|
|
384
395
|
|
|
@@ -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.
|
|
94
|
-
18.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
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=
|
|
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
|
|
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
|
-
|
|
215
|
-
if
|
|
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
|
|
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,
|
|
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(
|
|
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};
|
|
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
|
-
|
|
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(
|
|
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
|
|