cdx-manager 0.21.5 → 0.22.0

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 (52) hide show
  1. package/README.md +1 -1
  2. package/VERSION +1 -1
  3. package/changelogs/CHANGELOGS_0_21_6.md +33 -0
  4. package/changelogs/CHANGELOGS_0_22_0.md +25 -0
  5. package/checksums/release-archives.json +22 -0
  6. package/docs/cli.md +17 -7
  7. package/docs/operations.md +80 -0
  8. package/docs/sessions-and-notifications.md +1 -1
  9. package/docs/technical-overview.md +1 -1
  10. package/install.ps1 +7 -4
  11. package/package.json +1 -1
  12. package/pyproject.toml +1 -1
  13. package/src/agent_notify.py +23 -7
  14. package/src/backup_bundle.py +60 -16
  15. package/src/claude_usage.py +1 -1
  16. package/src/cli.py +15 -4
  17. package/src/cli_args.py +76 -19
  18. package/src/cli_helpers.py +23 -9
  19. package/src/codex_usage.py +1 -1
  20. package/src/commands/launch.py +41 -3
  21. package/src/commands/maintenance.py +1 -0
  22. package/src/commands/rescue.py +207 -0
  23. package/src/commands/runs.py +26 -3
  24. package/src/commands/status.py +56 -6
  25. package/src/config.py +17 -1
  26. package/src/context_store.py +11 -6
  27. package/src/errors.py +5 -1
  28. package/src/handoff.py +22 -11
  29. package/src/health.py +2 -2
  30. package/src/interactive_usage.py +20 -5
  31. package/src/notify.py +23 -18
  32. package/src/provider_background.py +13 -8
  33. package/src/provider_runtime.py +101 -8
  34. package/src/retro.py +131 -0
  35. package/src/rtk_savings.py +68 -0
  36. package/src/run_registry.py +27 -1
  37. package/src/run_usage.py +36 -9
  38. package/src/session_backup.py +16 -5
  39. package/src/session_helpers.py +4 -0
  40. package/src/session_ranking.py +10 -0
  41. package/src/session_service.py +39 -34
  42. package/src/session_status.py +86 -2
  43. package/src/shared_memory.py +102 -0
  44. package/src/status_source.py +2 -0
  45. package/src/status_view.py +23 -23
  46. package/src/tray_autostart.py +21 -2
  47. package/src/tray_events.py +23 -4
  48. package/src/tray_instance.py +11 -2
  49. package/src/tray_logics.py +1 -1
  50. package/src/update_all.py +1 -1
  51. package/src/update_check.py +1 -1
  52. package/src/update_manager.py +17 -7
package/src/cli.py CHANGED
@@ -6,6 +6,7 @@ import sys
6
6
  from datetime import datetime, timezone
7
7
 
8
8
  from .agent_notify import handle_notify
9
+ from .cli_args import SET_USAGE
9
10
  from .cli_commands import (
10
11
  API_SCHEMA_VERSION,
11
12
  STATUS_USAGE,
@@ -70,8 +71,10 @@ from .cli_render import (
70
71
  _visible_len,
71
72
  format_error,
72
73
  )
74
+ from .commands.rescue import handle_rescue
73
75
  from .config import PROVIDER_OLLAMA
74
76
  from .errors import CdxError
77
+ from .retro import handle_retro
75
78
  from .session_service import create_session_service
76
79
  from .status_view import (
77
80
  _format_blocking_quota,
@@ -174,6 +177,8 @@ _COMMAND_HANDLERS = {
174
177
  "rename": handle_rename,
175
178
  "resume": handle_resume,
176
179
  "can-resume": handle_can_resume,
180
+ "rescue": handle_rescue,
181
+ "retro": handle_retro,
177
182
  "rmv": handle_remove,
178
183
  "run": handle_run,
179
184
  "run-report": handle_run_report,
@@ -235,7 +240,7 @@ def _print_help(use_color=False):
235
240
  f" {_style('cdx reset <name> [--yes] [--json]', '36', use_color)}",
236
241
  f" {_style('cdx auth refresh <name|all> [--json]', '36', use_color)}",
237
242
  f" {_style('cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json', '36', use_color)}",
238
- f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--refresh] --json', '36', use_color)}",
243
+ f" {_style('cdx run [session] --cwd PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json', '36', use_color)}",
239
244
  f" {_style('cdx runs [--limit N] [--since 7d|today|DATE] --json', '36', use_color)}",
240
245
  f" {_style('cdx run-status <run_id> --json', '36', use_color)}",
241
246
  f" {_style('cdx run-report <run_id> --json', '36', use_color)}",
@@ -247,13 +252,15 @@ def _print_help(use_color=False):
247
252
  f" {_style('cdx config <name> [--json]', '36', use_color)}",
248
253
  f" {_style('cdx configs [--json]', '36', use_color)}",
249
254
  f" {_style('cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]', '36', use_color)}",
250
- f" {_style('cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--model MODEL] [--priority 0..100] [--json]', '36', use_color)}",
255
+ f" {_style(SET_USAGE[len('Usage: '):], '36', use_color)}",
251
256
  f" {_style('cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--model|--priority|--all) [--json]', '36', use_color)}",
252
257
  f" {_style('cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
253
- f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]', '36', use_color)}",
258
+ f" {_style('cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]', '36', use_color)}",
254
259
  f" {_style('cdx last [--json]', '36', use_color)}",
255
260
  f" {_style('cdx resume <name> [--json]', '36', use_color)}",
256
261
  f" {_style('cdx can-resume <name> [--json]', '36', use_color)}",
262
+ f" {_style('cdx rescue [<session>] [--to <target>] [--yes] [--dry-run] [--json]', '36', use_color)}",
263
+ f" {_style('cdx retro [--limit N] [--json]', '36', use_color)}",
257
264
  f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
258
265
  f" {_style('cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]', '36', use_color)}",
259
266
  f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
@@ -284,6 +291,7 @@ def _print_help(use_color=False):
284
291
  f" {_style('cdx ready', '36', use_color)} # next session to become ready",
285
292
  f" {_style('cdx <name> [--json]', '36', use_color)}",
286
293
  f" {_style('cdx <name> --prompt TEXT|--prompt-file PATH', '36', use_color)} # start with a first message",
294
+ f" {_style('cdx <name> --dir PATH --task TEXT --end-state TEXT', '36', use_color)} # start on a task, with what the go-ahead covers",
287
295
  f" {_style('cdx --help', '36', use_color)}",
288
296
  f" {_style('cdx --version', '36', use_color)}",
289
297
  ])
@@ -533,7 +541,6 @@ def main(argv, options=None):
533
541
  stderr = options.get("stderr", sys.stderr)
534
542
  stdin_is_tty = options.get("stdin", {}).get("isTTY", hasattr(sys.stdin, "isatty") and sys.stdin.isatty())
535
543
  use_color = _should_use_color(env, stdout)
536
- service = options.get("service") or create_session_service({"env": env})
537
544
  spawn = options.get("spawn")
538
545
  spawn_sync = options.get("spawn_sync")
539
546
  refresh_fn = options.get("refreshClaudeSessionStatus")
@@ -559,6 +566,10 @@ def main(argv, options=None):
559
566
  out(f"{_print_version()}\n")
560
567
  return 0
561
568
 
569
+ # Built after --help/--version: those must answer even where the home
570
+ # directory cannot be resolved (a service or task with a minimal environment).
571
+ service = options.get("service") or create_session_service({"env": env})
572
+
562
573
  if argv == ["--json"]:
563
574
  rows = service["format_list_rows"]()
564
575
  notices = _get_update_notices(service, env, options)
package/src/cli_args.py CHANGED
@@ -16,6 +16,7 @@ from .config import (
16
16
  PROVIDERS,
17
17
  REASONING_EFFORT_VALUES,
18
18
  normalize_permission,
19
+ parse_on_off,
19
20
  )
20
21
  from .errors import CdxArgumentError, CdxError
21
22
  from .provider_runtime import _normalize_reasoning_effort
@@ -32,26 +33,27 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
32
33
  MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
33
34
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
34
35
  LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
35
- SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
36
- UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
36
+ SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--notify on|off] [--notify-preview on|off] [--scrub-env on|off] [--shared-memory on|off] [--retro on|off] [--policy use|ask-first|never] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
37
+ UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--notify|--notify-preview|--scrub-env|--shared-memory|--retro|--policy|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
37
38
  SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
38
39
  CONFIG_USAGE = "Usage: cdx config <name> [--json]"
39
40
  CONFIGS_USAGE = "Usage: cdx configs [--json]"
40
41
  HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
41
- STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
42
+ STATS_USAGE = "Usage: cdx stats [name] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--by session|project|model] [--json]"
43
+ STATS_GROUPS = ("session", "project", "model")
42
44
  LAST_USAGE = "Usage: cdx last [--json]"
43
45
  RESUME_USAGE = "Usage: cdx resume <name> [--json]"
44
46
  CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
45
47
  SELECT_USAGE = "Usage: cdx select --provider PROVIDER [--min-reasoning-effort minimal|low|medium|high|xhigh] [--min-power minimal|low|medium|high|xhigh] [--require-ready] [--refresh] --json"
46
48
  NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
47
- RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--detach] [--failover] [--refresh] --json"
49
+ RUN_USAGE = "Usage: cdx run [session] (--cwd|--dir) PATH (--prompt-file PATH|--prompt TEXT|--prompt-file -) [--provider PROVIDER] [--model MODEL] [--kind assistant|code-review|retro] [--reasoning-effort minimal|low|medium|high|xhigh] [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full|workspace-write|read-only|danger-full-access] [--timeout-seconds N] [--label TEXT] [--max-parallel N] [--detach] [--failover] [--refresh] --json"
48
50
  RUN_JSON_REQUIRED = "cdx run: --json is required."
49
51
  RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
50
52
  RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
51
53
  RUN_CWD_REQUIRED = "cdx run: --cwd PATH is required."
52
54
  RUN_PROMPT_SOURCE_REQUIRED = "cdx run: specify exactly one prompt source: --prompt TEXT or --prompt-file PATH."
53
55
  RUN_DETACH_FAILOVER_CONFLICT = "cdx run: --failover needs to watch the run, which --detach gives up."
54
- RUN_KIND_VALUES = ("assistant", "code-review")
56
+ RUN_KIND_VALUES = ("assistant", "code-review", "retro")
55
57
  # Aliases of the shared definitions in config, kept only so the existing
56
58
  # RUN_* spellings in this module keep reading naturally. They are references,
57
59
  # not copies: config.py is the single owner.
@@ -213,13 +215,12 @@ def _parse_toggle_args(args, usage):
213
215
  return {"name": parsed["names"][0], "json": parsed["json"]}
214
216
 
215
217
 
216
- def _parse_fast_value(value):
217
- text = str(value).strip().lower()
218
- if text in ("on", "true", "1", "yes"):
219
- return True
220
- if text in ("off", "false", "0", "no"):
221
- return False
222
- raise CdxError(SET_USAGE)
218
+ def _parse_fast_value(value, label="fast"):
219
+ return parse_on_off(value, label)
220
+
221
+
222
+ def _on_off(label):
223
+ return lambda value: parse_on_off(value, label)
223
224
 
224
225
 
225
226
  def _parse_priority_value(value):
@@ -248,15 +249,19 @@ def _parse_set_args(args):
248
249
  "--power": {"key": "power", "type": "str", "default": None},
249
250
  "--permission": {"key": "permission", "type": "str", "default": None},
250
251
  "--fast": {"key": "fast", "type": "str", "default": None, "transform": _parse_fast_value},
251
- "--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _parse_fast_value},
252
- "--logics": {"key": "logics", "type": "str", "default": None, "transform": _parse_fast_value},
253
- "--notify": {"key": "notify", "type": "str", "default": None, "transform": _parse_fast_value},
254
- "--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _parse_fast_value},
252
+ "--rtk": {"key": "rtk", "type": "str", "default": None, "transform": _on_off("rtk")},
253
+ "--logics": {"key": "logics", "type": "str", "default": None, "transform": _on_off("logics")},
254
+ "--notify": {"key": "notify", "type": "str", "default": None, "transform": _on_off("notify")},
255
+ "--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _on_off("notify preview")},
256
+ "--scrub-env": {"key": "scrub_env", "type": "str", "default": None, "transform": _on_off("scrub env")},
257
+ "--shared-memory": {"key": "shared_memory", "type": "str", "default": None, "transform": _on_off("shared memory")},
258
+ "--retro": {"key": "retro", "type": "str", "default": None, "transform": _on_off("retro")},
255
259
  "--model": {"key": "model", "type": "str", "default": None},
256
260
  "--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
257
261
  "--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
258
262
  "--extra-args": {"key": "extra_args", "type": "str", "default": None},
259
263
  "--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
264
+ "--policy": {"key": "policy", "type": "str", "default": None},
260
265
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
261
266
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
262
267
  "--json": {"key": "json", "type": "bool", "default": False},
@@ -273,7 +278,7 @@ def _parse_set_args(args):
273
278
  raise CdxError(SET_USAGE)
274
279
  settings = {
275
280
  key: parsed[key]
276
- for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "model", "fallback_model", "budget", "extra_args", "priority")
281
+ for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy")
277
282
  if parsed[key] is not None
278
283
  }
279
284
  if not settings:
@@ -297,11 +302,15 @@ def _parse_unset_args(args):
297
302
  "--logics": {"key": "logics", "type": "bool", "default": False},
298
303
  "--notify": {"key": "notify", "type": "bool", "default": False},
299
304
  "--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
305
+ "--scrub-env": {"key": "scrub_env", "type": "bool", "default": False},
306
+ "--shared-memory": {"key": "shared_memory", "type": "bool", "default": False},
307
+ "--retro": {"key": "retro", "type": "bool", "default": False},
300
308
  "--model": {"key": "model", "type": "bool", "default": False},
301
309
  "--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
302
310
  "--budget": {"key": "budget", "type": "bool", "default": False},
303
311
  "--extra-args": {"key": "extra_args", "type": "bool", "default": False},
304
312
  "--priority": {"key": "priority", "type": "bool", "default": False},
313
+ "--policy": {"key": "policy", "type": "bool", "default": False},
305
314
  "--all": {"key": "all", "type": "bool", "default": False},
306
315
  "--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
307
316
  "--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
@@ -318,8 +327,8 @@ def _parse_unset_args(args):
318
327
  if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
319
328
  raise CdxError(UNSET_USAGE)
320
329
  _UNSET_KEYS = (
321
- "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview",
322
- "model", "fallback_model", "budget", "extra_args", "priority",
330
+ "power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro",
331
+ "model", "fallback_model", "budget", "extra_args", "priority", "policy",
323
332
  )
324
333
  keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
325
334
  if not keys:
@@ -510,8 +519,17 @@ def cdx_schema():
510
519
  "provider_start_failed",
511
520
  "provider_failed",
512
521
  "provider_timeout",
522
+ "run_cancelled",
523
+ "too_many_runs",
524
+ "failover_exhausted",
525
+ "failover_limit_reached",
526
+ "invalid_request",
513
527
  "cdx_error",
514
528
  ],
529
+ # The error code on a run record the stale sweep closed.
530
+ "run_record": [
531
+ "stale_process",
532
+ ],
515
533
  "run_lookup": [
516
534
  "run_not_found",
517
535
  "run_output_unavailable",
@@ -522,6 +540,26 @@ def cdx_schema():
522
540
  "network_disabled_by_permission",
523
541
  "session_selected_without_status",
524
542
  "limit_ignored_with_since",
543
+ "claude_auth_probe_failed",
544
+ "claude_refresh_failed",
545
+ "disk_cleanup_available",
546
+ "update_available",
547
+ "logics_manager_update_available",
548
+ "update_version_check_failed",
549
+ "update_version_mismatch",
550
+ "logics_manager_missing",
551
+ "logics_viewer_failed",
552
+ "logics_viewer_root_invalid",
553
+ "tray_autostart_unsupported",
554
+ "tray_companion_missing",
555
+ "tray_companion_not_aligned",
556
+ "tray_companion_not_available",
557
+ "tray_companion_not_installed",
558
+ "tray_companion_restart_pending",
559
+ "tray_companion_updated",
560
+ "tray_plugin_action_unknown",
561
+ "tray_plugin_unknown",
562
+ "tray_terminal_invalid",
525
563
  ],
526
564
  }
527
565
 
@@ -583,6 +621,17 @@ def _parse_run_effort(reasoning_effort, power):
583
621
  ) from error
584
622
 
585
623
 
624
+ def _parse_max_parallel(value):
625
+ try:
626
+ parsed = int(value)
627
+ except (TypeError, ValueError):
628
+ parsed = 0
629
+ if parsed < 1:
630
+ raise CdxArgumentError("cdx run: --max-parallel takes a whole number of at least 1.",
631
+ code=ARG_CODE_INVALID_VALUE, arguments=["--max-parallel"])
632
+ return parsed
633
+
634
+
586
635
  def _parse_run_args(args):
587
636
  parsed = _parse_run_flag_args(args, {
588
637
  "--cwd": {"key": "cwd", "type": "str", "default": None},
@@ -599,6 +648,8 @@ def _parse_run_args(args):
599
648
  "--detach": {"key": "detach", "type": "bool", "default": False},
600
649
  "--failover": {"key": "failover", "type": "bool", "default": False},
601
650
  "--refresh": {"key": "refresh", "type": "bool", "default": False},
651
+ "--label": {"key": "label", "type": "str", "default": None},
652
+ "--max-parallel": {"key": "max_parallel", "type": "str", "default": None, "transform": _parse_max_parallel},
602
653
  "--json": {"key": "json", "type": "bool", "default": False},
603
654
  }, RUN_USAGE, positionals_key="names", max_positionals=1)
604
655
  if not parsed["json"]:
@@ -653,6 +704,8 @@ def _parse_run_args(args):
653
704
  "timeout_seconds": parsed["timeout_seconds"],
654
705
  "detach": parsed["detach"],
655
706
  "failover": parsed["failover"],
707
+ "label": parsed["label"],
708
+ "max_parallel": parsed["max_parallel"],
656
709
  "refresh": parsed["refresh"],
657
710
  "reasoning_effort": effort.get("reasoning_effort"),
658
711
  "power": effort.get("power"),
@@ -817,12 +870,16 @@ def _parse_stats_args(args, now=None):
817
870
  "--since": {"key": "since", "type": "str", "default": None},
818
871
  "--from": {"key": "from", "type": "str", "default": None},
819
872
  "--to": {"key": "to", "type": "str", "default": None},
873
+ "--by": {"key": "by", "type": "str", "default": "session"},
820
874
  "--json": {"key": "json", "type": "bool", "default": False},
821
875
  }, STATS_USAGE, positionals_key="names", max_positionals=1)
876
+ if parsed["by"] not in STATS_GROUPS:
877
+ raise CdxError(STATS_USAGE)
822
878
  period = _parse_history_period(parsed, now, STATS_USAGE)
823
879
  return {
824
880
  "name": parsed["names"][0] if parsed["names"] else None,
825
881
  "period": period,
882
+ "by": parsed["by"],
826
883
  "json": parsed["json"],
827
884
  }
828
885
 
@@ -249,13 +249,23 @@ def _bootstrap_claude_setup_token(session, ctx):
249
249
  ctx["out"](
250
250
  "Minting a long-lived Claude setup-token for this session.\n"
251
251
  )
252
- run_info = _run_interactive_provider_command(
253
- session,
254
- "setup-token",
255
- spawn=ctx.get("spawn"),
256
- env_override=ctx.get("env"),
257
- signal_emitter=ctx.get("signal_emitter"),
258
- )
252
+ try:
253
+ run_info = _run_interactive_provider_command(
254
+ session,
255
+ "setup-token",
256
+ spawn=ctx.get("spawn"),
257
+ env_override=ctx.get("env"),
258
+ signal_emitter=ctx.get("signal_emitter"),
259
+ )
260
+ except Exception as error:
261
+ # A failed or interrupted run can still have printed the token.
262
+ failed_path = (getattr(error, "run_info", None) or {}).get("transcript_path")
263
+ if failed_path:
264
+ try:
265
+ os.remove(failed_path)
266
+ except OSError:
267
+ pass
268
+ raise
259
269
  transcript_path = run_info.get("transcript_path")
260
270
  if not transcript_path or not os.path.isfile(transcript_path):
261
271
  raise CdxError(
@@ -303,11 +313,15 @@ def _format_launch_config(session, use_color=False):
303
313
  ("logics", "Logics"),
304
314
  ("notify", "Agent alerts"),
305
315
  ("notify_preview", "Response preview"),
316
+ ("scrub_env", "Scrub agent env"),
317
+ ("shared_memory", "Shared memory"),
318
+ ("retro", "Session retro"),
306
319
  ("model", "Model"),
307
320
  ("fallback_model", "Fallback"),
308
321
  ("budget", "Budget"),
309
322
  ("extra_args", "Extra args"),
310
323
  ("priority", "Priority"),
324
+ ("policy", "Policy"),
311
325
  ]:
312
326
  rows.append([
313
327
  _dim(label, use_color),
@@ -329,14 +343,14 @@ def _format_launch_settings_hint(name="<name>"):
329
343
  )
330
344
 
331
345
  def _format_launch_setting_value(launch, key, use_color=False):
332
- if key in ("fast", "rtk", "logics", "notify", "notify_preview"):
346
+ if key in ("fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
333
347
  if launch.get(key) is True:
334
348
  return _style("on", "32", use_color)
335
349
  if launch.get(key) is False:
336
350
  return _style("off", "2", use_color)
337
351
  if key == "logics":
338
352
  return _dim("auto", use_color)
339
- if key in ("notify", "notify_preview"):
353
+ if key in ("notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
340
354
  return _style("off", "2", use_color)
341
355
  return _dim("default", use_color)
342
356
  value = launch.get(key)
@@ -247,7 +247,7 @@ def _request_codex_app_server(auth_home, method, params, failure_reason, timeout
247
247
  stdout=subprocess.PIPE,
248
248
  stderr=subprocess.DEVNULL,
249
249
  env=env,
250
- text=True,
250
+ text=True, encoding="utf-8", errors="replace",
251
251
  bufsize=1,
252
252
  )
253
253
  thread = threading.Thread(target=_reader_thread, args=(process.stdout, output), daemon=True)
@@ -6,6 +6,7 @@ Split out of cli_commands.py. Moved verbatim; re-exported by cli_commands.
6
6
  import os
7
7
  import shlex
8
8
 
9
+ from .. import retro, shared_memory
9
10
  from ..agent_notify import notifications_enabled, provision, supports_agent_alerts
10
11
  from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag
11
12
  from ..cli_helpers import (
@@ -19,7 +20,7 @@ from ..cli_helpers import (
19
20
  _write_update_notice,
20
21
  )
21
22
  from ..cli_render import _dim, _info, _success, _warn
22
- from ..config import PROVIDER_CODEX
23
+ from ..config import PROVIDER_CLAUDE, PROVIDER_CODEX
23
24
  from ..errors import CdxError
24
25
  from ..handoff import HandoffSourceError, launch_prompt, load_entry, prepare_entry, resolve_source
25
26
  from ..interactive_usage import extract_interactive_usage, transcript_predates_run, usage_delta
@@ -33,12 +34,24 @@ from ..provider_runtime import (
33
34
  from ..run_command import PROMPT_STDIN, read_run_prompt
34
35
  from ..run_usage import estimate_cost
35
36
 
36
- LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH] [--resume] [--json]"
37
+ LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--json]"
38
+
39
+ #: The first message of a session started with --task. The end state is what the
40
+ #: user's go-ahead covers: a bare "show me first" made sessions stop after a commit
41
+ #: and wait for a push they could have made.
42
+ TASK_PROMPT = """Task: {task}
43
+
44
+ {end_state}
45
+
46
+ Before starting, read this repository's instructions (AGENTS.md, CLAUDE.md and what they reference) and use its skills where one fits; they override general habits."""
47
+ END_STATE = """End state: {end_state}
48
+ Reaching it is approved: take every step it needs, including the commits, pushes or pull requests it names, without asking again. Stop and ask only for a decision it does not cover."""
49
+ NO_END_STATE = """No end state was given: stop and show the result before any commit, push or pull request."""
37
50
 
38
51
 
39
52
  def _parse_launch_args(args):
40
53
  """Return the per-launch directory, initial prompt source and ordinary launch flags."""
41
- values = {"--dir": None, "--prompt": None, "--prompt-file": None}
54
+ values = {"--dir": None, "--prompt": None, "--prompt-file": None, "--task": None, "--end-state": None}
42
55
  cleaned = []
43
56
  index = 0
44
57
  while index < len(args):
@@ -59,6 +72,13 @@ def _parse_launch_args(args):
59
72
  values[flag] = value
60
73
  if values["--prompt"] is not None and values["--prompt-file"] is not None:
61
74
  raise CdxError("cdx <name>: --prompt and --prompt-file are mutually exclusive.")
75
+ if values["--end-state"] is not None and values["--task"] is None:
76
+ raise CdxError("cdx <name>: --end-state goes with --task.")
77
+ if values["--task"] is not None:
78
+ if values["--prompt"] is not None or values["--prompt-file"] is not None:
79
+ raise CdxError("cdx <name>: --task replaces --prompt and --prompt-file.")
80
+ end = END_STATE.format(end_state=values["--end-state"]) if values["--end-state"] else NO_END_STATE
81
+ values["--prompt"] = TASK_PROMPT.format(task=values["--task"], end_state=end)
62
82
  if values["--prompt-file"] == PROMPT_STDIN:
63
83
  raise CdxError("cdx <name>: --prompt-file - is not supported; the interactive session needs standard input.")
64
84
  prompt = {"prompt": values["--prompt"], "prompt_file": values["--prompt-file"]}
@@ -202,6 +222,21 @@ def _notification_provisioning_enabled(session, ctx, json_flag):
202
222
  return True
203
223
 
204
224
 
225
+ def _link_shared_memory(session, ctx, cwd, json_flag):
226
+ if session["provider"] != PROVIDER_CLAUDE or (session.get("launch") or {}).get("shared_memory") is not True:
227
+ return
228
+ try:
229
+ status, detail = shared_memory.link(_get_auth_home(session), ctx["service"]["base_dir"], cwd)
230
+ except OSError as error:
231
+ status, detail = "unsupported", str(error)
232
+ if json_flag or status == "linked":
233
+ return
234
+ if status == "conflict":
235
+ detail = f"merge these files by hand first: {', '.join(detail)}"
236
+ message = f"Shared memory not linked for {session['name']}"
237
+ ctx["out"](f"{_warn(message, ctx['use_color'])} {detail}\n")
238
+
239
+
205
240
  def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None):
206
241
  json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
207
242
  if force_json is not None:
@@ -262,6 +297,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
262
297
  # a real turn — so it gets no instruction it does not need.
263
298
  notice += " — approve them once in Codex to enable"
264
299
  ctx["out"](f"{_info(notice, ctx['use_color'])}\n")
300
+ _link_shared_memory(session, ctx, cwd, json_flag)
265
301
  runtime_run_id = None
266
302
 
267
303
  def runtime_lifecycle(event, info):
@@ -335,6 +371,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
335
371
  })
336
372
  if not json_flag:
337
373
  ctx["out"](f"{_goodbye_line(session, success_run_info, ctx['use_color'])}\n")
374
+ if retro.start(session, cwd, success_run_info.get("provider_transcript_path"), env=ctx.get("env")) and not json_flag:
375
+ ctx["out"](f"{_dim('Session retro started; read it later with `cdx retro`', ctx['use_color'])}\n")
338
376
  if json_flag:
339
377
  extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
340
378
  if capability:
@@ -775,6 +775,7 @@ def handle_update(rest, ctx):
775
775
  package_root=ctx["options"].get("packageRoot"),
776
776
  prefix=ctx["options"].get("prefix"),
777
777
  base_prefix=ctx["options"].get("basePrefix"),
778
+ env=ctx.get("env"),
778
779
  )
779
780
  results = run_update_plan(plan, runner=ctx["options"].get("runUpdate"), env=ctx.get("env"))
780
781
  failed = any((result.get("returncode") != 0) for result in results)