tetra-cli 0.2.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 (62) hide show
  1. tetra_cli/__init__.py +6 -0
  2. tetra_cli/api_client/__init__.py +10 -0
  3. tetra_cli/api_client/client.py +173 -0
  4. tetra_cli/api_client/config.py +125 -0
  5. tetra_cli/api_client/operations/__init__.py +9 -0
  6. tetra_cli/api_client/operations/accounts.py +303 -0
  7. tetra_cli/api_client/operations/ai.py +278 -0
  8. tetra_cli/api_client/operations/analysis.py +190 -0
  9. tetra_cli/api_client/operations/api_keys.py +145 -0
  10. tetra_cli/api_client/operations/archive.py +114 -0
  11. tetra_cli/api_client/operations/awards.py +123 -0
  12. tetra_cli/api_client/operations/capacity.py +84 -0
  13. tetra_cli/api_client/operations/conversations.py +447 -0
  14. tetra_cli/api_client/operations/conversations_2.py +262 -0
  15. tetra_cli/api_client/operations/cosmetics.py +148 -0
  16. tetra_cli/api_client/operations/dashboard.py +282 -0
  17. tetra_cli/api_client/operations/data.py +250 -0
  18. tetra_cli/api_client/operations/events.py +734 -0
  19. tetra_cli/api_client/operations/gamification.py +470 -0
  20. tetra_cli/api_client/operations/goals.py +1144 -0
  21. tetra_cli/api_client/operations/groups.py +647 -0
  22. tetra_cli/api_client/operations/issues.py +198 -0
  23. tetra_cli/api_client/operations/offset.py +61 -0
  24. tetra_cli/api_client/operations/onboarding.py +284 -0
  25. tetra_cli/api_client/operations/outcome_schemas.py +292 -0
  26. tetra_cli/api_client/operations/peer_connections.py +243 -0
  27. tetra_cli/api_client/operations/plaid.py +329 -0
  28. tetra_cli/api_client/operations/reminders.py +273 -0
  29. tetra_cli/api_client/operations/scratches.py +280 -0
  30. tetra_cli/api_client/operations/skill_trees.py +160 -0
  31. tetra_cli/api_client/operations/social_2.py +560 -0
  32. tetra_cli/api_client/operations/social_3.py +618 -0
  33. tetra_cli/api_client/operations/social_4.py +527 -0
  34. tetra_cli/api_client/operations/strava.py +215 -0
  35. tetra_cli/api_client/operations/stripe.py +113 -0
  36. tetra_cli/api_client/operations/tags.py +488 -0
  37. tetra_cli/api_client/operations/values.py +867 -0
  38. tetra_cli/api_client/operations/values_2.py +584 -0
  39. tetra_cli/api_client/operations/watch.py +105 -0
  40. tetra_cli/api_client/operations/webhooks.py +50 -0
  41. tetra_cli/api_client/operations/xp.py +27 -0
  42. tetra_cli/cli/__init__.py +5 -0
  43. tetra_cli/cli/__main__.py +5 -0
  44. tetra_cli/cli/app.py +86 -0
  45. tetra_cli/cli/commands/__init__.py +1 -0
  46. tetra_cli/cli/commands/auth.py +201 -0
  47. tetra_cli/cli/commands/guide.py +8 -0
  48. tetra_cli/cli/commands/messages.py +161 -0
  49. tetra_cli/cli/commands/skill.py +71 -0
  50. tetra_cli/cli/context.py +13 -0
  51. tetra_cli/cli/generate.py +282 -0
  52. tetra_cli/cli/output.py +58 -0
  53. tetra_cli/mcp_gen.py +137 -0
  54. tetra_cli/ontology.py +70 -0
  55. tetra_cli/registry.py +118 -0
  56. tetra_cli/skill/SKILL.md +69 -0
  57. tetra_cli/skill/__init__.py +1 -0
  58. tetra_cli-0.2.0.dist-info/METADATA +140 -0
  59. tetra_cli-0.2.0.dist-info/RECORD +62 -0
  60. tetra_cli-0.2.0.dist-info/WHEEL +5 -0
  61. tetra_cli-0.2.0.dist-info/entry_points.txt +2 -0
  62. tetra_cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1144 @@
1
+ """Pure async operations for the goals API."""
2
+ from typing import Any
3
+
4
+ from tetra_cli.api_client import TetraClient, TetraClientError
5
+ from tetra_cli.registry import arg, operation, opt
6
+
7
+ # Mapping of status labels (case-insensitive, whitespace-tolerant) to the
8
+ # numeric values defined in ``tetra.core.core.Status``. Kept here — inside the
9
+ # op layer — so both the CLI and the MCP surface (both generated from these
10
+ # ops) inherit the same label→int normalization. The previous CLI-only sugar
11
+ # (removed when the goals group moved to the generator) used this exact map.
12
+ _STATUS_LABELS: dict[str, int] = {
13
+ "created": 1,
14
+ "in_progress": 2,
15
+ "archived": 3,
16
+ "abandoned": 4,
17
+ "completed": 5,
18
+ }
19
+
20
+
21
+ def _normalize_status(value: int | str) -> int:
22
+ """Coerce a status given as an int or a known label string to its int.
23
+
24
+ Accepts plain ints (1-5) unchanged, integer-as-string ("3"), and the
25
+ canonical labels (case-insensitive, whitespace-tolerant). Raises
26
+ ``TetraClientError`` — the same error type ops raise for bad input, which
27
+ the CLI generator renders as a clean ``api_error`` — on an unknown label.
28
+ """
29
+ if isinstance(value, int):
30
+ return value
31
+ raw = value.strip()
32
+ if raw.lstrip("-").isdigit():
33
+ return int(raw)
34
+ label = raw.lower()
35
+ if label in _STATUS_LABELS:
36
+ return _STATUS_LABELS[label]
37
+ accepted = ", ".join(sorted(_STATUS_LABELS))
38
+ raise TetraClientError(
39
+ f"--status {value!r} is not recognized; accepted: {accepted}"
40
+ )
41
+
42
+
43
+ @operation(
44
+ cli="goals list",
45
+ summary="List all goals with their hierarchy and status.",
46
+ covers=[("GET", "/api/v1/goals/")],
47
+ )
48
+ async def list_goals(client: TetraClient) -> dict[str, Any]:
49
+ """List all goals with their hierarchy and status.
50
+
51
+ Args:
52
+ client: Authenticated TetraClient
53
+
54
+ Returns:
55
+ Dict with 'goals' list
56
+ """
57
+ return await client.get("/api/v1/goals/")
58
+
59
+
60
+ @operation(
61
+ cli="goals get",
62
+ summary="Get a single goal by UID or hierarchical name.",
63
+ covers=[("GET", "/api/v1/goals/{uid_or_name}")],
64
+ params={"uid_or_name": arg(help="Goal UID or full hierarchical name")},
65
+ )
66
+ async def get_goal(client: TetraClient, uid_or_name: str) -> dict[str, Any]:
67
+ """Get a single goal by UID or hierarchical name.
68
+
69
+ Args:
70
+ client: Authenticated TetraClient
71
+ uid_or_name: Goal UID or full hierarchical name (e.g., 'Exercise > Weight Training')
72
+
73
+ Returns:
74
+ Goal dict
75
+ """
76
+ return await client.get(f"/api/v1/goals/{uid_or_name}")
77
+
78
+
79
+ @operation(
80
+ cli="goals create",
81
+ summary="Create a new goal.",
82
+ covers=[("POST", "/api/v1/goals/")],
83
+ params={
84
+ "name": arg(help="Leaf name (e.g. 'Bench Press')"),
85
+ "values": opt("--value", repeatable=True, help="Value name (repeatable)"),
86
+ "depends_on": opt("--depends-on", repeatable=True,
87
+ help="Prerequisite goal name (repeatable)"),
88
+ "outcome_names": opt("--outcome-name", repeatable=True,
89
+ help="Record-outcome field name (repeatable)"),
90
+ "metric_config": opt("--metric-config", json=True,
91
+ help="Metric configuration as a JSON dict"),
92
+ "importance": opt("--importance", min=1, max=5, help="Importance (1-5)"),
93
+ "urgency": opt("--urgency", min=1, max=5, help="Urgency (1-5)"),
94
+ "expected_time": opt("--expected-time",
95
+ help="Expected time to complete, in seconds"),
96
+ },
97
+ )
98
+ async def create_goal(
99
+ client: TetraClient,
100
+ *,
101
+ name: str,
102
+ parent: str | None = None,
103
+ values: list[str] | None = None,
104
+ depends_on: list[str] | None = None,
105
+ description: str | None = None,
106
+ importance: int | None = None,
107
+ urgency: int | None = None,
108
+ expected_time: int | None = None,
109
+ metric_config: dict[str, Any] | None = None,
110
+ outcome_shape: str | None = None,
111
+ outcome_names: list[str] | None = None,
112
+ group_uid: str | None = None,
113
+ for_account_uid: str | None = None,
114
+ ) -> dict[str, Any]:
115
+ """Create a new goal.
116
+
117
+ Only the fields explicitly provided are included in the request body;
118
+ optional fields that are None are omitted so API defaults apply.
119
+
120
+ Args:
121
+ client: Authenticated TetraClient
122
+ name: Leaf name for the goal (e.g., 'Bench Press')
123
+ parent: Parent goal's full hierarchical name
124
+ values: Value names this goal contributes to
125
+ depends_on: Names of prerequisite goals that must complete first
126
+ description: Human-readable description
127
+ importance: Importance level (1-5)
128
+ urgency: Urgency level (1-5)
129
+ expected_time: Expected time to complete the goal, in seconds.
130
+ metric_config: Custom metric configuration dict
131
+ outcome_shape: Shape of outcome data ('numeric', 'record', etc.)
132
+ outcome_names: Field names for record-type outcomes
133
+
134
+ Returns:
135
+ Created goal dict with assigned UID
136
+ """
137
+ body: dict[str, Any] = {"name": name}
138
+ if parent:
139
+ body["parent"] = parent
140
+ if values:
141
+ body["values"] = values
142
+ if depends_on:
143
+ body["depends_on"] = depends_on
144
+ if description:
145
+ body["description"] = description
146
+ if importance is not None:
147
+ body["importance"] = importance
148
+ if urgency is not None:
149
+ body["urgency"] = urgency
150
+ if expected_time is not None:
151
+ body["expected_time"] = expected_time
152
+ if metric_config:
153
+ body["metric_config"] = metric_config
154
+ if outcome_shape:
155
+ body["outcome_shape"] = outcome_shape
156
+ if outcome_names:
157
+ body["outcome_names"] = outcome_names
158
+ # Creation context — pair (group_uid, for_account_uid) picks shape
159
+ # A/B/C per docs/architecture/creation-context.md.
160
+ if group_uid is not None:
161
+ body["group_uid"] = group_uid
162
+ if for_account_uid is not None:
163
+ body["for_account_uid"] = for_account_uid
164
+ return await client.post("/api/v1/goals/", json=body)
165
+
166
+
167
+ @operation(
168
+ cli="goals update",
169
+ summary="Update an existing goal. Only provided fields are changed.",
170
+ covers=[("PUT", "/api/v1/goals/{uid}")],
171
+ params={
172
+ "uid": arg(help="Goal UID"),
173
+ "name": opt("--name", help="New leaf name"),
174
+ "values": opt("--value", repeatable=True, clears=True,
175
+ help="Value name (repeatable; replaces all)"),
176
+ "depends_on": opt("--depends-on", repeatable=True, clears=True,
177
+ help="Prerequisite goal name (repeatable; replaces all)"),
178
+ "metric_config": opt("--metric-config", json=True,
179
+ help="Metric configuration as a JSON dict"),
180
+ "outcome_names": opt("--outcome-name", repeatable=True,
181
+ help="Record-outcome field name (repeatable)"),
182
+ "importance": opt("--importance", min=1, max=5, help="Importance (1-5)"),
183
+ "urgency": opt("--urgency", min=1, max=5, help="Urgency (1-5)"),
184
+ "expected_time": opt("--expected-time",
185
+ help="Expected time to complete, in seconds"),
186
+ "status": opt(
187
+ "--status",
188
+ help=("Goal status as a label (created | in_progress | archived | "
189
+ "abandoned | completed) or its numeric form (1-5)."),
190
+ ),
191
+ },
192
+ )
193
+ async def update_goal(
194
+ client: TetraClient,
195
+ *,
196
+ uid: str,
197
+ name: str | None = None,
198
+ parent: str | None = None,
199
+ description: str | None = None,
200
+ importance: int | None = None,
201
+ urgency: int | None = None,
202
+ expected_time: int | None = None,
203
+ values: list[str] | None = None,
204
+ depends_on: list[str] | None = None,
205
+ metric_config: dict[str, Any] | None = None,
206
+ outcome_shape: str | None = None,
207
+ outcome_names: list[str] | None = None,
208
+ status: str | None = None,
209
+ ) -> dict[str, Any]:
210
+ """Update an existing goal. Only provided (non-None) fields are changed.
211
+
212
+ Args:
213
+ client: Authenticated TetraClient
214
+ uid: Goal UID (required)
215
+ name: New leaf name
216
+ parent: New parent path (empty string to move to root)
217
+ description: New description
218
+ importance: New importance (1-5)
219
+ urgency: New urgency (1-5)
220
+ expected_time: New expected time to complete, in seconds.
221
+ values: New value names (replaces all)
222
+ depends_on: New prerequisite goal names (replaces all)
223
+ metric_config: New metric configuration
224
+ outcome_shape: New outcome shape
225
+ outcome_names: New outcome field names
226
+ status: New status as either a numeric value (1=CREATED,
227
+ 2=IN_PROGRESS, 3=ARCHIVED, 4=ABANDONED, 5=COMPLETED) or a label
228
+ string ("created" … "completed", case-insensitive). The op
229
+ normalizes labels→int so the CLI/MCP surfaces share the behavior.
230
+
231
+ Returns:
232
+ Updated goal dict
233
+
234
+ Raises:
235
+ TetraClientError: If ``status`` is a string that is neither a known
236
+ label nor a numeric value.
237
+ """
238
+ body: dict[str, Any] = {}
239
+ if name is not None:
240
+ body["name"] = name
241
+ if parent is not None:
242
+ body["parent"] = parent
243
+ if description is not None:
244
+ body["description"] = description
245
+ if importance is not None:
246
+ body["importance"] = importance
247
+ if urgency is not None:
248
+ body["urgency"] = urgency
249
+ if expected_time is not None:
250
+ body["expected_time"] = expected_time
251
+ if values is not None:
252
+ body["values"] = values
253
+ if depends_on is not None:
254
+ body["depends_on"] = depends_on
255
+ if metric_config is not None:
256
+ body["metric_config"] = metric_config
257
+ if outcome_shape is not None:
258
+ body["outcome_shape"] = outcome_shape
259
+ if outcome_names is not None:
260
+ body["outcome_names"] = outcome_names
261
+ if status is not None:
262
+ body["status"] = _normalize_status(status)
263
+ return await client.put(f"/api/v1/goals/{uid}", json=body)
264
+
265
+
266
+ @operation(
267
+ cli="goals delete",
268
+ summary="Delete a goal permanently.",
269
+ covers=[("DELETE", "/api/v1/goals/{uid}")],
270
+ params={"uid": arg(help="Goal UID to delete")},
271
+ )
272
+ async def delete_goal(client: TetraClient, uid: str) -> dict[str, Any]:
273
+ """Delete a goal permanently.
274
+
275
+ Args:
276
+ client: Authenticated TetraClient
277
+ uid: Goal UID to delete
278
+
279
+ Returns:
280
+ Deletion confirmation dict
281
+ """
282
+ return await client.delete(f"/api/v1/goals/{uid}")
283
+
284
+
285
+ @operation(
286
+ cli="goals status",
287
+ summary="Get achievement progress and status for a goal.",
288
+ covers=[("GET", "/api/v1/goals/{uid}/status")],
289
+ params={"uid": arg(help="Goal UID")},
290
+ )
291
+ async def get_goal_status(client: TetraClient, uid: str) -> dict[str, Any]:
292
+ """Get achievement progress and status for a goal.
293
+
294
+ Args:
295
+ client: Authenticated TetraClient
296
+ uid: Goal UID
297
+
298
+ Returns:
299
+ Dict with status, percentage, current_value, target, and metric_type
300
+ """
301
+ return await client.get(f"/api/v1/goals/{uid}/status")
302
+
303
+
304
+ @operation(
305
+ cli="goals rankings",
306
+ summary="Get all goals ranked by progress, tier, and achievement status.",
307
+ covers=[("GET", "/api/v1/goals/rankings")],
308
+ )
309
+ async def get_goal_rankings(client: TetraClient) -> dict[str, Any]:
310
+ """Get all goals ranked by progress, tier, and achievement status.
311
+
312
+ Args:
313
+ client: Authenticated TetraClient
314
+
315
+ Returns:
316
+ Dict with 'rankings' list of goals sorted by progress
317
+ """
318
+ return await client.get("/api/v1/goals/rankings")
319
+
320
+
321
+ @operation(
322
+ cli="goals metrics",
323
+ summary="List all metrics for a goal (active + archived history).",
324
+ covers=[("GET", "/api/v1/goals/{uid}/metrics")],
325
+ params={"goal_uid": arg(help="Goal UID")},
326
+ )
327
+ async def list_goal_metrics(
328
+ client: TetraClient, goal_uid: str,
329
+ ) -> list[dict[str, Any]]:
330
+ """List all metrics for a goal (active + archived).
331
+
332
+ Reading metrics before adding new ones prevents accidental duplication.
333
+ Each metric exposes ``is_active`` (True for the current one) and the
334
+ full ``metric_config`` so an agent can summarize the existing rule.
335
+
336
+ Args:
337
+ client: Authenticated TetraClient
338
+ goal_uid: Goal UID
339
+
340
+ Returns:
341
+ List of metric dicts ordered by creation. Empty list if none.
342
+ """
343
+ return await client.get(f"/api/v1/goals/{goal_uid}/metrics")
344
+
345
+
346
+ @operation(
347
+ cli="goals metric-active",
348
+ summary="Get the single active metric for a goal (null if none).",
349
+ covers=[("GET", "/api/v1/goals/{uid}/metrics/active")],
350
+ params={"goal_uid": arg(help="Goal UID")},
351
+ )
352
+ async def get_active_goal_metric(
353
+ client: TetraClient, goal_uid: str,
354
+ ) -> dict[str, Any] | None:
355
+ """Return the single active metric for a goal, or None if there is none.
356
+
357
+ The active-metric endpoint returns ``null`` (not 404) when no active
358
+ metric exists, so this is safe to call before deciding whether to
359
+ create one.
360
+ """
361
+ return await client.get(f"/api/v1/goals/{goal_uid}/metrics/active")
362
+
363
+
364
+ @operation(
365
+ cli="goals metric-create",
366
+ summary="Create a new metric for a goal (archives the current active one).",
367
+ covers=[("POST", "/api/v1/goals/{uid}/metrics")],
368
+ params={
369
+ "goal_uid": arg(help="Goal UID"),
370
+ "metric_config": opt("--metric-config", json=True,
371
+ help="MetricConfig payload as a JSON dict"),
372
+ "tier_override": opt("--tier-override", choices=["D", "C", "B", "A"],
373
+ help="Optional user-set difficulty tier (D/C/B/A)."),
374
+ },
375
+ )
376
+ async def create_goal_metric(
377
+ client: TetraClient,
378
+ goal_uid: str,
379
+ *,
380
+ name: str,
381
+ metric_config: dict[str, Any],
382
+ tier_override: str | None = None,
383
+ ) -> dict[str, Any]:
384
+ """Create a new metric for a goal.
385
+
386
+ Creating a metric automatically archives any currently-active metric
387
+ on the goal — there is at most one active metric at any time.
388
+
389
+ The server derives the computed tier from metric semantics and
390
+ cumulative effort. Pass ``tier_override`` to mark the goal as harder
391
+ than the metric suggests — accepted values are D/C/B/A. S is rejected
392
+ because reaching it requires the globally-shared-goal + top-decile
393
+ gate, not opt-in.
394
+
395
+ Args:
396
+ client: Authenticated TetraClient
397
+ goal_uid: Goal UID
398
+ name: Human-readable metric name (e.g. "Quarterly call")
399
+ metric_config: ``MetricConfig`` payload — see docs/architecture/rules-spec.md
400
+ tier_override: Optional user-set tier (D/C/B/A).
401
+ """
402
+ body: dict[str, Any] = {"name": name, "metric_config": metric_config}
403
+ if tier_override is not None:
404
+ body["tier_override"] = tier_override
405
+ return await client.post(f"/api/v1/goals/{goal_uid}/metrics", json=body)
406
+
407
+
408
+ # Sentinel for "field not provided" so callers can distinguish leaving
409
+ # tier_override alone from clearing it (passing None).
410
+ _UNSET: Any = object()
411
+
412
+
413
+ @operation(
414
+ cli="goals metric-update",
415
+ summary="Update an existing goal metric (name, config, tier, or activate).",
416
+ covers=[("PUT", "/api/v1/goals/{uid}/metrics/{metric_uid}")],
417
+ params={
418
+ "goal_uid": arg(help="Goal UID"),
419
+ "metric_uid": arg(help="Metric UID"),
420
+ "metric_config": opt("--metric-config", json=True,
421
+ help="MetricConfig payload as a JSON dict"),
422
+ "tier_override": opt("--tier-override", choices=["D", "C", "B", "A"],
423
+ help="Set the user difficulty tier (D/C/B/A)."),
424
+ },
425
+ )
426
+ async def update_goal_metric(
427
+ client: TetraClient,
428
+ goal_uid: str,
429
+ metric_uid: str,
430
+ *,
431
+ name: str | None = None,
432
+ metric_config: dict[str, Any] | None = None,
433
+ tier_override: Any = _UNSET,
434
+ activate: bool | None = None,
435
+ ) -> dict[str, Any]:
436
+ """Update an existing goal metric.
437
+
438
+ ``activate=True`` makes this metric active (archiving the previously
439
+ active one). Pass any subset of fields you want to change.
440
+
441
+ Pass ``tier_override`` to set the user override (D/C/B/A) or
442
+ explicitly ``None`` to clear it; omit it to leave the existing
443
+ override untouched. The server still computes the underlying tier
444
+ from metric semantics — override is a display/XP overlay.
445
+ """
446
+ body: dict[str, Any] = {}
447
+ if name is not None:
448
+ body["name"] = name
449
+ if metric_config is not None:
450
+ body["metric_config"] = metric_config
451
+ if tier_override is not _UNSET:
452
+ body["tier_override"] = tier_override
453
+ if activate is not None:
454
+ body["activate"] = activate
455
+ return await client.put(
456
+ f"/api/v1/goals/{goal_uid}/metrics/{metric_uid}", json=body,
457
+ )
458
+
459
+
460
+ async def delete_goal_metric(
461
+ client: TetraClient, goal_uid: str, metric_uid: str,
462
+ ) -> dict[str, Any]:
463
+ """Permanently delete a goal metric."""
464
+ return await client.delete(
465
+ f"/api/v1/goals/{goal_uid}/metrics/{metric_uid}",
466
+ )
467
+
468
+
469
+ @operation(
470
+ cli="goals move",
471
+ summary="Move a goal to a new parent and/or position in the sibling order.",
472
+ covers=[("POST", "/api/v1/goals/move")],
473
+ params={
474
+ "uid": arg(help="Goal UID to move"),
475
+ "new_parent": opt("--new-parent",
476
+ help="New parent goal name (omit to keep current)."),
477
+ "new_priority": opt("--priority", min=1,
478
+ help="New priority among siblings (1 = top)."),
479
+ },
480
+ )
481
+ async def move_goal(
482
+ client: TetraClient,
483
+ *,
484
+ uid: str,
485
+ new_parent: str | None = None,
486
+ new_priority: int | None = None,
487
+ ) -> dict[str, Any]:
488
+ """Move a goal to a new parent and/or position in the sibling order.
489
+
490
+ The API treats ``new_parent: null`` as "promote to root" — there is no
491
+ "keep current parent" sentinel on the wire. So when the caller only
492
+ wants to reorder (priority change, no parent change), this helper
493
+ fetches the goal first and forwards its current parent so the route
494
+ doesn't accidentally promote it to root.
495
+
496
+ Args:
497
+ client: Authenticated TetraClient
498
+ uid: Goal UID to move
499
+ new_parent: New parent goal name. Omit to keep current parent.
500
+ Pass an empty string ``""`` to explicitly promote to root.
501
+ new_priority: Target priority among siblings (1 = top). The API
502
+ normalizes neighboring siblings atomically.
503
+
504
+ Returns:
505
+ Move result with ``uid``, ``name``, ``parent``, ``priority``,
506
+ ``siblings_updated``.
507
+ """
508
+ body: dict[str, Any] = {"uid": uid}
509
+ if new_parent is None:
510
+ # Caller didn't specify — pre-fetch and use current parent so the
511
+ # API doesn't interpret missing as "move to root."
512
+ current = await client.get(f"/api/v1/goals/{uid}")
513
+ if current.get("parent"):
514
+ body["new_parent"] = current["parent"]
515
+ # else: goal is already a root, omit new_parent (API treats omitted
516
+ # the same as null = root, which is correct in this case).
517
+ elif new_parent == "":
518
+ # Explicit root promotion: send null on the wire.
519
+ body["new_parent"] = None
520
+ else:
521
+ body["new_parent"] = new_parent
522
+ if new_priority is not None:
523
+ body["new_priority"] = new_priority
524
+ return await client.post("/api/v1/goals/move", json=body)
525
+
526
+
527
+ @operation(
528
+ cli="goals batch-delete",
529
+ summary="Soft-delete multiple goals at once (archived for restore).",
530
+ covers=[("POST", "/api/v1/goals/batch-delete")],
531
+ params={
532
+ "uids": opt("--uid", repeatable=True, help="Goal UID to delete (repeatable)."),
533
+ },
534
+ )
535
+ async def batch_delete_goals(
536
+ client: TetraClient,
537
+ *,
538
+ uids: list[str],
539
+ delete_children: bool = False,
540
+ delete_events: bool = False,
541
+ reason: str | None = None,
542
+ ) -> dict[str, Any]:
543
+ """Soft-delete multiple goals at once.
544
+
545
+ Each goal is archived for potential restoration. Children and events are
546
+ either deleted alongside the goal or have their references cleared,
547
+ depending on the flags.
548
+
549
+ Args:
550
+ client: Authenticated TetraClient
551
+ uids: Goal UIDs to delete
552
+ delete_children: If True, delete child goals; else clear their parent.
553
+ delete_events: If True, delete events; else remove the goal from them.
554
+ reason: Optional human-readable reason recorded with the deletion.
555
+
556
+ Returns:
557
+ Dict with ``deleted`` (list of UIDs) and ``failed`` (list of errors).
558
+ """
559
+ body: dict[str, Any] = {
560
+ "uids": uids,
561
+ "delete_children": delete_children,
562
+ "delete_events": delete_events,
563
+ }
564
+ if reason is not None:
565
+ body["reason"] = reason
566
+ return await client.post("/api/v1/goals/batch-delete", json=body)
567
+
568
+
569
+ @operation(
570
+ cli="goals batch-priorities",
571
+ summary="Batch-reorder goals within a sibling group.",
572
+ covers=[("POST", "/api/v1/goals/batch-update-priorities")],
573
+ params={
574
+ "updates": opt("--updates", json=True,
575
+ help="JSON map of {goal_uid: new_priority} (1-based, sequential)."),
576
+ "parent": opt("--parent", help="Parent goal name (omit for root siblings)."),
577
+ },
578
+ )
579
+ async def batch_update_goal_priorities(
580
+ client: TetraClient,
581
+ *,
582
+ updates: dict[str, int],
583
+ parent: str | None = None,
584
+ ) -> dict[str, Any]:
585
+ """Batch-update priorities for goals within one sibling group.
586
+
587
+ All goals must share the same parent. Priorities must be sequential
588
+ starting from 1 (the server validates this).
589
+
590
+ Args:
591
+ client: Authenticated TetraClient
592
+ updates: Map of ``{goal_uid: new_priority}`` (1 = top).
593
+ parent: Parent goal name (null/omit for root-level siblings).
594
+
595
+ Returns:
596
+ Dict with ``updated`` count and the affected ``uids``.
597
+ """
598
+ body: dict[str, Any] = {"updates": updates}
599
+ if parent is not None:
600
+ body["parent"] = parent
601
+ return await client.post("/api/v1/goals/batch-update-priorities", json=body)
602
+
603
+
604
+ @operation(
605
+ cli="goals merge",
606
+ summary="Merge multiple goals into a new goal, reassigning their events.",
607
+ covers=[("POST", "/api/v1/goals/merge")],
608
+ params={
609
+ "source_uids": opt("--source-uid", repeatable=True,
610
+ help="Source goal UID to merge (repeatable; 2+ required)."),
611
+ "new_goal": opt("--new-goal", json=True,
612
+ help="New goal data as a JSON dict (at least {\"name\": ...})."),
613
+ },
614
+ )
615
+ async def merge_goals(
616
+ client: TetraClient,
617
+ *,
618
+ source_uids: list[str],
619
+ new_goal: dict[str, Any],
620
+ ) -> dict[str, Any]:
621
+ """Merge two or more goals into a new goal.
622
+
623
+ Events from all source goals are reassigned to the new goal, and the
624
+ source goals are soft-deleted after the merge.
625
+
626
+ Args:
627
+ client: Authenticated TetraClient
628
+ source_uids: UIDs of the goals to merge (at least 2).
629
+ new_goal: New goal payload (must include ``name``; may include
630
+ ``parent``, ``values``, etc.).
631
+
632
+ Returns:
633
+ The created goal dict.
634
+ """
635
+ body: dict[str, Any] = {"source_uids": source_uids, "new_goal": new_goal}
636
+ return await client.post("/api/v1/goals/merge", json=body)
637
+
638
+
639
+ @operation(
640
+ cli="goals status-all",
641
+ summary="Get achievement status for all goals (two-phase: cached + pending).",
642
+ covers=[("GET", "/api/v1/goals/status/all")],
643
+ params={
644
+ "as_of": opt("--as-of", help="Status as of this date (YYYY-MM-DD) or ISO datetime."),
645
+ "group_uid": opt("--group-uid", help="Group UID (omit for personal goals)."),
646
+ "target_account_uid": opt("--target-account-uid",
647
+ help="View a specific member's status (needs group_uid)."),
648
+ },
649
+ )
650
+ async def get_all_goals_status(
651
+ client: TetraClient,
652
+ *,
653
+ as_of: str | None = None,
654
+ group_uid: str | None = None,
655
+ target_account_uid: str | None = None,
656
+ ) -> dict[str, Any]:
657
+ """Get achievement status for all goals.
658
+
659
+ Returns cached statuses immediately plus a list of pending goal UIDs
660
+ whose status is being recalculated in the background.
661
+
662
+ Args:
663
+ client: Authenticated TetraClient
664
+ as_of: Optional point-in-time for the calculation.
665
+ group_uid: Optional group scope (omit for personal goals).
666
+ target_account_uid: Optional member to view (requires group_uid).
667
+
668
+ Returns:
669
+ Dict with ``statuses``, ``pending_goal_uids``, and ``calculation_id``.
670
+ """
671
+ params: dict[str, Any] = {}
672
+ if as_of is not None:
673
+ params["as_of"] = as_of
674
+ if group_uid is not None:
675
+ params["group_uid"] = group_uid
676
+ if target_account_uid is not None:
677
+ params["target_account_uid"] = target_account_uid
678
+ return await client.get("/api/v1/goals/status/all", params=params or None)
679
+
680
+
681
+ @operation(
682
+ cli="goals status-recalculate",
683
+ summary="Clear the goal status cache and recalculate all goal statuses.",
684
+ covers=[("POST", "/api/v1/goals/status/recalculate")],
685
+ params={
686
+ "as_of": opt("--as-of", help="Recalculate as of this date/datetime (default now)."),
687
+ },
688
+ )
689
+ async def recalculate_all_goals(
690
+ client: TetraClient,
691
+ *,
692
+ as_of: str | None = None,
693
+ ) -> dict[str, Any]:
694
+ """Clear all goal status cache and recalculate every goal's status.
695
+
696
+ Args:
697
+ client: Authenticated TetraClient
698
+ as_of: Optional point-in-time for the recalculation (default: now).
699
+
700
+ Returns:
701
+ Dict with ``cache_cleared`` and ``goals_recalculated`` counts.
702
+ """
703
+ params: dict[str, Any] = {}
704
+ if as_of is not None:
705
+ params["as_of"] = as_of
706
+ return await client.post(
707
+ "/api/v1/goals/status/recalculate"
708
+ + ("?" + "&".join(f"{k}={v}" for k, v in params.items()) if params else ""),
709
+ )
710
+
711
+
712
+ @operation(
713
+ cli="goals children",
714
+ summary="Get the direct child goals of a goal.",
715
+ covers=[("GET", "/api/v1/goals/{uid}/children")],
716
+ params={
717
+ "uid": arg(help="Goal UID"),
718
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
719
+ },
720
+ )
721
+ async def get_goal_children(
722
+ client: TetraClient,
723
+ uid: str,
724
+ *,
725
+ group_uid: str | None = None,
726
+ ) -> list[dict[str, Any]]:
727
+ """Get the direct children of a goal.
728
+
729
+ Args:
730
+ client: Authenticated TetraClient
731
+ uid: Parent goal UID.
732
+ group_uid: Optional group scope.
733
+
734
+ Returns:
735
+ List of child goal dicts.
736
+ """
737
+ params: dict[str, Any] = {}
738
+ if group_uid is not None:
739
+ params["group_uid"] = group_uid
740
+ return await client.get(
741
+ f"/api/v1/goals/{uid}/children", params=params or None,
742
+ )
743
+
744
+
745
+ @operation(
746
+ cli="goals completion-status",
747
+ summary="Check whether a goal can be marked complete (lists blockers).",
748
+ covers=[("GET", "/api/v1/goals/{uid}/completion-status")],
749
+ params={"uid": arg(help="Goal UID")},
750
+ )
751
+ async def get_goal_completion_status(
752
+ client: TetraClient, uid: str,
753
+ ) -> dict[str, Any]:
754
+ """Get a goal's completion status (can it be marked complete?).
755
+
756
+ Reports unmet transitive dependencies and unachieved child goals that
757
+ block completion.
758
+
759
+ Args:
760
+ client: Authenticated TetraClient
761
+ uid: Goal UID.
762
+
763
+ Returns:
764
+ Dict with ``can_complete``, ``blocking_dependencies``,
765
+ ``blocking_children``.
766
+ """
767
+ return await client.get(f"/api/v1/goals/{uid}/completion-status")
768
+
769
+
770
+ @operation(
771
+ cli="goals detail-analytics",
772
+ summary="Get comprehensive analytics for a single goal's detail view.",
773
+ covers=[("GET", "/api/v1/goals/{uid}/detail-analytics")],
774
+ params={
775
+ "uid": arg(help="Goal UID"),
776
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
777
+ "target_account_uid": opt("--target-account-uid",
778
+ help="View a member's analytics (requires mentor permission)."),
779
+ },
780
+ )
781
+ async def get_goal_detail_analytics(
782
+ client: TetraClient,
783
+ uid: str,
784
+ *,
785
+ group_uid: str | None = None,
786
+ target_account_uid: str | None = None,
787
+ ) -> dict[str, Any]:
788
+ """Get comprehensive detail-view analytics for a goal.
789
+
790
+ Includes XP history, streaks, summary factoids, consistency heatmap,
791
+ mood-vs-performance scatter, and missed-reasons word cloud.
792
+
793
+ Args:
794
+ client: Authenticated TetraClient
795
+ uid: Goal UID.
796
+ group_uid: Optional group scope.
797
+ target_account_uid: Optional member to view.
798
+
799
+ Returns:
800
+ Goal detail analytics dict.
801
+ """
802
+ params: dict[str, Any] = {}
803
+ if group_uid is not None:
804
+ params["group_uid"] = group_uid
805
+ if target_account_uid is not None:
806
+ params["target_account_uid"] = target_account_uid
807
+ return await client.get(
808
+ f"/api/v1/goals/{uid}/detail-analytics", params=params or None,
809
+ )
810
+
811
+
812
+ @operation(
813
+ cli="goals events",
814
+ summary="Get paginated events for a goal (with filtering and sorting).",
815
+ covers=[("GET", "/api/v1/goals/{uid}/events")],
816
+ params={
817
+ "uid": arg(help="Goal UID"),
818
+ "tag_uids": opt("--tag-uid", repeatable=True,
819
+ help="Tag UID filter (repeatable; event must match)."),
820
+ "sort_order": opt("--sort-order", choices=["asc", "desc"],
821
+ help="Date sort order (default desc = newest first)."),
822
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
823
+ "target_account_uid": opt("--target-account-uid",
824
+ help="View a specific member's events."),
825
+ },
826
+ )
827
+ async def get_goal_events(
828
+ client: TetraClient,
829
+ uid: str,
830
+ *,
831
+ include_descendants: bool = True,
832
+ group_uid: str | None = None,
833
+ target_account_uid: str | None = None,
834
+ limit: int = 50,
835
+ offset: int = 0,
836
+ tag_uids: list[str] | None = None,
837
+ search: str | None = None,
838
+ date_from: str | None = None,
839
+ date_to: str | None = None,
840
+ sort_order: str = "desc",
841
+ ) -> dict[str, Any]:
842
+ """Get paginated events for a goal.
843
+
844
+ Tag UIDs are sent comma-joined (the backend splits on commas and treats
845
+ them as an AND filter expanded to descendants).
846
+
847
+ Args:
848
+ client: Authenticated TetraClient
849
+ uid: Goal UID.
850
+ include_descendants: Include events from child goals (default True).
851
+ group_uid: Optional group scope.
852
+ target_account_uid: Optional member whose events to show.
853
+ limit: Max events per page (1-500, default 50).
854
+ offset: Events to skip (default 0).
855
+ tag_uids: Tag UID filters (event must have all).
856
+ search: Case-insensitive description search.
857
+ date_from: Inclusive start date (YYYY-MM-DD).
858
+ date_to: Inclusive end date (YYYY-MM-DD).
859
+ sort_order: ``asc`` or ``desc`` (default ``desc``).
860
+
861
+ Returns:
862
+ Dict with ``items``, ``has_more``, ``offset``, ``limit``.
863
+ """
864
+ params: dict[str, Any] = {
865
+ "include_descendants": include_descendants,
866
+ "limit": limit,
867
+ "offset": offset,
868
+ "sort_order": sort_order,
869
+ }
870
+ if group_uid is not None:
871
+ params["group_uid"] = group_uid
872
+ if target_account_uid is not None:
873
+ params["target_account_uid"] = target_account_uid
874
+ if tag_uids:
875
+ params["tag_uids"] = ",".join(tag_uids)
876
+ if search is not None:
877
+ params["search"] = search
878
+ if date_from is not None:
879
+ params["date_from"] = date_from
880
+ if date_to is not None:
881
+ params["date_to"] = date_to
882
+ return await client.get(f"/api/v1/goals/{uid}/events", params=params)
883
+
884
+
885
+ @operation(
886
+ cli="goals progress-history",
887
+ summary="Get a goal's progress snapshots over time (for charting).",
888
+ covers=[("GET", "/api/v1/goals/{uid}/progress-history")],
889
+ params={
890
+ "uid": arg(help="Goal UID"),
891
+ "metric_config": opt("--metric-config", json=True,
892
+ help="Override MetricConfig as a JSON dict (else the active one)."),
893
+ "tuple_fields": opt("--tuple-field", repeatable=True,
894
+ help="Field name for tuple outcomes (repeatable)."),
895
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
896
+ "target_account_uid": opt("--target-account-uid",
897
+ help="View a member's progress history."),
898
+ },
899
+ )
900
+ async def get_goal_progress_history(
901
+ client: TetraClient,
902
+ uid: str,
903
+ *,
904
+ start: str | None = None,
905
+ end: str | None = None,
906
+ metric_config: dict[str, Any] | None = None,
907
+ tuple_fields: list[str] | None = None,
908
+ group_uid: str | None = None,
909
+ target_account_uid: str | None = None,
910
+ ) -> dict[str, Any]:
911
+ """Get progress history for a goal at each event date.
912
+
913
+ The backend takes ``metric_config`` and ``tuple_fields`` as encoded query
914
+ params (JSON string and comma-joined list, respectively), so this op
915
+ serializes them before sending.
916
+
917
+ Args:
918
+ client: Authenticated TetraClient
919
+ uid: Goal UID.
920
+ start: Inclusive start datetime (ISO).
921
+ end: Inclusive end datetime (ISO).
922
+ metric_config: Optional metric config override (else the goal's active).
923
+ tuple_fields: Field names for tuple outcomes.
924
+ group_uid: Optional group scope.
925
+ target_account_uid: Optional member to view.
926
+
927
+ Returns:
928
+ Dict with ``goal_uid`` and ``points`` (progress snapshots).
929
+ """
930
+ import json as _json
931
+
932
+ params: dict[str, Any] = {}
933
+ if start is not None:
934
+ params["start"] = start
935
+ if end is not None:
936
+ params["end"] = end
937
+ if metric_config is not None:
938
+ params["metric_config"] = _json.dumps(metric_config)
939
+ if tuple_fields:
940
+ params["tuple_fields"] = ",".join(tuple_fields)
941
+ if group_uid is not None:
942
+ params["group_uid"] = group_uid
943
+ if target_account_uid is not None:
944
+ params["target_account_uid"] = target_account_uid
945
+ return await client.get(
946
+ f"/api/v1/goals/{uid}/progress-history", params=params or None,
947
+ )
948
+
949
+
950
+ @operation(
951
+ cli="goals time-history",
952
+ summary="Get time/count-per-period aggregation history for a goal's events.",
953
+ covers=[("GET", "/api/v1/goals/{uid}/time-history")],
954
+ params={
955
+ "uid": arg(help="Goal UID"),
956
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
957
+ "target_account_uid": opt("--target-account-uid",
958
+ help="View a member's time history."),
959
+ },
960
+ )
961
+ async def get_goal_time_history(
962
+ client: TetraClient,
963
+ uid: str,
964
+ *,
965
+ start: str | None = None,
966
+ end: str | None = None,
967
+ group_uid: str | None = None,
968
+ target_account_uid: str | None = None,
969
+ ) -> dict[str, Any]:
970
+ """Get time-based aggregation history for a goal's events.
971
+
972
+ Returns duration-per-period or event-count-per-period data, used as a
973
+ fallback chart on goal detail when the goal has no metric.
974
+
975
+ Args:
976
+ client: Authenticated TetraClient
977
+ uid: Goal UID.
978
+ start: Inclusive start date (YYYY-MM-DD).
979
+ end: Inclusive end date (YYYY-MM-DD).
980
+ group_uid: Optional group scope.
981
+ target_account_uid: Optional member to view.
982
+
983
+ Returns:
984
+ Time history aggregation dict.
985
+ """
986
+ params: dict[str, Any] = {}
987
+ if start is not None:
988
+ params["start"] = start
989
+ if end is not None:
990
+ params["end"] = end
991
+ if group_uid is not None:
992
+ params["group_uid"] = group_uid
993
+ if target_account_uid is not None:
994
+ params["target_account_uid"] = target_account_uid
995
+ return await client.get(
996
+ f"/api/v1/goals/{uid}/time-history", params=params or None,
997
+ )
998
+
999
+
1000
+ @operation(
1001
+ cli="goals week-events",
1002
+ summary="Get this-week and last-week events for a goal, with summaries.",
1003
+ covers=[("GET", "/api/v1/goals/{uid}/week-events")],
1004
+ params={
1005
+ "uid": arg(help="Goal UID"),
1006
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
1007
+ "target_account_uid": opt("--target-account-uid",
1008
+ help="View a member's week events."),
1009
+ },
1010
+ )
1011
+ async def get_goal_week_events(
1012
+ client: TetraClient,
1013
+ uid: str,
1014
+ *,
1015
+ group_uid: str | None = None,
1016
+ target_account_uid: str | None = None,
1017
+ ) -> dict[str, Any]:
1018
+ """Get this-week and last-week events for a goal, with summaries.
1019
+
1020
+ Args:
1021
+ client: Authenticated TetraClient
1022
+ uid: Goal UID.
1023
+ group_uid: Optional group scope.
1024
+ target_account_uid: Optional member to view.
1025
+
1026
+ Returns:
1027
+ Dict with this-week/last-week event lists and summaries.
1028
+ """
1029
+ params: dict[str, Any] = {}
1030
+ if group_uid is not None:
1031
+ params["group_uid"] = group_uid
1032
+ if target_account_uid is not None:
1033
+ params["target_account_uid"] = target_account_uid
1034
+ return await client.get(
1035
+ f"/api/v1/goals/{uid}/week-events", params=params or None,
1036
+ )
1037
+
1038
+
1039
+ @operation(
1040
+ cli="goals status-stages",
1041
+ summary="Get the stage-by-stage breakdown of a goal's status calculation.",
1042
+ covers=[("GET", "/api/v1/goals/{uid}/status/stages")],
1043
+ params={
1044
+ "uid": arg(help="Goal UID"),
1045
+ "as_of": opt("--as-of", help="Compute as of this date (YYYY-MM-DD) or ISO datetime."),
1046
+ "group_uid": opt("--group-uid", help="Group UID for group-scoped lookup."),
1047
+ },
1048
+ )
1049
+ async def get_goal_status_stages(
1050
+ client: TetraClient,
1051
+ uid: str,
1052
+ *,
1053
+ as_of: str | None = None,
1054
+ group_uid: str | None = None,
1055
+ ) -> dict[str, Any]:
1056
+ """Get the stage-by-stage breakdown of a goal's status calculation.
1057
+
1058
+ Only stage-based metric types (count, duration, money, outcome,
1059
+ moneytime) produce stages; other types return an empty stages list.
1060
+
1061
+ Args:
1062
+ client: Authenticated TetraClient
1063
+ uid: Goal UID.
1064
+ as_of: Optional point-in-time for the calculation.
1065
+ group_uid: Optional group scope.
1066
+
1067
+ Returns:
1068
+ Dict with ``stages``, ``achieved``, ``actual``, ``target``.
1069
+ """
1070
+ params: dict[str, Any] = {}
1071
+ if as_of is not None:
1072
+ params["as_of"] = as_of
1073
+ if group_uid is not None:
1074
+ params["group_uid"] = group_uid
1075
+ return await client.get(
1076
+ f"/api/v1/goals/{uid}/status/stages", params=params or None,
1077
+ )
1078
+
1079
+
1080
+ @operation(
1081
+ cli="goals rename-field",
1082
+ summary="Rename a record/tuple field on a goal and all its events.",
1083
+ covers=[("POST", "/api/v1/goals/{uid}/rename-field")],
1084
+ params={
1085
+ "uid": arg(help="Goal UID"),
1086
+ "old_name": opt("--old-name", help="Current field name to rename."),
1087
+ "new_name": opt("--new-name", help="New field name."),
1088
+ },
1089
+ )
1090
+ async def rename_goal_field(
1091
+ client: TetraClient,
1092
+ uid: str,
1093
+ *,
1094
+ old_name: str,
1095
+ new_name: str,
1096
+ ) -> dict[str, Any]:
1097
+ """Rename a tuple/record field on a goal and across all its events.
1098
+
1099
+ Atomically updates ``goal.outcome_names`` and every event's outcome
1100
+ records so the field name stays consistent.
1101
+
1102
+ Args:
1103
+ client: Authenticated TetraClient
1104
+ uid: Goal UID.
1105
+ old_name: Existing field name.
1106
+ new_name: Replacement field name.
1107
+
1108
+ Returns:
1109
+ Dict with ``success``, ``events_updated``, and the new
1110
+ ``outcome_names``.
1111
+ """
1112
+ body: dict[str, Any] = {"old_name": old_name, "new_name": new_name}
1113
+ return await client.post(f"/api/v1/goals/{uid}/rename-field", json=body)
1114
+
1115
+
1116
+ @operation(
1117
+ cli="goals reorder",
1118
+ summary="Persist the root ordering of the interleaved 'all' goals view.",
1119
+ covers=[("POST", "/api/v1/goals/reorder-all")],
1120
+ params={
1121
+ "order": opt("--uid", repeatable=True,
1122
+ help="Root goal UID, in top-to-bottom order (repeatable)."),
1123
+ },
1124
+ )
1125
+ async def reorder_all_goals(
1126
+ client: TetraClient,
1127
+ *,
1128
+ order: list[str],
1129
+ ) -> dict[str, Any]:
1130
+ """Persist the root ordering of the interleaved "all" goals view.
1131
+
1132
+ Stored as a per-account overlay; canonical per-scope priorities are never
1133
+ touched, so personal and group orders stay independent.
1134
+
1135
+ Args:
1136
+ client: Authenticated TetraClient
1137
+ order: Full top-to-bottom sequence of root goal UIDs across scopes.
1138
+
1139
+ Returns:
1140
+ Dict with ``updated`` (count of overlay rows written).
1141
+ """
1142
+ return await client.post(
1143
+ "/api/v1/goals/reorder-all", json={"order": order}
1144
+ )