cdx-manager 0.21.6 → 0.23.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.
- package/README.md +1 -1
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_22_0.md +25 -0
- package/changelogs/CHANGELOGS_0_23_0.md +19 -0
- package/checksums/release-archives.json +22 -0
- package/docs/cli.md +22 -11
- package/docs/operations.md +79 -0
- package/docs/sessions-and-notifications.md +1 -1
- package/docs/technical-overview.md +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_notify.py +22 -6
- package/src/cli.py +15 -5
- package/src/cli_args.py +69 -11
- package/src/cli_commands.py +3 -0
- package/src/cli_helpers.py +22 -9
- package/src/commands/launch.py +66 -7
- package/src/commands/maintenance.py +1 -0
- package/src/commands/rescue.py +207 -0
- package/src/commands/runs.py +26 -2
- package/src/commands/status.py +94 -6
- package/src/handoff.py +22 -11
- package/src/interactive_usage.py +20 -5
- package/src/provider_background.py +12 -7
- package/src/provider_runtime.py +61 -3
- package/src/retro.py +131 -0
- package/src/rtk_savings.py +68 -0
- package/src/run_registry.py +78 -3
- package/src/run_usage.py +36 -9
- package/src/session_backup.py +16 -5
- package/src/session_helpers.py +4 -0
- package/src/session_ranking.py +10 -0
- package/src/session_service.py +22 -2
- package/src/session_status.py +105 -2
- package/src/shared_memory.py +102 -0
- package/src/status_source.py +2 -0
- package/src/status_view.py +23 -23
- package/src/tray_autostart.py +21 -2
- package/src/tray_instance.py +11 -2
- package/src/update_manager.py +10 -4
package/src/cli_args.py
CHANGED
|
@@ -33,26 +33,28 @@ CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text..
|
|
|
33
33
|
MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
|
|
34
34
|
HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-native-transcript PATH | --source-transcript PATH] [--json]"
|
|
35
35
|
LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--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] [--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|--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]"
|
|
38
38
|
SETTING_ALIAS_USAGE = "Usage: cdx power|perm|fast|model <name|all|provider:PROVIDER|a,b> <value|default> [--json]"
|
|
39
39
|
CONFIG_USAGE = "Usage: cdx config <name> [--json]"
|
|
40
40
|
CONFIGS_USAGE = "Usage: cdx configs [--json]"
|
|
41
41
|
HISTORY_USAGE = "Usage: cdx history [name] [--limit N] [--summary] [--since 7d|today|DATE] [--from DATE] [--to DATE] [--json]"
|
|
42
|
-
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")
|
|
43
44
|
LAST_USAGE = "Usage: cdx last [--json]"
|
|
44
|
-
RESUME_USAGE = "Usage: cdx resume <name> [--json]"
|
|
45
|
-
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--json]"
|
|
45
|
+
RESUME_USAGE = "Usage: cdx resume <name> [--conversation <id>] [--json]"
|
|
46
|
+
CAN_RESUME_USAGE = "Usage: cdx can-resume <name> [--conversation <id>] [--json]"
|
|
47
|
+
SESSIONS_USAGE = "Usage: cdx sessions [--all] --json"
|
|
46
48
|
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"
|
|
47
49
|
NEXT_USAGE = "Usage: cdx next [--json] [--refresh]"
|
|
48
|
-
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"
|
|
50
|
+
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"
|
|
49
51
|
RUN_JSON_REQUIRED = "cdx run: --json is required."
|
|
50
52
|
RUN_TARGET_REQUIRED = "cdx run: specify a session name or --provider PROVIDER."
|
|
51
53
|
RUN_SESSION_PROVIDER_CONFLICT = "cdx run: cannot specify both a session name and --provider."
|
|
52
54
|
RUN_CWD_REQUIRED = "cdx run: --cwd PATH is required."
|
|
53
55
|
RUN_PROMPT_SOURCE_REQUIRED = "cdx run: specify exactly one prompt source: --prompt TEXT or --prompt-file PATH."
|
|
54
56
|
RUN_DETACH_FAILOVER_CONFLICT = "cdx run: --failover needs to watch the run, which --detach gives up."
|
|
55
|
-
RUN_KIND_VALUES = ("assistant", "code-review")
|
|
57
|
+
RUN_KIND_VALUES = ("assistant", "code-review", "retro")
|
|
56
58
|
# Aliases of the shared definitions in config, kept only so the existing
|
|
57
59
|
# RUN_* spellings in this module keep reading naturally. They are references,
|
|
58
60
|
# not copies: config.py is the single owner.
|
|
@@ -60,7 +62,7 @@ RUN_PERMISSION_VALUES = PERMISSION_INPUT_VALUES
|
|
|
60
62
|
RUN_PERMISSION_CANONICAL_VALUES = PERMISSION_VALUES
|
|
61
63
|
RUN_PERMISSION_ALIASES = PERMISSION_ALIASES
|
|
62
64
|
RUN_EFFORT_VALUES = REASONING_EFFORT_VALUES
|
|
63
|
-
RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] --json"
|
|
65
|
+
RUNS_USAGE = "Usage: cdx runs [--limit N] [--since 7d|today|DATE] [--running] --json"
|
|
64
66
|
RUN_STATUS_USAGE = "Usage: cdx run-status <run_id> --json"
|
|
65
67
|
RUN_REPORT_USAGE = "Usage: cdx run-report <run_id> --json"
|
|
66
68
|
RUN_TAIL_USAGE = "Usage: cdx run-tail <run_id> [--lines N] --json"
|
|
@@ -79,6 +81,28 @@ ARG_CODE_OUT_OF_RANGE = "argument_value_out_of_range"
|
|
|
79
81
|
ARG_CODE_UNKNOWN = "unknown_argument"
|
|
80
82
|
|
|
81
83
|
|
|
84
|
+
def _pop_value_flag(args, flag, usage):
|
|
85
|
+
"""Remove `flag VALUE` / `flag=VALUE` from args; return (value or None, rest)."""
|
|
86
|
+
value, rest, index = None, [], 0
|
|
87
|
+
while index < len(args):
|
|
88
|
+
arg = args[index]
|
|
89
|
+
name, sep, inline = arg.partition("=")
|
|
90
|
+
if name != flag:
|
|
91
|
+
rest.append(arg)
|
|
92
|
+
index += 1
|
|
93
|
+
continue
|
|
94
|
+
if sep:
|
|
95
|
+
candidate, index = inline, index + 1
|
|
96
|
+
elif index + 1 < len(args):
|
|
97
|
+
candidate, index = args[index + 1], index + 2
|
|
98
|
+
else:
|
|
99
|
+
raise CdxError(usage)
|
|
100
|
+
if value is not None or not candidate:
|
|
101
|
+
raise CdxError(usage)
|
|
102
|
+
value = candidate
|
|
103
|
+
return value, rest
|
|
104
|
+
|
|
105
|
+
|
|
82
106
|
def _parse_json_flag(args):
|
|
83
107
|
json_flag = "--json" in args
|
|
84
108
|
cleaned = [arg for arg in args if arg != "--json"]
|
|
@@ -253,11 +277,14 @@ def _parse_set_args(args):
|
|
|
253
277
|
"--notify": {"key": "notify", "type": "str", "default": None, "transform": _on_off("notify")},
|
|
254
278
|
"--notify-preview": {"key": "notify_preview", "type": "str", "default": None, "transform": _on_off("notify preview")},
|
|
255
279
|
"--scrub-env": {"key": "scrub_env", "type": "str", "default": None, "transform": _on_off("scrub env")},
|
|
280
|
+
"--shared-memory": {"key": "shared_memory", "type": "str", "default": None, "transform": _on_off("shared memory")},
|
|
281
|
+
"--retro": {"key": "retro", "type": "str", "default": None, "transform": _on_off("retro")},
|
|
256
282
|
"--model": {"key": "model", "type": "str", "default": None},
|
|
257
283
|
"--fallback-model": {"key": "fallback_model", "type": "str", "default": None},
|
|
258
284
|
"--budget": {"key": "budget", "type": "str", "default": None, "transform": _parse_budget_value},
|
|
259
285
|
"--extra-args": {"key": "extra_args", "type": "str", "default": None},
|
|
260
286
|
"--priority": {"key": "priority", "type": "str", "default": None, "transform": _parse_priority_value},
|
|
287
|
+
"--policy": {"key": "policy", "type": "str", "default": None},
|
|
261
288
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
262
289
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, SET_USAGE)},
|
|
263
290
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
@@ -274,7 +301,7 @@ def _parse_set_args(args):
|
|
|
274
301
|
raise CdxError(SET_USAGE)
|
|
275
302
|
settings = {
|
|
276
303
|
key: parsed[key]
|
|
277
|
-
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "model", "fallback_model", "budget", "extra_args", "priority")
|
|
304
|
+
for key in ("power", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro", "model", "fallback_model", "budget", "extra_args", "priority", "policy")
|
|
278
305
|
if parsed[key] is not None
|
|
279
306
|
}
|
|
280
307
|
if not settings:
|
|
@@ -299,11 +326,14 @@ def _parse_unset_args(args):
|
|
|
299
326
|
"--notify": {"key": "notify", "type": "bool", "default": False},
|
|
300
327
|
"--notify-preview": {"key": "notify_preview", "type": "bool", "default": False},
|
|
301
328
|
"--scrub-env": {"key": "scrub_env", "type": "bool", "default": False},
|
|
329
|
+
"--shared-memory": {"key": "shared_memory", "type": "bool", "default": False},
|
|
330
|
+
"--retro": {"key": "retro", "type": "bool", "default": False},
|
|
302
331
|
"--model": {"key": "model", "type": "bool", "default": False},
|
|
303
332
|
"--fallback-model": {"key": "fallback_model", "type": "bool", "default": False},
|
|
304
333
|
"--budget": {"key": "budget", "type": "bool", "default": False},
|
|
305
334
|
"--extra-args": {"key": "extra_args", "type": "bool", "default": False},
|
|
306
335
|
"--priority": {"key": "priority", "type": "bool", "default": False},
|
|
336
|
+
"--policy": {"key": "policy", "type": "bool", "default": False},
|
|
307
337
|
"--all": {"key": "all", "type": "bool", "default": False},
|
|
308
338
|
"--sessions": {"key": "sessions", "type": "str", "default": None, "transform": _parse_set_unset_sessions},
|
|
309
339
|
"--provider": {"key": "provider", "type": "str", "default": None, "transform": lambda value: _parse_provider_filter(value, UNSET_USAGE)},
|
|
@@ -320,8 +350,8 @@ def _parse_unset_args(args):
|
|
|
320
350
|
if not parsed["names"] and not parsed["sessions"] and not parsed["provider"]:
|
|
321
351
|
raise CdxError(UNSET_USAGE)
|
|
322
352
|
_UNSET_KEYS = (
|
|
323
|
-
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env",
|
|
324
|
-
"model", "fallback_model", "budget", "extra_args", "priority",
|
|
353
|
+
"power", "reasoning_effort", "permission", "fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro",
|
|
354
|
+
"model", "fallback_model", "budget", "extra_args", "priority", "policy",
|
|
325
355
|
)
|
|
326
356
|
keys = list(_UNSET_KEYS) if parsed["all"] else [key for key in _UNSET_KEYS if parsed[key]]
|
|
327
357
|
if not keys:
|
|
@@ -513,6 +543,9 @@ def cdx_schema():
|
|
|
513
543
|
"provider_failed",
|
|
514
544
|
"provider_timeout",
|
|
515
545
|
"run_cancelled",
|
|
546
|
+
"too_many_runs",
|
|
547
|
+
"failover_exhausted",
|
|
548
|
+
"failover_limit_reached",
|
|
516
549
|
"invalid_request",
|
|
517
550
|
"cdx_error",
|
|
518
551
|
],
|
|
@@ -611,6 +644,17 @@ def _parse_run_effort(reasoning_effort, power):
|
|
|
611
644
|
) from error
|
|
612
645
|
|
|
613
646
|
|
|
647
|
+
def _parse_max_parallel(value):
|
|
648
|
+
try:
|
|
649
|
+
parsed = int(value)
|
|
650
|
+
except (TypeError, ValueError):
|
|
651
|
+
parsed = 0
|
|
652
|
+
if parsed < 1:
|
|
653
|
+
raise CdxArgumentError("cdx run: --max-parallel takes a whole number of at least 1.",
|
|
654
|
+
code=ARG_CODE_INVALID_VALUE, arguments=["--max-parallel"])
|
|
655
|
+
return parsed
|
|
656
|
+
|
|
657
|
+
|
|
614
658
|
def _parse_run_args(args):
|
|
615
659
|
parsed = _parse_run_flag_args(args, {
|
|
616
660
|
"--cwd": {"key": "cwd", "type": "str", "default": None},
|
|
@@ -627,6 +671,8 @@ def _parse_run_args(args):
|
|
|
627
671
|
"--detach": {"key": "detach", "type": "bool", "default": False},
|
|
628
672
|
"--failover": {"key": "failover", "type": "bool", "default": False},
|
|
629
673
|
"--refresh": {"key": "refresh", "type": "bool", "default": False},
|
|
674
|
+
"--label": {"key": "label", "type": "str", "default": None},
|
|
675
|
+
"--max-parallel": {"key": "max_parallel", "type": "str", "default": None, "transform": _parse_max_parallel},
|
|
630
676
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
631
677
|
}, RUN_USAGE, positionals_key="names", max_positionals=1)
|
|
632
678
|
if not parsed["json"]:
|
|
@@ -681,6 +727,8 @@ def _parse_run_args(args):
|
|
|
681
727
|
"timeout_seconds": parsed["timeout_seconds"],
|
|
682
728
|
"detach": parsed["detach"],
|
|
683
729
|
"failover": parsed["failover"],
|
|
730
|
+
"label": parsed["label"],
|
|
731
|
+
"max_parallel": parsed["max_parallel"],
|
|
684
732
|
"refresh": parsed["refresh"],
|
|
685
733
|
"reasoning_effort": effort.get("reasoning_effort"),
|
|
686
734
|
"power": effort.get("power"),
|
|
@@ -845,12 +893,16 @@ def _parse_stats_args(args, now=None):
|
|
|
845
893
|
"--since": {"key": "since", "type": "str", "default": None},
|
|
846
894
|
"--from": {"key": "from", "type": "str", "default": None},
|
|
847
895
|
"--to": {"key": "to", "type": "str", "default": None},
|
|
896
|
+
"--by": {"key": "by", "type": "str", "default": "session"},
|
|
848
897
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
849
898
|
}, STATS_USAGE, positionals_key="names", max_positionals=1)
|
|
899
|
+
if parsed["by"] not in STATS_GROUPS:
|
|
900
|
+
raise CdxError(STATS_USAGE)
|
|
850
901
|
period = _parse_history_period(parsed, now, STATS_USAGE)
|
|
851
902
|
return {
|
|
852
903
|
"name": parsed["names"][0] if parsed["names"] else None,
|
|
853
904
|
"period": period,
|
|
905
|
+
"by": parsed["by"],
|
|
854
906
|
"json": parsed["json"],
|
|
855
907
|
}
|
|
856
908
|
|
|
@@ -1004,8 +1056,13 @@ def _parse_runs_args(rest, now=None):
|
|
|
1004
1056
|
parsed = _parse_flag_args(rest, {
|
|
1005
1057
|
"--limit": {"key": "limit", "type": "str", "default": 20, "transform": _parse_positive_int},
|
|
1006
1058
|
"--since": {"key": "since", "type": "str", "default": None},
|
|
1059
|
+
"--running": {"key": "running", "type": "bool", "default": False},
|
|
1007
1060
|
"--json": {"key": "json", "type": "bool", "default": False},
|
|
1008
1061
|
}, RUNS_USAGE, max_positionals=0)
|
|
1062
|
+
if parsed["running"] and parsed["since"] is not None:
|
|
1063
|
+
# --since returns completed runs only, --running the ones not completed:
|
|
1064
|
+
# together they can only ever answer "nothing".
|
|
1065
|
+
raise CdxError("cdx runs: --running and --since are mutually exclusive.")
|
|
1009
1066
|
since = None
|
|
1010
1067
|
if parsed["since"] is not None:
|
|
1011
1068
|
# Same cursor forms as `cdx history --since`, parsed by the same code:
|
|
@@ -1019,6 +1076,7 @@ def _parse_runs_args(rest, now=None):
|
|
|
1019
1076
|
# a watchdog behind by more than `limit` completions would never see
|
|
1020
1077
|
# the older ones at all.
|
|
1021
1078
|
"limit_given": any(arg == "--limit" or arg.startswith("--limit=") for arg in rest),
|
|
1079
|
+
"running": parsed["running"],
|
|
1022
1080
|
"json": parsed["json"],
|
|
1023
1081
|
}
|
|
1024
1082
|
|
package/src/cli_commands.py
CHANGED
|
@@ -265,6 +265,9 @@ from .commands.status import (
|
|
|
265
265
|
from .commands.status import (
|
|
266
266
|
handle_next as handle_next,
|
|
267
267
|
)
|
|
268
|
+
from .commands.status import (
|
|
269
|
+
handle_sessions as handle_sessions,
|
|
270
|
+
)
|
|
268
271
|
from .commands.status import (
|
|
269
272
|
handle_stats as handle_stats,
|
|
270
273
|
)
|
package/src/cli_helpers.py
CHANGED
|
@@ -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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
@@ -304,11 +314,14 @@ def _format_launch_config(session, use_color=False):
|
|
|
304
314
|
("notify", "Agent alerts"),
|
|
305
315
|
("notify_preview", "Response preview"),
|
|
306
316
|
("scrub_env", "Scrub agent env"),
|
|
317
|
+
("shared_memory", "Shared memory"),
|
|
318
|
+
("retro", "Session retro"),
|
|
307
319
|
("model", "Model"),
|
|
308
320
|
("fallback_model", "Fallback"),
|
|
309
321
|
("budget", "Budget"),
|
|
310
322
|
("extra_args", "Extra args"),
|
|
311
323
|
("priority", "Priority"),
|
|
324
|
+
("policy", "Policy"),
|
|
312
325
|
]:
|
|
313
326
|
rows.append([
|
|
314
327
|
_dim(label, use_color),
|
|
@@ -330,14 +343,14 @@ def _format_launch_settings_hint(name="<name>"):
|
|
|
330
343
|
)
|
|
331
344
|
|
|
332
345
|
def _format_launch_setting_value(launch, key, use_color=False):
|
|
333
|
-
if key in ("fast", "rtk", "logics", "notify", "notify_preview", "scrub_env"):
|
|
346
|
+
if key in ("fast", "rtk", "logics", "notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
334
347
|
if launch.get(key) is True:
|
|
335
348
|
return _style("on", "32", use_color)
|
|
336
349
|
if launch.get(key) is False:
|
|
337
350
|
return _style("off", "2", use_color)
|
|
338
351
|
if key == "logics":
|
|
339
352
|
return _dim("auto", use_color)
|
|
340
|
-
if key in ("notify", "notify_preview", "scrub_env"):
|
|
353
|
+
if key in ("notify", "notify_preview", "scrub_env", "shared_memory", "retro"):
|
|
341
354
|
return _style("off", "2", use_color)
|
|
342
355
|
return _dim("default", use_color)
|
|
343
356
|
value = launch.get(key)
|
package/src/commands/launch.py
CHANGED
|
@@ -6,8 +6,9 @@ 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
|
-
from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag
|
|
11
|
+
from ..cli_args import CAN_RESUME_USAGE, HANDOFF_USAGE, RESUME_USAGE, _parse_json_flag, _pop_value_flag
|
|
11
12
|
from ..cli_helpers import (
|
|
12
13
|
API_SCHEMA_VERSION,
|
|
13
14
|
_json_failure,
|
|
@@ -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
|
|
@@ -29,16 +30,29 @@ from ..provider_runtime import (
|
|
|
29
30
|
_ensure_session_authentication,
|
|
30
31
|
_get_auth_home,
|
|
31
32
|
_run_interactive_provider_command,
|
|
33
|
+
with_requested_conversation,
|
|
32
34
|
)
|
|
33
35
|
from ..run_command import PROMPT_STDIN, read_run_prompt
|
|
34
36
|
from ..run_usage import estimate_cost
|
|
35
37
|
|
|
36
|
-
LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH] [--resume] [--json]"
|
|
38
|
+
LAUNCH_USAGE = "Usage: cdx <name> [--dir PATH] [--prompt TEXT|--prompt-file PATH|--task TEXT [--end-state TEXT]] [--resume] [--json]"
|
|
39
|
+
|
|
40
|
+
#: The first message of a session started with --task. The end state is what the
|
|
41
|
+
#: user's go-ahead covers: a bare "show me first" made sessions stop after a commit
|
|
42
|
+
#: and wait for a push they could have made.
|
|
43
|
+
TASK_PROMPT = """Task: {task}
|
|
44
|
+
|
|
45
|
+
{end_state}
|
|
46
|
+
|
|
47
|
+
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."""
|
|
48
|
+
END_STATE = """End state: {end_state}
|
|
49
|
+
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."""
|
|
50
|
+
NO_END_STATE = """No end state was given: stop and show the result before any commit, push or pull request."""
|
|
37
51
|
|
|
38
52
|
|
|
39
53
|
def _parse_launch_args(args):
|
|
40
54
|
"""Return the per-launch directory, initial prompt source and ordinary launch flags."""
|
|
41
|
-
values = {"--dir": None, "--prompt": None, "--prompt-file": None}
|
|
55
|
+
values = {"--dir": None, "--prompt": None, "--prompt-file": None, "--task": None, "--end-state": None}
|
|
42
56
|
cleaned = []
|
|
43
57
|
index = 0
|
|
44
58
|
while index < len(args):
|
|
@@ -59,6 +73,13 @@ def _parse_launch_args(args):
|
|
|
59
73
|
values[flag] = value
|
|
60
74
|
if values["--prompt"] is not None and values["--prompt-file"] is not None:
|
|
61
75
|
raise CdxError("cdx <name>: --prompt and --prompt-file are mutually exclusive.")
|
|
76
|
+
if values["--end-state"] is not None and values["--task"] is None:
|
|
77
|
+
raise CdxError("cdx <name>: --end-state goes with --task.")
|
|
78
|
+
if values["--task"] is not None:
|
|
79
|
+
if values["--prompt"] is not None or values["--prompt-file"] is not None:
|
|
80
|
+
raise CdxError("cdx <name>: --task replaces --prompt and --prompt-file.")
|
|
81
|
+
end = END_STATE.format(end_state=values["--end-state"]) if values["--end-state"] else NO_END_STATE
|
|
82
|
+
values["--prompt"] = TASK_PROMPT.format(task=values["--task"], end_state=end)
|
|
62
83
|
if values["--prompt-file"] == PROMPT_STDIN:
|
|
63
84
|
raise CdxError("cdx <name>: --prompt-file - is not supported; the interactive session needs standard input.")
|
|
64
85
|
prompt = {"prompt": values["--prompt"], "prompt_file": values["--prompt-file"]}
|
|
@@ -140,12 +161,14 @@ def _format_resume_capability(capability, use_color=False):
|
|
|
140
161
|
)
|
|
141
162
|
|
|
142
163
|
def handle_can_resume(rest, ctx):
|
|
164
|
+
conversation_id, rest = _pop_value_flag(rest, "--conversation", CAN_RESUME_USAGE)
|
|
143
165
|
json_flag, args = _parse_json_flag(rest)
|
|
144
166
|
if len(args) != 1:
|
|
145
167
|
raise CdxError(CAN_RESUME_USAGE)
|
|
146
168
|
session = ctx["service"]["get_session"](args[0])
|
|
147
169
|
if not session:
|
|
148
170
|
raise CdxError(f"Unknown session: {args[0]}")
|
|
171
|
+
requested = with_requested_conversation(session, conversation_id) if conversation_id else session
|
|
149
172
|
if session.get("enabled", True) is False:
|
|
150
173
|
capability = {
|
|
151
174
|
"session": session["name"],
|
|
@@ -155,8 +178,19 @@ def handle_can_resume(rest, ctx):
|
|
|
155
178
|
"reason": "session_disabled",
|
|
156
179
|
"command_preview": [],
|
|
157
180
|
}
|
|
181
|
+
elif requested is None:
|
|
182
|
+
capability = {
|
|
183
|
+
"session": session["name"],
|
|
184
|
+
"provider": session["provider"],
|
|
185
|
+
"resumable": False,
|
|
186
|
+
"strategy": "provider_conversation_id",
|
|
187
|
+
"identity": conversation_id,
|
|
188
|
+
"provenance": "requested",
|
|
189
|
+
"reason": "conversation_not_found",
|
|
190
|
+
"command_preview": [],
|
|
191
|
+
}
|
|
158
192
|
else:
|
|
159
|
-
capability = _resume_capability_for_session(
|
|
193
|
+
capability = _resume_capability_for_session(requested, ctx)
|
|
160
194
|
if json_flag:
|
|
161
195
|
_write_json(ctx, {
|
|
162
196
|
"schema_version": API_SCHEMA_VERSION,
|
|
@@ -168,13 +202,15 @@ def handle_can_resume(rest, ctx):
|
|
|
168
202
|
return 0
|
|
169
203
|
|
|
170
204
|
def handle_resume(rest, ctx):
|
|
205
|
+
conversation_id, rest = _pop_value_flag(rest, "--conversation", RESUME_USAGE)
|
|
171
206
|
directory, rest, prompt = _parse_launch_args(rest)
|
|
172
207
|
if prompt["prompt"] is not None or prompt["prompt_file"] is not None:
|
|
173
208
|
raise CdxError("cdx resume: --prompt and --prompt-file are not supported when resuming.")
|
|
174
209
|
json_flag, args = _parse_json_flag(rest)
|
|
175
210
|
if len(args) != 1:
|
|
176
211
|
raise CdxError(RESUME_USAGE)
|
|
177
|
-
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory
|
|
212
|
+
return handle_launch(args[0], ctx, resume=True, force_json=json_flag, directory=directory,
|
|
213
|
+
conversation_id=conversation_id)
|
|
178
214
|
|
|
179
215
|
|
|
180
216
|
def _notification_provisioning_enabled(session, ctx, json_flag):
|
|
@@ -202,7 +238,23 @@ def _notification_provisioning_enabled(session, ctx, json_flag):
|
|
|
202
238
|
return True
|
|
203
239
|
|
|
204
240
|
|
|
205
|
-
def
|
|
241
|
+
def _link_shared_memory(session, ctx, cwd, json_flag):
|
|
242
|
+
if session["provider"] != PROVIDER_CLAUDE or (session.get("launch") or {}).get("shared_memory") is not True:
|
|
243
|
+
return
|
|
244
|
+
try:
|
|
245
|
+
status, detail = shared_memory.link(_get_auth_home(session), ctx["service"]["base_dir"], cwd)
|
|
246
|
+
except OSError as error:
|
|
247
|
+
status, detail = "unsupported", str(error)
|
|
248
|
+
if json_flag or status == "linked":
|
|
249
|
+
return
|
|
250
|
+
if status == "conflict":
|
|
251
|
+
detail = f"merge these files by hand first: {', '.join(detail)}"
|
|
252
|
+
message = f"Shared memory not linked for {session['name']}"
|
|
253
|
+
ctx["out"](f"{_warn(message, ctx['use_color'])} {detail}\n")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=None, directory=None,
|
|
257
|
+
conversation_id=None):
|
|
206
258
|
json_flag = "--json" in ctx.get("raw_args", ctx["options"].get("raw_args", []))
|
|
207
259
|
if force_json is not None:
|
|
208
260
|
json_flag = force_json
|
|
@@ -215,6 +267,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
215
267
|
raise CdxError(f"Unknown session: {command}")
|
|
216
268
|
if session.get("enabled", True) is False:
|
|
217
269
|
raise CdxError(f"Session is disabled: {command}")
|
|
270
|
+
if conversation_id and with_requested_conversation(session, conversation_id) is None:
|
|
271
|
+
raise CdxError(f"Conversation {conversation_id} not found in session {command}.", code="conversation_not_found")
|
|
218
272
|
capability = _resume_capability_for_session(session, ctx) if resume else None
|
|
219
273
|
if capability and not capability["resumable"]:
|
|
220
274
|
raise CdxError(
|
|
@@ -222,6 +276,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
222
276
|
)
|
|
223
277
|
cwd = _launch_directory(session, ctx, directory, json_flag)
|
|
224
278
|
session = ctx["service"]["launch_session"](command, resume=resume)
|
|
279
|
+
if conversation_id:
|
|
280
|
+
session = with_requested_conversation(session, conversation_id)
|
|
225
281
|
capability = _resume_capability_for_session(session, ctx) if resume else None
|
|
226
282
|
if not json_flag and not ctx["stdin_is_tty"]:
|
|
227
283
|
ctx["out"](f"Using directory: {cwd}\n")
|
|
@@ -262,6 +318,7 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
262
318
|
# a real turn — so it gets no instruction it does not need.
|
|
263
319
|
notice += " — approve them once in Codex to enable"
|
|
264
320
|
ctx["out"](f"{_info(notice, ctx['use_color'])}\n")
|
|
321
|
+
_link_shared_memory(session, ctx, cwd, json_flag)
|
|
265
322
|
runtime_run_id = None
|
|
266
323
|
|
|
267
324
|
def runtime_lifecycle(event, info):
|
|
@@ -335,6 +392,8 @@ def handle_launch(command, ctx, initial_prompt=None, resume=False, force_json=No
|
|
|
335
392
|
})
|
|
336
393
|
if not json_flag:
|
|
337
394
|
ctx["out"](f"{_goodbye_line(session, success_run_info, ctx['use_color'])}\n")
|
|
395
|
+
if retro.start(session, cwd, success_run_info.get("provider_transcript_path"), env=ctx.get("env")) and not json_flag:
|
|
396
|
+
ctx["out"](f"{_dim('Session retro started; read it later with `cdx retro`', ctx['use_color'])}\n")
|
|
338
397
|
if json_flag:
|
|
339
398
|
extra = {"session": ctx["service"]["get_session"](session["name"]), "cwd": cwd}
|
|
340
399
|
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)
|