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
taskledger/api/tasks.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from taskledger.services.tasks import (
|
|
4
|
+
activate_task,
|
|
5
|
+
add_file_link,
|
|
6
|
+
add_requirement,
|
|
7
|
+
add_todo,
|
|
8
|
+
can_perform,
|
|
9
|
+
cancel_task,
|
|
10
|
+
clear_active_task,
|
|
11
|
+
close_task,
|
|
12
|
+
create_task,
|
|
13
|
+
deactivate_task,
|
|
14
|
+
edit_task,
|
|
15
|
+
list_file_links,
|
|
16
|
+
list_task_summaries,
|
|
17
|
+
next_action,
|
|
18
|
+
next_todo,
|
|
19
|
+
reindex,
|
|
20
|
+
remove_file_link,
|
|
21
|
+
remove_requirement,
|
|
22
|
+
repair_task_record,
|
|
23
|
+
resolve_active_task,
|
|
24
|
+
set_todo_done,
|
|
25
|
+
show_active_task,
|
|
26
|
+
show_task,
|
|
27
|
+
show_todo,
|
|
28
|
+
task_dossier,
|
|
29
|
+
todo_status,
|
|
30
|
+
waive_requirement,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"create_task",
|
|
35
|
+
"show_active_task",
|
|
36
|
+
"activate_task",
|
|
37
|
+
"deactivate_task",
|
|
38
|
+
"clear_active_task",
|
|
39
|
+
"resolve_active_task",
|
|
40
|
+
"list_task_summaries",
|
|
41
|
+
"show_task",
|
|
42
|
+
"edit_task",
|
|
43
|
+
"cancel_task",
|
|
44
|
+
"close_task",
|
|
45
|
+
"add_requirement",
|
|
46
|
+
"remove_requirement",
|
|
47
|
+
"waive_requirement",
|
|
48
|
+
"add_file_link",
|
|
49
|
+
"remove_file_link",
|
|
50
|
+
"list_file_links",
|
|
51
|
+
"add_todo",
|
|
52
|
+
"set_todo_done",
|
|
53
|
+
"show_todo",
|
|
54
|
+
"todo_status",
|
|
55
|
+
"next_todo",
|
|
56
|
+
"task_dossier",
|
|
57
|
+
"next_action",
|
|
58
|
+
"can_perform",
|
|
59
|
+
"reindex",
|
|
60
|
+
"repair_task_record",
|
|
61
|
+
]
|
taskledger/cli.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Any, cast
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from taskledger.api.handoff import render_handoff
|
|
10
|
+
from taskledger.api.project import (
|
|
11
|
+
init_project,
|
|
12
|
+
project_export,
|
|
13
|
+
project_import,
|
|
14
|
+
project_snapshot,
|
|
15
|
+
project_status,
|
|
16
|
+
project_status_summary,
|
|
17
|
+
)
|
|
18
|
+
from taskledger.api.search import (
|
|
19
|
+
dependencies_for_module,
|
|
20
|
+
grep_workspace,
|
|
21
|
+
search_workspace,
|
|
22
|
+
symbols_workspace,
|
|
23
|
+
)
|
|
24
|
+
from taskledger.cli_actor import app as actors_app
|
|
25
|
+
from taskledger.cli_actor import harness_app
|
|
26
|
+
from taskledger.cli_common import (
|
|
27
|
+
CLIState,
|
|
28
|
+
TaskOption,
|
|
29
|
+
emit_error,
|
|
30
|
+
emit_payload,
|
|
31
|
+
launch_error_exit_code,
|
|
32
|
+
render_json,
|
|
33
|
+
resolve_cli_task,
|
|
34
|
+
resolve_workspace_root,
|
|
35
|
+
)
|
|
36
|
+
from taskledger.cli_implement import register_implement_v2_commands
|
|
37
|
+
from taskledger.cli_migrate import migrate_app
|
|
38
|
+
from taskledger.cli_misc import (
|
|
39
|
+
emit_can_command,
|
|
40
|
+
emit_doctor_command,
|
|
41
|
+
emit_doctor_indexes_command,
|
|
42
|
+
emit_doctor_locks_command,
|
|
43
|
+
emit_doctor_schema_command,
|
|
44
|
+
emit_next_action_command,
|
|
45
|
+
emit_reindex_command,
|
|
46
|
+
register_file_v2_commands,
|
|
47
|
+
register_handoff_v2_commands,
|
|
48
|
+
register_intro_v2_commands,
|
|
49
|
+
register_link_v2_commands,
|
|
50
|
+
register_lock_v2_commands,
|
|
51
|
+
register_require_v2_commands,
|
|
52
|
+
register_todo_v2_commands,
|
|
53
|
+
)
|
|
54
|
+
from taskledger.cli_plan import register_plan_v2_commands
|
|
55
|
+
from taskledger.cli_question import register_question_v2_commands
|
|
56
|
+
from taskledger.cli_task import register_task_v2_commands
|
|
57
|
+
from taskledger.cli_validate import register_validate_v2_commands
|
|
58
|
+
from taskledger.errors import LaunchError
|
|
59
|
+
from taskledger.services.dashboard import dashboard, render_dashboard_text
|
|
60
|
+
from taskledger.services.web_dashboard import (
|
|
61
|
+
DashboardServerConfig,
|
|
62
|
+
launch_dashboard_server,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
app = typer.Typer(add_completion=False, help="Manage staged taskledger coding work.")
|
|
66
|
+
task_app = typer.Typer(add_completion=False, help="Manage coding tasks.")
|
|
67
|
+
plan_app = typer.Typer(add_completion=False, help="Manage plan versions.")
|
|
68
|
+
question_app = typer.Typer(add_completion=False, help="Manage planning questions.")
|
|
69
|
+
implement_app = typer.Typer(add_completion=False, help="Manage implementation runs.")
|
|
70
|
+
validate_app = typer.Typer(add_completion=False, help="Manage validation runs.")
|
|
71
|
+
todo_app = typer.Typer(add_completion=False, help="Manage task todos.")
|
|
72
|
+
intro_app = typer.Typer(add_completion=False, help="Manage shared introductions.")
|
|
73
|
+
file_app = typer.Typer(add_completion=False, help="Manage task file links.")
|
|
74
|
+
link_app = typer.Typer(
|
|
75
|
+
add_completion=False,
|
|
76
|
+
help="Manage external and typed task links.",
|
|
77
|
+
)
|
|
78
|
+
require_app = typer.Typer(add_completion=False, help="Manage task requirements.")
|
|
79
|
+
lock_app = typer.Typer(add_completion=False, help="Inspect and repair locks.")
|
|
80
|
+
handoff_app = typer.Typer(add_completion=False, help="Render fresh-context handoffs.")
|
|
81
|
+
repair_app = typer.Typer(add_completion=False, help="Repair taskledger state.")
|
|
82
|
+
doctor_app = typer.Typer(
|
|
83
|
+
add_completion=False,
|
|
84
|
+
help="Inspect taskledger integrity.",
|
|
85
|
+
invoke_without_command=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
app.add_typer(task_app, name="task")
|
|
89
|
+
app.add_typer(plan_app, name="plan")
|
|
90
|
+
app.add_typer(question_app, name="question")
|
|
91
|
+
app.add_typer(implement_app, name="implement")
|
|
92
|
+
app.add_typer(validate_app, name="validate")
|
|
93
|
+
app.add_typer(todo_app, name="todo")
|
|
94
|
+
app.add_typer(intro_app, name="intro")
|
|
95
|
+
app.add_typer(file_app, name="file")
|
|
96
|
+
app.add_typer(link_app, name="link")
|
|
97
|
+
app.add_typer(require_app, name="require")
|
|
98
|
+
app.add_typer(lock_app, name="lock")
|
|
99
|
+
app.add_typer(handoff_app, name="handoff")
|
|
100
|
+
app.add_typer(doctor_app, name="doctor")
|
|
101
|
+
app.add_typer(repair_app, name="repair")
|
|
102
|
+
app.add_typer(migrate_app, name="migrate")
|
|
103
|
+
app.add_typer(actors_app, name="actor")
|
|
104
|
+
app.add_typer(harness_app, name="harness")
|
|
105
|
+
|
|
106
|
+
register_task_v2_commands(task_app)
|
|
107
|
+
register_plan_v2_commands(plan_app)
|
|
108
|
+
register_question_v2_commands(question_app)
|
|
109
|
+
register_implement_v2_commands(implement_app)
|
|
110
|
+
register_validate_v2_commands(validate_app)
|
|
111
|
+
register_todo_v2_commands(todo_app)
|
|
112
|
+
register_intro_v2_commands(intro_app)
|
|
113
|
+
register_file_v2_commands(file_app)
|
|
114
|
+
register_link_v2_commands(link_app)
|
|
115
|
+
register_require_v2_commands(require_app)
|
|
116
|
+
register_lock_v2_commands(lock_app)
|
|
117
|
+
register_handoff_v2_commands(handoff_app)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command("context")
|
|
121
|
+
def context_command(
|
|
122
|
+
ctx: typer.Context,
|
|
123
|
+
task_ref: TaskOption = None,
|
|
124
|
+
context_for: Annotated[
|
|
125
|
+
str,
|
|
126
|
+
typer.Option(
|
|
127
|
+
"--for",
|
|
128
|
+
help=(
|
|
129
|
+
"Context role: planner, implementer, validator, "
|
|
130
|
+
"spec-reviewer, code-reviewer, reviewer, full."
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
] = "full",
|
|
134
|
+
scope: Annotated[
|
|
135
|
+
str | None,
|
|
136
|
+
typer.Option("--scope", help="Context scope: task, todo, or run."),
|
|
137
|
+
] = None,
|
|
138
|
+
todo_id: Annotated[
|
|
139
|
+
str | None, typer.Option("--todo", help="Focus on one todo id.")
|
|
140
|
+
] = None,
|
|
141
|
+
focus_run_id: Annotated[
|
|
142
|
+
str | None, typer.Option("--run", help="Focus on one run id.")
|
|
143
|
+
] = None,
|
|
144
|
+
format_name: Annotated[str, typer.Option("--format")] = "markdown",
|
|
145
|
+
) -> None:
|
|
146
|
+
state = ctx.obj
|
|
147
|
+
assert isinstance(state, CLIState)
|
|
148
|
+
try:
|
|
149
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
150
|
+
payload = render_handoff(
|
|
151
|
+
state.cwd,
|
|
152
|
+
task.id,
|
|
153
|
+
context_for=context_for,
|
|
154
|
+
scope=scope,
|
|
155
|
+
todo_id=todo_id,
|
|
156
|
+
focus_run_id=focus_run_id,
|
|
157
|
+
format_name=format_name,
|
|
158
|
+
)
|
|
159
|
+
except LaunchError as exc:
|
|
160
|
+
emit_error(ctx, exc)
|
|
161
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
162
|
+
human = (
|
|
163
|
+
payload
|
|
164
|
+
if isinstance(payload, str)
|
|
165
|
+
else render_json(payload)
|
|
166
|
+
if format_name == "json"
|
|
167
|
+
else None
|
|
168
|
+
)
|
|
169
|
+
emit_payload(ctx, payload, human=human)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.callback()
|
|
173
|
+
def main(
|
|
174
|
+
ctx: typer.Context,
|
|
175
|
+
cwd: Annotated[
|
|
176
|
+
Path | None,
|
|
177
|
+
typer.Option(
|
|
178
|
+
"--cwd",
|
|
179
|
+
help="Workspace root. Defaults to the current directory.",
|
|
180
|
+
),
|
|
181
|
+
] = None,
|
|
182
|
+
root: Annotated[
|
|
183
|
+
Path | None,
|
|
184
|
+
typer.Option(
|
|
185
|
+
"--root",
|
|
186
|
+
help="Workspace root. Preferred alias for --cwd.",
|
|
187
|
+
),
|
|
188
|
+
] = None,
|
|
189
|
+
json_output: Annotated[
|
|
190
|
+
bool,
|
|
191
|
+
typer.Option("--json", help="Render machine-readable JSON."),
|
|
192
|
+
] = False,
|
|
193
|
+
) -> None:
|
|
194
|
+
if cwd is not None and root is not None and cwd != root:
|
|
195
|
+
raise typer.BadParameter(
|
|
196
|
+
"Use either --cwd or --root, not both with different values."
|
|
197
|
+
)
|
|
198
|
+
raw_cwd = (root or cwd or Path.cwd()).expanduser().resolve()
|
|
199
|
+
try:
|
|
200
|
+
resolved_cwd = resolve_workspace_root(raw_cwd)
|
|
201
|
+
except LaunchError as exc:
|
|
202
|
+
ctx.obj = CLIState(cwd=raw_cwd, json_output=json_output)
|
|
203
|
+
emit_error(ctx, exc)
|
|
204
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
205
|
+
ctx.obj = CLIState(cwd=resolved_cwd, json_output=json_output)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command("init")
|
|
209
|
+
def init_command(
|
|
210
|
+
ctx: typer.Context,
|
|
211
|
+
taskledger_dir: Annotated[
|
|
212
|
+
Path | None,
|
|
213
|
+
typer.Option("--taskledger-dir", help="Durable taskledger storage root."),
|
|
214
|
+
] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
state = ctx.obj
|
|
217
|
+
assert isinstance(state, CLIState)
|
|
218
|
+
payload = init_project(state.cwd, taskledger_dir=taskledger_dir)
|
|
219
|
+
emit_payload(
|
|
220
|
+
ctx,
|
|
221
|
+
payload,
|
|
222
|
+
human="\n".join(
|
|
223
|
+
[
|
|
224
|
+
f"initialized taskledger: {payload['root']}",
|
|
225
|
+
*[f"- {item}" for item in cast(list[str], payload["created"])],
|
|
226
|
+
]
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.command("status")
|
|
232
|
+
def status_command(
|
|
233
|
+
ctx: typer.Context,
|
|
234
|
+
full: Annotated[
|
|
235
|
+
bool,
|
|
236
|
+
typer.Option(
|
|
237
|
+
"--full",
|
|
238
|
+
help="Show the full status payload instead of the compact summary.",
|
|
239
|
+
),
|
|
240
|
+
] = False,
|
|
241
|
+
) -> None:
|
|
242
|
+
state = ctx.obj
|
|
243
|
+
assert isinstance(state, CLIState)
|
|
244
|
+
try:
|
|
245
|
+
payload = (
|
|
246
|
+
project_status(state.cwd) if full else project_status_summary(state.cwd)
|
|
247
|
+
)
|
|
248
|
+
except LaunchError as exc:
|
|
249
|
+
emit_error(ctx, exc)
|
|
250
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
251
|
+
emit_payload(ctx, payload)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command("view")
|
|
255
|
+
def view_command(
|
|
256
|
+
ctx: typer.Context,
|
|
257
|
+
task_ref: TaskOption = None,
|
|
258
|
+
) -> None:
|
|
259
|
+
state = ctx.obj
|
|
260
|
+
assert isinstance(state, CLIState)
|
|
261
|
+
try:
|
|
262
|
+
payload = dashboard(state.cwd, ref=task_ref)
|
|
263
|
+
except LaunchError as exc:
|
|
264
|
+
emit_error(ctx, exc)
|
|
265
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
266
|
+
human = render_dashboard_text(payload)
|
|
267
|
+
emit_payload(ctx, payload, human=human)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command("serve")
|
|
271
|
+
def serve_command(
|
|
272
|
+
ctx: typer.Context,
|
|
273
|
+
task_ref: TaskOption = None,
|
|
274
|
+
host: Annotated[str, typer.Option("--host")] = "127.0.0.1",
|
|
275
|
+
port: Annotated[int, typer.Option("--port")] = 8765,
|
|
276
|
+
refresh_ms: Annotated[int, typer.Option("--refresh-ms")] = 1000,
|
|
277
|
+
open_browser: Annotated[bool, typer.Option("--open/--no-open")] = False,
|
|
278
|
+
) -> None:
|
|
279
|
+
state = ctx.obj
|
|
280
|
+
assert isinstance(state, CLIState)
|
|
281
|
+
try:
|
|
282
|
+
handle = launch_dashboard_server(
|
|
283
|
+
DashboardServerConfig(
|
|
284
|
+
workspace_root=state.cwd,
|
|
285
|
+
host=host,
|
|
286
|
+
port=port,
|
|
287
|
+
task_ref=task_ref,
|
|
288
|
+
refresh_ms=refresh_ms,
|
|
289
|
+
open_browser=open_browser,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
except LaunchError as exc:
|
|
293
|
+
emit_error(ctx, exc)
|
|
294
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
295
|
+
emit_payload(
|
|
296
|
+
ctx,
|
|
297
|
+
{
|
|
298
|
+
"kind": "serve_started",
|
|
299
|
+
"url": handle.url,
|
|
300
|
+
"host": handle.host,
|
|
301
|
+
"port": handle.port,
|
|
302
|
+
},
|
|
303
|
+
human=f"Serving taskledger dashboard at {handle.url}\nPress Ctrl-C to stop.",
|
|
304
|
+
)
|
|
305
|
+
try:
|
|
306
|
+
handle.serve_forever()
|
|
307
|
+
except KeyboardInterrupt:
|
|
308
|
+
pass
|
|
309
|
+
finally:
|
|
310
|
+
handle.close()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@doctor_app.callback()
|
|
314
|
+
def doctor_command(ctx: typer.Context) -> None:
|
|
315
|
+
if ctx.invoked_subcommand is not None:
|
|
316
|
+
return
|
|
317
|
+
emit_doctor_command(ctx)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@doctor_app.command("locks")
|
|
321
|
+
def doctor_locks_command(ctx: typer.Context) -> None:
|
|
322
|
+
emit_doctor_locks_command(ctx)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@doctor_app.command("schema")
|
|
326
|
+
def doctor_schema_command(ctx: typer.Context) -> None:
|
|
327
|
+
emit_doctor_schema_command(ctx)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@doctor_app.command("indexes")
|
|
331
|
+
def doctor_indexes_command(ctx: typer.Context) -> None:
|
|
332
|
+
emit_doctor_indexes_command(ctx)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.command("next-action")
|
|
336
|
+
def next_action_command(
|
|
337
|
+
ctx: typer.Context,
|
|
338
|
+
task_ref: TaskOption = None,
|
|
339
|
+
) -> None:
|
|
340
|
+
emit_next_action_command(ctx, task_ref)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@app.command("can")
|
|
344
|
+
def can_command(
|
|
345
|
+
ctx: typer.Context,
|
|
346
|
+
action_or_task: Annotated[str, typer.Argument(..., help="Action name.")],
|
|
347
|
+
task_ref: TaskOption = None,
|
|
348
|
+
) -> None:
|
|
349
|
+
emit_can_command(ctx, task_ref, action_or_task)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("reindex")
|
|
353
|
+
def reindex_command(ctx: typer.Context) -> None:
|
|
354
|
+
emit_reindex_command(ctx)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@repair_app.command("index")
|
|
358
|
+
def repair_index_command(ctx: typer.Context) -> None:
|
|
359
|
+
emit_reindex_command(ctx)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@repair_app.command("lock")
|
|
363
|
+
def repair_lock_command(
|
|
364
|
+
ctx: typer.Context,
|
|
365
|
+
reason: Annotated[str, typer.Option("--reason")],
|
|
366
|
+
task_ref: Annotated[
|
|
367
|
+
str | None,
|
|
368
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
369
|
+
] = None,
|
|
370
|
+
) -> None:
|
|
371
|
+
from taskledger.api.locks import break_lock
|
|
372
|
+
|
|
373
|
+
state = ctx.obj
|
|
374
|
+
assert isinstance(state, CLIState)
|
|
375
|
+
try:
|
|
376
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
377
|
+
payload = break_lock(state.cwd, task.id, reason=reason)
|
|
378
|
+
except LaunchError as exc:
|
|
379
|
+
emit_error(ctx, exc)
|
|
380
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
381
|
+
emit_payload(ctx, payload, human=f"repaired lock for {payload['task_id']}")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@repair_app.command("task")
|
|
385
|
+
def repair_task_command(
|
|
386
|
+
ctx: typer.Context,
|
|
387
|
+
reason: Annotated[str, typer.Option("--reason")],
|
|
388
|
+
task_ref: Annotated[
|
|
389
|
+
str | None,
|
|
390
|
+
typer.Option("--task", help="Task ref. Defaults to the active task."),
|
|
391
|
+
] = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
from taskledger.api.tasks import repair_task_record
|
|
394
|
+
|
|
395
|
+
state = ctx.obj
|
|
396
|
+
assert isinstance(state, CLIState)
|
|
397
|
+
try:
|
|
398
|
+
task = resolve_cli_task(state.cwd, task_ref)
|
|
399
|
+
payload = repair_task_record(state.cwd, task.id, reason=reason)
|
|
400
|
+
except LaunchError as exc:
|
|
401
|
+
emit_error(ctx, exc)
|
|
402
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
403
|
+
emit_payload(ctx, payload, human=f"inspected repair for {payload['task_id']}")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@repair_app.command("task-dirs")
|
|
407
|
+
def repair_task_dirs_command(ctx: typer.Context) -> None:
|
|
408
|
+
from taskledger.services.doctor import cleanup_orphan_slug_dirs
|
|
409
|
+
|
|
410
|
+
state = ctx.obj
|
|
411
|
+
assert isinstance(state, CLIState)
|
|
412
|
+
payload = cleanup_orphan_slug_dirs(state.cwd)
|
|
413
|
+
removed = cast(list[str], payload.get("removed", []))
|
|
414
|
+
names = ", ".join(removed) if removed else "(none)"
|
|
415
|
+
emit_payload(
|
|
416
|
+
ctx,
|
|
417
|
+
payload,
|
|
418
|
+
human=f"removed {payload['count']} orphan slug directories: {names}",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@app.command("export")
|
|
423
|
+
def export_command(
|
|
424
|
+
ctx: typer.Context,
|
|
425
|
+
include_bodies: Annotated[
|
|
426
|
+
bool,
|
|
427
|
+
typer.Option("--include-bodies", help="Include Markdown bodies in the export."),
|
|
428
|
+
] = False,
|
|
429
|
+
include_run_artifacts: Annotated[
|
|
430
|
+
bool,
|
|
431
|
+
typer.Option(
|
|
432
|
+
"--include-run-artifacts",
|
|
433
|
+
help="Include run artifact files in the export payload.",
|
|
434
|
+
),
|
|
435
|
+
] = False,
|
|
436
|
+
) -> None:
|
|
437
|
+
state = ctx.obj
|
|
438
|
+
assert isinstance(state, CLIState)
|
|
439
|
+
payload = project_export(
|
|
440
|
+
state.cwd,
|
|
441
|
+
include_bodies=include_bodies,
|
|
442
|
+
include_run_artifacts=include_run_artifacts,
|
|
443
|
+
)
|
|
444
|
+
emit_payload(ctx, payload, human="exported taskledger state")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.command("import")
|
|
448
|
+
def import_command(
|
|
449
|
+
ctx: typer.Context,
|
|
450
|
+
source: Annotated[Path, typer.Argument(..., exists=True, readable=True)],
|
|
451
|
+
replace: Annotated[
|
|
452
|
+
bool,
|
|
453
|
+
typer.Option("--replace", help="Replace existing taskledger state."),
|
|
454
|
+
] = False,
|
|
455
|
+
) -> None:
|
|
456
|
+
state = ctx.obj
|
|
457
|
+
assert isinstance(state, CLIState)
|
|
458
|
+
text = source.read_text(encoding="utf-8")
|
|
459
|
+
try:
|
|
460
|
+
payload = project_import(state.cwd, text=text, replace=replace)
|
|
461
|
+
except LaunchError as exc:
|
|
462
|
+
emit_error(ctx, exc)
|
|
463
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
464
|
+
emit_payload(ctx, payload, human="imported taskledger state")
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@app.command("snapshot")
|
|
468
|
+
def snapshot_command(
|
|
469
|
+
ctx: typer.Context,
|
|
470
|
+
output_dir: Annotated[Path, typer.Argument(..., file_okay=False, dir_okay=True)],
|
|
471
|
+
include_bodies: Annotated[
|
|
472
|
+
bool,
|
|
473
|
+
typer.Option(
|
|
474
|
+
"--include-bodies",
|
|
475
|
+
help="Include Markdown bodies in the snapshot export.",
|
|
476
|
+
),
|
|
477
|
+
] = False,
|
|
478
|
+
include_run_artifacts: Annotated[
|
|
479
|
+
bool,
|
|
480
|
+
typer.Option(
|
|
481
|
+
"--include-run-artifacts",
|
|
482
|
+
help="Include run artifact files in the snapshot export.",
|
|
483
|
+
),
|
|
484
|
+
] = False,
|
|
485
|
+
) -> None:
|
|
486
|
+
state = ctx.obj
|
|
487
|
+
assert isinstance(state, CLIState)
|
|
488
|
+
try:
|
|
489
|
+
payload = project_snapshot(
|
|
490
|
+
state.cwd,
|
|
491
|
+
output_dir=output_dir,
|
|
492
|
+
include_bodies=include_bodies,
|
|
493
|
+
include_run_artifacts=include_run_artifacts,
|
|
494
|
+
)
|
|
495
|
+
except LaunchError as exc:
|
|
496
|
+
emit_error(ctx, exc)
|
|
497
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
498
|
+
emit_payload(ctx, payload, human=f"wrote snapshot to {payload['snapshot_dir']}")
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
@app.command("search")
|
|
502
|
+
def search_command(
|
|
503
|
+
ctx: typer.Context,
|
|
504
|
+
query: Annotated[str, typer.Argument(..., help="Search query.")],
|
|
505
|
+
repo_refs: Annotated[list[str] | None, typer.Option("--repo")] = None,
|
|
506
|
+
limit: Annotated[int, typer.Option("--limit")] = 50,
|
|
507
|
+
) -> None:
|
|
508
|
+
_emit_search_results(
|
|
509
|
+
ctx,
|
|
510
|
+
lambda cwd: search_workspace(
|
|
511
|
+
cwd,
|
|
512
|
+
query=query,
|
|
513
|
+
repo_refs=tuple(repo_refs or ()),
|
|
514
|
+
limit=limit,
|
|
515
|
+
),
|
|
516
|
+
title="SEARCH",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@app.command("grep")
|
|
521
|
+
def grep_command(
|
|
522
|
+
ctx: typer.Context,
|
|
523
|
+
pattern: Annotated[str, typer.Argument(..., help="Regex pattern.")],
|
|
524
|
+
repo_refs: Annotated[list[str] | None, typer.Option("--repo")] = None,
|
|
525
|
+
limit: Annotated[int, typer.Option("--limit")] = 100,
|
|
526
|
+
) -> None:
|
|
527
|
+
_emit_search_results(
|
|
528
|
+
ctx,
|
|
529
|
+
lambda cwd: grep_workspace(
|
|
530
|
+
cwd,
|
|
531
|
+
pattern=pattern,
|
|
532
|
+
repo_refs=tuple(repo_refs or ()),
|
|
533
|
+
limit=limit,
|
|
534
|
+
),
|
|
535
|
+
title="GREP",
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@app.command("symbols")
|
|
540
|
+
def symbols_command(
|
|
541
|
+
ctx: typer.Context,
|
|
542
|
+
query: Annotated[str, typer.Argument(..., help="Symbol query.")],
|
|
543
|
+
repo_refs: Annotated[list[str] | None, typer.Option("--repo")] = None,
|
|
544
|
+
limit: Annotated[int, typer.Option("--limit")] = 50,
|
|
545
|
+
) -> None:
|
|
546
|
+
_emit_search_results(
|
|
547
|
+
ctx,
|
|
548
|
+
lambda cwd: symbols_workspace(
|
|
549
|
+
cwd,
|
|
550
|
+
query=query,
|
|
551
|
+
repo_refs=tuple(repo_refs or ()),
|
|
552
|
+
limit=limit,
|
|
553
|
+
),
|
|
554
|
+
title="SYMBOLS",
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@app.command("deps")
|
|
559
|
+
def deps_command(
|
|
560
|
+
ctx: typer.Context,
|
|
561
|
+
repo_ref: Annotated[str, typer.Argument(..., help="Repo ref.")],
|
|
562
|
+
module: Annotated[str, typer.Argument(..., help="Module path.")],
|
|
563
|
+
) -> None:
|
|
564
|
+
state = ctx.obj
|
|
565
|
+
assert isinstance(state, CLIState)
|
|
566
|
+
try:
|
|
567
|
+
payload = dependencies_for_module(
|
|
568
|
+
state.cwd,
|
|
569
|
+
repo_ref=repo_ref,
|
|
570
|
+
module=module,
|
|
571
|
+
)
|
|
572
|
+
except LaunchError as exc:
|
|
573
|
+
emit_error(ctx, str(exc))
|
|
574
|
+
raise typer.Exit(code=1) from exc
|
|
575
|
+
emit_payload(ctx, payload)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _emit_search_results(
|
|
579
|
+
ctx: typer.Context,
|
|
580
|
+
factory: Callable[..., Any],
|
|
581
|
+
*,
|
|
582
|
+
title: str,
|
|
583
|
+
) -> None:
|
|
584
|
+
state = ctx.obj
|
|
585
|
+
assert isinstance(state, CLIState)
|
|
586
|
+
try:
|
|
587
|
+
results = factory(state.cwd)
|
|
588
|
+
except LaunchError as exc:
|
|
589
|
+
emit_error(ctx, str(exc))
|
|
590
|
+
raise typer.Exit(code=1) from exc
|
|
591
|
+
human = (
|
|
592
|
+
"\n".join([title, *[item.path for item in results]])
|
|
593
|
+
if results
|
|
594
|
+
else f"{title}\n(empty)"
|
|
595
|
+
)
|
|
596
|
+
emit_payload(ctx, [item.to_dict() for item in results], human=human)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def cli_main() -> None:
|
|
600
|
+
app()
|