ccx-native 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +807 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +269 -0
- package/ccx/claude/host/session.py +5244 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1558 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2424 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/history_store.py +131 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1454 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/models.py +225 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +296 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
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.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.
|