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
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,10 +1,42 @@
1
1
  {
2
2
  "name": "ccx-native",
3
- "version": "0.0.1",
4
- "description": "Placeholder for CCX: persistent native Claude Code and Codex agents. First release coming soon.",
5
- "author": "Fernando",
6
- "license": "UNLICENSED",
7
- "bin": { "ccx": "bin/ccx" },
8
- "files": ["bin/ccx", "README.md"],
9
- "homepage": "https://github.com/oneferdev/ccx"
3
+ "version": "0.2.0",
4
+ "description": "Native Claude Code and Codex sessions shared by your agents, scripts and terminals. Works with your own Claude and ChatGPT 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"
10
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.2.0"
8
+ description = "Native Claude Code and Codex sessions shared by your agents, scripts and terminals. Works with your own Claude and ChatGPT 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,166 @@
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 history TARGET...`: earlier turns as text. Lines carry addresses
65
+ (`#184.0`); `--turns 184.0` prints one message whole, `--turns 180-186` the
66
+ exchange. Search the workers you name with `--grep RE`, `--cmd pytest` or
67
+ `--errors` (the newest 20 hits each); `--view map` gives a long history at a
68
+ glance.
69
+ - After a compaction, read what you lost with `ccx history` on yourself
70
+ (`$CCX_SELF_WORKER_ID`), not the raw transcript. See
71
+ [references/history.md](references/history.md).
72
+ - `ccx logs TARGET`: the whole conversation as JSON, for programs.
73
+ - `ccx list`: find workers by name, state or folder.
74
+
75
+ ## Permissions
76
+
77
+ Workers default to full permissions: they can read, write and run anything your
78
+ user can, without permission prompts. Put read-only or "no edits" boundaries in
79
+ the task. Restrict permissions only if the user asks.
80
+
81
+ ## Reuse, compact or start fresh
82
+
83
+ Treat a worker like a teammate with working memory: reuse it when that memory
84
+ helps the next task, compact it when a smaller context will repay the cost of
85
+ summarizing it, and start fresh when the next task doesn't benefit from it.
86
+
87
+ `ccx peek TARGET --status --json` shows both what you need:
88
+ `context.tokens_used` (what the worker carries now, not total usage) and
89
+ `last_turn_age_s`.
90
+
91
+ - **Reuse** for the same task when the history helps. Design and strategy
92
+ conversations deserve long contexts; don't cut them for size alone.
93
+ - **Start fresh** for unrelated work or a different working set, even in the
94
+ same repository.
95
+ - **Compact** with `ccx compact TARGET` at a clean boundary above about 150k
96
+ tokens when most context is implementation residue (tool output) and the
97
+ next task uses the same files or domain. Summarizing costs tokens; when in
98
+ doubt, continue. Engines also compact automatically.
99
+
100
+ **Claude only:** treat the prompt cache as warm for one hour after the last
101
+ turn. Changing model or effort (`ccx set TARGET --model M --effort E`)
102
+ invalidates it. Above about 100k tokens with a warm cache, compact before
103
+ switching. With a cold cache, switch directly or start fresh if the history
104
+ is dispensable. These cache rules don't apply to Codex.
105
+
106
+ ## Variables: structured state between workers
107
+
108
+ Workers can share JSON state without files or parsing prose. Attach it with
109
+ `--var NAME=JSON` on `new`, `send` or `fork`; inside the worker, plain
110
+ `python3` sees `NAME` as a variable it can read and change, and changes
111
+ persist. Read it back with `ccx vars TARGET NAME --json`, and give another
112
+ worker, on either engine, the same shared state with `--vars-from TARGET`.
113
+
114
+ ```bash
115
+ A=$(ccx new --engine claude --cwd ~/repo --var 'plan={"steps": [], "done": false}' \
116
+ "Draft an implementation plan. With python3, append steps to plan['steps'], then set plan['done'] = True last.")
117
+ ccx wait "$A"
118
+ ccx vars "$A" plan --json
119
+ ```
120
+
121
+ Read [references/variables.md](references/variables.md) when sharing state
122
+ between workers or when `python3` doesn't see the variables.
123
+
124
+ ## Controllers: programmable workflows that run unattended
125
+
126
+ A controller is a Python script that drives many workers on its own, so you
127
+ don't have to start, wait on and collect each one by hand. It's ordinary
128
+ Python: it can branch on results, loop or fan out as the work requires. Its
129
+ steps are recorded, so a run can stop for a decision and resume later without
130
+ redoing finished steps, and workers hand back structured results through
131
+ shared variables instead of prose. A few lines from one:
132
+
133
+ ```python
134
+ futures = [review.submit(area) for area in ("api", "storage")] # workers in parallel
135
+ findings = [f.result() for f in futures] # structured results
136
+ if not current.inputs: # stop for a decision
137
+ current.report(status="needs-review", checkpoint="findings", data=findings)
138
+ return
139
+ ```
140
+
141
+ Start one with `ccx controller run SCRIPT` (a Codex parent adds
142
+ `--notify-parent` before the script). Read
143
+ [references/controllers.md](references/controllers.md) before writing one.
144
+
145
+ ## When something looks off
146
+
147
+ A wait timeout doesn't mean the work failed. Check `peek` or `report`;
148
+ for uncertain delivery, use `ccx reconcile TARGET --delivery-id ID`.
149
+ A server or watcher left running doesn't mean the task is unfinished.
150
+
151
+ Exit codes: 0 success · 1 bad input or usage · 2 not found · 3 unavailable ·
152
+ 4 waiting for input · 5 refused in this state · 6 incomplete (timeout, failed
153
+ turn or uncertain delivery) · 7 usage cap · 8 Anthropic rate limit ·
154
+ 9 transient API error.
155
+
156
+ More in [references/troubleshooting.md](references/troubleshooting.md).
157
+
158
+ ## Also useful
159
+
160
+ - `ccx fork TARGET ["prompt"]` copies a settled conversation into a new worker.
161
+ - `ccx attach TARGET` opens the worker in a terminal; Ctrl-Z detaches.
162
+ - `--cap-5h PCT` or `--cap-7d PCT` on `new` or `send` stops that turn when
163
+ account usage reaches PCT%. `ccx usage` shows the current levels.
164
+ - `ccx models` lists each engine's models and the efforts each supports. A
165
+ Claude alias such as `opus` can point to a newer model after a Claude Code
166
+ update; pass the full id from the Resolved column to stay on one model.
@@ -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,50 @@
1
+ # Reading worker history
2
+
3
+ `ccx history` prints past conversations of the workers you name, as text, and
4
+ searches them. It never sends anything and works on stopped and busy workers.
5
+
6
+ ## What a line means
7
+
8
+ #184.0 10-25 00:40 U: do we need to support apple silicon?
9
+ .7 A*: Yes for the CLI, no for the dashboard…
10
+ [12 tools · 4m10 · pytest×3, git×2]
11
+
12
+ - `#184.0` is turn 184, message 0: the same numbers as `ccx logs`. Inside a
13
+ turn, `.7` is message 7 of that turn.
14
+ - `U` user, `U+` a message sent while the turn ran, `N[kind]` a notification
15
+ or event (`bg`, `monitor`, `agent`, `cron`, `system`, `ccx`, …), `/` a slash
16
+ command, `!` an interrupt, `A` assistant text, `A*` the final answer, `A…`
17
+ the latest text of a turn still running, `T` a tool call (`✗` failed).
18
+ - `══ #212.3 compaction` marks a compaction; `--turns 212.3` prints its summary
19
+ when one is stored (Codex encrypts its summaries).
20
+
21
+ ## Recipes
22
+
23
+ | need | command |
24
+ |---|---|
25
+ | what a worker did lately | `ccx history W` |
26
+ | a long history at a glance | `ccx history W --view map` |
27
+ | where something was decided | `ccx history W --grep 'cache|eager' --role user,final` |
28
+ | the exchange around a hit | `ccx history W --turns 1032-1034` |
29
+ | one message in full | `ccx history W --turns 1033.12` |
30
+ | every instruction the user gave | `ccx history W --role user --turns 1-` |
31
+ | what woke a worker | `ccx history W --role notify` |
32
+ | how a worker tested | `ccx history W --cmd pytest --role tool,output` |
33
+ | what failed | `ccx history W --errors` |
34
+ | your children's last turns | `ccx history A B C --turns -1` |
35
+ | several workers in one period | `ccx history A B --since '11-07 21:00' --until '11-07 22:00'` |
36
+ | after your own compaction | `ccx history $CCX_SELF_WORKER_ID --turns <latest compaction address> --max-chars 2000` |
37
+
38
+ ## Things worth knowing
39
+
40
+ - A search shows the newest 20 hits per worker, oldest first; the count line
41
+ says how to see older ones. `-C 1` reads the matching turns instead of hit
42
+ lines.
43
+ - `--cmd` matches the command each shell call ran, not words inside it:
44
+ `--cmd python` finds `cd x && timeout 30 .venv/bin/python -m pytest`, not a
45
+ message that mentions python. `--grep` searches text.
46
+ - Tool output is searched only with `--role output`. A cut summary
47
+ (`--max-chars`) keeps its end, where current work and next steps are.
48
+ - Turn times come from the session; times are local, and the header names the
49
+ zone.
50
+ - Searching your own running turn finds your own `ccx history` call.