ccx-native 0.0.1 → 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.
- package/LICENSE +21 -0
- package/README.md +241 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
package/ccx.py
ADDED
package/package.json
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccx-native",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "Fernando",
|
|
6
|
-
"license": "
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
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"
|
|
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.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.
|