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,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))
|