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
taskledger/cli_plan.py ADDED
@@ -0,0 +1,478 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from taskledger.api.plans import (
9
+ approve_plan,
10
+ diff_plan,
11
+ lint_plan,
12
+ list_plan_versions,
13
+ materialize_plan_todos,
14
+ propose_plan,
15
+ regenerate_plan_from_answers,
16
+ reject_plan,
17
+ revise_plan,
18
+ run_planning_command,
19
+ show_plan,
20
+ start_planning,
21
+ upsert_plan,
22
+ )
23
+ from taskledger.cli_common import (
24
+ TaskOption,
25
+ cli_state_from_context,
26
+ emit_error,
27
+ emit_payload,
28
+ launch_error_exit_code,
29
+ read_text_input,
30
+ resolve_cli_task,
31
+ )
32
+ from taskledger.domain.states import EXIT_CODE_VALIDATION_FAILED
33
+ from taskledger.errors import LaunchError
34
+ from taskledger.services.actors import resolve_actor, resolve_harness
35
+ from taskledger.services.plan_lint import PlanLintPayload
36
+
37
+
38
+ def register_plan_v2_commands(app: typer.Typer) -> None: # noqa: C901
39
+ @app.command("start")
40
+ def start_command(
41
+ ctx: typer.Context,
42
+ task_ref: TaskOption = None,
43
+ actor: Annotated[
44
+ str | None,
45
+ typer.Option("--actor", help="Actor type: user, agent, or system."),
46
+ ] = None,
47
+ actor_name: Annotated[
48
+ str | None,
49
+ typer.Option("--actor-name", help="Actor name."),
50
+ ] = None,
51
+ actor_role: Annotated[
52
+ str | None,
53
+ typer.Option("--actor-role", help="Actor role in task lifecycle."),
54
+ ] = None,
55
+ harness: Annotated[
56
+ str | None,
57
+ typer.Option("--harness", help="Harness name."),
58
+ ] = None,
59
+ session_id: Annotated[
60
+ str | None,
61
+ typer.Option("--session-id", help="Session identifier."),
62
+ ] = None,
63
+ ) -> None:
64
+ state = cli_state_from_context(ctx)
65
+ try:
66
+ task = resolve_cli_task(state.cwd, task_ref)
67
+ resolved_actor = resolve_actor(
68
+ actor_type=actor,
69
+ actor_name=actor_name,
70
+ role=actor_role,
71
+ session_id=session_id,
72
+ )
73
+ resolved_harness = resolve_harness(name=harness, session_id=session_id)
74
+ payload = start_planning(
75
+ state.cwd,
76
+ task.id,
77
+ actor=resolved_actor,
78
+ harness=resolved_harness,
79
+ )
80
+ except LaunchError as exc:
81
+ emit_error(ctx, exc)
82
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
83
+ emit_payload(ctx, payload, human=f"started planning {payload['task_id']}")
84
+
85
+ @app.command("propose")
86
+ def propose_command(
87
+ ctx: typer.Context,
88
+ task_ref: TaskOption = None,
89
+ text: Annotated[str | None, typer.Option("--text")] = None,
90
+ from_file: Annotated[Path | None, typer.Option("--file")] = None,
91
+ criterion: Annotated[list[str] | None, typer.Option("--criterion")] = None,
92
+ ) -> None:
93
+ state = cli_state_from_context(ctx)
94
+ try:
95
+ task = resolve_cli_task(state.cwd, task_ref)
96
+ payload = propose_plan(
97
+ state.cwd,
98
+ task.id,
99
+ body=read_text_input(text=text, from_file=from_file),
100
+ criteria=tuple(criterion or ()),
101
+ )
102
+ except LaunchError as exc:
103
+ emit_error(ctx, exc)
104
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
105
+ emit_payload(
106
+ ctx,
107
+ payload,
108
+ human=f"proposed plan v{payload['plan_version']} for {payload['task_id']}",
109
+ )
110
+
111
+ @app.command("draft")
112
+ def draft_command(
113
+ ctx: typer.Context,
114
+ task_ref: TaskOption = None,
115
+ ask_questions: Annotated[bool, typer.Option("--ask-questions")] = False,
116
+ ) -> None:
117
+ state = cli_state_from_context(ctx)
118
+ try:
119
+ task = resolve_cli_task(state.cwd, task_ref)
120
+ payload = {
121
+ "kind": "plan_draft_context",
122
+ "task_id": task.id,
123
+ "ask_questions": ask_questions,
124
+ "next_action": (
125
+ 'taskledger question add --text "..." --required-for-plan'
126
+ if ask_questions
127
+ else "taskledger plan propose --file plan.md"
128
+ ),
129
+ }
130
+ except LaunchError as exc:
131
+ emit_error(ctx, exc)
132
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
133
+ emit_payload(ctx, payload, human=str(payload["next_action"]))
134
+
135
+ @app.command("regenerate")
136
+ def regenerate_command(
137
+ ctx: typer.Context,
138
+ task_ref: TaskOption = None,
139
+ from_answers: Annotated[bool, typer.Option("--from-answers")] = False,
140
+ text: Annotated[str | None, typer.Option("--text")] = None,
141
+ from_file: Annotated[Path | None, typer.Option("--file")] = None,
142
+ allow_open_questions: Annotated[
143
+ bool,
144
+ typer.Option("--allow-open-questions"),
145
+ ] = False,
146
+ ) -> None:
147
+ state = cli_state_from_context(ctx)
148
+ try:
149
+ if not from_answers:
150
+ raise LaunchError("plan regenerate requires --from-answers.")
151
+ task = resolve_cli_task(state.cwd, task_ref)
152
+ payload = regenerate_plan_from_answers(
153
+ state.cwd,
154
+ task.id,
155
+ body=read_text_input(text=text, from_file=from_file),
156
+ allow_open_questions=allow_open_questions,
157
+ )
158
+ except LaunchError as exc:
159
+ emit_error(ctx, exc)
160
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
161
+ emit_payload(
162
+ ctx,
163
+ payload,
164
+ human=f"regenerated plan v{payload['plan_version']} "
165
+ f"for {payload['task_id']}",
166
+ )
167
+
168
+ @app.command("upsert")
169
+ def upsert_command(
170
+ ctx: typer.Context,
171
+ task_ref: TaskOption = None,
172
+ from_answers: Annotated[bool, typer.Option("--from-answers")] = False,
173
+ text: Annotated[str | None, typer.Option("--text")] = None,
174
+ from_file: Annotated[Path | None, typer.Option("--file")] = None,
175
+ criterion: Annotated[list[str] | None, typer.Option("--criterion")] = None,
176
+ allow_open_questions: Annotated[
177
+ bool,
178
+ typer.Option("--allow-open-questions"),
179
+ ] = False,
180
+ ) -> None:
181
+ state = cli_state_from_context(ctx)
182
+ try:
183
+ task = resolve_cli_task(state.cwd, task_ref)
184
+ payload = upsert_plan(
185
+ state.cwd,
186
+ task.id,
187
+ body=read_text_input(text=text, from_file=from_file),
188
+ criteria=tuple(criterion or ()),
189
+ from_answers=from_answers,
190
+ allow_open_questions=allow_open_questions,
191
+ )
192
+ except LaunchError as exc:
193
+ emit_error(ctx, exc)
194
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
195
+ operation = str(payload.get("operation", "upserted"))
196
+ emit_payload(
197
+ ctx,
198
+ payload,
199
+ human=f"{operation} plan v{payload['plan_version']} "
200
+ f"for {payload['task_id']}",
201
+ )
202
+
203
+ @app.command("materialize-todos")
204
+ def materialize_todos_command(
205
+ ctx: typer.Context,
206
+ version: Annotated[int, typer.Option("--version")],
207
+ task_ref: TaskOption = None,
208
+ dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
209
+ ) -> None:
210
+ state = cli_state_from_context(ctx)
211
+ try:
212
+ task = resolve_cli_task(state.cwd, task_ref)
213
+ payload = materialize_plan_todos(
214
+ state.cwd,
215
+ task.id,
216
+ version=version,
217
+ dry_run=dry_run,
218
+ )
219
+ except LaunchError as exc:
220
+ emit_error(ctx, exc)
221
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
222
+ emit_payload(
223
+ ctx,
224
+ payload,
225
+ human=f"materialized {payload['materialized_todos']} todos",
226
+ )
227
+
228
+ @app.command("show")
229
+ def show_command(
230
+ ctx: typer.Context,
231
+ task_ref: TaskOption = None,
232
+ version: Annotated[int | None, typer.Option("--version")] = None,
233
+ ) -> None:
234
+ state = cli_state_from_context(ctx)
235
+ try:
236
+ task = resolve_cli_task(state.cwd, task_ref)
237
+ payload = show_plan(state.cwd, task.id, version=version)
238
+ except LaunchError as exc:
239
+ emit_error(ctx, exc)
240
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
241
+ plan = payload["plan"]
242
+ assert isinstance(plan, dict)
243
+ emit_payload(
244
+ ctx,
245
+ payload,
246
+ human=f"plan v{plan['plan_version']} ({plan['status']})",
247
+ )
248
+
249
+ @app.command("list")
250
+ def list_command(
251
+ ctx: typer.Context,
252
+ task_ref: TaskOption = None,
253
+ ) -> None:
254
+ state = cli_state_from_context(ctx)
255
+ try:
256
+ task = resolve_cli_task(state.cwd, task_ref)
257
+ payload = list_plan_versions(state.cwd, task.id)
258
+ except LaunchError as exc:
259
+ emit_error(ctx, exc)
260
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
261
+ plans = payload["plans"]
262
+ assert isinstance(plans, list)
263
+ lines = ["PLANS"]
264
+ for item in plans:
265
+ if isinstance(item, dict):
266
+ lines.append(f"v{item['plan_version']} {item['status']}")
267
+ emit_payload(
268
+ ctx, payload, human="\n".join(lines) if plans else "PLANS\n(empty)"
269
+ )
270
+
271
+ @app.command("diff")
272
+ def diff_command(
273
+ ctx: typer.Context,
274
+ task_ref: TaskOption = None,
275
+ from_version: Annotated[int, typer.Option("--from")] = 1,
276
+ to_version: Annotated[int, typer.Option("--to")] = 1,
277
+ ) -> None:
278
+ state = cli_state_from_context(ctx)
279
+ try:
280
+ task = resolve_cli_task(state.cwd, task_ref)
281
+ payload = diff_plan(
282
+ state.cwd,
283
+ task.id,
284
+ from_version=from_version,
285
+ to_version=to_version,
286
+ )
287
+ except LaunchError as exc:
288
+ emit_error(ctx, exc)
289
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
290
+ emit_payload(ctx, payload, human=str(payload["diff"]))
291
+
292
+ @app.command("lint")
293
+ def lint_command(
294
+ ctx: typer.Context,
295
+ task_ref: TaskOption = None,
296
+ version: Annotated[int | None, typer.Option("--version")] = None,
297
+ strict: Annotated[bool, typer.Option("--strict")] = False,
298
+ ) -> None:
299
+ state = cli_state_from_context(ctx)
300
+ try:
301
+ task = resolve_cli_task(state.cwd, task_ref)
302
+ payload = lint_plan(
303
+ state.cwd,
304
+ task.id,
305
+ version=version,
306
+ strict=strict,
307
+ )
308
+ except LaunchError as exc:
309
+ emit_error(ctx, exc)
310
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
311
+ emit_payload(ctx, payload, human=_render_plan_lint(payload))
312
+ if not payload["passed"]:
313
+ raise typer.Exit(code=EXIT_CODE_VALIDATION_FAILED)
314
+
315
+ @app.command("approve")
316
+ def approve_command(
317
+ ctx: typer.Context,
318
+ version: Annotated[int, typer.Option("--version")],
319
+ actor: Annotated[str, typer.Option("--actor")] = "agent",
320
+ actor_name: Annotated[str | None, typer.Option("--actor-name")] = None,
321
+ note: Annotated[str | None, typer.Option("--note")] = None,
322
+ allow_agent_approval: Annotated[
323
+ bool, typer.Option("--allow-agent-approval")
324
+ ] = False,
325
+ reason: Annotated[str | None, typer.Option("--reason")] = None,
326
+ allow_empty_criteria: Annotated[
327
+ bool, typer.Option("--allow-empty-criteria")
328
+ ] = False,
329
+ no_materialize_todos: Annotated[
330
+ bool, typer.Option("--no-materialize-todos")
331
+ ] = False,
332
+ allow_open_questions: Annotated[
333
+ bool, typer.Option("--allow-open-questions")
334
+ ] = False,
335
+ allow_empty_todos: Annotated[bool, typer.Option("--allow-empty-todos")] = False,
336
+ allow_lint_errors: Annotated[bool, typer.Option("--allow-lint-errors")] = False,
337
+ task_ref: TaskOption = None,
338
+ ) -> None:
339
+ state = cli_state_from_context(ctx)
340
+ try:
341
+ task = resolve_cli_task(state.cwd, task_ref)
342
+ payload = approve_plan(
343
+ state.cwd,
344
+ task.id,
345
+ version=version,
346
+ actor_type=actor,
347
+ actor_name=actor_name,
348
+ note=note,
349
+ allow_agent_approval=allow_agent_approval,
350
+ reason=reason,
351
+ allow_empty_criteria=allow_empty_criteria,
352
+ materialize_todos=not no_materialize_todos,
353
+ allow_open_questions=allow_open_questions,
354
+ allow_empty_todos=allow_empty_todos,
355
+ allow_lint_errors=allow_lint_errors,
356
+ )
357
+ except LaunchError as exc:
358
+ emit_error(ctx, exc)
359
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
360
+ emit_payload(
361
+ ctx,
362
+ payload,
363
+ human=f"approved plan v{payload['plan_version']} for {payload['task_id']}",
364
+ )
365
+
366
+ @app.command("accept")
367
+ def accept_command(
368
+ ctx: typer.Context,
369
+ version: Annotated[int, typer.Option("--version")],
370
+ note: Annotated[str | None, typer.Option("--note")] = None,
371
+ task_ref: TaskOption = None,
372
+ allow_lint_errors: Annotated[bool, typer.Option("--allow-lint-errors")] = False,
373
+ ) -> None:
374
+ state = cli_state_from_context(ctx)
375
+ try:
376
+ task = resolve_cli_task(state.cwd, task_ref)
377
+ payload = approve_plan(
378
+ state.cwd,
379
+ task.id,
380
+ version=version,
381
+ actor_type="user",
382
+ note=note,
383
+ allow_lint_errors=allow_lint_errors,
384
+ reason=note if allow_lint_errors else None,
385
+ )
386
+ except LaunchError as exc:
387
+ emit_error(ctx, exc)
388
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
389
+ emit_payload(
390
+ ctx,
391
+ payload,
392
+ human=f"accepted plan v{payload['plan_version']} for {payload['task_id']}",
393
+ )
394
+
395
+ @app.command("reject")
396
+ def reject_command(
397
+ ctx: typer.Context,
398
+ task_ref: TaskOption = None,
399
+ reason: Annotated[str | None, typer.Option("--reason")] = None,
400
+ ) -> None:
401
+ state = cli_state_from_context(ctx)
402
+ try:
403
+ task = resolve_cli_task(state.cwd, task_ref)
404
+ payload = reject_plan(state.cwd, task.id, reason=reason)
405
+ except LaunchError as exc:
406
+ emit_error(ctx, exc)
407
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
408
+ emit_payload(ctx, payload, human=f"rejected plan for {payload['task_id']}")
409
+
410
+ @app.command("revise")
411
+ def revise_command(
412
+ ctx: typer.Context,
413
+ task_ref: TaskOption = None,
414
+ ) -> None:
415
+ state = cli_state_from_context(ctx)
416
+ try:
417
+ task = resolve_cli_task(state.cwd, task_ref)
418
+ payload = revise_plan(state.cwd, task.id)
419
+ except LaunchError as exc:
420
+ emit_error(ctx, exc)
421
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
422
+ emit_payload(ctx, payload, human=f"restarted planning {payload['task_id']}")
423
+
424
+ @app.command(
425
+ "command",
426
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
427
+ )
428
+ def plan_command_command(
429
+ ctx: typer.Context,
430
+ task_ref: TaskOption = None,
431
+ ) -> None:
432
+ state = cli_state_from_context(ctx)
433
+ argv = tuple(ctx.args)
434
+ try:
435
+ task = resolve_cli_task(state.cwd, task_ref)
436
+ payload = run_planning_command(
437
+ state.cwd,
438
+ task.id,
439
+ argv=argv,
440
+ )
441
+ except LaunchError as exc:
442
+ emit_error(ctx, exc)
443
+ raise typer.Exit(code=launch_error_exit_code(exc)) from exc
444
+ emit_payload(
445
+ ctx,
446
+ payload,
447
+ human=f"ran planning command exit={payload['exit_code']}",
448
+ )
449
+
450
+
451
+ def _render_plan_lint(payload: PlanLintPayload) -> str:
452
+ passed = payload["passed"]
453
+ task_id = payload["task_id"]
454
+ plan_id = payload["plan_id"]
455
+ summary = payload["summary"]
456
+ issues = payload["issues"]
457
+ assert isinstance(summary, dict)
458
+ assert isinstance(issues, list)
459
+
460
+ status_word = "PASSED" if passed else "FAILED"
461
+ header = f"PLAN LINT {status_word} {task_id} {plan_id}"
462
+ lines = [header]
463
+ errors = summary.get("errors", 0)
464
+ warnings = summary.get("warnings", 0)
465
+ assert isinstance(errors, int) and isinstance(warnings, int)
466
+ lines.append(f"errors: {errors} warnings: {warnings}")
467
+ lines.append("")
468
+ for issue in issues:
469
+ assert isinstance(issue, dict)
470
+ sev = issue["severity"]
471
+ code = issue["code"]
472
+ location = issue["location"]
473
+ message = issue["message"]
474
+ assert isinstance(sev, str) and isinstance(code, str)
475
+ assert isinstance(location, str) and isinstance(message, str)
476
+ lines.append(f"[{sev}] {code} at {location}")
477
+ lines.append(f" {message}")
478
+ return "\n".join(lines)