ccx-native 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +807 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +269 -0
- package/ccx/claude/host/session.py +5244 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1558 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2424 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/history_store.py +131 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1454 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/models.py +225 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +296 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from ccx import constants
|
|
8
|
+
from ccx.constants import (
|
|
9
|
+
EXIT_ERROR,
|
|
10
|
+
EXIT_UNAVAILABLE,
|
|
11
|
+
USAGE_STALE_LIMIT_S,
|
|
12
|
+
)
|
|
13
|
+
from ccx.claude.external import import_eligibility
|
|
14
|
+
from ccx.errors import (
|
|
15
|
+
CcxError,
|
|
16
|
+
EngineCliNotFound,
|
|
17
|
+
InvalidRequest,
|
|
18
|
+
SessionUnavailable,
|
|
19
|
+
SessionWaiting,
|
|
20
|
+
)
|
|
21
|
+
from ccx.ops.semantics import (
|
|
22
|
+
payload_guidance as _payload_guidance, # noqa: F401
|
|
23
|
+
pending_input_kind,
|
|
24
|
+
)
|
|
25
|
+
from ccx.claude.usage import (
|
|
26
|
+
_caps_from_args,
|
|
27
|
+
cap_tripped,
|
|
28
|
+
fetch_usage,
|
|
29
|
+
unknown_cap_metrics,
|
|
30
|
+
)
|
|
31
|
+
from ccx.cli.commands._common import _prepare_parent_notification, _resolve_prompt_source, emit_waiting
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _initial_caps(args, prompt: str | None, *, engine: str = "claude") -> dict | None:
|
|
35
|
+
"""Per-turn caps for a create's first prompt, refused if already tripped."""
|
|
36
|
+
from ccx.errors import BudgetStopped
|
|
37
|
+
caps = _caps_from_args(args)
|
|
38
|
+
if caps:
|
|
39
|
+
from ccx.workers import _check_caps
|
|
40
|
+
_check_caps(caps, engine=engine)
|
|
41
|
+
if not prompt:
|
|
42
|
+
raise InvalidRequest("initial cap requires a prompt")
|
|
43
|
+
if engine == "codex":
|
|
44
|
+
# codexd checks the fresh cached window immediately before turn/start.
|
|
45
|
+
return caps
|
|
46
|
+
usage = fetch_usage(stale_limit=USAGE_STALE_LIMIT_S)
|
|
47
|
+
missing = unknown_cap_metrics(usage, caps)
|
|
48
|
+
if missing:
|
|
49
|
+
sys.stderr.write(f"[ccx] usage cap preflight skipped unknown/stale metrics: {', '.join(missing)}\n")
|
|
50
|
+
tripped = cap_tripped(usage, caps)
|
|
51
|
+
if tripped:
|
|
52
|
+
key, current, cap = tripped
|
|
53
|
+
raise BudgetStopped(
|
|
54
|
+
f"usage cap {key} already exceeded ({current:.0f}% >= {cap:g}%); no worker created",
|
|
55
|
+
reason=f"{key} {current:.0f}% >= {cap:g}%", phase="preflight",
|
|
56
|
+
)
|
|
57
|
+
return caps
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _emit_create_partial(op: str, error, args) -> None:
|
|
61
|
+
if args.json:
|
|
62
|
+
print(json.dumps({
|
|
63
|
+
"worker": error.worker, "worker_id": error.worker_id,
|
|
64
|
+
"short": error.short, "phase": error.phase,
|
|
65
|
+
"delivery_id": error.delivery_id,
|
|
66
|
+
"delivery_status": error.delivery_status,
|
|
67
|
+
"partial": True, "error": str(error),
|
|
68
|
+
**({"waiting_for": error.cause.waiting_for,
|
|
69
|
+
"request_id": (error.cause.waiting_for or {}).get("id"),
|
|
70
|
+
"pending_kind": pending_input_kind(error.cause.waiting_for)}
|
|
71
|
+
if isinstance(error.cause, SessionWaiting) else {}),
|
|
72
|
+
}, indent=2))
|
|
73
|
+
elif isinstance(error.cause, SessionWaiting):
|
|
74
|
+
# The first turn is parked on a gate: say which and how to clear it.
|
|
75
|
+
emit_waiting(error.worker_id, header=f"Short: {error.short} worker_id={error.worker_id}")
|
|
76
|
+
else:
|
|
77
|
+
print(f"Short: {error.short} worker_id={error.worker_id}")
|
|
78
|
+
cause = f": {error.cause}" if error.cause is not None and str(error.cause) else ""
|
|
79
|
+
sys.stderr.write(
|
|
80
|
+
f"[ccx] {op}: {error} ({error.phase}{cause}); existing worker={error.worker}; "
|
|
81
|
+
f"inspect it with `ccx peek {error.short}`\n")
|
|
82
|
+
raise SystemExit(error.exit_code) from error
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def cmd_new(args):
|
|
86
|
+
return _prepare_full_uuid_new(args, engine=args.engine)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_resume(args):
|
|
90
|
+
"""Restore the one original native worker without sending a new turn."""
|
|
91
|
+
from ccx.workers import WorkerClient
|
|
92
|
+
try:
|
|
93
|
+
result = WorkerClient().resume(args.target)
|
|
94
|
+
except CcxError as error:
|
|
95
|
+
print(f"ccx resume: {error}", file=sys.stderr)
|
|
96
|
+
raise SystemExit(error.exit_code) from error
|
|
97
|
+
print(json.dumps(result, ensure_ascii=False, sort_keys=True)
|
|
98
|
+
if args.json else result["worker"])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_fork(args):
|
|
102
|
+
"""Original full-UUID fork and optional first turn through creation."""
|
|
103
|
+
from math import isfinite
|
|
104
|
+
from ccx.errors import CreatePartialFailure, OperationConflict
|
|
105
|
+
from ccx.ops.forking import fork
|
|
106
|
+
from ccx.store.root import GenerationError
|
|
107
|
+
from ccx.store.identity import IdentityError, UnknownWorker
|
|
108
|
+
|
|
109
|
+
parent = _prepare_parent_notification(args)
|
|
110
|
+
prompt = _resolve_prompt_source(args, "prompt", "fork", required=False)
|
|
111
|
+
if args.timeout is not None and not (isfinite(args.timeout) and args.timeout > 0):
|
|
112
|
+
print("ccx fork: --timeout must be positive finite seconds; "
|
|
113
|
+
"no child identity claimed", file=sys.stderr)
|
|
114
|
+
raise SystemExit(1)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
from ccx.controllers import live_vars
|
|
118
|
+
if args.var is not None and args.vars_from is not None:
|
|
119
|
+
raise InvalidRequest("--var and --vars-from are mutually exclusive")
|
|
120
|
+
attachment = None
|
|
121
|
+
if args.vars_from is not None:
|
|
122
|
+
from ccx.store.discovery import resolve_worker
|
|
123
|
+
source_engine, source_id = resolve_worker(args.vars_from).split(":", 1)
|
|
124
|
+
attachment = live_vars.prepare_shared_durable_attachment(source_engine, source_id)
|
|
125
|
+
elif args.var is not None:
|
|
126
|
+
attachment = live_vars.prepare_durable_attachment(
|
|
127
|
+
live_vars.parse_cli_vars(args.var)
|
|
128
|
+
)
|
|
129
|
+
caps = _initial_caps(args, prompt, engine=(
|
|
130
|
+
"codex" if args.target.startswith("codex:") else "claude"))
|
|
131
|
+
deadline = time.monotonic() + args.timeout if args.timeout is not None else None
|
|
132
|
+
result = fork(
|
|
133
|
+
args.target, worker_id=args.worker_id, name=args.name,
|
|
134
|
+
model=args.model, effort=args.effort, created_via="cli",
|
|
135
|
+
prompt=prompt, caps=caps, wait=args.wait,
|
|
136
|
+
attachment=attachment, delivery_id=args.delivery_id,
|
|
137
|
+
parent=parent, settle=bool(args.settle), deadline=deadline,
|
|
138
|
+
)
|
|
139
|
+
except CreatePartialFailure as error:
|
|
140
|
+
output = {
|
|
141
|
+
"ok": False, "code": "partial_failure", "phase": error.phase,
|
|
142
|
+
"worker": error.worker, "worker_id": error.worker_id,
|
|
143
|
+
"engine": error.engine, "short": error.short,
|
|
144
|
+
"created": error.info.get("created"),
|
|
145
|
+
"reconciled": error.info.get("reconciled"),
|
|
146
|
+
"native_ready": error.info.get("native_ready") is True,
|
|
147
|
+
"activation_state": error.info.get("activation_state"),
|
|
148
|
+
"native_start_error": error.info.get("native_start_error"),
|
|
149
|
+
"cause_type": type(error.cause).__name__ if error.cause else None,
|
|
150
|
+
"integrity_error": bool(error.info.get("integrity_error")),
|
|
151
|
+
"message": str(error),
|
|
152
|
+
"delivery_id": error.delivery_id,
|
|
153
|
+
"delivery_status": error.delivery_status,
|
|
154
|
+
**({"waiting_for": error.cause.waiting_for,
|
|
155
|
+
"request_id": (error.cause.waiting_for or {}).get("id"),
|
|
156
|
+
"pending_kind": pending_input_kind(error.cause.waiting_for)}
|
|
157
|
+
if isinstance(error.cause, SessionWaiting) else {}),
|
|
158
|
+
}
|
|
159
|
+
refusal = error.info.get("native_rejection")
|
|
160
|
+
if isinstance(refusal, dict):
|
|
161
|
+
output.update(
|
|
162
|
+
code="native_rejected", activation_state="rejected",
|
|
163
|
+
message=refusal["message"], native_start_error=refusal["message"],
|
|
164
|
+
native_rejection=dict(refusal),
|
|
165
|
+
)
|
|
166
|
+
local = error.info.get("native_local_refusal")
|
|
167
|
+
if isinstance(local, dict):
|
|
168
|
+
if local.get("proven") is True and output["activation_state"] == "local-refused":
|
|
169
|
+
output.update(
|
|
170
|
+
code="local_refused", message=local["reason"],
|
|
171
|
+
native_start_error=local["reason"],
|
|
172
|
+
)
|
|
173
|
+
output["native_local_refusal"] = dict(local)
|
|
174
|
+
if error.info.get("initial_delivery_conflict") is True:
|
|
175
|
+
output["code"] = "conflict"
|
|
176
|
+
error.exit_code = OperationConflict.exit_code
|
|
177
|
+
elif error.info.get("initial_delivery_rejected") is True:
|
|
178
|
+
output["code"] = "delivery_rejected"
|
|
179
|
+
if args.json:
|
|
180
|
+
print(json.dumps(output, sort_keys=True, ensure_ascii=False))
|
|
181
|
+
else:
|
|
182
|
+
cause = f": {error.cause}" if error.cause is not None and str(error.cause) else ""
|
|
183
|
+
print(
|
|
184
|
+
f"ccx fork: {error.worker} identity claimed; phase {error.phase}{cause} "
|
|
185
|
+
f"remains unavailable. Retain exact --worker-id {error.worker_id}, "
|
|
186
|
+
f"retry with the same full source {args.target}; never remint",
|
|
187
|
+
file=sys.stderr,
|
|
188
|
+
)
|
|
189
|
+
raise SystemExit(error.exit_code) from error
|
|
190
|
+
except CcxError as error:
|
|
191
|
+
if args.json and isinstance(error, EngineCliNotFound):
|
|
192
|
+
print(json.dumps({"ok": False, "code": "runtime_unavailable",
|
|
193
|
+
"message": str(error)}, sort_keys=True))
|
|
194
|
+
else:
|
|
195
|
+
print(f"ccx fork rejected: {error}", file=sys.stderr)
|
|
196
|
+
raise SystemExit(error.exit_code) from error
|
|
197
|
+
except IdentityError as error:
|
|
198
|
+
print(f"ccx fork: invalid or unowned full UUID: {error}", file=sys.stderr)
|
|
199
|
+
raise SystemExit(2 if isinstance(error, UnknownWorker) else 1) from error
|
|
200
|
+
except (GenerationError, OSError) as error:
|
|
201
|
+
print(f"ccx fork unavailable: {error}", file=sys.stderr)
|
|
202
|
+
raise SystemExit(3) from error
|
|
203
|
+
|
|
204
|
+
if (result.get("native_ready") is not True
|
|
205
|
+
or result.get("activation_state") != "confirmed"
|
|
206
|
+
or not isinstance(result.get("forked_from"), dict)):
|
|
207
|
+
print(
|
|
208
|
+
f"ccx fork: native activation unproven for {result['worker']}; "
|
|
209
|
+
f"retain exact --worker-id {result['worker_id']}; never remint",
|
|
210
|
+
file=sys.stderr,
|
|
211
|
+
)
|
|
212
|
+
raise SystemExit(3)
|
|
213
|
+
if args.json:
|
|
214
|
+
public = {key: result[key] for key in (
|
|
215
|
+
"worker", "worker_id", "engine", "short", "native_source",
|
|
216
|
+
"native_ready", "created", "reconciled",
|
|
217
|
+
)}
|
|
218
|
+
public.update(
|
|
219
|
+
ok=True, name=result["record"].get("name"),
|
|
220
|
+
cwd=result["record"].get("cwd"),
|
|
221
|
+
forked_from=result["forked_from"],
|
|
222
|
+
)
|
|
223
|
+
if prompt is not None:
|
|
224
|
+
public.update(delivery_id=result["delivery_id"],
|
|
225
|
+
delivery_status=result["delivery_status"],
|
|
226
|
+
delivery=result["delivery"])
|
|
227
|
+
if args.wait:
|
|
228
|
+
public.update(waited=True, turn_ref=result["turn_ref"],
|
|
229
|
+
report=result["report"])
|
|
230
|
+
if parent is not None and result.get("notification"):
|
|
231
|
+
public["notify_parent"] = result["notification"]
|
|
232
|
+
print(json.dumps(public, ensure_ascii=False, sort_keys=True))
|
|
233
|
+
else:
|
|
234
|
+
if args.wait:
|
|
235
|
+
print(f"worker: {result['worker']}")
|
|
236
|
+
print(result["report"]["text"])
|
|
237
|
+
else:
|
|
238
|
+
print(result["worker"])
|
|
239
|
+
if parent is not None and result.get("notification"):
|
|
240
|
+
notice = result["notification"]
|
|
241
|
+
print(f"notify: armed {notice['event_id']} -> Codex parent {notice['parent']}")
|
|
242
|
+
print(f"notify: {notice['instruction']}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _cmd_import_codex(args, *, source=None):
|
|
246
|
+
from ccx.ops.codex_import import import_thread
|
|
247
|
+
from ccx.errors import CreatePartialFailure
|
|
248
|
+
try:
|
|
249
|
+
result = import_thread(
|
|
250
|
+
args.session, source=source, model=getattr(args, "model", None),
|
|
251
|
+
effort=getattr(args, "effort", None),
|
|
252
|
+
)
|
|
253
|
+
print(result["worker"])
|
|
254
|
+
sys.stderr.write(
|
|
255
|
+
"[ccx] imported " + result["worker"] +
|
|
256
|
+
": copied Codex history into a new native fork; the original "
|
|
257
|
+
"Codex thread is unchanged. Reimporting creates another fork.\n"
|
|
258
|
+
)
|
|
259
|
+
except CreatePartialFailure as error:
|
|
260
|
+
print(error.worker)
|
|
261
|
+
sys.stderr.write(
|
|
262
|
+
f"[ccx] Codex import: {error} ({error.phase}); "
|
|
263
|
+
f"worker={error.worker}; retain that worker ID, "
|
|
264
|
+
"do not replay an indeterminate native fork\n"
|
|
265
|
+
)
|
|
266
|
+
raise SystemExit(error.exit_code) from error
|
|
267
|
+
except CcxError as error:
|
|
268
|
+
sys.stderr.write(f"[ccx] Codex import: {error}\n")
|
|
269
|
+
raise SystemExit(error.exit_code) from error
|
|
270
|
+
except (OSError, ValueError) as error:
|
|
271
|
+
sys.stderr.write(f"[ccx] Codex import: native source unavailable: {error}\n")
|
|
272
|
+
raise SystemExit(EXIT_UNAVAILABLE) from error
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def cmd_import(args):
|
|
276
|
+
"""Import an exact Claude SID, or fork an independent Codex native thread."""
|
|
277
|
+
from uuid import UUID
|
|
278
|
+
from ccx.claude.external import exact_import_candidate
|
|
279
|
+
from ccx.store.root import require_ccx_active
|
|
280
|
+
from ccx.store.identity import mint_worker_id
|
|
281
|
+
from ccx.ops.creation import create as create_worker
|
|
282
|
+
from ccx.errors import CreatePartialFailure, OperationConflict
|
|
283
|
+
|
|
284
|
+
sid = args.session
|
|
285
|
+
if getattr(args, "engine", "claude") == "codex":
|
|
286
|
+
return _cmd_import_codex(args)
|
|
287
|
+
if getattr(args, "engine", "claude") is None:
|
|
288
|
+
# Exact native IDs only, never worker aliases or names. Claude is a
|
|
289
|
+
# cheap local lookup; probe Codex only when Claude doesn't know the ID.
|
|
290
|
+
from ccx.errors import SessionNotFound
|
|
291
|
+
from ccx.codex.runtime import read_import_source
|
|
292
|
+
try:
|
|
293
|
+
claude_candidate = exact_import_candidate(sid)
|
|
294
|
+
except SessionNotFound:
|
|
295
|
+
claude_candidate = None
|
|
296
|
+
except CcxError as error:
|
|
297
|
+
sys.stderr.write(f"[ccx] import: {error}\n")
|
|
298
|
+
raise SystemExit(error.exit_code) from error
|
|
299
|
+
if claude_candidate is None:
|
|
300
|
+
try:
|
|
301
|
+
codex_candidate = read_import_source(sid)
|
|
302
|
+
except SessionNotFound:
|
|
303
|
+
sys.stderr.write(f"[ccx] import: no native session/thread for {sid}\n")
|
|
304
|
+
raise SystemExit(2)
|
|
305
|
+
except CcxError as error:
|
|
306
|
+
sys.stderr.write(f"[ccx] import: {error}\n")
|
|
307
|
+
raise SystemExit(error.exit_code) from error
|
|
308
|
+
return _cmd_import_codex(args, source=codex_candidate)
|
|
309
|
+
if getattr(args, "model", None) is not None or getattr(args, "effort", None) is not None:
|
|
310
|
+
sys.stderr.write("[ccx] import: --model/--effort are Codex-only\n")
|
|
311
|
+
raise SystemExit(EXIT_ERROR)
|
|
312
|
+
try:
|
|
313
|
+
if not isinstance(sid, str) or str(UUID(sid)) != sid:
|
|
314
|
+
raise InvalidRequest("Import needs a complete native session ID")
|
|
315
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
316
|
+
|
|
317
|
+
def previous_owner():
|
|
318
|
+
owned = store.import_native_owner(sid)
|
|
319
|
+
if owned is None:
|
|
320
|
+
return None
|
|
321
|
+
if owned["provenance"] == "ccx-import" and owned["confirmed"]:
|
|
322
|
+
print(owned["worker"])
|
|
323
|
+
sys.stderr.write("[ccx] import: already managed; using original "
|
|
324
|
+
+ owned["worker"] + " (no new resume)\n")
|
|
325
|
+
return owned["worker"]
|
|
326
|
+
if owned["provenance"] == "ccx-import":
|
|
327
|
+
raise SessionUnavailable("Claude import is still "
|
|
328
|
+
+ owned["activation_state"] + "; retain "
|
|
329
|
+
+ owned["worker"] + ", never resume twice")
|
|
330
|
+
raise OperationConflict("Claude session is already managed by "
|
|
331
|
+
+ owned["worker"], worker_id=owned["worker_id"])
|
|
332
|
+
|
|
333
|
+
if previous_owner():
|
|
334
|
+
return
|
|
335
|
+
source = exact_import_candidate(sid)
|
|
336
|
+
eligible, _code, reason = import_eligibility(source)
|
|
337
|
+
if not eligible:
|
|
338
|
+
raise SessionUnavailable(reason)
|
|
339
|
+
transcript = source["transcript"]
|
|
340
|
+
st = os.stat(transcript)
|
|
341
|
+
original = {
|
|
342
|
+
"sessionId": sid, "cwd": source["cwd"],
|
|
343
|
+
"transcriptJsonl": transcript,
|
|
344
|
+
"importedAt": int(time.time() * 1000),
|
|
345
|
+
"preImportTranscriptSize": st.st_size,
|
|
346
|
+
"preImportTranscriptMtime": int(st.st_mtime * 1000),
|
|
347
|
+
"wasLive": bool(source["live"]),
|
|
348
|
+
}
|
|
349
|
+
try:
|
|
350
|
+
result = create_worker(
|
|
351
|
+
engine="claude", prompt=None, cwd=source["cwd"],
|
|
352
|
+
worker_id=mint_worker_id(), name=source.get("name"),
|
|
353
|
+
permissions="full", created_via="import",
|
|
354
|
+
import_session_id=sid, imported_from=original,
|
|
355
|
+
start_timeout=15,
|
|
356
|
+
)
|
|
357
|
+
except OperationConflict:
|
|
358
|
+
# Another caller may have won the same native-SID lock after our
|
|
359
|
+
# first read; reconcile its original owner, never mint a fallback.
|
|
360
|
+
if previous_owner():
|
|
361
|
+
return
|
|
362
|
+
raise
|
|
363
|
+
print(result["worker"])
|
|
364
|
+
sys.stderr.write(
|
|
365
|
+
"[ccx] imported " + result["worker"] + ": original native Claude "
|
|
366
|
+
"history resumed under ccx hooks (--dangerously-skip-permissions).\n"
|
|
367
|
+
"[ccx] Do NOT continue using the old Claude terminal for this session.\n"
|
|
368
|
+
)
|
|
369
|
+
except CreatePartialFailure as error:
|
|
370
|
+
# Identity is already durable. Retain/print the same full UUID even
|
|
371
|
+
# when the first resume or genuine SessionStart was inconclusive.
|
|
372
|
+
print(error.worker)
|
|
373
|
+
underlying = (error.info.get("native_start_error") or
|
|
374
|
+
str(error.cause or ""))
|
|
375
|
+
sys.stderr.write(f"[ccx] import: {error} ({error.phase}); "
|
|
376
|
+
f"worker={error.worker}; {underlying}; "
|
|
377
|
+
"do not retry a native resume\n")
|
|
378
|
+
raise SystemExit(error.exit_code) from error
|
|
379
|
+
except CcxError as error:
|
|
380
|
+
sys.stderr.write(f"[ccx] import: {error}\n")
|
|
381
|
+
raise SystemExit(error.exit_code) from error
|
|
382
|
+
except (OSError, ValueError) as error:
|
|
383
|
+
sys.stderr.write(f"[ccx] import: source or generation unavailable: {error}\n")
|
|
384
|
+
raise SystemExit(EXIT_UNAVAILABLE) from error
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def cmd_init(args) -> None:
|
|
388
|
+
"""Explicit fresh install on an empty directory or a missing root leaf."""
|
|
389
|
+
from ccx.store.root import GenerationError, create_missing_root_leaf, initialize_empty_root, root_status, root_status_message
|
|
390
|
+
original_env_root = constants._ORIGINAL_CCX_ROOT_ENV
|
|
391
|
+
if args.root is not None:
|
|
392
|
+
if not pathlib.Path(args.root).expanduser().is_absolute():
|
|
393
|
+
print("ccx init: --root must be absolute", file=sys.stderr)
|
|
394
|
+
raise SystemExit(1)
|
|
395
|
+
root = constants.select_root(args.root)
|
|
396
|
+
else:
|
|
397
|
+
root = constants.CCX_ROOT
|
|
398
|
+
try:
|
|
399
|
+
status = root_status(root)
|
|
400
|
+
if status.kind == "initialized":
|
|
401
|
+
print(f"ccx init: CCX is already initialized in {root}", file=sys.stderr)
|
|
402
|
+
raise SystemExit(1)
|
|
403
|
+
if status.kind == "unsafe":
|
|
404
|
+
raise GenerationError(root_status_message(status))
|
|
405
|
+
# First run: hold and verify the existing parent, create only its leaf;
|
|
406
|
+
# a concurrent replacement is caught by initialize_empty_root's fd.
|
|
407
|
+
expected_inode = create_missing_root_leaf(root) if status.kind == "missing" else None
|
|
408
|
+
store = initialize_empty_root(root, _expected_inode=expected_inode)
|
|
409
|
+
except (OSError, GenerationError) as exc:
|
|
410
|
+
print(f"ccx init refused: {exc}", file=sys.stderr)
|
|
411
|
+
raise SystemExit(3) from None
|
|
412
|
+
result = {
|
|
413
|
+
"layout": 2,
|
|
414
|
+
"root": str(store.root),
|
|
415
|
+
"generation": store.generation,
|
|
416
|
+
"phase": "ACTIVE",
|
|
417
|
+
"workerCount": 0,
|
|
418
|
+
"newEmptyInstall": True,
|
|
419
|
+
}
|
|
420
|
+
print(json.dumps(result, sort_keys=True) if args.json else
|
|
421
|
+
f"initialized empty CCX generation {store.generation} in {store.root}")
|
|
422
|
+
if original_env_root and args.root is not None:
|
|
423
|
+
from ccx.store.root import canonical_root
|
|
424
|
+
previous = canonical_root(original_env_root)
|
|
425
|
+
if previous != store.root:
|
|
426
|
+
print(
|
|
427
|
+
f"ccx init: initialized {store.root}; later commands will use "
|
|
428
|
+
f"CCX_ROOT={previous} unless you change or unset CCX_ROOT",
|
|
429
|
+
file=sys.stderr,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _prepare_full_uuid_new(args, *, engine: str) -> None:
|
|
434
|
+
"""CLI preclaim intent and presentation; native creation belongs to creation.create."""
|
|
435
|
+
from math import isfinite
|
|
436
|
+
from ccx.ops.creation import create
|
|
437
|
+
from ccx.ops.creation_steps import validated_create_spec
|
|
438
|
+
from ccx.errors import BudgetStopped, CcxError, CreatePartialFailure, InvalidRequest, OperationConflict
|
|
439
|
+
from ccx.store.root import GenerationError
|
|
440
|
+
from ccx.store.identity import IdentityError, canonical_worker_id
|
|
441
|
+
|
|
442
|
+
notify_parent = bool(getattr(args, "notify_parent", False))
|
|
443
|
+
settle = bool(getattr(args, "settle", False))
|
|
444
|
+
permissions = getattr(args, "permissions", None)
|
|
445
|
+
sandbox = getattr(args, "sandbox", None)
|
|
446
|
+
approval_policy = getattr(args, "approval_policy", None)
|
|
447
|
+
start_timeout = getattr(args, "start_timeout", None)
|
|
448
|
+
try:
|
|
449
|
+
if engine == "claude":
|
|
450
|
+
if start_timeout is not None:
|
|
451
|
+
raise InvalidRequest("--start-timeout is Codex-only")
|
|
452
|
+
if settle:
|
|
453
|
+
raise InvalidRequest("--settle is Codex-only")
|
|
454
|
+
if args.permission_mode is not None and permissions != "restricted":
|
|
455
|
+
raise InvalidRequest(
|
|
456
|
+
"--permission-mode requires explicit --permissions restricted"
|
|
457
|
+
)
|
|
458
|
+
else:
|
|
459
|
+
if settle and not notify_parent:
|
|
460
|
+
raise InvalidRequest("--settle requires --notify-parent")
|
|
461
|
+
from ccx.workers import _check_caps
|
|
462
|
+
if _caps_from_args(args):
|
|
463
|
+
_check_caps(_caps_from_args(args), engine="codex")
|
|
464
|
+
if start_timeout is not None and not (
|
|
465
|
+
isfinite(start_timeout) and 0 < start_timeout <= 90
|
|
466
|
+
):
|
|
467
|
+
raise InvalidRequest("--start-timeout must be finite, positive and at most 90")
|
|
468
|
+
# The existing native-spec validator already knows which provider flags
|
|
469
|
+
# and generic/granular permission combinations are legal. Run it before
|
|
470
|
+
# preparing variables, notifying a parent or claiming the worker UUID.
|
|
471
|
+
validated_create_spec(
|
|
472
|
+
engine=engine, cwd=args.cwd, model=args.model, effort=args.effort,
|
|
473
|
+
idle_timeout=args.idle_timeout, permissions=permissions,
|
|
474
|
+
sandbox=sandbox, approval_policy=approval_policy,
|
|
475
|
+
agent=getattr(args, "agent", None),
|
|
476
|
+
append_system_prompt=getattr(args, "append_system_prompt", None),
|
|
477
|
+
allowed_tools=getattr(args, "allowed_tools", None),
|
|
478
|
+
disallowed_tools=getattr(args, "disallowed_tools", None),
|
|
479
|
+
add_dir=getattr(args, "add_dir", None),
|
|
480
|
+
permission_mode=getattr(args, "permission_mode", None),
|
|
481
|
+
)
|
|
482
|
+
if args.worker_id is not None:
|
|
483
|
+
canonical_worker_id(args.worker_id)
|
|
484
|
+
if args.var is not None and args.vars_from is not None:
|
|
485
|
+
raise InvalidRequest("--var and --vars-from are mutually exclusive")
|
|
486
|
+
except (InvalidRequest, IdentityError) as error:
|
|
487
|
+
print(f"ccx new: {error}; no worker identity claimed", file=sys.stderr)
|
|
488
|
+
raise SystemExit(EXIT_ERROR) from error
|
|
489
|
+
|
|
490
|
+
prompt = _resolve_prompt_source(args, "prompt", "new", required=False)
|
|
491
|
+
if prompt is not None and (not isinstance(prompt, str) or not prompt.strip()):
|
|
492
|
+
print("ccx new: the initial prompt must not be blank; "
|
|
493
|
+
"no worker identity claimed", file=sys.stderr)
|
|
494
|
+
raise SystemExit(EXIT_ERROR)
|
|
495
|
+
if args.wait and prompt is None:
|
|
496
|
+
print("ccx new: --wait requires an initial prompt; no worker identity claimed",
|
|
497
|
+
file=sys.stderr)
|
|
498
|
+
raise SystemExit(EXIT_ERROR)
|
|
499
|
+
if args.timeout is not None and (not args.wait or not (
|
|
500
|
+
isfinite(args.timeout) and args.timeout > 0
|
|
501
|
+
)):
|
|
502
|
+
print("ccx new: --timeout requires --wait and positive finite seconds; "
|
|
503
|
+
"no worker identity claimed", file=sys.stderr)
|
|
504
|
+
raise SystemExit(EXIT_ERROR)
|
|
505
|
+
invoke = {
|
|
506
|
+
"engine": engine, "cwd": args.cwd, "worker_id": args.worker_id,
|
|
507
|
+
"prompt": prompt,
|
|
508
|
+
"wait": args.wait,
|
|
509
|
+
"name": args.name, "model": args.model, "effort": args.effort,
|
|
510
|
+
"idle_timeout": args.idle_timeout, "timeout": args.timeout,
|
|
511
|
+
"created_via": "cli",
|
|
512
|
+
}
|
|
513
|
+
if engine == "claude":
|
|
514
|
+
invoke.update(
|
|
515
|
+
agent=getattr(args, "agent", None),
|
|
516
|
+
append_system_prompt=getattr(args, "append_system_prompt", None),
|
|
517
|
+
allowed_tools=getattr(args, "allowed_tools", None),
|
|
518
|
+
disallowed_tools=getattr(args, "disallowed_tools", None),
|
|
519
|
+
add_dir=getattr(args, "add_dir", None),
|
|
520
|
+
permission_mode=getattr(args, "permission_mode", None),
|
|
521
|
+
permissions=permissions,
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
invoke.update(permissions=permissions, sandbox=sandbox,
|
|
525
|
+
approval_policy=approval_policy)
|
|
526
|
+
invoke["settle"] = settle
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
from ccx.controllers import live_vars
|
|
530
|
+
attachment = None
|
|
531
|
+
if args.vars_from is not None:
|
|
532
|
+
from ccx.store.discovery import resolve_worker
|
|
533
|
+
source_engine, source_id = resolve_worker(args.vars_from).split(":", 1)
|
|
534
|
+
attachment = live_vars.prepare_shared_durable_attachment(source_engine, source_id)
|
|
535
|
+
elif args.var is not None:
|
|
536
|
+
attachment = live_vars.prepare_durable_attachment(
|
|
537
|
+
live_vars.parse_cli_vars(args.var)
|
|
538
|
+
)
|
|
539
|
+
invoke["attachment"] = attachment
|
|
540
|
+
if engine == "claude":
|
|
541
|
+
invoke["caps"] = _initial_caps(args, prompt)
|
|
542
|
+
parent = _prepare_parent_notification(args)
|
|
543
|
+
else:
|
|
544
|
+
invoke["caps"] = _initial_caps(args, prompt, engine="codex")
|
|
545
|
+
# Reuse the authenticated Codex parent gate; do not claim first.
|
|
546
|
+
from ccx.cli.codex import _prepare_parent_notification as _prepare_codex_parent_notification
|
|
547
|
+
parent = _prepare_codex_parent_notification(args)
|
|
548
|
+
invoke["parent"] = parent
|
|
549
|
+
result = create(
|
|
550
|
+
**invoke, start_timeout=start_timeout if engine == "codex" else None,
|
|
551
|
+
)
|
|
552
|
+
except CreatePartialFailure as error:
|
|
553
|
+
output = {
|
|
554
|
+
"ok": False, "code": "partial_failure", "phase": error.phase,
|
|
555
|
+
"worker_id": error.worker_id, "worker": error.worker,
|
|
556
|
+
"engine": error.engine, "short": error.short,
|
|
557
|
+
"created": error.info.get("created"),
|
|
558
|
+
"reconciled": error.info.get("reconciled"),
|
|
559
|
+
"native_ready": error.info.get("native_ready") is True,
|
|
560
|
+
"delivery_id": error.delivery_id,
|
|
561
|
+
"delivery_status": error.delivery_status,
|
|
562
|
+
"turn_ref": error.info.get("turn_ref"),
|
|
563
|
+
"message": str(error),
|
|
564
|
+
"native_start_error": error.info.get("native_start_error"),
|
|
565
|
+
"cause_type": type(error.cause).__name__ if error.cause else None,
|
|
566
|
+
"integrity_error": bool(error.info.get("integrity_error")),
|
|
567
|
+
"activation_state": error.info.get("activation_state"),
|
|
568
|
+
}
|
|
569
|
+
if isinstance(error.cause, BudgetStopped):
|
|
570
|
+
# An original worker was claimed, but a usage cap stopped its
|
|
571
|
+
# first turn. Preserve the handle and report the typed outcome.
|
|
572
|
+
reason = error.cause.reason or str(error.cause)
|
|
573
|
+
output.update(code="budget_stopped", status="budget-stopped",
|
|
574
|
+
reason=reason)
|
|
575
|
+
if args.json:
|
|
576
|
+
print(json.dumps(output, sort_keys=True, ensure_ascii=False))
|
|
577
|
+
else:
|
|
578
|
+
print(error.worker)
|
|
579
|
+
print("status: budget-stopped")
|
|
580
|
+
print(f"reason: {reason}")
|
|
581
|
+
raise SystemExit(BudgetStopped.exit_code) from error
|
|
582
|
+
if isinstance(error.info.get("native_rejection"), dict):
|
|
583
|
+
refusal = error.info["native_rejection"]
|
|
584
|
+
output.update(
|
|
585
|
+
code="native_rejected", activation_state="rejected",
|
|
586
|
+
message=refusal["message"], native_start_error=refusal["message"],
|
|
587
|
+
native_rejection=dict(refusal),
|
|
588
|
+
)
|
|
589
|
+
if isinstance(error.info.get("native_local_refusal"), dict):
|
|
590
|
+
diagnostic = error.info["native_local_refusal"]
|
|
591
|
+
if diagnostic.get("proven") is True and output["activation_state"] == "local-refused":
|
|
592
|
+
output.update(
|
|
593
|
+
code="local_refused", message=diagnostic["reason"],
|
|
594
|
+
native_start_error=diagnostic["reason"],
|
|
595
|
+
)
|
|
596
|
+
output["native_local_refusal"] = dict(diagnostic)
|
|
597
|
+
if error.info.get("initial_delivery_conflict") is True:
|
|
598
|
+
output["code"] = "conflict"
|
|
599
|
+
error.exit_code = OperationConflict.exit_code
|
|
600
|
+
elif error.info.get("initial_delivery_rejected") is True:
|
|
601
|
+
output["code"] = "delivery_rejected"
|
|
602
|
+
if error.phase == "initial-delivery" and error.cause is not None:
|
|
603
|
+
output["initial_delivery_error"] = str(error.cause)
|
|
604
|
+
if error.phase == "wait-report" and error.cause is not None:
|
|
605
|
+
cause = error.cause
|
|
606
|
+
output["turn_status"] = getattr(cause, "turn_status", None)
|
|
607
|
+
output["turn_error"] = getattr(cause, "turn_error", None)
|
|
608
|
+
output["waiting_for"] = getattr(cause, "waiting_for", None)
|
|
609
|
+
if error.info.get("signed_out") is True:
|
|
610
|
+
output.update(code="signed_out", message=str(error))
|
|
611
|
+
if args.json:
|
|
612
|
+
print(json.dumps(output, sort_keys=True, ensure_ascii=False))
|
|
613
|
+
elif output["code"] == "signed_out":
|
|
614
|
+
print(f"ccx new: {error} (worker {error.worker} did not start)",
|
|
615
|
+
file=sys.stderr)
|
|
616
|
+
elif output["code"] in {"local_refused", "native_rejected"}:
|
|
617
|
+
print(f"ccx {engine} new: {output['message']} "
|
|
618
|
+
f"(worker {error.worker}; no native replay)", file=sys.stderr)
|
|
619
|
+
else:
|
|
620
|
+
print(
|
|
621
|
+
f"ccx new: {error.worker} identity claimed; creation phase "
|
|
622
|
+
f"{error.phase} remains unavailable. Retain exact worker_id "
|
|
623
|
+
f"{error.worker_id}, never remint.", file=sys.stderr,
|
|
624
|
+
)
|
|
625
|
+
raise SystemExit(error.exit_code)
|
|
626
|
+
except BudgetStopped as exc:
|
|
627
|
+
print(f"ccx new rejected: {exc}", file=sys.stderr)
|
|
628
|
+
raise SystemExit(exc.exit_code) from exc
|
|
629
|
+
except (InvalidRequest, OperationConflict, GenerationError, CcxError) as exc:
|
|
630
|
+
model_error = (
|
|
631
|
+
engine == "codex" and args.model is not None
|
|
632
|
+
and isinstance(exc, InvalidRequest)
|
|
633
|
+
and str(exc).startswith(("unsupported Codex model ", "Codex model must "))
|
|
634
|
+
)
|
|
635
|
+
if args.json and (isinstance(exc, EngineCliNotFound) or model_error):
|
|
636
|
+
print(json.dumps({"ok": False,
|
|
637
|
+
"code": ("runtime_unavailable" if isinstance(exc, EngineCliNotFound)
|
|
638
|
+
else "invalid_request"),
|
|
639
|
+
"message": str(exc)}, sort_keys=True))
|
|
640
|
+
else:
|
|
641
|
+
print(f"ccx new rejected: {exc}", file=sys.stderr)
|
|
642
|
+
raise SystemExit(exc.exit_code if isinstance(exc, CcxError) else 3) from None
|
|
643
|
+
if result.get("native_ready") is not True or result.get("activation_state") != "confirmed":
|
|
644
|
+
raise RuntimeError("unproven native activation must never exit as a successful ccx new")
|
|
645
|
+
if args.json:
|
|
646
|
+
public = {key: result[key] for key in (
|
|
647
|
+
"worker", "worker_id", "engine", "short", "native_source",
|
|
648
|
+
"native_ready", "created", "reconciled",
|
|
649
|
+
)}
|
|
650
|
+
if prompt is not None:
|
|
651
|
+
public.update(delivery_id=result["delivery_id"],
|
|
652
|
+
delivery_status=result["delivery_status"],
|
|
653
|
+
delivery=result["delivery"])
|
|
654
|
+
if args.wait:
|
|
655
|
+
public.update(waited=True, turn_ref=result["turn_ref"],
|
|
656
|
+
report=result["report"])
|
|
657
|
+
if notify_parent and result.get("notification"):
|
|
658
|
+
public["notify_parent"] = result["notification"]
|
|
659
|
+
print(json.dumps({**public, "ok": True}, sort_keys=True))
|
|
660
|
+
else:
|
|
661
|
+
if args.wait:
|
|
662
|
+
print(f"worker: {result['worker']}")
|
|
663
|
+
print(result["report"]["text"])
|
|
664
|
+
else:
|
|
665
|
+
print(result["worker"])
|
|
666
|
+
if notify_parent and result.get("notification"):
|
|
667
|
+
notice = result["notification"]
|
|
668
|
+
print(f"notify: armed {notice['event_id']} -> Codex parent {notice['parent']}")
|
|
669
|
+
print(f"notify: {notice['instruction']}")
|