taskledger 0.1.0__py3-none-any.whl

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 (67) hide show
  1. taskledger/__init__.py +5 -0
  2. taskledger/__main__.py +6 -0
  3. taskledger/_version.py +24 -0
  4. taskledger/api/__init__.py +13 -0
  5. taskledger/api/handoff.py +247 -0
  6. taskledger/api/introductions.py +9 -0
  7. taskledger/api/locks.py +4 -0
  8. taskledger/api/plans.py +31 -0
  9. taskledger/api/project.py +185 -0
  10. taskledger/api/questions.py +19 -0
  11. taskledger/api/search.py +87 -0
  12. taskledger/api/task_runs.py +38 -0
  13. taskledger/api/tasks.py +61 -0
  14. taskledger/cli.py +600 -0
  15. taskledger/cli_actor.py +196 -0
  16. taskledger/cli_common.py +617 -0
  17. taskledger/cli_implement.py +409 -0
  18. taskledger/cli_migrate.py +328 -0
  19. taskledger/cli_misc.py +984 -0
  20. taskledger/cli_plan.py +478 -0
  21. taskledger/cli_question.py +350 -0
  22. taskledger/cli_task.py +257 -0
  23. taskledger/cli_validate.py +285 -0
  24. taskledger/command_inventory.py +125 -0
  25. taskledger/domain/__init__.py +2 -0
  26. taskledger/domain/models.py +1697 -0
  27. taskledger/domain/policies.py +542 -0
  28. taskledger/domain/states.py +320 -0
  29. taskledger/errors.py +165 -0
  30. taskledger/exchange.py +343 -0
  31. taskledger/ids.py +19 -0
  32. taskledger/py.typed +0 -0
  33. taskledger/search.py +349 -0
  34. taskledger/services/__init__.py +1 -0
  35. taskledger/services/actors.py +245 -0
  36. taskledger/services/dashboard.py +306 -0
  37. taskledger/services/doctor.py +435 -0
  38. taskledger/services/handoff.py +1029 -0
  39. taskledger/services/handoff_lifecycle.py +154 -0
  40. taskledger/services/navigation.py +930 -0
  41. taskledger/services/phase5_lock_transfer.py +96 -0
  42. taskledger/services/plan_lint.py +397 -0
  43. taskledger/services/serve_read_model.py +852 -0
  44. taskledger/services/tasks.py +4224 -0
  45. taskledger/services/validation.py +221 -0
  46. taskledger/services/web_dashboard.py +1742 -0
  47. taskledger/storage/__init__.py +39 -0
  48. taskledger/storage/atomic.py +57 -0
  49. taskledger/storage/common.py +90 -0
  50. taskledger/storage/events.py +98 -0
  51. taskledger/storage/frontmatter.py +57 -0
  52. taskledger/storage/indexes.py +42 -0
  53. taskledger/storage/init.py +187 -0
  54. taskledger/storage/locks.py +83 -0
  55. taskledger/storage/meta.py +103 -0
  56. taskledger/storage/migrations.py +207 -0
  57. taskledger/storage/paths.py +166 -0
  58. taskledger/storage/project_config.py +393 -0
  59. taskledger/storage/repos.py +256 -0
  60. taskledger/storage/task_store.py +836 -0
  61. taskledger/timeutils.py +7 -0
  62. taskledger-0.1.0.dist-info/METADATA +411 -0
  63. taskledger-0.1.0.dist-info/RECORD +67 -0
  64. taskledger-0.1.0.dist-info/WHEEL +5 -0
  65. taskledger-0.1.0.dist-info/entry_points.txt +2 -0
  66. taskledger-0.1.0.dist-info/licenses/LICENSE +201 -0
  67. taskledger-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, cast
4
+
5
+ from taskledger.errors import LaunchError
6
+
7
+ TASKLEDGER_SCHEMA_VERSION = 1
8
+ TASKLEDGER_V2_FILE_VERSION = "v2"
9
+ TASKLEDGER_STORAGE_LAYOUT_VERSION = 2
10
+ TASKLEDGER_RECORD_SCHEMA_VERSION = TASKLEDGER_SCHEMA_VERSION
11
+ TASKLEDGER_TASK_FILE_VERSION = TASKLEDGER_V2_FILE_VERSION
12
+ OBJECT_TYPES = frozenset(
13
+ {
14
+ "task",
15
+ "plan",
16
+ "question",
17
+ "run",
18
+ "validation",
19
+ "change",
20
+ "intro",
21
+ "lock",
22
+ "event",
23
+ "todo",
24
+ "todos",
25
+ "link",
26
+ "links",
27
+ "requirement",
28
+ "requirements",
29
+ "handoff",
30
+ }
31
+ )
32
+
33
+ ActorType = Literal["agent", "user", "system"]
34
+ ActorRole = Literal["planner", "implementer", "validator", "reviewer", "operator"]
35
+ HarnessKind = Literal["agent_harness", "manual", "ci", "unknown"]
36
+ HandoffMode = Literal["planning", "implementation", "validation", "review", "full"]
37
+ ContextFor = Literal[
38
+ "planner",
39
+ "implementer",
40
+ "validator",
41
+ "reviewer",
42
+ "spec-reviewer",
43
+ "code-reviewer",
44
+ "full",
45
+ ]
46
+ ContextScope = Literal["task", "todo", "run"]
47
+ ContextFormat = Literal["markdown", "json", "text"]
48
+ HandoffStatus = Literal["open", "claimed", "closed", "cancelled"]
49
+ LockPolicy = Literal["none", "retain", "release", "transfer"]
50
+ TodoSource = Literal["user", "planner", "implementer", "plan"]
51
+
52
+ TaskStatusStage = Literal[
53
+ "draft",
54
+ "planning",
55
+ "plan_review",
56
+ "approved",
57
+ "implementing",
58
+ "implemented",
59
+ "validating",
60
+ "done",
61
+ "failed_validation",
62
+ "cancelled",
63
+ ]
64
+ ActiveTaskStatusStage = Literal["planning", "implementing", "validating"]
65
+ RunType = Literal["planning", "implementation", "validation"]
66
+ PlanStatus = Literal["draft", "proposed", "accepted", "superseded", "rejected"]
67
+ QuestionStatus = Literal["open", "answered", "dismissed"]
68
+ RunStatus = Literal[
69
+ "running", "paused", "finished", "passed", "failed", "blocked", "aborted"
70
+ ]
71
+ ValidationResult = Literal["passed", "failed", "blocked"]
72
+ ValidationCheckStatus = Literal["pass", "fail", "warn", "not_run"]
73
+ FileLinkKind = Literal["code", "test", "doc", "config", "dir", "other", "artifact"]
74
+ TodoStatus = Literal["open", "active", "done", "blocked", "skipped"]
75
+ EventName = Literal[
76
+ "task.created",
77
+ "task.updated",
78
+ "task.cancelled",
79
+ "stage.entered",
80
+ "stage.completed",
81
+ "stage.failed",
82
+ "plan.started",
83
+ "plan.proposed",
84
+ "plan.approved",
85
+ "plan.rejected",
86
+ "question.added",
87
+ "question.answered",
88
+ "question.dismissed",
89
+ "implementation.started",
90
+ "implementation.logged",
91
+ "implementation.finished",
92
+ "validation.started",
93
+ "validation.finished",
94
+ "change.logged",
95
+ "todo.added",
96
+ "todo.toggled",
97
+ "todo.started",
98
+ "todo.blocked",
99
+ "todo.skipped",
100
+ "todo.completed",
101
+ "lock.acquired",
102
+ "lock.released",
103
+ "lock.broken",
104
+ "lock.transferred",
105
+ "handoff.created",
106
+ "handoff.claimed",
107
+ "handoff.closed",
108
+ "handoff.cancelled",
109
+ "run.paused",
110
+ "run.resumed",
111
+ "actor.resolved",
112
+ "doctor.reindexed",
113
+ ]
114
+
115
+ ACTIVE_TASK_STAGES = frozenset({"planning", "implementing", "validating"})
116
+ DURABLE_TASK_STATUSES = frozenset(
117
+ {
118
+ "draft",
119
+ "plan_review",
120
+ "approved",
121
+ "implemented",
122
+ "failed_validation",
123
+ "done",
124
+ "cancelled",
125
+ }
126
+ )
127
+ RUN_TYPES = frozenset({"planning", "implementation", "validation"})
128
+ RUN_STATUSES = frozenset(
129
+ {"running", "paused", "finished", "passed", "failed", "blocked", "aborted"}
130
+ )
131
+ VALIDATION_CHECK_STATUSES = frozenset({"pass", "fail", "warn", "not_run"})
132
+ IMPLEMENTABLE_TASK_STAGES = frozenset({"approved", "failed_validation"})
133
+ CANCELLABLE_TASK_STAGES = frozenset(ACTIVE_TASK_STAGES) | {
134
+ "draft",
135
+ "plan_review",
136
+ "approved",
137
+ "implemented",
138
+ "failed_validation",
139
+ }
140
+
141
+ ALLOWED_STAGE_TRANSITIONS: dict[TaskStatusStage, frozenset[TaskStatusStage]] = {
142
+ "draft": frozenset({"plan_review", "cancelled"}),
143
+ "planning": frozenset({"plan_review", "cancelled"}),
144
+ "plan_review": frozenset({"draft", "approved", "cancelled"}),
145
+ "approved": frozenset({"implemented", "cancelled"}),
146
+ "implementing": frozenset({"implemented", "cancelled"}),
147
+ "implemented": frozenset({"done", "failed_validation", "cancelled"}),
148
+ "validating": frozenset({"done", "failed_validation", "cancelled"}),
149
+ "failed_validation": frozenset(
150
+ {"implementing", "approved", "plan_review", "cancelled"}
151
+ ),
152
+ "done": frozenset(),
153
+ "cancelled": frozenset(),
154
+ }
155
+
156
+ EXIT_CODE_SUCCESS = 0
157
+ EXIT_CODE_GENERIC_FAILURE = 1
158
+ EXIT_CODE_BAD_INPUT = 2
159
+ EXIT_CODE_WORKFLOW_REJECTION = 3
160
+ EXIT_CODE_LOCK_CONFLICT = 4
161
+ EXIT_CODE_MISSING = 5
162
+ EXIT_CODE_DATA_INTEGRITY = 6
163
+ EXIT_CODE_STORAGE_ERROR = 6
164
+ EXIT_CODE_VALIDATION_FAILED = 7
165
+ EXIT_CODE_INVALID_TRANSITION = EXIT_CODE_WORKFLOW_REJECTION
166
+ EXIT_CODE_APPROVAL_REQUIRED = EXIT_CODE_WORKFLOW_REJECTION
167
+ EXIT_CODE_DEPENDENCY_BLOCKED = EXIT_CODE_WORKFLOW_REJECTION
168
+ EXIT_CODE_STALE_LOCK_REQUIRES_BREAK = EXIT_CODE_LOCK_CONFLICT
169
+ EXIT_CODE_INDEX_REBUILD_FAILED = EXIT_CODE_STORAGE_ERROR
170
+
171
+
172
+ def is_active_stage(stage: TaskStatusStage) -> bool:
173
+ return stage in ACTIVE_TASK_STAGES
174
+
175
+
176
+ def can_transition(current: TaskStatusStage, target: TaskStatusStage) -> bool:
177
+ return target in ALLOWED_STAGE_TRANSITIONS[current]
178
+
179
+
180
+ def require_transition(current: TaskStatusStage, target: TaskStatusStage) -> None:
181
+ if can_transition(current, target):
182
+ return
183
+ raise LaunchError(f"Invalid stage transition: {current} -> {target}")
184
+
185
+
186
+ def normalize_task_status_stage(value: str) -> TaskStatusStage:
187
+ if value not in ALLOWED_STAGE_TRANSITIONS:
188
+ raise LaunchError(f"Unsupported task stage: {value}")
189
+ return value
190
+
191
+
192
+ def normalize_run_type(value: str) -> RunType:
193
+ if value not in {"planning", "implementation", "validation"}:
194
+ raise LaunchError(f"Unsupported run type: {value}")
195
+ return cast(RunType, value)
196
+
197
+
198
+ def normalize_plan_status(value: str) -> PlanStatus:
199
+ if value not in {"draft", "proposed", "accepted", "superseded", "rejected"}:
200
+ raise LaunchError(f"Unsupported plan status: {value}")
201
+ return cast(PlanStatus, value)
202
+
203
+
204
+ def normalize_question_status(value: str) -> QuestionStatus:
205
+ if value not in {"open", "answered", "dismissed"}:
206
+ raise LaunchError(f"Unsupported question status: {value}")
207
+ return cast(QuestionStatus, value)
208
+
209
+
210
+ def normalize_run_status(value: str) -> RunStatus:
211
+ if value not in {
212
+ "running",
213
+ "paused",
214
+ "finished",
215
+ "passed",
216
+ "failed",
217
+ "blocked",
218
+ "aborted",
219
+ }:
220
+ raise LaunchError(f"Unsupported run status: {value}")
221
+ return cast(RunStatus, value)
222
+
223
+
224
+ def normalize_validation_result(value: str) -> ValidationResult:
225
+ if value not in {"passed", "failed", "blocked"}:
226
+ raise LaunchError(f"Unsupported validation result: {value}")
227
+ return cast(ValidationResult, value)
228
+
229
+
230
+ def normalize_validation_check_status(value: str) -> ValidationCheckStatus:
231
+ normalized = "not_run" if value == "skip" else value
232
+ if normalized not in {"pass", "fail", "warn", "not_run"}:
233
+ raise LaunchError(f"Unsupported validation check status: {value}")
234
+ return cast(ValidationCheckStatus, normalized)
235
+
236
+
237
+ def normalize_file_link_kind(value: str) -> FileLinkKind:
238
+ normalized = "dir" if value == "directory" else value
239
+ if normalized not in {"code", "test", "doc", "config", "dir", "other", "artifact"}:
240
+ raise LaunchError(f"Unsupported file link kind: {value}")
241
+ return cast(FileLinkKind, normalized)
242
+
243
+
244
+ def normalize_todo_status(value: str) -> TodoStatus:
245
+ if value not in {"open", "active", "done", "blocked", "skipped"}:
246
+ raise LaunchError(f"Unsupported todo status: {value}")
247
+ return cast(TodoStatus, value)
248
+
249
+
250
+ TODO_STATUSES = frozenset({"open", "active", "done", "blocked", "skipped"})
251
+
252
+
253
+ def normalize_actor_type(value: str) -> ActorType:
254
+ if value not in {"agent", "user", "system"}:
255
+ raise LaunchError(f"Unsupported actor type: {value!r}")
256
+ return cast(ActorType, value)
257
+
258
+
259
+ def normalize_actor_role(value: str) -> ActorRole:
260
+ if value not in {"planner", "implementer", "validator", "reviewer", "operator"}:
261
+ raise LaunchError(f"Unsupported actor role: {value!r}")
262
+ return cast(ActorRole, value)
263
+
264
+
265
+ def normalize_harness_kind(value: str) -> HarnessKind:
266
+ if value not in {"agent_harness", "manual", "ci", "unknown"}:
267
+ raise LaunchError(f"Unsupported harness kind: {value!r}")
268
+ return cast(HarnessKind, value)
269
+
270
+
271
+ def normalize_handoff_mode(value: str) -> HandoffMode:
272
+ if value not in {"planning", "implementation", "validation", "review", "full"}:
273
+ raise LaunchError(f"Unsupported handoff mode: {value!r}")
274
+ return cast(HandoffMode, value)
275
+
276
+
277
+ def normalize_context_for(value: str) -> ContextFor:
278
+ normalized = {
279
+ "planning": "planner",
280
+ "implementation": "implementer",
281
+ "validation": "validator",
282
+ "review": "reviewer",
283
+ "spec": "spec-reviewer",
284
+ "code": "code-reviewer",
285
+ }.get(value, value)
286
+ if normalized not in {
287
+ "planner",
288
+ "implementer",
289
+ "validator",
290
+ "reviewer",
291
+ "spec-reviewer",
292
+ "code-reviewer",
293
+ "full",
294
+ }:
295
+ raise LaunchError(f"Unsupported context role: {value!r}")
296
+ return cast(ContextFor, normalized)
297
+
298
+
299
+ def normalize_context_scope(value: str) -> ContextScope:
300
+ if value not in {"task", "todo", "run"}:
301
+ raise LaunchError(f"Unsupported context scope: {value!r}")
302
+ return cast(ContextScope, value)
303
+
304
+
305
+ def normalize_context_format(value: str) -> ContextFormat:
306
+ if value not in {"markdown", "json", "text"}:
307
+ raise LaunchError(f"Unsupported context format: {value!r}")
308
+ return cast(ContextFormat, value)
309
+
310
+
311
+ def normalize_handoff_status(value: str) -> HandoffStatus:
312
+ if value not in {"open", "claimed", "closed", "cancelled"}:
313
+ raise LaunchError(f"Unsupported handoff status: {value!r}")
314
+ return cast(HandoffStatus, value)
315
+
316
+
317
+ def normalize_lock_policy(value: str) -> LockPolicy:
318
+ if value not in {"none", "retain", "release", "transfer"}:
319
+ raise LaunchError(f"Unsupported lock policy: {value!r}")
320
+ return cast(LockPolicy, value)
taskledger/errors.py ADDED
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+
5
+
6
+ class TaskledgerError(Exception):
7
+ """Base class for public taskledger errors."""
8
+
9
+ code = "TASKLEDGER_ERROR"
10
+ exit_code = 1
11
+ error_type = "TaskledgerError"
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ code: str | None = None,
18
+ exit_code: int | None = None,
19
+ error_type: str | None = None,
20
+ remediation: Sequence[str] | None = None,
21
+ details: Mapping[str, object] | None = None,
22
+ data: Mapping[str, object] | None = None,
23
+ task_id: str | None = None,
24
+ blocking_refs: Sequence[str] | None = None,
25
+ ) -> None:
26
+ super().__init__(message)
27
+ resolved_code = code if code is not None else self.code
28
+ resolved_exit_code = exit_code if exit_code is not None else self.exit_code
29
+ resolved_error_type = error_type if error_type is not None else self.error_type
30
+ resolved_details = dict(details or data or {})
31
+ resolved_blocking_refs = tuple(str(item) for item in (blocking_refs or ()))
32
+
33
+ self.code = resolved_code
34
+ self.message = message
35
+ self.exit_code = resolved_exit_code
36
+ self.error_type = resolved_error_type
37
+ self.details = resolved_details
38
+ self.task_id = task_id
39
+ self.blocking_refs = resolved_blocking_refs
40
+ self.remediation = [str(item) for item in remediation or ()]
41
+
42
+ self.taskledger_exit_code = resolved_exit_code
43
+ self.taskledger_error_code = resolved_code
44
+ if error_type is not None:
45
+ self.taskledger_error_type = error_type
46
+ else:
47
+ self.taskledger_error_type = resolved_error_type
48
+ self.taskledger_remediation = list(self.remediation)
49
+ self.taskledger_data = self.to_error_payload()
50
+
51
+ def to_error_payload(self) -> dict[str, object]:
52
+ payload: dict[str, object] = {
53
+ "code": self.code,
54
+ "message": self.message,
55
+ }
56
+ if self.details:
57
+ payload["details"] = dict(self.details)
58
+ if self.task_id is not None:
59
+ payload["task_id"] = self.task_id
60
+ if self.blocking_refs:
61
+ payload["blocking_refs"] = list(self.blocking_refs)
62
+ return payload
63
+
64
+
65
+ class UnsupportedAgentError(TaskledgerError):
66
+ """Raised when the requested agent is not supported."""
67
+
68
+ code = "UNSUPPORTED_AGENT"
69
+
70
+
71
+ class AgentNotInstalledError(TaskledgerError):
72
+ """Raised when the requested agent executable is not available."""
73
+
74
+ code = "AGENT_NOT_INSTALLED"
75
+
76
+
77
+ class LaunchError(TaskledgerError):
78
+ """Raised when the child process cannot be prepared or started."""
79
+
80
+ code = "LAUNCH_ERROR"
81
+ error_type = "LaunchError"
82
+
83
+
84
+ class NoActiveTask(LaunchError):
85
+ code = "NO_ACTIVE_TASK"
86
+ exit_code = 5
87
+ error_type = "NoActiveTask"
88
+
89
+ def __init__(self) -> None:
90
+ super().__init__(
91
+ "No active task is set. Run `taskledger task activate <task-ref>` "
92
+ "or pass `--task <task-ref>`.",
93
+ remediation=[
94
+ "Run `taskledger task list` to find a task.",
95
+ "Run `taskledger task activate <task-ref>` to set the active task.",
96
+ "Or pass `--task <task-ref>` to this command.",
97
+ ],
98
+ )
99
+
100
+
101
+ class InvalidPromptError(TaskledgerError):
102
+ """Raised when prompt input is empty or invalid."""
103
+
104
+ code = "INVALID_INPUT"
105
+ exit_code = 2
106
+ error_type = "ValidationError"
107
+
108
+
109
+ class NotFound(TaskledgerError):
110
+ code = "NOT_FOUND"
111
+ exit_code = 5
112
+ error_type = "NotFound"
113
+
114
+
115
+ class ValidationError(TaskledgerError):
116
+ code = "VALIDATION_FAILED"
117
+ exit_code = 7
118
+ error_type = "ValidationError"
119
+
120
+
121
+ class InvalidStageTransition(TaskledgerError):
122
+ code = "INVALID_STAGE_TRANSITION"
123
+ exit_code = 3
124
+ error_type = "InvalidStageTransition"
125
+
126
+
127
+ class ApprovalRequired(TaskledgerError):
128
+ code = "APPROVAL_REQUIRED"
129
+ exit_code = 3
130
+ error_type = "ApprovalRequired"
131
+
132
+
133
+ class DependencyIncomplete(TaskledgerError):
134
+ code = "DEPENDENCY_INCOMPLETE"
135
+ exit_code = 3
136
+ error_type = "DependencyIncomplete"
137
+
138
+
139
+ class LockConflict(LaunchError):
140
+ code = "LOCK_CONFLICT"
141
+ exit_code = 4
142
+ error_type = "LockConflict"
143
+
144
+
145
+ class StaleLockRequiresBreak(TaskledgerError):
146
+ code = "STALE_LOCK_REQUIRES_BREAK"
147
+ exit_code = 4
148
+ error_type = "StaleLockRequiresBreak"
149
+
150
+
151
+ class StorageCorruption(LaunchError):
152
+ code = "STORAGE_CORRUPTION"
153
+ exit_code = 6
154
+ error_type = "StorageCorruption"
155
+
156
+
157
+ class ActiveTaskNotFound(StorageCorruption):
158
+ code = "ACTIVE_TASK_NOT_FOUND"
159
+ error_type = "StorageCorruption"
160
+
161
+
162
+ class IndexRebuildFailed(TaskledgerError):
163
+ code = "INDEX_REBUILD_FAILED"
164
+ exit_code = 6
165
+ error_type = "IndexRebuildFailed"