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,409 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, cast
4
+
5
+ import typer
6
+
7
+ from taskledger.api.task_runs import (
8
+ add_change,
9
+ add_implementation_artifact,
10
+ add_implementation_deviation,
11
+ finish_implementation,
12
+ log_implementation,
13
+ restart_implementation,
14
+ run_implementation_command,
15
+ scan_changes,
16
+ show_task_run,
17
+ start_implementation,
18
+ )
19
+ from taskledger.api.tasks import todo_status
20
+ from taskledger.cli_common import (
21
+ TaskOption,
22
+ cli_state_from_context,
23
+ emit_error,
24
+ emit_payload,
25
+ launch_error_exit_code,
26
+ render_json,
27
+ resolve_cli_task,
28
+ )
29
+ from taskledger.errors import LaunchError
30
+ from taskledger.services.actors import resolve_actor, resolve_harness
31
+ from taskledger.storage.task_store import load_todos
32
+
33
+
34
+ def register_implement_v2_commands(app: typer.Typer) -> None: # noqa: C901
35
+ @app.command("start")
36
+ def start_command(
37
+ ctx: typer.Context,
38
+ task_ref: TaskOption = None,
39
+ actor: Annotated[
40
+ str | None,
41
+ typer.Option("--actor", help="Actor type: user, agent, or system."),
42
+ ] = None,
43
+ actor_name: Annotated[
44
+ str | None,
45
+ typer.Option("--actor-name", help="Actor name."),
46
+ ] = None,
47
+ actor_role: Annotated[
48
+ str | None,
49
+ typer.Option("--actor-role", help="Actor role in task lifecycle."),
50
+ ] = None,
51
+ harness: Annotated[
52
+ str | None,
53
+ typer.Option("--harness", help="Harness name."),
54
+ ] = None,
55
+ session_id: Annotated[
56
+ str | None,
57
+ typer.Option("--session-id", help="Session identifier."),
58
+ ] = None,
59
+ ) -> None:
60
+ state = cli_state_from_context(ctx)
61
+ try:
62
+ task = resolve_cli_task(state.cwd, task_ref)
63
+ resolved_actor = resolve_actor(
64
+ actor_type=actor,
65
+ actor_name=actor_name,
66
+ role=actor_role,
67
+ session_id=session_id,
68
+ )
69
+ resolved_harness = resolve_harness(name=harness, session_id=session_id)
70
+ payload = start_implementation(
71
+ state.cwd,
72
+ task.id,
73
+ actor=resolved_actor,
74
+ harness=resolved_harness,
75
+ )
76
+ except LaunchError as exc:
77
+ emit_error(ctx, exc)
78
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
79
+ emit_payload(
80
+ ctx,
81
+ payload,
82
+ human=f"started implementation {payload['run_id']}",
83
+ )
84
+
85
+ @app.command("restart")
86
+ def restart_command(
87
+ ctx: typer.Context,
88
+ summary: Annotated[str, typer.Option("--summary")],
89
+ task_ref: TaskOption = None,
90
+ actor: Annotated[
91
+ str | None,
92
+ typer.Option("--actor", help="Actor type: user, agent, or system."),
93
+ ] = None,
94
+ actor_name: Annotated[
95
+ str | None,
96
+ typer.Option("--actor-name", help="Actor name."),
97
+ ] = None,
98
+ actor_role: Annotated[
99
+ str | None,
100
+ typer.Option("--actor-role", help="Actor role in task lifecycle."),
101
+ ] = None,
102
+ harness: Annotated[
103
+ str | None,
104
+ typer.Option("--harness", help="Harness name."),
105
+ ] = None,
106
+ session_id: Annotated[
107
+ str | None,
108
+ typer.Option("--session-id", help="Session identifier."),
109
+ ] = None,
110
+ ) -> None:
111
+ state = cli_state_from_context(ctx)
112
+ try:
113
+ task = resolve_cli_task(state.cwd, task_ref)
114
+ resolved_actor = resolve_actor(
115
+ actor_type=actor,
116
+ actor_name=actor_name,
117
+ role=actor_role,
118
+ session_id=session_id,
119
+ )
120
+ resolved_harness = resolve_harness(name=harness, session_id=session_id)
121
+ payload = restart_implementation(
122
+ state.cwd,
123
+ task.id,
124
+ summary=summary,
125
+ actor=resolved_actor,
126
+ harness=resolved_harness,
127
+ )
128
+ except LaunchError as exc:
129
+ emit_error(ctx, exc)
130
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
131
+ emit_payload(
132
+ ctx,
133
+ payload,
134
+ human=f"restarted implementation {payload['run_id']}",
135
+ )
136
+
137
+ @app.command("log")
138
+ def log_command(
139
+ ctx: typer.Context,
140
+ message: Annotated[str | None, typer.Option("--message")] = None,
141
+ task_ref: TaskOption = None,
142
+ ) -> None:
143
+ state = cli_state_from_context(ctx)
144
+ try:
145
+ task = resolve_cli_task(state.cwd, task_ref)
146
+ if message is None:
147
+ payload = show_task_run(
148
+ state.cwd,
149
+ task.id,
150
+ run_id=None,
151
+ run_type="implementation",
152
+ )
153
+ emit_payload(ctx, payload, human=str(payload["run"]))
154
+ return
155
+ run = log_implementation(state.cwd, task.id, message=message)
156
+ except LaunchError as exc:
157
+ emit_error(ctx, exc)
158
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
159
+ emit_payload(ctx, run.to_dict(), human=f"logged implementation {run.run_id}")
160
+
161
+ def _emit_change_command(
162
+ ctx: typer.Context,
163
+ path: Annotated[str, typer.Option("--path")],
164
+ kind: Annotated[str, typer.Option("--kind")] = "edit",
165
+ summary: Annotated[str, typer.Option("--summary")] = "",
166
+ command: Annotated[str | None, typer.Option("--command")] = None,
167
+ git_diff_stat: Annotated[str | None, typer.Option("--git-diff-stat")] = None,
168
+ task_ref: str | None = None,
169
+ ) -> None:
170
+ state = cli_state_from_context(ctx)
171
+ try:
172
+ task = resolve_cli_task(state.cwd, task_ref)
173
+ change = add_change(
174
+ state.cwd,
175
+ task.id,
176
+ path=path,
177
+ kind=kind,
178
+ summary=summary,
179
+ command=command,
180
+ git_diff_stat=git_diff_stat,
181
+ )
182
+ except LaunchError as exc:
183
+ emit_error(ctx, exc)
184
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
185
+ emit_payload(ctx, change.to_dict(), human=f"logged change {change.change_id}")
186
+
187
+ @app.command("change")
188
+ def change_command(
189
+ ctx: typer.Context,
190
+ path: Annotated[str, typer.Option("--path")],
191
+ kind: Annotated[str, typer.Option("--kind")] = "edit",
192
+ summary: Annotated[str, typer.Option("--summary")] = "",
193
+ command: Annotated[str | None, typer.Option("--command")] = None,
194
+ git_diff_stat: Annotated[str | None, typer.Option("--git-diff-stat")] = None,
195
+ task_ref: TaskOption = None,
196
+ ) -> None:
197
+ _emit_change_command(ctx, path, kind, summary, command, git_diff_stat, task_ref)
198
+
199
+ @app.command("scan-changes")
200
+ def scan_changes_command(
201
+ ctx: typer.Context,
202
+ from_git: Annotated[bool, typer.Option("--from-git")] = False,
203
+ summary: Annotated[str, typer.Option("--summary")] = "",
204
+ task_ref: TaskOption = None,
205
+ ) -> None:
206
+ state = cli_state_from_context(ctx)
207
+ try:
208
+ task = resolve_cli_task(state.cwd, task_ref)
209
+ change = scan_changes(
210
+ state.cwd,
211
+ task.id,
212
+ from_git=from_git,
213
+ summary=summary,
214
+ )
215
+ except LaunchError as exc:
216
+ emit_error(ctx, exc)
217
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
218
+ emit_payload(ctx, change.to_dict(), human=f"logged change {change.change_id}")
219
+
220
+ @app.command(
221
+ "command",
222
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
223
+ )
224
+ def command_command(
225
+ ctx: typer.Context,
226
+ task_ref: TaskOption = None,
227
+ ) -> None:
228
+ state = cli_state_from_context(ctx)
229
+ argv = tuple(ctx.args)
230
+ try:
231
+ task = resolve_cli_task(state.cwd, task_ref)
232
+ payload = run_implementation_command(
233
+ state.cwd,
234
+ task.id,
235
+ argv=argv,
236
+ )
237
+ except LaunchError as exc:
238
+ emit_error(ctx, exc)
239
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
240
+ emit_payload(
241
+ ctx,
242
+ payload,
243
+ human=f"ran implementation command exit={payload['exit_code']}",
244
+ )
245
+
246
+ @app.command("deviation")
247
+ def deviation_command(
248
+ ctx: typer.Context,
249
+ message: Annotated[str, typer.Option("--message")],
250
+ task_ref: TaskOption = None,
251
+ ) -> None:
252
+ state = cli_state_from_context(ctx)
253
+ try:
254
+ task = resolve_cli_task(state.cwd, task_ref)
255
+ run = add_implementation_deviation(state.cwd, task.id, message=message)
256
+ except LaunchError as exc:
257
+ emit_error(ctx, exc)
258
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
259
+ emit_payload(ctx, run.to_dict(), human=f"logged deviation on {run.run_id}")
260
+
261
+ @app.command("artifact")
262
+ def artifact_command(
263
+ ctx: typer.Context,
264
+ path: Annotated[str, typer.Option("--path")],
265
+ summary: Annotated[str, typer.Option("--summary")],
266
+ task_ref: TaskOption = None,
267
+ ) -> None:
268
+ state = cli_state_from_context(ctx)
269
+ try:
270
+ task = resolve_cli_task(state.cwd, task_ref)
271
+ run = add_implementation_artifact(
272
+ state.cwd,
273
+ task.id,
274
+ path=path,
275
+ summary=summary,
276
+ )
277
+ except LaunchError as exc:
278
+ emit_error(ctx, exc)
279
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
280
+ emit_payload(ctx, run.to_dict(), human=f"logged artifact on {run.run_id}")
281
+
282
+ @app.command("finish")
283
+ def finish_command(
284
+ ctx: typer.Context,
285
+ summary: Annotated[str, typer.Option("--summary")],
286
+ task_ref: TaskOption = None,
287
+ ) -> None:
288
+ state = cli_state_from_context(ctx)
289
+ try:
290
+ task = resolve_cli_task(state.cwd, task_ref)
291
+ payload = finish_implementation(state.cwd, task.id, summary=summary)
292
+ except LaunchError as exc:
293
+ if state.json_output:
294
+ emit_error(ctx, exc)
295
+ else:
296
+ from taskledger.cli_common import _error_envelope
297
+
298
+ typer.echo(
299
+ render_json(
300
+ _error_envelope(
301
+ ctx,
302
+ exc,
303
+ data=None,
304
+ remediation=None,
305
+ exit_code=None,
306
+ error_type=None,
307
+ )
308
+ )
309
+ )
310
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
311
+ if state.json_output:
312
+ emit_payload(
313
+ ctx,
314
+ payload,
315
+ human=f"finished implementation {payload['run_id']}",
316
+ )
317
+ else:
318
+ typer.echo(render_json(payload))
319
+
320
+ @app.command("show")
321
+ def show_command(
322
+ ctx: typer.Context,
323
+ run_id: Annotated[str | None, typer.Option("--run")] = None,
324
+ task_ref: TaskOption = None,
325
+ ) -> None:
326
+ state = cli_state_from_context(ctx)
327
+ try:
328
+ task = resolve_cli_task(state.cwd, task_ref)
329
+ payload = show_task_run(
330
+ state.cwd,
331
+ task.id,
332
+ run_id=run_id,
333
+ run_type="implementation",
334
+ )
335
+ except LaunchError as exc:
336
+ emit_error(ctx, exc)
337
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
338
+ run = payload["run"]
339
+ assert isinstance(run, dict)
340
+ emit_payload(ctx, payload, human=f"{run['run_id']} {run['status']}")
341
+
342
+ @app.command("status")
343
+ def status_command(
344
+ ctx: typer.Context,
345
+ task_ref: TaskOption = None,
346
+ ) -> None:
347
+ state = cli_state_from_context(ctx)
348
+ try:
349
+ task = resolve_cli_task(state.cwd, task_ref)
350
+ payload = todo_status(state.cwd, task.id)
351
+ except LaunchError as exc:
352
+ emit_error(ctx, exc)
353
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
354
+
355
+ # Build human-readable output
356
+ total = payload.get("total", 0)
357
+ done = payload.get("done", 0)
358
+ can_finish = payload.get("can_finish_implementation", False)
359
+ lines = [
360
+ f"IMPLEMENTATION STATUS {payload['task_id']} {done}/{total} todos done"
361
+ ]
362
+
363
+ todos = load_todos(state.cwd, task.id).todos
364
+ for todo in todos:
365
+ status_mark = "[x]" if todo.done else "[ ]"
366
+ lines.append(f"{status_mark} {todo.id} {todo.text}")
367
+
368
+ if can_finish:
369
+ lines.append(
370
+ "\nReady: All todos done. "
371
+ "Run 'taskledger implement finish --summary \"...\"'"
372
+ )
373
+ else:
374
+ lines.append(
375
+ f"\nBlocked: {cast(int, total) - cast(int, done)} todos not done."
376
+ )
377
+
378
+ emit_payload(ctx, payload, human="\n".join(lines))
379
+
380
+ @app.command("checklist")
381
+ def checklist_command(
382
+ ctx: typer.Context,
383
+ task_ref: TaskOption = None,
384
+ ) -> None:
385
+ state = cli_state_from_context(ctx)
386
+ try:
387
+ task = resolve_cli_task(state.cwd, task_ref)
388
+ payload = todo_status(state.cwd, task.id)
389
+ except LaunchError as exc:
390
+ emit_error(ctx, exc)
391
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
392
+
393
+ # Build human-readable checklist output
394
+ total = cast(int, payload.get("total", 0))
395
+ done = cast(int, payload.get("done", 0))
396
+ can_finish = payload.get("can_finish_implementation", False)
397
+ lines = [f"TODO CHECKLIST: {done}/{total} done"]
398
+
399
+ todos = load_todos(state.cwd, task.id).todos
400
+ for todo in todos:
401
+ status_mark = "[x]" if todo.done else "[ ]"
402
+ lines.append(f"{status_mark} {todo.id} {todo.text}")
403
+
404
+ if can_finish:
405
+ lines.append("\n✓ All todos done!")
406
+ else:
407
+ lines.append(f"\n{total - done} todos remaining")
408
+
409
+ emit_payload(ctx, payload, human="\n".join(lines))