ccx-native 0.0.0-stage → 0.1.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/bin/ccx +56 -0
  4. package/ccx/__init__.py +163 -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 +762 -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 +250 -0
  16. package/ccx/claude/host/session.py +5184 -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 +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -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/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +40 -4
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
package/ccx.py ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+ """ccx: run persistent Claude Code and Codex workers."""
3
+ from __future__ import annotations
4
+
5
+ from ccx.cli.main import main
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "ccx-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Control and monitor native Claude Code and Codex sessions from the CLI, Python or RPC, on your own subscriptions.",
5
+ "author": "Fernando <ccx@delega.ai>",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "ccx": "bin/ccx"
9
+ },
10
+ "files": [
11
+ "bin/ccx",
12
+ "ccx.py",
13
+ "ccx/**/*.py",
14
+ "!ccx/codex/schema.py",
15
+ "skills/",
16
+ "README.md",
17
+ "LICENSE",
18
+ "pyproject.toml"
19
+ ],
20
+ "scripts": {
21
+ "test": "python3 -m compileall -q ccx ccx.py"
22
+ },
23
+ "keywords": [
24
+ "claude-code",
25
+ "codex",
26
+ "agent",
27
+ "terminal",
28
+ "pty",
29
+ "automation"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/ccx-native/ccx.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/ccx-native/ccx/issues"
40
+ },
41
+ "homepage": "https://github.com/ccx-native/ccx#readme"
42
+ }
package/pyproject.toml ADDED
@@ -0,0 +1,87 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ccx-native"
7
+ version = "0.1.0"
8
+ description = "Control and monitor native Claude Code and Codex sessions from the CLI, Python or RPC, on your own subscriptions."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Fernando", email = "ccx@delega.ai" }]
14
+ dependencies = []
15
+ keywords = ["claude-code", "codex", "agent", "cli", "terminal", "automation"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: MacOS",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
29
+ "Topic :: Software Development",
30
+ ]
31
+
32
+ [project.scripts]
33
+ ccx = "ccx.cli.main:main"
34
+
35
+ [dependency-groups]
36
+ dev = ["pytest>=8", "pytest-xdist>=3.6"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/ccx-native/ccx"
40
+ Repository = "https://github.com/ccx-native/ccx"
41
+ Issues = "https://github.com/ccx-native/ccx/issues"
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ # Allowlist: hatchling's default sdist ships every file not ignored by VCS,
45
+ # which would include internal memos, plans and evidence at the repo root.
46
+ # Only the package, its tests, public docs and project metadata are published.
47
+ only-include = [
48
+ "ccx",
49
+ "ccx.py",
50
+ "bin",
51
+ "docs",
52
+ "examples",
53
+ "scripts",
54
+ "skills",
55
+ "tests",
56
+ "README.md",
57
+ "CHANGELOG.md",
58
+ "LICENSE",
59
+ "CONTRIBUTING.md",
60
+ "SECURITY.md",
61
+ "package.json",
62
+ "pyproject.toml",
63
+ ]
64
+ exclude = [
65
+ "/tests/evidence",
66
+ ]
67
+
68
+ [tool.hatch.build.targets.wheel]
69
+ packages = ["ccx"]
70
+ # Maintainer tooling for scripts/check_codex_protocol.py and its contract
71
+ # test; nothing at runtime imports it, so installs do not need it.
72
+ exclude = ["/ccx/codex/schema.py"]
73
+
74
+ [tool.hatch.build.targets.wheel.force-include]
75
+ # The agent skills ship as package data; `ccx skills` prints where they are.
76
+ "skills" = "ccx/_skills"
77
+
78
+ [tool.ruff]
79
+ target-version = "py311"
80
+
81
+ [tool.ruff.lint]
82
+ # Start with what can only be a bug: syntax errors, invalid comparisons and
83
+ # undefined names. Widen (e.g. F401 unused imports) once existing hits are fixed.
84
+ select = ["E9", "F63", "F7", "F82"]
85
+
86
+ [tool.pytest.ini_options]
87
+ testpaths = ["tests"]
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: ccx
3
+ description: Run persistent Claude Code and Codex workers with the ccx CLI. Start workers, send follow-ups, wait or get notified, read replies, and coordinate work with Python controllers.
4
+ ---
5
+
6
+ # ccx workers
7
+
8
+ If `ccx` is missing, install it with `uv tool install ccx-native` (or
9
+ `pipx install ccx-native`). The engine you pick must be installed and signed in.
10
+
11
+ Workers keep their own history and stay reachable after your conversation ends.
12
+ Use `ccx help COMMAND` for flags.
13
+
14
+ ## The core loop
15
+
16
+ For Claude Code or a person:
17
+
18
+ ```bash
19
+ W=$(ccx new --engine codex --cwd ~/repo -n auth-tests "Fix the failing tests in tests/auth.")
20
+ ccx wait "$W" # until it finishes or needs input
21
+ ccx report "$W" # its last reply
22
+ ccx send "$W" "Now add a regression test."
23
+ ```
24
+
25
+ - Keep the handle from `new`. Worker targets accept it, the full UUID or a
26
+ unique UUID prefix of at least 8 lowercase hex characters. Names are labels
27
+ for `ccx list`, never targets.
28
+ - Give a self-contained task and the right `--cwd`. Project instructions,
29
+ skills, MCP servers and config come from that folder; your conversation
30
+ isn't inherited.
31
+ - For long or literal prompts use `--file task.md`, or `--stdin <<'EOF'` with
32
+ the delimiter quoted so the shell leaves backticks and `$VARS` alone.
33
+ - Model and effort default to the engine's settings. Claude accepts aliases
34
+ (`opus`, `sonnet`, `haiku`); Codex needs a full model id from its model list.
35
+
36
+ ## Waiting depends on who you are
37
+
38
+ - **Claude Code or a person:** use `--wait` or `ccx wait`; `send` waits by
39
+ default. Claude can background a long wait and let its harness wake it.
40
+ - **Codex parent:** add `--notify-parent` to `new`, `send` or `compact`, or arm a
41
+ running worker with `ccx notify TARGET`. Then carry on or end your turn: ccx
42
+ resumes you when it settles. Don't wait, sleep or poll. This works from a ccx
43
+ Codex worker, the Codex app, or a default `codex` terminal. A `codex` started
44
+ with `-c`, `--profile` or `--no-daemon`, and `codex exec`, can't be woken; ccx
45
+ refuses and says to wait instead. In the notification, trust the turn's
46
+ `status`; `workerState` is only a hint.
47
+ Run `ccx` with full access: it starts processes and writes outside the workspace.
48
+ - `--no-wait` returns at once and wakes nobody.
49
+
50
+ ## Talking to a worker
51
+
52
+ | The worker is | Do |
53
+ |---|---|
54
+ | idle or stopped | `send`; stopped workers revive with their history. |
55
+ | busy | `steer TARGET "..."`, or let it settle before `send`. |
56
+ | waiting for input | `peek`, then `approve`/`reject` a permission request; on Claude, `answer` a question or `respond` to an MCP form. `send` is refused. |
57
+ | going the wrong way | `stop` cancels the turn, not background work it started. `kill` stops the runtime; `send` revives it with its history. |
58
+
59
+ ## Reading results
60
+
61
+ - `ccx peek TARGET`: state, latest messages, tools and errors. Start here.
62
+ `ccx peek A B --status` checks several workers at once.
63
+ - `ccx report TARGET`: the last reply.
64
+ - `ccx logs TARGET`: conversation JSON when the report isn't enough.
65
+ - `ccx list`: find workers by name, state or folder.
66
+
67
+ ## Permissions
68
+
69
+ Workers default to full permissions: they can read, write and run anything your
70
+ user can, without permission prompts. Put read-only or "no edits" boundaries in
71
+ the task. Restrict permissions only if the user asks.
72
+
73
+ ## Reuse, compact or start fresh
74
+
75
+ Treat a worker like a teammate with working memory: reuse it when that memory
76
+ helps the next task, compact it when a smaller context will repay the cost of
77
+ summarizing it, and start fresh when the next task doesn't benefit from it.
78
+
79
+ `ccx peek TARGET --status --json` shows both what you need:
80
+ `context.tokens_used` (what the worker carries now, not total usage) and
81
+ `last_turn_age_s`.
82
+
83
+ - **Reuse** for the same task when the history helps. Design and strategy
84
+ conversations deserve long contexts; don't cut them for size alone.
85
+ - **Start fresh** for unrelated work or a different working set, even in the
86
+ same repository.
87
+ - **Compact** with `ccx compact TARGET` at a clean boundary above about 150k
88
+ tokens when most context is implementation residue (tool output) and the
89
+ next task uses the same files or domain. Summarizing costs tokens; when in
90
+ doubt, continue. Engines also compact automatically.
91
+
92
+ **Claude only:** treat the prompt cache as warm for one hour after the last
93
+ turn. Changing model or effort (`ccx set TARGET --model M --effort E`)
94
+ invalidates it. Above about 100k tokens with a warm cache, compact before
95
+ switching. With a cold cache, switch directly or start fresh if the history
96
+ is dispensable. These cache rules don't apply to Codex.
97
+
98
+ ## Variables: structured state between workers
99
+
100
+ Workers can share JSON state without files or parsing prose. Attach it with
101
+ `--var NAME=JSON` on `new`, `send` or `fork`; inside the worker, plain
102
+ `python3` sees `NAME` as a variable it can read and change, and changes
103
+ persist. Read it back with `ccx vars TARGET NAME --json`, and give another
104
+ worker, on either engine, the same shared state with `--vars-from TARGET`.
105
+
106
+ ```bash
107
+ A=$(ccx new --engine claude --cwd ~/repo --var 'plan={"steps": [], "done": false}' \
108
+ "Draft an implementation plan. With python3, append steps to plan['steps'], then set plan['done'] = True last.")
109
+ ccx wait "$A"
110
+ ccx vars "$A" plan --json
111
+ ```
112
+
113
+ Read [references/variables.md](references/variables.md) when sharing state
114
+ between workers or when `python3` doesn't see the variables.
115
+
116
+ ## Controllers: programmable workflows that run unattended
117
+
118
+ A controller is a Python script that drives many workers on its own, so you
119
+ don't have to start, wait on and collect each one by hand. It's ordinary
120
+ Python: it can branch on results, loop or fan out as the work requires. Its
121
+ steps are recorded, so a run can stop for a decision and resume later without
122
+ redoing finished steps, and workers hand back structured results through
123
+ shared variables instead of prose. A few lines from one:
124
+
125
+ ```python
126
+ futures = [review.submit(area) for area in ("api", "storage")] # workers in parallel
127
+ findings = [f.result() for f in futures] # structured results
128
+ if not current.inputs: # stop for a decision
129
+ current.report(status="needs-review", checkpoint="findings", data=findings)
130
+ return
131
+ ```
132
+
133
+ Start one with `ccx controller run SCRIPT` (a Codex parent adds
134
+ `--notify-parent` before the script). Read
135
+ [references/controllers.md](references/controllers.md) before writing one.
136
+
137
+ ## When something looks off
138
+
139
+ A wait timeout doesn't mean the work failed. Check `peek` or `report`;
140
+ for uncertain delivery, use `ccx reconcile TARGET --delivery-id ID`.
141
+ A server or watcher left running doesn't mean the task is unfinished.
142
+
143
+ Exit codes: 0 success · 1 bad input or usage · 2 not found · 3 unavailable ·
144
+ 4 waiting for input · 5 refused in this state · 6 incomplete (timeout, failed
145
+ turn or uncertain delivery) · 7 usage cap · 8 Anthropic rate limit ·
146
+ 9 transient API error.
147
+
148
+ More in [references/troubleshooting.md](references/troubleshooting.md).
149
+
150
+ ## Also useful
151
+
152
+ - `ccx fork TARGET ["prompt"]` copies a settled conversation into a new worker.
153
+ - `ccx attach TARGET` opens the worker in a terminal; Ctrl-Z detaches.
154
+ - `--cap-5h PCT` or `--cap-7d PCT` on `new` or `send` stops that turn when
155
+ account usage reaches PCT%. `ccx usage` shows the current levels.
@@ -0,0 +1,244 @@
1
+ # ccx controllers
2
+
3
+ A controller is a Python script that drives ccx workers. Its `@task` results
4
+ are recorded, so a run can resume and skip finished steps.
5
+
6
+ - [A complete controller](#a-complete-controller)
7
+ - [Replay rules](#replay-rules)
8
+ - [Structured results through variables](#structured-results-through-variables)
9
+ - [Checkpoints and input](#checkpoints-and-input)
10
+ - [Loops and failures](#loops-and-failures)
11
+ - [Before you finish](#before-you-finish)
12
+ - [Python API](#python-api)
13
+ - [CLI](#cli)
14
+
15
+ ## A complete controller
16
+
17
+ Review two areas in parallel, stop for a decision, then apply what the user
18
+ approves in a worktree:
19
+
20
+ ```python
21
+ from ccx import WorkerClient, controller, current, task
22
+
23
+ workers = WorkerClient()
24
+
25
+ @task
26
+ def review(area):
27
+ shared = {"result": {"done": False, "findings": []}}
28
+ workers.create(
29
+ engine="codex", cwd=str(current.workspace), name=f"review-{area}",
30
+ prompt=f"Review {area} without editing. With python3, append findings "
31
+ "to result['findings'], then set result['done'] = True last.",
32
+ variables=shared, wait=True)
33
+ result = shared["result"]
34
+ if not result["done"]:
35
+ raise RuntimeError("Review result is incomplete")
36
+ return result
37
+
38
+ @task(workspace="worktree", commit="Apply approved fixes")
39
+ def implement(input_id, instruction, findings):
40
+ created = workers.create(
41
+ engine="codex", cwd=str(current.workspace), name=f"fix-{input_id[:8]}",
42
+ prompt=f"Apply only this: {instruction}\nFindings:\n{findings}",
43
+ wait=True)
44
+ return {"worker": created["worker"]}
45
+
46
+ @controller(name="review-fix", workspace="/repo")
47
+ def main():
48
+ areas = ("api", "storage")
49
+ futures = [review.submit(area) for area in areas] # start all, fixed order
50
+ findings = {area: f.result() for area, f in zip(areas, futures)}
51
+
52
+ if not current.inputs: # first run: ask
53
+ current.report(status="needs-review", checkpoint="findings", data={
54
+ "findings": findings,
55
+ "resume": f"ccx controller resume {current.run_id} --note '...'"})
56
+ return
57
+ for item in current.inputs: # after resume: act
58
+ implement(item.id, item.comment, findings)
59
+ current.report(status="done", data=findings)
60
+
61
+ if __name__ == "__main__":
62
+ main()
63
+ ```
64
+
65
+ On `ccx controller resume RUN --note "Fix the SQL injection only"`, both
66
+ `review` tasks return their recorded results at once, and the note arrives as
67
+ `item.comment`.
68
+
69
+ ## Replay rules
70
+
71
+ Code outside tasks runs again on every resume; a task that finished runs once.
72
+
73
+ - Outside tasks, don't read the clock, random values, files, Git state or
74
+ workers. Do it in a task and use the returned value.
75
+ - Outside tasks, branch only on task results, script arguments and
76
+ `current.inputs`.
77
+ - Call tasks in the same order with the same arguments every time. A
78
+ different call raises `ControllerReplayMismatch`.
79
+ - A task's result never changes. To act on new input, call a new task with
80
+ `item.id`; don't expect a recorded task to return something else.
81
+
82
+ ```python
83
+ files = subprocess.check_output(["git", "ls-files", "src"], text=True).split()
84
+ for f in files: review.submit(f) # wrong: a different list on resume breaks replay
85
+
86
+ @task
87
+ def list_files():
88
+ return subprocess.check_output(["git", "ls-files", "src"], text=True).split()
89
+ for f in list_files(): review.submit(f) # right: the list is recorded once
90
+ ```
91
+
92
+ Resume needs the script unchanged and the same script arguments. After editing
93
+ the script, start a new run; it starts from the top. Workers keep running when
94
+ a controller stops, and ordinary `ccx` commands work on them.
95
+
96
+ ## Structured results through variables
97
+
98
+ Pass `variables=` instead of parsing worker prose. Inside the worker, plain
99
+ `python3` or `python` reads and updates those names; `command python3` runs
100
+ normal Python.
101
+
102
+ - Read results from the dict you passed: a worker can rebind a top-level name.
103
+ - Set a sentinel such as `done: False` and ask the worker to set it last, so an
104
+ untouched value can't pass as an empty result.
105
+ - Live variables last for one attempt. Keep the worker call and the JSON you
106
+ return from it in the same task; otherwise a replay raises
107
+ `ControllerLiveStateLost`.
108
+ - Return small JSON values (verdicts, paths, worker handles), not transcripts.
109
+
110
+ ## Checkpoints and input
111
+
112
+ `current.report(...)` publishes status; it doesn't stop the run. To stop for a
113
+ decision, report a stable `checkpoint` id with what's needed to decide
114
+ (findings, options, the resume command), then return from the controller.
115
+ Workers stay alive meanwhile, so resuming days later is fine.
116
+
117
+ ```bash
118
+ ccx controller resume RUN --note "..." # resume with a note
119
+ ccx controller resume RUN --worker codex:<uuid> --comment "..."
120
+ ccx controller input RUN --note "..." # record without resuming
121
+ ```
122
+
123
+ Input is recorded for the controller; it isn't sent to any worker. Loop over
124
+ every `current.inputs` item in order and pass `item.id` to the task that
125
+ handles it.
126
+
127
+ ## Loops and failures
128
+
129
+ Bound every loop, and let code decide when it's done rather than a worker's
130
+ claim:
131
+
132
+ ```python
133
+ @task
134
+ def fix_round(n, failing):
135
+ workers.send(fixer, f"Fix these failures:\n{failing}", wait=True)
136
+ run = subprocess.run(["pytest", "-q"], cwd=str(current.workspace),
137
+ capture_output=True, text=True)
138
+ return {"passed": run.returncode == 0, "failing": run.stdout[-2000:]}
139
+
140
+ result = {"passed": False, "failing": initial}
141
+ for n in range(1, 4):
142
+ result = fix_round(n, result["failing"])
143
+ if result["passed"]:
144
+ break
145
+ else:
146
+ current.report(status="stuck", checkpoint="tests-failing", data=result)
147
+ return
148
+ ```
149
+
150
+ - A task that raised or was interrupted runs again from its start on resume;
151
+ finished tasks, including parallel siblings, stay recorded. A worktree task
152
+ reuses its worktree, so write prompts that check the current state first.
153
+ - A worker call can raise `ControllerOperationIndeterminate` (acceptance
154
+ unknown), `SessionWaiting` (input needed) or `TurnIncomplete`. `create` wraps
155
+ first-turn failures in `CreatePartialFailure`; retain its worker and delivery
156
+ ids. Catch failures inside the task, inspect the worker and return a result
157
+ for a checkpoint before deciding whether to send again.
158
+ - Don't wrap task calls in a broad `except Exception`: it would also swallow
159
+ replay errors.
160
+ - Quota is a reading, not a reservation. Check it right before a wave with
161
+ `evaluate_quota(workers.usage("claude"), {"five_hour": 70})` and checkpoint
162
+ if `allowed` is false; `caps={"5h": 80}` on `create` or `send` stops a turn.
163
+
164
+ ## Before you finish
165
+
166
+ - `python -m py_compile SCRIPT` passes.
167
+ - Worker calls, and anything time-, file- or Git-dependent, happen inside tasks.
168
+ - Every task returns small JSON; every loop has a bound and a checkpoint for
169
+ hitting it.
170
+ - Irreversible actions (push, deploy, messages) follow a checkpoint, or the
171
+ task checks whether they already happened.
172
+
173
+ ## Python API
174
+
175
+ Everything below is importable from `ccx`. Worker arguments are the handle a
176
+ create returns (`claude:<uuid>` / `codex:<uuid>`) or the full UUID; prefixes
177
+ and names are not accepted. Results are plain dicts.
178
+
179
+ **Controller building blocks**
180
+
181
+ | Name | Use |
182
+ |---|---|
183
+ | `@controller(name=None, workspace=None)` | The entry point. `workspace` is the base folder (default: where `ccx controller run` started). |
184
+ | `@task(name=None, workspace="shared", commit=False)` | A recorded step. `workspace="worktree"` runs it in its own Git worktree; `commit="message"` commits its changes there. |
185
+ | `task.submit(*args)` | Start a task in parallel; `.result()` waits for its value. |
186
+ | `task_output(value=..., report=...)` | Return JSON `value` plus prose `report`; on replay the task returns the same `.value` and `.report`. |
187
+ | `current.workspace` | The task's folder inside a task, else the controller's. |
188
+ | `current.run_id`, `current.controller_name`, `current.run_dir` | Run identity and its storage folder. |
189
+ | `current.inputs`, `current.latest_input` | Recorded input: `id`, `created_at`, `worker`, `worker_name`, `comment` (a `--note` has `worker=None`). |
190
+ | `current.workers` | Workers this run created. |
191
+ | `current.report(status, checkpoint=None, summary=None, report=None, data=None)` | Publish the run's status; `data` must be JSON. |
192
+ | `evaluate_quota(usage, limits)` | Compare a `workers.usage(...)` reading with limits such as `{"five_hour": 70}`; returns `allowed` and details. |
193
+
194
+ **`WorkerClient()` methods**
195
+
196
+ | Method | Use |
197
+ |---|---|
198
+ | `create(engine, cwd, prompt=None, name=None, model=None, effort=None, permissions="full", wait=False, timeout=None, idle_timeout=None, caps=None, variables=None, variables_from=None, worker_id=None)` | New worker. Returns `worker`, `worker_id`, `engine`, `state`, and with `prompt` + `wait=True` `reply` (the reply text) and `report`. |
199
+ | `send(worker, text, wait=True, timeout=None, caps=None, variables=None, variables_from=None, delivery_id=None)` | New turn on an idle or stopped worker. `reply` holds the reply text when there is one. |
200
+ | `steer(worker, text, wait=True, timeout=120, delivery_id=None)` | Add to a busy worker's running turn. |
201
+ | `wait(worker, timeout=None)` | Until idle, waiting for input or stopped. A timeout is `status="timeout"`, not an exception. |
202
+ | `report(worker, max_chars=None, turn_ref=None)` | The latest finished reply: `text`, `truncated`, `turn_ref`. Raises `SessionUnavailable` when no turn has finished. |
203
+ | `status(worker)`, `list(engine=None, cwd=None, state=None, parent=None, limit=None)` | Recorded state; nothing is sent. |
204
+ | `context(worker)` | Carried context: `tokens_used`, `window_size`, `percent_used`. |
205
+ | `stop(worker)`, `kill(worker)`, `resume(worker)` | Cancel the turn; stop the runtime; reload without a turn. |
206
+ | `fork(worker, prompt=None, name=None, wait=False, ...)` | New worker from a copy of a settled worker's history. |
207
+ | `compact(worker, wait=True)` | The engine's own compaction. |
208
+ | `approve(worker, request_id=None)`, `reject(worker, request_id=None, reason=None)` | Resolve a permission request. |
209
+ | `set_model(worker, model)`, `set_effort(worker, effort)`, `set_name(worker, name)` | Settings for future turns. |
210
+ | `variables(worker, name=None)` | Read a worker's durable JSON variables. |
211
+ | `usage(engine=None)` | Account quota for one or both engines. |
212
+ | `conversation_page(worker, cursor=None)`, `events(worker, after=None)` | Bounded history and lifecycle events. |
213
+
214
+ `caps` takes `{"5h": PCT, "7d": PCT}` (Claude also `"opus"`).
215
+
216
+ **Exceptions** (all subclass `CcxError`): `ControllerReplayMismatch` (task calls
217
+ changed on resume), `ControllerLiveStateLost` (live variables from a failed
218
+ attempt), `ControllerOperationIndeterminate` (acceptance unknown),
219
+ `SessionWaiting` (input needed), `TurnIncomplete` (the turn didn't finish),
220
+ `CreatePartialFailure` (creation didn't finish; it carries the worker id),
221
+ `ControllerLaunchRequired` (the script wasn't started by `ccx controller run`).
222
+
223
+ Full reference: [Python API](https://github.com/ccx-native/ccx/blob/main/docs/python/README.md)
224
+ and [controllers guide](https://github.com/ccx-native/ccx/blob/main/docs/controllers.md).
225
+
226
+ ## CLI
227
+
228
+ Options go before the script; arguments after it go to the script.
229
+
230
+ ```bash
231
+ ccx controller run [--notify-parent] [--name NAME] SCRIPT [ARGS...]
232
+ ccx controller resume [--notify-parent] RUN [ARGS...]
233
+ ccx controller status RUN # tasks, worktrees and workers
234
+ ccx controller report RUN [--json] # --json includes the reported data
235
+ ccx controller logs RUN
236
+ ccx controller stop RUN # workers keep running
237
+ ccx controller list
238
+ ```
239
+
240
+ A Codex parent adds `--notify-parent` and ends its turn (`ccx controller
241
+ notify RUN` follows a run already going); Claude or a person waits by default.
242
+ `RUN` is a controller name (its newest run) or a unique run-id prefix. Launch
243
+ through `ccx controller run`, never directly with Python. ccx never merges,
244
+ pushes or removes worktrees: review and integrate the results yourself.
@@ -0,0 +1,94 @@
1
+ # ccx troubleshooting
2
+
3
+ ## Delivery and steering
4
+
5
+ `send` and `steer` have delivery ids (`--json`, or supply `--delivery-id`).
6
+ Retry the same message with the same id to avoid duplicate delivery.
7
+ `ccx reconcile TARGET --delivery-id ID` checks without sending; Codex also
8
+ accepts `--turn-id`.
9
+
10
+ If a `new` reply was lost, `ccx list` may find the worker. For retryable
11
+ creation, choose a UUIDv4 first and reuse it with `--worker-id`; bare `new`
12
+ always creates another worker.
13
+
14
+ A Claude `steer` watches for where the message landed:
15
+
16
+ | Status | Meaning |
17
+ |---|---|
18
+ | `consumed-current-turn` | the running turn took it |
19
+ | `started-next-turn` | it runs as the next turn |
20
+ | `accepted-pending` | ccx holds it; placement is unproven. Wait or reconcile; don't resend. |
21
+ | `unknown` | reconcile before retrying |
22
+
23
+ Pending Claude steering blocks other sends and steers; the refusal names its
24
+ delivery id. Codex steering accepts or rejects at once.
25
+
26
+ ## Stopped, failed and revived workers
27
+
28
+ Idle workers stop after `--idle-timeout`. Stopped or failed workers revive on
29
+ `send`, keeping their history. Interrupted turns aren't resumed or resent:
30
+ check `report`, then send what remains.
31
+
32
+ `ccx resume TARGET` reloads without starting a turn. `ccx shutdown` stops
33
+ runtimes, preserving histories; `--engine` limits it to one engine.
34
+
35
+ ## Workers waiting on background work
36
+
37
+ A Claude worker can finish a turn while a background task or monitor keeps
38
+ running, then wake for another turn. `peek` and `report` show background tasks.
39
+
40
+ To follow finite work the worker is waiting on (tests or a build):
41
+
42
+ - Claude or a person: `ccx wait TARGET --next-turn`
43
+ - Codex: `ccx notify TARGET --next-turn`
44
+
45
+ A server or watcher left running doesn't mean the work is unfinished.
46
+
47
+ ## Input requests
48
+
49
+ `ccx peek TARGET` shows pending input and its request id.
50
+
51
+ - **Question (Claude):** `ccx answer TARGET "Option label"`; see help for
52
+ multiple questions or selections.
53
+ - **Permission:** `ccx approve TARGET` or `ccx reject TARGET`.
54
+ - **MCP server form (Claude):** `ccx respond TARGET --accept --field
55
+ name=value`, `--decline` or `--cancel`.
56
+
57
+ To follow the resumed turn, Claude/human parents add `--wait` where supported;
58
+ Codex parents use `--notify-parent`.
59
+
60
+ ## Context and compaction
61
+
62
+ Right after compaction, `ccx context TARGET --json` may show `tokens_used: null`
63
+ until the next reading; `reason` explains why.
64
+
65
+ Claude compaction revives stopped workers; Codex compaction needs an idle
66
+ worker. `ccx notify TARGET` follows a compaction already running.
67
+
68
+ `ccx set TARGET --model M --effort E` needs an idle or stopped worker. Codex
69
+ uses the settings next turn. For a running, idle Claude worker, add `--restart`
70
+ to stop its runtime; the next send revives it with the new settings. A stopped
71
+ Claude worker uses them on revival. Let compaction settle before `set`.
72
+
73
+ ## Usage caps
74
+
75
+ Caps stop a turn at the given account usage percentage (exit 7). Check
76
+ `ccx usage` first. Caps apply only to that message; later sends need their own.
77
+ If usage can't be read, the turn keeps running.
78
+
79
+ ## Deeper inspection
80
+
81
+ - `ccx peek TARGET --full`: untruncated messages and tool errors.
82
+ - `ccx logs TARGET --full`: conversation JSON without tool-result truncation,
83
+ including stopped workers. Claude's `--jsonl` gives the raw transcript.
84
+ - `ccx events TARGET --follow`: lifecycle events for diagnostics.
85
+ - `ccx import ID`: bring an idle Claude session into ccx, or fork a Codex
86
+ thread whose latest turn completed.
87
+
88
+ ## Engine differences
89
+
90
+ - `answer`, `respond`, `--cap-opus`, `--next-turn` and `logs --jsonl` are
91
+ Claude-only. `--settle` (also wait for workers a Codex worker started) and
92
+ `reconcile --turn-id` are Codex-only.
93
+ - Codex workers share one ccx-managed Codex daemon. `kill` unloads a single
94
+ worker and leaves the others running.