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.
- taskledger/__init__.py +5 -0
- taskledger/__main__.py +6 -0
- taskledger/_version.py +24 -0
- taskledger/api/__init__.py +13 -0
- taskledger/api/handoff.py +247 -0
- taskledger/api/introductions.py +9 -0
- taskledger/api/locks.py +4 -0
- taskledger/api/plans.py +31 -0
- taskledger/api/project.py +185 -0
- taskledger/api/questions.py +19 -0
- taskledger/api/search.py +87 -0
- taskledger/api/task_runs.py +38 -0
- taskledger/api/tasks.py +61 -0
- taskledger/cli.py +600 -0
- taskledger/cli_actor.py +196 -0
- taskledger/cli_common.py +617 -0
- taskledger/cli_implement.py +409 -0
- taskledger/cli_migrate.py +328 -0
- taskledger/cli_misc.py +984 -0
- taskledger/cli_plan.py +478 -0
- taskledger/cli_question.py +350 -0
- taskledger/cli_task.py +257 -0
- taskledger/cli_validate.py +285 -0
- taskledger/command_inventory.py +125 -0
- taskledger/domain/__init__.py +2 -0
- taskledger/domain/models.py +1697 -0
- taskledger/domain/policies.py +542 -0
- taskledger/domain/states.py +320 -0
- taskledger/errors.py +165 -0
- taskledger/exchange.py +343 -0
- taskledger/ids.py +19 -0
- taskledger/py.typed +0 -0
- taskledger/search.py +349 -0
- taskledger/services/__init__.py +1 -0
- taskledger/services/actors.py +245 -0
- taskledger/services/dashboard.py +306 -0
- taskledger/services/doctor.py +435 -0
- taskledger/services/handoff.py +1029 -0
- taskledger/services/handoff_lifecycle.py +154 -0
- taskledger/services/navigation.py +930 -0
- taskledger/services/phase5_lock_transfer.py +96 -0
- taskledger/services/plan_lint.py +397 -0
- taskledger/services/serve_read_model.py +852 -0
- taskledger/services/tasks.py +4224 -0
- taskledger/services/validation.py +221 -0
- taskledger/services/web_dashboard.py +1742 -0
- taskledger/storage/__init__.py +39 -0
- taskledger/storage/atomic.py +57 -0
- taskledger/storage/common.py +90 -0
- taskledger/storage/events.py +98 -0
- taskledger/storage/frontmatter.py +57 -0
- taskledger/storage/indexes.py +42 -0
- taskledger/storage/init.py +187 -0
- taskledger/storage/locks.py +83 -0
- taskledger/storage/meta.py +103 -0
- taskledger/storage/migrations.py +207 -0
- taskledger/storage/paths.py +166 -0
- taskledger/storage/project_config.py +393 -0
- taskledger/storage/repos.py +256 -0
- taskledger/storage/task_store.py +836 -0
- taskledger/timeutils.py +7 -0
- taskledger-0.1.0.dist-info/METADATA +411 -0
- taskledger-0.1.0.dist-info/RECORD +67 -0
- taskledger-0.1.0.dist-info/WHEEL +5 -0
- taskledger-0.1.0.dist-info/entry_points.txt +2 -0
- taskledger-0.1.0.dist-info/licenses/LICENSE +201 -0
- taskledger-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Literal, cast
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from taskledger.api.questions import (
|
|
11
|
+
add_question,
|
|
12
|
+
answer_question,
|
|
13
|
+
answer_questions,
|
|
14
|
+
dismiss_question,
|
|
15
|
+
list_open_questions,
|
|
16
|
+
question_status,
|
|
17
|
+
)
|
|
18
|
+
from taskledger.cli_common import (
|
|
19
|
+
cli_state_from_context,
|
|
20
|
+
emit_error,
|
|
21
|
+
emit_payload,
|
|
22
|
+
launch_error_exit_code,
|
|
23
|
+
read_text_input,
|
|
24
|
+
resolve_cli_task,
|
|
25
|
+
)
|
|
26
|
+
from taskledger.domain.models import ActorRef
|
|
27
|
+
from taskledger.errors import LaunchError
|
|
28
|
+
from taskledger.storage.task_store import list_questions
|
|
29
|
+
|
|
30
|
+
_QUESTION_ANSWER_RE = re.compile(r"^(q-\d+):\s*(.+)$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _UniqueKeyLoader(yaml.SafeLoader):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _construct_mapping_unique_keys(
|
|
38
|
+
loader: yaml.Loader,
|
|
39
|
+
node: yaml.nodes.MappingNode,
|
|
40
|
+
deep: bool = False,
|
|
41
|
+
) -> dict[object, object]:
|
|
42
|
+
mapping: dict[object, object] = {}
|
|
43
|
+
for key_node, value_node in node.value:
|
|
44
|
+
key = loader.construct_object(key_node, deep=deep)
|
|
45
|
+
if key in mapping:
|
|
46
|
+
raise LaunchError(f"Duplicate key in answers input: {key}")
|
|
47
|
+
mapping[key] = loader.construct_object(value_node, deep=deep)
|
|
48
|
+
return mapping
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_UniqueKeyLoader.add_constructor(
|
|
52
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
|
|
53
|
+
_construct_mapping_unique_keys,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _parse_answer_many_input(raw: str) -> dict[str, str]:
|
|
58
|
+
try:
|
|
59
|
+
parsed = yaml.load(raw, Loader=_UniqueKeyLoader)
|
|
60
|
+
except yaml.YAMLError as exc:
|
|
61
|
+
raise LaunchError(f"Invalid answers YAML: {exc}") from exc
|
|
62
|
+
if isinstance(parsed, dict):
|
|
63
|
+
raw_answers = parsed.get("answers", parsed)
|
|
64
|
+
if not isinstance(raw_answers, dict):
|
|
65
|
+
raise LaunchError("answers must be a mapping of question ids to text.")
|
|
66
|
+
answers: dict[str, str] = {}
|
|
67
|
+
for key, value in raw_answers.items():
|
|
68
|
+
question_id = str(key).strip()
|
|
69
|
+
if not re.fullmatch(r"q-\d+", question_id):
|
|
70
|
+
raise LaunchError(f"Invalid question id in answers input: {key}")
|
|
71
|
+
if not isinstance(value, str):
|
|
72
|
+
raise LaunchError(f"Answer for {question_id} must be text.")
|
|
73
|
+
answers[question_id] = value
|
|
74
|
+
return answers
|
|
75
|
+
answers = {}
|
|
76
|
+
for line_number, line in enumerate(raw.splitlines(), start=1):
|
|
77
|
+
stripped = line.strip()
|
|
78
|
+
if not stripped:
|
|
79
|
+
continue
|
|
80
|
+
match = _QUESTION_ANSWER_RE.match(stripped)
|
|
81
|
+
if match is None:
|
|
82
|
+
raise LaunchError(
|
|
83
|
+
"Plain answer input must use 'q-0001: answer' lines; "
|
|
84
|
+
f"line {line_number} was invalid."
|
|
85
|
+
)
|
|
86
|
+
question_id, answer = match.groups()
|
|
87
|
+
if question_id in answers:
|
|
88
|
+
raise LaunchError(f"Duplicate question id in answers input: {question_id}")
|
|
89
|
+
answers[question_id] = answer
|
|
90
|
+
if not answers:
|
|
91
|
+
raise LaunchError("At least one answer is required.")
|
|
92
|
+
return answers
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _add_command(
|
|
96
|
+
ctx: typer.Context,
|
|
97
|
+
text: Annotated[str, typer.Option("--text")],
|
|
98
|
+
required_for_plan: Annotated[
|
|
99
|
+
bool,
|
|
100
|
+
typer.Option("--required-for-plan"),
|
|
101
|
+
] = False,
|
|
102
|
+
task_ref: Annotated[
|
|
103
|
+
str | None,
|
|
104
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
105
|
+
] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
state = cli_state_from_context(ctx)
|
|
108
|
+
try:
|
|
109
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
110
|
+
question = add_question(
|
|
111
|
+
state.cwd,
|
|
112
|
+
task.id,
|
|
113
|
+
text=text,
|
|
114
|
+
required_for_plan=required_for_plan,
|
|
115
|
+
)
|
|
116
|
+
except LaunchError as exc:
|
|
117
|
+
emit_error(ctx, exc)
|
|
118
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
119
|
+
emit_payload(ctx, question.to_dict(), human=f"added question {question.id}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _list_command(
|
|
123
|
+
ctx: typer.Context,
|
|
124
|
+
status: Annotated[
|
|
125
|
+
str | None,
|
|
126
|
+
typer.Option(
|
|
127
|
+
"--status",
|
|
128
|
+
help="Comma-separated status filter, e.g. answered,dismissed.",
|
|
129
|
+
),
|
|
130
|
+
] = None,
|
|
131
|
+
task_ref: Annotated[
|
|
132
|
+
str | None,
|
|
133
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
134
|
+
] = None,
|
|
135
|
+
) -> None:
|
|
136
|
+
state = cli_state_from_context(ctx)
|
|
137
|
+
try:
|
|
138
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
139
|
+
all_questions = list_questions(state.cwd, task.id)
|
|
140
|
+
except LaunchError as exc:
|
|
141
|
+
emit_error(ctx, exc)
|
|
142
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
143
|
+
status_filter: set[str] | None = None
|
|
144
|
+
if status is not None:
|
|
145
|
+
status_filter = {s.strip() for s in status.split(",") if s.strip()}
|
|
146
|
+
filtered = (
|
|
147
|
+
[q for q in all_questions if q.status in status_filter]
|
|
148
|
+
if status_filter is not None
|
|
149
|
+
else all_questions
|
|
150
|
+
)
|
|
151
|
+
payload = [item.to_dict() for item in filtered]
|
|
152
|
+
lines = ["QUESTIONS"]
|
|
153
|
+
for item in payload:
|
|
154
|
+
lines.append(f"{item['id']} {item['status']} {item['question']}")
|
|
155
|
+
emit_payload(
|
|
156
|
+
ctx,
|
|
157
|
+
payload,
|
|
158
|
+
human="\n".join(lines) if payload else "QUESTIONS\n(empty)",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _answer_command(
|
|
163
|
+
ctx: typer.Context,
|
|
164
|
+
question_id: Annotated[str, typer.Argument(..., help="Question id.")],
|
|
165
|
+
text: Annotated[str, typer.Option("--text")],
|
|
166
|
+
actor: Annotated[str, typer.Option("--actor")] = "user",
|
|
167
|
+
task_ref: Annotated[
|
|
168
|
+
str | None,
|
|
169
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
170
|
+
] = None,
|
|
171
|
+
) -> None:
|
|
172
|
+
state = cli_state_from_context(ctx)
|
|
173
|
+
try:
|
|
174
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
175
|
+
question = answer_question(
|
|
176
|
+
state.cwd,
|
|
177
|
+
task.id,
|
|
178
|
+
question_id,
|
|
179
|
+
text=text,
|
|
180
|
+
actor=ActorRef(
|
|
181
|
+
actor_type=cast(Literal["agent", "user", "system"], actor),
|
|
182
|
+
actor_name=actor,
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
except LaunchError as exc:
|
|
186
|
+
emit_error(ctx, exc)
|
|
187
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
188
|
+
emit_payload(ctx, question.to_dict(), human=f"answered {question.id}")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _answer_many_command(
|
|
192
|
+
ctx: typer.Context,
|
|
193
|
+
text: Annotated[str | None, typer.Option("--text")] = None,
|
|
194
|
+
from_file: Annotated[Path | None, typer.Option("--file")] = None,
|
|
195
|
+
actor: Annotated[str, typer.Option("--actor")] = "user",
|
|
196
|
+
task_ref: Annotated[
|
|
197
|
+
str | None,
|
|
198
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
199
|
+
] = None,
|
|
200
|
+
) -> None:
|
|
201
|
+
state = cli_state_from_context(ctx)
|
|
202
|
+
try:
|
|
203
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
204
|
+
raw = read_text_input(text=text, from_file=from_file)
|
|
205
|
+
payload = answer_questions(
|
|
206
|
+
state.cwd,
|
|
207
|
+
task.id,
|
|
208
|
+
_parse_answer_many_input(raw),
|
|
209
|
+
actor=ActorRef(
|
|
210
|
+
actor_type=cast(Literal["agent", "user", "system"], actor),
|
|
211
|
+
actor_name=actor,
|
|
212
|
+
),
|
|
213
|
+
answer_source="harness",
|
|
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(
|
|
219
|
+
ctx,
|
|
220
|
+
payload,
|
|
221
|
+
human=(
|
|
222
|
+
f"answered {len(cast(list[object], payload['answered_question_ids']))} "
|
|
223
|
+
"questions\n"
|
|
224
|
+
f"Required open: {payload['required_open']}\n"
|
|
225
|
+
f"Plan regeneration needed: {payload['plan_regeneration_needed']}\n"
|
|
226
|
+
f"Next: {payload['next_action']}"
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _dismiss_command(
|
|
232
|
+
ctx: typer.Context,
|
|
233
|
+
question_id: Annotated[str, typer.Argument(..., help="Question id.")],
|
|
234
|
+
task_ref: Annotated[
|
|
235
|
+
str | None,
|
|
236
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
237
|
+
] = None,
|
|
238
|
+
) -> None:
|
|
239
|
+
state = cli_state_from_context(ctx)
|
|
240
|
+
try:
|
|
241
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
242
|
+
question = dismiss_question(state.cwd, task.id, question_id)
|
|
243
|
+
except LaunchError as exc:
|
|
244
|
+
emit_error(ctx, exc)
|
|
245
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
246
|
+
emit_payload(ctx, question.to_dict(), human=f"dismissed {question.id}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _open_command(
|
|
250
|
+
ctx: typer.Context,
|
|
251
|
+
task_ref: Annotated[
|
|
252
|
+
str | None,
|
|
253
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
254
|
+
] = None,
|
|
255
|
+
) -> None:
|
|
256
|
+
state = cli_state_from_context(ctx)
|
|
257
|
+
try:
|
|
258
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
259
|
+
payload = list_open_questions(state.cwd, task.id)
|
|
260
|
+
except LaunchError as exc:
|
|
261
|
+
emit_error(ctx, exc)
|
|
262
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
263
|
+
questions = payload["questions"]
|
|
264
|
+
assert isinstance(questions, list)
|
|
265
|
+
lines = ["OPEN QUESTIONS"]
|
|
266
|
+
for item in questions:
|
|
267
|
+
if isinstance(item, dict):
|
|
268
|
+
lines.append(f"{item['id']} {item['question']}")
|
|
269
|
+
emit_payload(
|
|
270
|
+
ctx,
|
|
271
|
+
payload,
|
|
272
|
+
human="\n".join(lines) if questions else "OPEN QUESTIONS\n(empty)",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _answers_command(
|
|
277
|
+
ctx: typer.Context,
|
|
278
|
+
format_name: Annotated[
|
|
279
|
+
str,
|
|
280
|
+
typer.Option("--format", help="Output format: markdown or json."),
|
|
281
|
+
] = "markdown",
|
|
282
|
+
task_ref: Annotated[
|
|
283
|
+
str | None,
|
|
284
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
285
|
+
] = None,
|
|
286
|
+
) -> None:
|
|
287
|
+
state = cli_state_from_context(ctx)
|
|
288
|
+
try:
|
|
289
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
290
|
+
all_questions = list_questions(state.cwd, task.id)
|
|
291
|
+
except LaunchError as exc:
|
|
292
|
+
emit_error(ctx, exc)
|
|
293
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
294
|
+
answered = [q for q in all_questions if q.status == "answered"]
|
|
295
|
+
payload_dicts = [q.to_dict() for q in answered]
|
|
296
|
+
payload = {
|
|
297
|
+
"kind": "question_answers",
|
|
298
|
+
"task_id": task.id,
|
|
299
|
+
"questions": payload_dicts,
|
|
300
|
+
}
|
|
301
|
+
if format_name == "json":
|
|
302
|
+
emit_payload(ctx, payload)
|
|
303
|
+
return
|
|
304
|
+
lines = ["ANSWERED QUESTIONS"]
|
|
305
|
+
for q in answered:
|
|
306
|
+
lines.append("")
|
|
307
|
+
lines.append(f"### {q.id}")
|
|
308
|
+
lines.append(f"Q: {q.question}")
|
|
309
|
+
lines.append(f"A: {q.answer}")
|
|
310
|
+
emit_payload(
|
|
311
|
+
ctx,
|
|
312
|
+
payload,
|
|
313
|
+
human="\n".join(lines) if answered else "ANSWERED QUESTIONS\n(empty)",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _status_command(
|
|
318
|
+
ctx: typer.Context,
|
|
319
|
+
task_ref: Annotated[
|
|
320
|
+
str | None,
|
|
321
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
322
|
+
] = None,
|
|
323
|
+
) -> None:
|
|
324
|
+
state = cli_state_from_context(ctx)
|
|
325
|
+
try:
|
|
326
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
327
|
+
payload = question_status(state.cwd, task.id)
|
|
328
|
+
except LaunchError as exc:
|
|
329
|
+
emit_error(ctx, exc)
|
|
330
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
331
|
+
emit_payload(
|
|
332
|
+
ctx,
|
|
333
|
+
payload,
|
|
334
|
+
human=(
|
|
335
|
+
f"Required open: {payload['required_open']}\n"
|
|
336
|
+
f"Plan regeneration needed: {payload['plan_regeneration_needed']}\n"
|
|
337
|
+
f"Next: {payload['next_action']}"
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def register_question_v2_commands(app: typer.Typer) -> None:
|
|
343
|
+
app.command("add")(_add_command)
|
|
344
|
+
app.command("list")(_list_command)
|
|
345
|
+
app.command("answer")(_answer_command)
|
|
346
|
+
app.command("answer-many")(_answer_many_command)
|
|
347
|
+
app.command("dismiss")(_dismiss_command)
|
|
348
|
+
app.command("open")(_open_command)
|
|
349
|
+
app.command("answers")(_answers_command)
|
|
350
|
+
app.command("status")(_status_command)
|
taskledger/cli_task.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, cast
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from taskledger.api.tasks import (
|
|
9
|
+
activate_task,
|
|
10
|
+
cancel_task,
|
|
11
|
+
close_task,
|
|
12
|
+
create_task,
|
|
13
|
+
deactivate_task,
|
|
14
|
+
edit_task,
|
|
15
|
+
list_task_summaries,
|
|
16
|
+
show_active_task,
|
|
17
|
+
show_task,
|
|
18
|
+
task_dossier,
|
|
19
|
+
)
|
|
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
|
+
read_text_input,
|
|
27
|
+
resolve_cli_task,
|
|
28
|
+
)
|
|
29
|
+
from taskledger.errors import LaunchError
|
|
30
|
+
from taskledger.services.tasks import list_events as _list_events
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_task_v2_commands(app: typer.Typer) -> None: # noqa: C901
|
|
34
|
+
@app.command("create")
|
|
35
|
+
def create_command(
|
|
36
|
+
ctx: typer.Context,
|
|
37
|
+
title_arg: Annotated[str, typer.Argument(..., help="Task title.")],
|
|
38
|
+
description: Annotated[
|
|
39
|
+
str | None,
|
|
40
|
+
typer.Option("--description", help="Task description."),
|
|
41
|
+
] = None,
|
|
42
|
+
slug: Annotated[str | None, typer.Option("--slug")] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
state = cli_state_from_context(ctx)
|
|
45
|
+
try:
|
|
46
|
+
task = create_task(
|
|
47
|
+
state.cwd,
|
|
48
|
+
title=title_arg,
|
|
49
|
+
description=read_text_input(
|
|
50
|
+
text=description or title_arg,
|
|
51
|
+
text_label="--description",
|
|
52
|
+
),
|
|
53
|
+
slug=slug or title_arg,
|
|
54
|
+
)
|
|
55
|
+
except LaunchError as exc:
|
|
56
|
+
emit_error(ctx, exc)
|
|
57
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
58
|
+
emit_payload(
|
|
59
|
+
ctx,
|
|
60
|
+
task.to_dict(),
|
|
61
|
+
human=f"created task {task.slug} ({task.id})",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@app.command("list")
|
|
65
|
+
def list_command(ctx: typer.Context) -> None:
|
|
66
|
+
state = cli_state_from_context(ctx)
|
|
67
|
+
payload = {"kind": "task_list", "tasks": list_task_summaries(state.cwd)}
|
|
68
|
+
human_lines = ["TASKS"]
|
|
69
|
+
if not payload["tasks"]:
|
|
70
|
+
human_lines.append("(empty)")
|
|
71
|
+
else:
|
|
72
|
+
for task in cast(list[dict[str, object]], payload["tasks"]):
|
|
73
|
+
active = task.get("active_stage")
|
|
74
|
+
stage = (
|
|
75
|
+
f"{task['status_stage']} [{active}]"
|
|
76
|
+
if active
|
|
77
|
+
else str(task["status_stage"])
|
|
78
|
+
)
|
|
79
|
+
human_lines.append(f"{task['slug']} {task['id']} {stage}")
|
|
80
|
+
emit_payload(ctx, payload, human="\n".join(human_lines))
|
|
81
|
+
|
|
82
|
+
@app.command("active")
|
|
83
|
+
def active_command(ctx: typer.Context) -> None:
|
|
84
|
+
state = cli_state_from_context(ctx)
|
|
85
|
+
try:
|
|
86
|
+
payload = show_active_task(state.cwd)
|
|
87
|
+
except LaunchError as exc:
|
|
88
|
+
emit_error(ctx, exc)
|
|
89
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
90
|
+
emit_payload(
|
|
91
|
+
ctx,
|
|
92
|
+
payload,
|
|
93
|
+
human=f"{payload['slug']} ({payload['task_id']})",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.command("activate")
|
|
97
|
+
def activate_command(
|
|
98
|
+
ctx: typer.Context,
|
|
99
|
+
ref: Annotated[str, typer.Argument(..., help="Task ref.")],
|
|
100
|
+
reason: Annotated[str | None, typer.Option("--reason")] = None,
|
|
101
|
+
force: Annotated[bool, typer.Option("--force")] = False,
|
|
102
|
+
) -> None:
|
|
103
|
+
state = cli_state_from_context(ctx)
|
|
104
|
+
try:
|
|
105
|
+
payload = activate_task(state.cwd, ref, reason=reason, force=force)
|
|
106
|
+
except LaunchError as exc:
|
|
107
|
+
emit_error(ctx, exc)
|
|
108
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
109
|
+
changed = "activated" if payload["changed"] else "already active"
|
|
110
|
+
emit_payload(ctx, payload, human=f"{changed} {payload['task_id']}")
|
|
111
|
+
|
|
112
|
+
@app.command("deactivate")
|
|
113
|
+
def deactivate_command(
|
|
114
|
+
ctx: typer.Context,
|
|
115
|
+
reason: Annotated[str, typer.Option("--reason")],
|
|
116
|
+
force: Annotated[bool, typer.Option("--force")] = False,
|
|
117
|
+
) -> None:
|
|
118
|
+
state = cli_state_from_context(ctx)
|
|
119
|
+
try:
|
|
120
|
+
payload = deactivate_task(state.cwd, reason=reason, force=force)
|
|
121
|
+
except LaunchError as exc:
|
|
122
|
+
emit_error(ctx, exc)
|
|
123
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
124
|
+
emit_payload(ctx, payload, human=f"deactivated {payload['task_id']}")
|
|
125
|
+
|
|
126
|
+
@app.command("show")
|
|
127
|
+
def show_command(
|
|
128
|
+
ctx: typer.Context,
|
|
129
|
+
task_ref: TaskOption = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
state = cli_state_from_context(ctx)
|
|
132
|
+
try:
|
|
133
|
+
resolved = resolve_cli_task(state.cwd, task_ref)
|
|
134
|
+
payload = show_task(state.cwd, resolved.id)
|
|
135
|
+
except LaunchError as exc:
|
|
136
|
+
emit_error(ctx, exc)
|
|
137
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
138
|
+
task = payload["task"]
|
|
139
|
+
assert isinstance(task, dict)
|
|
140
|
+
emit_payload(
|
|
141
|
+
ctx,
|
|
142
|
+
payload,
|
|
143
|
+
human=(
|
|
144
|
+
f"{task['title']} ({task['id']})\n"
|
|
145
|
+
f"status: {task['status_stage']}\n"
|
|
146
|
+
f"active_stage: {task.get('active_stage') or 'none'}\n"
|
|
147
|
+
f"slug: {task['slug']}"
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@app.command("edit")
|
|
152
|
+
def edit_command(
|
|
153
|
+
ctx: typer.Context,
|
|
154
|
+
task_ref: TaskOption = None,
|
|
155
|
+
title: Annotated[str | None, typer.Option("--title")] = None,
|
|
156
|
+
description: Annotated[str | None, typer.Option("--description")] = None,
|
|
157
|
+
from_file: Annotated[Path | None, typer.Option("--from-file")] = None,
|
|
158
|
+
priority: Annotated[str | None, typer.Option("--priority")] = None,
|
|
159
|
+
owner: Annotated[str | None, typer.Option("--owner")] = None,
|
|
160
|
+
add_label: Annotated[list[str] | None, typer.Option("--add-label")] = None,
|
|
161
|
+
remove_label: Annotated[
|
|
162
|
+
list[str] | None,
|
|
163
|
+
typer.Option("--remove-label"),
|
|
164
|
+
] = None,
|
|
165
|
+
add_note: Annotated[list[str] | None, typer.Option("--add-note")] = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
state = cli_state_from_context(ctx)
|
|
168
|
+
try:
|
|
169
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
170
|
+
task = edit_task(
|
|
171
|
+
state.cwd,
|
|
172
|
+
task.id,
|
|
173
|
+
title=title,
|
|
174
|
+
description=(
|
|
175
|
+
read_text_input(text=description, from_file=from_file)
|
|
176
|
+
if description is not None or from_file is not None
|
|
177
|
+
else None
|
|
178
|
+
),
|
|
179
|
+
priority=priority,
|
|
180
|
+
owner=owner,
|
|
181
|
+
add_labels=tuple(add_label or ()),
|
|
182
|
+
remove_labels=tuple(remove_label or ()),
|
|
183
|
+
add_notes=tuple(add_note or ()),
|
|
184
|
+
)
|
|
185
|
+
except LaunchError as exc:
|
|
186
|
+
emit_error(ctx, exc)
|
|
187
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
188
|
+
emit_payload(ctx, task.to_dict(), human=f"updated task {task.id}")
|
|
189
|
+
|
|
190
|
+
@app.command("cancel")
|
|
191
|
+
def cancel_command(
|
|
192
|
+
ctx: typer.Context,
|
|
193
|
+
task_ref: TaskOption = None,
|
|
194
|
+
reason: Annotated[str | None, typer.Option("--reason")] = None,
|
|
195
|
+
) -> None:
|
|
196
|
+
state = cli_state_from_context(ctx)
|
|
197
|
+
try:
|
|
198
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
199
|
+
payload = cancel_task(state.cwd, task.id, reason=reason)
|
|
200
|
+
except LaunchError as exc:
|
|
201
|
+
emit_error(ctx, exc)
|
|
202
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
203
|
+
emit_payload(ctx, payload, human=f"cancelled task {payload['task_id']}")
|
|
204
|
+
|
|
205
|
+
@app.command("close")
|
|
206
|
+
def close_command(
|
|
207
|
+
ctx: typer.Context,
|
|
208
|
+
task_ref: TaskOption = None,
|
|
209
|
+
) -> None:
|
|
210
|
+
state = cli_state_from_context(ctx)
|
|
211
|
+
try:
|
|
212
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
213
|
+
payload = close_task(state.cwd, task.id)
|
|
214
|
+
except LaunchError as exc:
|
|
215
|
+
emit_error(ctx, exc)
|
|
216
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
217
|
+
emit_payload(ctx, payload, human=f"closed task {payload['task_id']}")
|
|
218
|
+
|
|
219
|
+
@app.command("events")
|
|
220
|
+
def events_command(
|
|
221
|
+
ctx: typer.Context,
|
|
222
|
+
task_ref: TaskOption = None,
|
|
223
|
+
all_tasks: Annotated[
|
|
224
|
+
bool, typer.Option("--all", help="Show events for all tasks.")
|
|
225
|
+
] = False,
|
|
226
|
+
limit: Annotated[int, typer.Option("--limit", help="Max events to show.")] = 50,
|
|
227
|
+
) -> None:
|
|
228
|
+
state = cli_state_from_context(ctx)
|
|
229
|
+
events = _list_events(state.cwd)
|
|
230
|
+
if not all_tasks:
|
|
231
|
+
try:
|
|
232
|
+
resolved = resolve_cli_task(state.cwd, task_ref)
|
|
233
|
+
except LaunchError as exc:
|
|
234
|
+
emit_error(ctx, exc)
|
|
235
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
236
|
+
events = [e for e in events if e.get("task_id") == resolved.id]
|
|
237
|
+
events = events[-limit:]
|
|
238
|
+
payload = {"kind": "event_list", "items": events}
|
|
239
|
+
from taskledger.cli_common import render_events_human
|
|
240
|
+
|
|
241
|
+
human = render_events_human(events)
|
|
242
|
+
emit_payload(ctx, payload, human=human, result_type="event_list")
|
|
243
|
+
|
|
244
|
+
@app.command("dossier")
|
|
245
|
+
def dossier_command(
|
|
246
|
+
ctx: typer.Context,
|
|
247
|
+
task_ref: TaskOption = None,
|
|
248
|
+
format_name: Annotated[str, typer.Option("--format")] = "markdown",
|
|
249
|
+
) -> None:
|
|
250
|
+
state = cli_state_from_context(ctx)
|
|
251
|
+
try:
|
|
252
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
253
|
+
payload = task_dossier(state.cwd, task.id, format_name=format_name)
|
|
254
|
+
except LaunchError as exc:
|
|
255
|
+
emit_error(ctx, exc)
|
|
256
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
257
|
+
emit_payload(ctx, payload, human=payload if isinstance(payload, str) else None)
|