amirfish1 / ccc-orchestration
Spawn, inject into, and ask questions of persistent sibling sessions via Claude Command Center (CCC).
Interact with long-running peer sessions via the CCC HTTP server. Use only for persistent tasks (e.g., "marketing assistant", "deploy watcher") that need to show on the user's kanban. For one-shot subtasks, use the built-in Task tool instead.
1. Setup
Port and Path Discovery
Find the CCC URL. DO NOT try to start CCC yourself.
CCC_URL="$(cat ~/.claude/command-center/port.txt 2>/dev/null || echo "${CCC_URL:-http://127.0.0.1:8090}")"
# port.txt can be stale — a now-dead secondary server may have left it pointing
# at a refused port (OPS-41). Verify liveness; fall back to the canonical 8090
# if the recorded URL doesn't answer, so live checks don't hit a dead port.
curl -sf --max-time 2 "$CCC_URL/api/health" >/dev/null 2>&1 || CCC_URL="http://127.0.0.1:8090"
REPO_PATH="${CCC_REPO_PATH:-$(pwd -P)}"1.2 Network Sandbox Constraint
CRITICAL: The Bash sandbox blocks loopback connections. All CCC curls from the Bash tool can fail purely due to network sandboxing, not because CCC is down. You must run all CCC curls with the network sandbox disabled (localhost IPC). Ensure your tool execution options disable sandboxing for these requests.
Identifying Your Own Session UUID
If you need to know your own session UUID (e.g. to register yourself in a chat or trace your own session) and $CLAUDE_SESSION_ID is empty or unset in your Bash shell:
- Locate the project directory under
~/.claude/projects/corresponding to your slugified current working directory (e.g.,/Users/me/Apps/Notes+Tasksbecomes~/.claude/projects/-Users-me-Apps-Notes-Tasks). - Find the newest
*.jsonlfile in that directory; because you are actively executing, your active session transcript is the most recently modified. - The basename of that file (without the
.jsonlextension) is your session UUID.
2. API Operations
All operations (except List) use curl -s -X POST "$CCC_URL<endpoint>" -H "Content-Type: application/json" -d '<json>'.
URL-Encoding repo_path
CRITICAL: Any parameter or payload value for repo_path must be properly URL-encoded (especially + to %2B and spaces to %20).
- Example:
/Users/me/Apps/Notes+Tasksmust be sent as/Users/me/Apps/Notes%2BTasks. - Failure to URL-encode characters like
+will result in HTTP 400 Bad Request because+is decoded as a space by the server.
Avoiding Slow List Calls
- List endpoints can be slow:
/api/sessions?all=1and/api/sessions?repo_path=...can exceed 15s on repositories with many transcripts. - List is skippable: You only need List as an anti-double-spawn guard. If you already have the target
session_id, do not call List; proceed directly to Inject or Ask. - If List is genuinely needed, use a generous timeout (e.g.
curl --max-time 30). - Lightweight Health Probe: Do not run
/api/sessions?all=1to check if CCC is alive. Use/api/versioninstead, which is lightweight and fast.
Operations
Lightweight Health Check (GET):
/api/version
Returns the version of CCC. Use this to quickly verify CCC is up and running.List Current Repo (GET):
/api/sessions?repo_path=<URL-encoded abs path>
Returns the unified session list for one repo. Always check if a session for your topic exists before spawning!Resolving a session by name: rows have no
nameortitlefield — matching those silently misses every session. The canonical name isdisplay_name(the server already resolves custom title → spawn-time name → AI title → first message into it); fall back to substring-matchingfirst_messageif needed. Use the matched row'ssession_idfor Inject/Ask.curl -s --max-time 30 "$CCC_URL/api/sessions?repo_path=$ENC_REPO" | python3 -c ' import json,sys q=sys.argv[1].lower() for r in json.load(sys.stdin): hay=((r.get("display_name") or "")+" "+(r.get("first_message") or "")).lower() if q in hay: print(r["session_id"], "|", r.get("display_name")); break ' "marketing assistant"List Spawned Runs (GET):
/api/sessions/spawned
Returns recent CCC-owned spawns withspawn_id,session_id,parent_session_id,engine,model,reasoning_effort,repo_path,cwd, andspawned_at. Use this if a spawn response hassession_id_pending: true, or to confirm which engine/model/effort a spawn actually resolved to.
Dedupe fields: rows also carrytask_key(the caller's dedupe token),task_summary(first 200 chars of the RAW prompt —command_summary/promptis the WRAPPED prompt and starts with boilerplate, so grep this instead),prompt_hash, anddisplay_name(the AI/enriched title, present only once the session has an archive row). Filters:?engine=,?task_key=(exact),?parent=<sid prefix, ≥8 chars>,?q=(case-insensitive substring over name/displayname/tasksummary/taskkey/prompt), `?includefinished=1(adds rows from spawned-history.json withendedat/exitcode—exit_code` is null for worker-owned and sweep-dropped rows).*List All (GET):
/api/sessions?all=1(optional&engine=codex|antigravity|claude)
Returns cross-repo sessions plus the spawned-run registry.Spawn:
/api/sessions/spawn
Payload:{"prompt": "...", "repo_path": "/abs/repo", "engine": "claude|codex|antigravity", "model": "...", "reasoning_effort": "low|medium|high|xhigh|max"}.repo_path(orcwd) is required.engine,model, andreasoning_effortare optional; when omitted, CCC uses the server-side defaults from Settings → Spawn defaults…. Legacygeminimaps toantigravity. When unsure of a model id, omitmodelentirely rather than guessing — a made-up id can spawn a broken session. If you do need to see valid ids,GET /api/engines/modelsreturns{"engines": {"codex": ["gpt-5.5", ...], "claude": [...], ...}, "efforts_by_engine": {"claude": ["low", ..., "max"], "codex": [...], "cursor": [], ...}, "enforced": ["codex"]}. Onlycodexis currently rejected (400) on an unrecognized model — e.g.gpt-5.5-codexis not a real id, the correct one isgpt-5.5.
Reasoning effort: the ladder is per engine, so read it fromefforts_by_enginerather than assuming one list. Claude acceptslow, medium, high, xhigh, max; Codex tops out atxhigh(nomax); Kimi offers only what its ownconfig.tomldeclares; an engine with an empty list has no effort concept at all. Unlikemodel, an effort the engine does not accept is dropped, not rejected: the spawn succeeds at that engine's default effort, so never infer from a200that your value was applied. Confirm viaGET /api/sessions/spawned(reasoning_efforton the row)."effort"is accepted as an alias.
Dedupe a spawn before paying for it: pass"task_key": "<stable-key>"(a ticket id or task slug, e.g."verify-d08302b3"— repo-scoped). If a live lane already carries the key, the response is200 {"ok": true, "existing": true, "pid": ..., "session_id": ...}and NO new session is spawned; a lane that already finished matches too ("finished": true,ended_at,exit_code) so "was this already done" is answered without respawning. Concurrent identical spawns collapse: the second request waits on the first's result ("deduped": "in_flight"). Two sharp edges: completion reports go to the lane's ORIGINAL dispatcher ("report_back": "original_dispatcher"), andtask_keyis deliberately notidempotency_key— that field means "replay this exact request" at the work-ledger layer. Without a key,"dedupe": truededupes on the raw prompt's exact hash instead. Prefer dedupe-firstGET /api/sessions/spawned?task_key=<key>&include_finished=1for a cheap pre-check.
Return address + hierarchy (recommended for spawned siblings): Add"report_to": "<your-session-id>"(aliases:return_to,reply_to) to the payload. CCC appends a footer to the spawned agent's prompt instructing it to POST a structured completion report back to your session via/api/inject-inputwhen it finishes (success or failure), and uses the same id asparent_session_idso the child appears nested under the dispatcher in Current Sessions. Use explicit"parent_session_id": "<session-id>"only when the visual parent differs from the report target.
Returns:{"ok": true, "session_id": "...", "spawn_id": "123", "parent_session_id": "...", "engine": "...", "repo_path": "...", "cwd": "...", "session_id_pending": false}. Prefersession_idimmediately; if pending, poll Spawned Runs byspawn_id.Inject (Fire & Forget):
/api/inject-input
Payload:{"session_id": "<uuid>", "text": "...", "peer_sender_sid": "<your-session-id>"}. CCC detects the target session's engine; live Claude sessions receive the message over native Unix Domain Socket (UDS) peer transport without interrupting active turns or touching FIFOs.peer_sender_sid(orsender_session_id) attributes the message to your session. Or use theccc send <target> <text>CLI.Ask (Sync/Wait):
/api/ask
Payload:{"session_id": "<uuid>", "text": "...", "timeout_ms": 60000}.
Returns:{"ok": true, "text": "reply"}. On timeout, work continues (you can re-ask or notify user). Requires a real enginesession_id, not only a pendingspawn_id.
Continuing Old Work: Refer, Don't Resume
Injecting/resuming a session whose context is large AND has gone cold (idle past the prompt-cache TTL) reloads its whole transcript — a real case re-loaded 430k tokens to do a few merges. Never ccc send/Inject a follow-up task into an old session just to pick up where it left off. Instead, spawn a fresh session that continues it:
- CLI:
ccc spawn --continue-from <old sid|prefix> "<task>"— resolves to the latest successor in the session's continuation chain, reuses its cwd/engine/model/effort, and seeds the new session with a short brief (title, transcript path,ccc brief/tail-by-lines pointers) instead of the whole transcript. - CLI (decide for you):
ccc send <sid> "<task>" --new-if-large-and-stale— sends normally unless the target is both large (context tokens) and stale (idle past the cache TTL), in which case it does the above instead. Add--dry-runto preview the decision without acting. - API equivalent:
POST /api/sessions/spawn-continue-fromwith{"continue_from": "<sid>", "prompt": "..."};GET /api/sessions/continuation-decision/<sid>for the large/stale decisionccc send's flag uses. - Any child or WatchTower ticket already reporting to the old session is automatically rebound to the new one; delivery to the old sid keeps landing on its latest successor from then on.
Group Chat Operations
You can manage group chats programmatically via the API endpoints or manually via the UI.
Create Chat (POST):
/api/group-chat/create
Payload:{"topic": "<title>", "session_ids": ["<uuid1>", "<uuid2>"], "include_human": true}.include_humanis optional and defaults totrue.
Returns:{"ok": true, "chat_path": "~/.claude/group-chats/slug-timestamp.md", "id": "<chat-uuid>", "uuid": "<chat-uuid>", "results": [...]}.
This registers the participants in the chat'sname_map, generates the.mdand.jsonsidecar files, and injects a group-chat check-in instruction (thegroup-chat-checkinskill) into the target sessions so they join automatically.Add Participant (POST):
/api/group-chat/add
Payload:{"chat_id": "<chat-uuid>", "session_id": "<uuid>", "display_name": "<name>"}(you can also passchat_pathinstead ofchat_id).display_nameis optional.
Returns:{"ok": true, "session_id": "<uuid>"}.
This registers the new participant session in the chat's metadata and injects a group-chat check-in instruction (thegroup-chat-checkinskill) into the session so it joins the live chat.
Manual UI Fallback:
- Open the CCC UI in your browser at
http://127.0.0.1:8090. - Navigate to the Chats or Group Chats tab and click Create New Chat.
- Enter the topic, search/select the participant sessions by UUID, and click Create/Start.
2.5 Federation — sessions on other machines
CCC instances can be paired across machines (Settings → Nodes & peers…).
You never write SSH commands — your local CCC routes everything.
Global session reference:
"<node_id>:<session_id>". Anywhere asession_idis accepted (Inject, Ask,report_to, group-chat targets),
a global ref transparently proxies to the owning CCC. A baresession_id
always means "this node" — existing calls are unchanged.Discover nodes (GET):
/api/federation/peers→ `{"self": {"node_id",
"displayname", ...}, "peers": [{"nodeid", "name", "transport", ...}]}`.Federated list (GET):
/api/sessions?federated=1→ one list across all
nodes. Each row carriesnode_id,node_name,ref(the global
reference), andstale(true when served from cache because the peer is
unreachable). Thenodesarray reports per-peer health — treatok: falsepeers as offline, not empty.Spawn on another node: add
"node": "<node_id or peer name>"to the
spawn payload. Keep passing YOUR localrepo_path— CCC translates it to
the stable repo identity and the target node maps it to its own clone
(paths never travel). Response gainsnode_id,ref, andspawn_ref.
Pass"report_to": "<your bare session id>"as usual; CCC globalizes it
so the child's completion report crosses machines back to you.Group chats across machines: chat rows include
host_node(the node
owning the chat files). If a chat'shost_nodeis not your node, do NOT
touch its path — call your own CCC with the id + host:
readGET /api/group-chat/read?id=<uuid>&host_node=<node_id>;
post/nudge/add: include"host_node": "<node_id>"in the JSON payload.Ownership (handoff) errors: a 409
not_ownerwithowner_nodemeans
the conversation was handed to another machine — re-address it as"<owner_node>:<session_id>".GET /api/federation/handoff/status?session_id=
shows the lease.Typed federation errors:
peer_offline(node unreachable),timeout
(work may still continue there),unpaired_peer(pair first),stale_mapping(repo not mapped on that node),unsupported_capability,routing_loop. Report these truthfully; do not retryunpaired_peerorunsupported_capability.
3. Strict Rules
- No one-shot tasks: Use the built-in
Tasktool for quick delegation. - No tight polling:
/api/askblocks until a reply or timeout. - No duplicate spawning: Check List Current Repo first. Users pay for each spawned session.
Triage Errors:
If acurlcall fails, do not immediately assume CCC is offline. Triage the failure based on the exit code or HTTP status:- exit 7 (Connection Refused): CCC is genuinely down. Ask the user to start CCC.
- exit 28 (Timeout): CCC is up but slow/under load. Retry with a longer timeout (e.g.,
--max-time 30), do not declare it down. - HTTP status 000 (or connection failure) while sandboxed: The loopback connection is blocked by the sandboxed environment. Retry with the network sandbox disabled before concluding anything.