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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +807 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +269 -0
  16. package/ccx/claude/host/session.py +5244 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. 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']}")