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,328 @@
|
|
|
1
|
+
"""CLI commands for taskledger migrate: status, plan, apply."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from taskledger.cli_common import (
|
|
11
|
+
CLIState,
|
|
12
|
+
emit_error,
|
|
13
|
+
emit_payload,
|
|
14
|
+
launch_error_exit_code,
|
|
15
|
+
)
|
|
16
|
+
from taskledger.domain.states import TASKLEDGER_STORAGE_LAYOUT_VERSION
|
|
17
|
+
from taskledger.errors import LaunchError
|
|
18
|
+
from taskledger.storage.migrations import (
|
|
19
|
+
apply_layout_migrations,
|
|
20
|
+
required_layout_migrations,
|
|
21
|
+
scan_records_for_migration,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
migrate_app = typer.Typer(
|
|
25
|
+
add_completion=False,
|
|
26
|
+
help="Inspect and apply storage migrations.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@migrate_app.command("status")
|
|
31
|
+
def migrate_status_command(ctx: typer.Context) -> None:
|
|
32
|
+
state = ctx.obj
|
|
33
|
+
assert isinstance(state, CLIState)
|
|
34
|
+
try:
|
|
35
|
+
from taskledger.storage.meta import read_storage_meta
|
|
36
|
+
|
|
37
|
+
meta = read_storage_meta(state.cwd)
|
|
38
|
+
except LaunchError as exc:
|
|
39
|
+
emit_error(ctx, exc)
|
|
40
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
41
|
+
|
|
42
|
+
if meta is None:
|
|
43
|
+
payload = {
|
|
44
|
+
"ok": True,
|
|
45
|
+
"status": "no_storage_meta",
|
|
46
|
+
"message": (
|
|
47
|
+
"No storage.yaml found."
|
|
48
|
+
" Run 'taskledger init' or 'taskledger migrate apply'."
|
|
49
|
+
),
|
|
50
|
+
"current_layout_version": None,
|
|
51
|
+
"target_layout_version": TASKLEDGER_STORAGE_LAYOUT_VERSION,
|
|
52
|
+
"pending_migrations": [],
|
|
53
|
+
"records_needing_migration": 0,
|
|
54
|
+
}
|
|
55
|
+
human = (
|
|
56
|
+
"MIGRATE STATUS\n"
|
|
57
|
+
" No storage.yaml found.\n"
|
|
58
|
+
" Run 'taskledger init' or 'taskledger migrate apply'."
|
|
59
|
+
)
|
|
60
|
+
emit_payload(ctx, payload, human=human)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
pending = required_layout_migrations(
|
|
65
|
+
meta.storage_layout_version, TASKLEDGER_STORAGE_LAYOUT_VERSION
|
|
66
|
+
)
|
|
67
|
+
except LaunchError:
|
|
68
|
+
pending = []
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
records = scan_records_for_migration(state.cwd)
|
|
72
|
+
except LaunchError:
|
|
73
|
+
records = []
|
|
74
|
+
|
|
75
|
+
needs_migration = len(pending) > 0 or len(records) > 0
|
|
76
|
+
payload = {
|
|
77
|
+
"ok": True,
|
|
78
|
+
"status": "migration_needed" if needs_migration else "up_to_date",
|
|
79
|
+
"current_layout_version": meta.storage_layout_version,
|
|
80
|
+
"target_layout_version": TASKLEDGER_STORAGE_LAYOUT_VERSION,
|
|
81
|
+
"pending_migrations": [
|
|
82
|
+
{"from": m.from_version, "to": m.to_version, "name": m.name}
|
|
83
|
+
for m in pending
|
|
84
|
+
],
|
|
85
|
+
"records_needing_migration": len(records),
|
|
86
|
+
}
|
|
87
|
+
if needs_migration:
|
|
88
|
+
human_lines = [
|
|
89
|
+
"MIGRATE STATUS",
|
|
90
|
+
f" Layout version: {meta.storage_layout_version}"
|
|
91
|
+
f" -> {TASKLEDGER_STORAGE_LAYOUT_VERSION}",
|
|
92
|
+
f" Pending layout migrations: {len(pending)}",
|
|
93
|
+
f" Records needing migration: {len(records)}",
|
|
94
|
+
]
|
|
95
|
+
else:
|
|
96
|
+
human_lines = [
|
|
97
|
+
"MIGRATE STATUS",
|
|
98
|
+
f" Layout version: {meta.storage_layout_version} (up to date)",
|
|
99
|
+
" No migrations needed.",
|
|
100
|
+
]
|
|
101
|
+
emit_payload(ctx, payload, human="\n".join(human_lines))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@migrate_app.command("plan")
|
|
105
|
+
def migrate_plan_command(ctx: typer.Context) -> None:
|
|
106
|
+
state = ctx.obj
|
|
107
|
+
assert isinstance(state, CLIState)
|
|
108
|
+
try:
|
|
109
|
+
from taskledger.storage.meta import read_storage_meta
|
|
110
|
+
|
|
111
|
+
meta = read_storage_meta(state.cwd)
|
|
112
|
+
except LaunchError as exc:
|
|
113
|
+
emit_error(ctx, exc)
|
|
114
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
115
|
+
|
|
116
|
+
if meta is None:
|
|
117
|
+
payload = {
|
|
118
|
+
"ok": True,
|
|
119
|
+
"status": "no_storage_meta",
|
|
120
|
+
"message": "No storage.yaml found. Nothing to plan.",
|
|
121
|
+
"migrations": [],
|
|
122
|
+
"records": [],
|
|
123
|
+
}
|
|
124
|
+
emit_payload(ctx, payload, human="MIGRATE PLAN\n No storage.yaml found.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
pending = required_layout_migrations(
|
|
129
|
+
meta.storage_layout_version, TASKLEDGER_STORAGE_LAYOUT_VERSION
|
|
130
|
+
)
|
|
131
|
+
except LaunchError as exc:
|
|
132
|
+
emit_error(ctx, exc)
|
|
133
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
records = scan_records_for_migration(state.cwd)
|
|
137
|
+
except LaunchError:
|
|
138
|
+
records = []
|
|
139
|
+
|
|
140
|
+
payload = {
|
|
141
|
+
"ok": True,
|
|
142
|
+
"current_layout_version": meta.storage_layout_version,
|
|
143
|
+
"target_layout_version": TASKLEDGER_STORAGE_LAYOUT_VERSION,
|
|
144
|
+
"migrations": [
|
|
145
|
+
{"from": m.from_version, "to": m.to_version, "name": m.name}
|
|
146
|
+
for m in pending
|
|
147
|
+
],
|
|
148
|
+
"records": [
|
|
149
|
+
{
|
|
150
|
+
"path": str(r.path),
|
|
151
|
+
"object_type": r.object_type,
|
|
152
|
+
"current_version": r.current_version,
|
|
153
|
+
"target_version": r.target_version,
|
|
154
|
+
}
|
|
155
|
+
for r in records
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
human_lines = [
|
|
159
|
+
"MIGRATE PLAN",
|
|
160
|
+
f" Layout: {meta.storage_layout_version}"
|
|
161
|
+
f" -> {TASKLEDGER_STORAGE_LAYOUT_VERSION}",
|
|
162
|
+
f" Layout migrations: {len(pending)}",
|
|
163
|
+
f" Records to migrate: {len(records)}",
|
|
164
|
+
]
|
|
165
|
+
for m in pending:
|
|
166
|
+
human_lines.append(
|
|
167
|
+
f" - {m.name} (layout {m.from_version} -> {m.to_version})"
|
|
168
|
+
)
|
|
169
|
+
for r in records:
|
|
170
|
+
human_lines.append(
|
|
171
|
+
f" - {r.path.name} ({r.object_type}:"
|
|
172
|
+
f" {r.current_version} -> {r.target_version})"
|
|
173
|
+
)
|
|
174
|
+
emit_payload(ctx, payload, human="\n".join(human_lines))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@migrate_app.command("apply")
|
|
178
|
+
def migrate_apply_command(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
backup: Annotated[
|
|
181
|
+
bool,
|
|
182
|
+
typer.Option("--backup", help="Create a snapshot before applying migrations."),
|
|
183
|
+
] = False,
|
|
184
|
+
dry_run: Annotated[
|
|
185
|
+
bool,
|
|
186
|
+
typer.Option("--dry-run", help="Show what would be done without writing."),
|
|
187
|
+
] = False,
|
|
188
|
+
) -> None:
|
|
189
|
+
state = ctx.obj
|
|
190
|
+
assert isinstance(state, CLIState)
|
|
191
|
+
try:
|
|
192
|
+
from taskledger.storage.meta import read_storage_meta
|
|
193
|
+
|
|
194
|
+
meta = read_storage_meta(state.cwd)
|
|
195
|
+
except LaunchError as exc:
|
|
196
|
+
emit_error(ctx, exc)
|
|
197
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
198
|
+
|
|
199
|
+
if meta is None:
|
|
200
|
+
payload = {
|
|
201
|
+
"ok": False,
|
|
202
|
+
"status": "no_storage_meta",
|
|
203
|
+
"message": "No storage.yaml found. Run 'taskledger init' first.",
|
|
204
|
+
}
|
|
205
|
+
emit_error(ctx, LaunchError(str(payload["message"])))
|
|
206
|
+
raise typer.Exit(code=2)
|
|
207
|
+
|
|
208
|
+
if meta.storage_layout_version > TASKLEDGER_STORAGE_LAYOUT_VERSION:
|
|
209
|
+
too_new = LaunchError(
|
|
210
|
+
f"Storage layout {meta.storage_layout_version} is newer than "
|
|
211
|
+
f"supported {TASKLEDGER_STORAGE_LAYOUT_VERSION}."
|
|
212
|
+
" Upgrade taskledger."
|
|
213
|
+
)
|
|
214
|
+
emit_error(ctx, too_new)
|
|
215
|
+
raise typer.Exit(code=launch_error_exit_code(too_new)) from too_new
|
|
216
|
+
|
|
217
|
+
if meta.storage_layout_version == TASKLEDGER_STORAGE_LAYOUT_VERSION:
|
|
218
|
+
payload = {
|
|
219
|
+
"ok": True,
|
|
220
|
+
"status": "up_to_date",
|
|
221
|
+
"message": "No migrations needed.",
|
|
222
|
+
"applied_migrations": [],
|
|
223
|
+
}
|
|
224
|
+
emit_payload(ctx, payload, human="MIGRATE APPLY\n Already up to date.")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Create backup if requested
|
|
228
|
+
snapshot_dir: str | None = None
|
|
229
|
+
if backup and not dry_run:
|
|
230
|
+
from taskledger.api.project import project_snapshot
|
|
231
|
+
|
|
232
|
+
snapshot_path = Path.cwd() / ".taskledger" / "snapshots"
|
|
233
|
+
result = project_snapshot(
|
|
234
|
+
state.cwd,
|
|
235
|
+
output_dir=snapshot_path,
|
|
236
|
+
)
|
|
237
|
+
snapshot_dir = str(result.get("snapshot_dir", "")) or None
|
|
238
|
+
|
|
239
|
+
# Apply migrations
|
|
240
|
+
try:
|
|
241
|
+
applied = apply_layout_migrations(
|
|
242
|
+
state.cwd,
|
|
243
|
+
meta.storage_layout_version,
|
|
244
|
+
dry_run=dry_run,
|
|
245
|
+
)
|
|
246
|
+
except LaunchError as exc:
|
|
247
|
+
emit_error(ctx, exc)
|
|
248
|
+
raise typer.Exit(code=launch_error_exit_code(exc)) from exc
|
|
249
|
+
|
|
250
|
+
# Write audit record
|
|
251
|
+
if not dry_run and applied:
|
|
252
|
+
_write_migration_audit(state.cwd, meta.storage_layout_version, applied)
|
|
253
|
+
|
|
254
|
+
# Rebuild indexes
|
|
255
|
+
if not dry_run and applied:
|
|
256
|
+
try:
|
|
257
|
+
from taskledger.storage.indexes import rebuild_v2_indexes
|
|
258
|
+
from taskledger.storage.task_store import resolve_v2_paths
|
|
259
|
+
|
|
260
|
+
v2_paths = resolve_v2_paths(state.cwd)
|
|
261
|
+
rebuild_v2_indexes(v2_paths)
|
|
262
|
+
except LaunchError:
|
|
263
|
+
pass # Index rebuild is best-effort after migration
|
|
264
|
+
|
|
265
|
+
payload = {
|
|
266
|
+
"ok": True,
|
|
267
|
+
"status": "dry_run" if dry_run else "applied",
|
|
268
|
+
"from_layout_version": meta.storage_layout_version,
|
|
269
|
+
"to_layout_version": TASKLEDGER_STORAGE_LAYOUT_VERSION,
|
|
270
|
+
"applied_migrations": applied,
|
|
271
|
+
"snapshot_dir": snapshot_dir,
|
|
272
|
+
}
|
|
273
|
+
human_lines = [
|
|
274
|
+
"MIGRATE APPLY" + (" (dry run)" if dry_run else ""),
|
|
275
|
+
f" Layout: {meta.storage_layout_version}"
|
|
276
|
+
f" -> {TASKLEDGER_STORAGE_LAYOUT_VERSION}",
|
|
277
|
+
f" Applied migrations: {len(applied)}",
|
|
278
|
+
]
|
|
279
|
+
for name in applied:
|
|
280
|
+
human_lines.append(f" - {name}")
|
|
281
|
+
if snapshot_dir:
|
|
282
|
+
human_lines.append(f" Backup: {snapshot_dir}")
|
|
283
|
+
emit_payload(ctx, payload, human="\n".join(human_lines))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _write_migration_audit(
|
|
287
|
+
workspace_root: Path,
|
|
288
|
+
from_version: int,
|
|
289
|
+
applied_names: list[str],
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Write a migration audit record to .taskledger/migrations/."""
|
|
292
|
+
from taskledger.storage.paths import resolve_taskledger_root
|
|
293
|
+
from taskledger.timeutils import utc_now_iso
|
|
294
|
+
|
|
295
|
+
project_dir = resolve_taskledger_root(workspace_root)
|
|
296
|
+
migrations_dir = project_dir / "migrations"
|
|
297
|
+
migrations_dir.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
|
|
299
|
+
timestamp = utc_now_iso().replace(":", "").replace("-", "").replace("+", "")
|
|
300
|
+
audit_path = migrations_dir / (
|
|
301
|
+
f"{timestamp[:15]}-layout-{from_version}"
|
|
302
|
+
f"-to-{TASKLEDGER_STORAGE_LAYOUT_VERSION}.md"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
from taskledger._version import __version__ as tl_version
|
|
307
|
+
except ImportError:
|
|
308
|
+
tl_version = "unknown"
|
|
309
|
+
|
|
310
|
+
front_matter = (
|
|
311
|
+
"---\n"
|
|
312
|
+
f"schema_version: 1\n"
|
|
313
|
+
f"object_type: migration_audit\n"
|
|
314
|
+
f"from_layout_version: {from_version}\n"
|
|
315
|
+
f"to_layout_version: {TASKLEDGER_STORAGE_LAYOUT_VERSION}\n"
|
|
316
|
+
f"taskledger_version: {tl_version}\n"
|
|
317
|
+
f'created_at: "{utc_now_iso()}"\n'
|
|
318
|
+
"---\n\n"
|
|
319
|
+
)
|
|
320
|
+
body = (
|
|
321
|
+
f"# Migration layout {from_version} -> {TASKLEDGER_STORAGE_LAYOUT_VERSION}\n\n"
|
|
322
|
+
)
|
|
323
|
+
body += "## Applied migrations\n\n"
|
|
324
|
+
for name in applied_names:
|
|
325
|
+
body += f"- {name}\n"
|
|
326
|
+
body += "\n"
|
|
327
|
+
|
|
328
|
+
audit_path.write_text(front_matter + body, encoding="utf-8")
|