@gonzih/cc-wire 0.1.7 → 0.3.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/README.md CHANGED
@@ -1,70 +1,178 @@
1
1
  # @gonzih/cc-wire
2
2
 
3
- Single source of truth for Redis channel names, key patterns, and message shapes across the cc-suite (`cc-agent`, `cc-tg`, `cc-agent-ui`).
3
+ Single source of truth for Redis channel names, key patterns, message shapes, and
4
+ storage runtime across the cc-suite (`cc-agent`, `cc-discord`, `cc-tg`, `cc-agent-ui`).
4
5
 
5
- No runtime dependencies. No Redis client. Just constants, builders, and types.
6
+ v0.3.0+: ships a typed storage runtime services call `createCcWire(redis)` and
7
+ never touch raw Redis or import key builders directly.
6
8
 
7
9
  ## Install
8
10
 
9
11
  ```sh
10
- npm install @gonzih/cc-wire
12
+ npm install @gonzih/cc-wire ioredis
11
13
  ```
12
14
 
13
- ## Usage
15
+ ## Architecture — Service Ownership
16
+
17
+ ```
18
+ ┌──────────────────────────────────────────────────────────────────┐
19
+ │ cc-wire (this package) │
20
+ │ Owns: Redis key contracts, type definitions │
21
+ └─────────────────────────────────┬────────────────────────────────┘
22
+ │ imported by
23
+ ┌───────────────────────┼───────────────────────┐
24
+ │ │ │
25
+ ▼ ▼ ▼
26
+ ┌─────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
27
+ │ cc-discord │ │ cc-tg │ │ cc-agent │
28
+ │ │ │ │ │ │
29
+ │ Owns: │ │ Owns: │ │ Owns: │
30
+ │ per-namespace │ │ money-brain session│ │ job execution │
31
+ │ Claude sessions│ │ (single session) │ │ (spawn_agent) │
32
+ │ Discord bridge │ │ Telegram bridge │ │ no meta-agent │
33
+ │ │ │ │ │ lifecycle │
34
+ │ Workspace: │ │ Workspace: │ │ │
35
+ │ ~/cc-discord- │ │ ~/money-brain │ │ │
36
+ │ workspace/{ns} │ │ │ │ │
37
+ └─────────────────┘ └─────────────────────┘ └──────────────────┘
38
+ ```
39
+
40
+ **Key principle:** cc-tg and cc-discord are completely isolated — they share no Redis keys,
41
+ no sessions, and no state. cc-agent is a pure job runner; it no longer manages meta-agent
42
+ lifecycle.
43
+
44
+ ## Runtime — `createCcWire(redis)`
45
+
46
+ Pass an [ioredis](https://github.com/redis/ioredis) `Redis` instance and get back
47
+ fully-typed storage APIs per service. No raw Redis commands, no key string management.
48
+
49
+ ```typescript
50
+ import { createClient } from "ioredis";
51
+ import { createCcWire } from "@gonzih/cc-wire";
52
+
53
+ const redis = new Redis(process.env.REDIS_URL);
54
+ const wire = createCcWire(redis);
55
+
56
+ // cc-discord: per-namespace session management
57
+ await wire.discord.enqueue("simorgh", { id: "...", source: "discord", ... });
58
+ const msg = await wire.discord.dequeue("simorgh"); // ChatMessage | null
59
+ await wire.discord.publishOutgoing("simorgh", msg); // PUBLISH + log + LTRIM
60
+ wire.discord.subscribeOutgoing("simorgh", (msg) => console.log(msg)); // live stream
61
+ await wire.discord.setStatus("simorgh", { isTyping: true, ... });
62
+ const status = await wire.discord.getStatus("simorgh"); // MetaAgentStatus | null
63
+ await wire.discord.writeChatLog("simorgh", msg);
64
+ const history = await wire.discord.getChatLog("simorgh", 50); // chronological
65
+ await wire.discord.notify("simorgh", { text: "job done" });
66
+ const notif = await wire.discord.pollNotify("simorgh"); // NotificationPayload | null
67
+ await wire.discord.registerChannel("1234567890", "simorgh", repoUrl);
68
+ const chan = await wire.discord.getChannel("1234567890");
69
+ const channels = await wire.discord.listChannels();
70
+
71
+ // cc-tg: single money-brain session
72
+ await wire.tg.publishOutgoing(msg);
73
+ wire.tg.subscribeOutgoing((msg) => console.log(msg));
74
+ await wire.tg.notify({ text: "job done" });
75
+ const tgNotif = await wire.tg.pollNotify();
76
+
77
+ // cc-agent: job queue
78
+ await wire.jobs.enqueue({ id: "job-001", repoUrl: "...", task: "..." });
79
+ const job = await wire.jobs.getStatus("job-001");
80
+ await wire.jobs.publishDone("job-001", { status: "done", score: 1.0 });
81
+
82
+ // shared master token
83
+ await wire.token.setMaster("my-token");
84
+ const token = await wire.token.getMaster();
85
+
86
+ // raw redis escape hatch (migration only)
87
+ const raw = wire._redis;
88
+ ```
89
+
90
+ ## Key builders (v0.2.x compat, use runtime in new code)
14
91
 
15
92
  ```typescript
16
93
  import {
17
- wikiKey,
18
- wikiUpdatedKey,
19
- notifyChannel,
20
- notifyListKey,
21
- notifyPublishCommand,
94
+ // cc-discord service keys
95
+ discordMetaInputKey,
96
+ discordMetaStatusKey,
97
+ discordChatOutgoing,
98
+ discordChatLog,
99
+ discordChatIncoming,
100
+ discordNotify,
101
+ // cc-tg service keys
102
+ tgChatOutgoing,
103
+ tgChatIncoming,
104
+ tgNotify,
105
+ // Service ownership constants
106
+ CC_DISCORD_WORKSPACE_ROOT,
107
+ CC_TG_WORKSPACE,
108
+ // Job keys (cc-agent)
22
109
  jobKey,
23
110
  jobIndexKey,
24
- chatLogKey,
111
+ // Shared utility
112
+ notifyPublishCommand,
25
113
  TTL,
26
114
  CAP,
27
- type Transport,
28
- type NotificationPayload,
115
+ type ChatMessage,
116
+ type SpawnParams,
29
117
  } from "@gonzih/cc-wire";
30
118
 
31
- // Wiki keys
32
- const hash = wikiKey("org/repo"); // "cca:wiki:org/repo"
33
- const ts = wikiUpdatedKey("org/repo"); // "cca:wiki:org/repo:updated"
119
+ // cc-discord — per-namespace session keys
120
+ const inputQueue = discordMetaInputKey("simorgh-mobile-app");
121
+ // => "cca:discord:meta:simorgh-mobile-app:input"
122
+ const statusKey = discordMetaStatusKey("simorgh-mobile-app");
123
+ // => "cca:discord:meta:simorgh-mobile-app:status"
124
+ const notifyChan = discordNotify("simorgh-mobile-app");
125
+ // => "cca:discord:notify:simorgh-mobile-app"
126
+
127
+ // cc-tg — single dedicated money-brain session
128
+ const tgOut = tgChatOutgoing(); // "cca:tg:chat:outgoing"
129
+ const tgIn = tgChatIncoming(); // "cca:tg:chat:incoming"
130
+ const tgNtfy = tgNotify(); // "cca:tg:notify"
131
+
132
+ // Workspace paths
133
+ const discordWs = `${process.env.HOME}/${CC_DISCORD_WORKSPACE_ROOT}/simorgh-mobile-app`;
134
+ const tgWs = `${process.env.HOME}/${CC_TG_WORKSPACE}`;
135
+
136
+ // ChatMessage — with service field
137
+ const msg: ChatMessage = {
138
+ id: crypto.randomUUID(),
139
+ source: "discord",
140
+ service: "cc-discord",
141
+ role: "user",
142
+ content: "deploy the app",
143
+ namespace: "simorgh-mobile-app",
144
+ timestamp: new Date().toISOString(),
145
+ };
146
+ ```
34
147
 
35
- // Job keys
36
- const job = jobKey("abc-123"); // "cca:job:abc-123"
37
- const idx = jobIndexKey("myns"); // "cca:jobs:myns"
148
+ ## Key Reference
38
149
 
39
- // Notify
40
- const chan = notifyChannel("myns"); // "cca:notify:myns"
41
- const list = notifyListKey("myns"); // "cca:notify:myns" (same key, dual-purpose)
150
+ ### cc-discord Keys
42
151
 
43
- // NotificationPayload routing controls which transports deliver the message
44
- const payload: NotificationPayload = {
45
- text: "Build finished ✓",
46
- routing: ["discord"], // omit or leave empty for all transports
47
- };
152
+ cc-discord owns all namespace-scoped Claude sessions. All keys live under `cca:discord:`.
48
153
 
49
- // notifyPublishCommand generate a redis-cli shell command
50
- const cmd = notifyPublishCommand("myns", payload);
51
- // => "redis-cli PUBLISH 'cca:notify:myns' '{\"text\":\"Build finished ✓\",\"routing\":[\"discord\"]}'"
154
+ | Builder | Pattern | Redis type | Owner | Description |
155
+ |---|---|---|---|---|
156
+ | `discordMetaInputKey(ns)` | `cca:discord:meta:{ns}:input` | LIST | cc-discord | Input queue for namespace session (RPUSH/RPOP). |
157
+ | `discordMetaStatusKey(ns)` | `cca:discord:meta:{ns}:status` | STRING (JSON) | cc-discord | Live session status (typing, tool, etc.), TTL 7d. |
158
+ | `discordChatOutgoing(ns)` | `cca:discord:chat:outgoing:{ns}` | CHANNEL | cc-discord | Outgoing messages to UI (pub/sub). |
159
+ | `discordChatLog(ns)` | `cca:discord:chat:log:{ns}` | LIST | cc-discord | Chat history, capped at 500, LIFO. |
160
+ | `discordChatIncoming(ns)` | `cca:discord:chat:incoming:{ns}` | CHANNEL | cc-discord | Incoming messages from UI/Discord (pub/sub). |
161
+ | `discordNotify(ns)` | `cca:discord:notify:{ns}` | CHANNEL + LIST | cc-discord | Job-completion notifications (PUBLISH + RPOP poll). |
52
162
 
53
- // Constants
54
- TTL.JOB_SECONDS // 604800
55
- CAP.CHAT_LOG // 500
56
- ```
163
+ ### cc-tg Keys
57
164
 
58
- ## Key Reference
165
+ cc-tg owns a single dedicated `money-brain` session with no namespace scoping.
59
166
 
60
- ### Wiki
167
+ | Builder | Pattern | Redis type | Owner | Description |
168
+ |---|---|---|---|---|
169
+ | `tgChatOutgoing()` | `cca:tg:chat:outgoing` | CHANNEL | cc-tg | Outgoing messages to UI (pub/sub). |
170
+ | `tgChatIncoming()` | `cca:tg:chat:incoming` | CHANNEL | cc-tg | Incoming messages from UI/Telegram (pub/sub). |
171
+ | `tgNotify()` | `cca:tg:notify` | CHANNEL + LIST | cc-tg | Job-completion notifications (PUBLISH + RPOP poll). |
61
172
 
62
- | Builder | Pattern | Redis type | Description |
63
- |---|---|---|---|
64
- | `wikiKey(repoSlug)` | `cca:wiki:{repoSlug}` | HASH | Per-repo wiki pages. Field = page name, value = markdown. |
65
- | `wikiUpdatedKey(repoSlug)` | `cca:wiki:{repoSlug}:updated` | STRING | ISO timestamp of last wiki update. |
173
+ ### cc-agent Keys (jobs only)
66
174
 
67
- ### Jobs
175
+ cc-agent is a pure job runner. It no longer manages meta-agent lifecycle.
68
176
 
69
177
  | Builder | Pattern | Redis type | Description |
70
178
  |---|---|---|---|
@@ -76,183 +184,80 @@ CAP.CHAT_LOG // 500
76
184
  | `jobDoneChannel(id)` | `cca:job:done:{id}` | CHANNEL | Job completion pub/sub. |
77
185
  | `jobDoneQueueKey(id)` | `cca:job:done:{id}:queue` | LIST | LPUSH/BLPOP queue for `wait_for_job`, TTL 7d. |
78
186
  | `jobIndexKey(ns)` | `cca:jobs:{ns}` | SET | Job IDs per namespace. |
187
+ | `EVENT_STREAM` | `cca:event-stream` | STREAM | Job status events (XADD/XREADGROUP). |
188
+ | `COORDINATOR_GROUP` | `coordinator` | — | Consumer group name. |
79
189
 
80
- ### Event Stream
81
-
82
- | Constant | Value | Description |
83
- |---|---|---|
84
- | `EVENT_STREAM` | `cca:event-stream` | Redis Stream — job status events. |
85
- | `COORDINATOR_GROUP` | `coordinator` | Consumer group name. |
86
-
87
- ### Notify / Chat
88
-
89
- | Builder | Pattern | Description |
90
- |---|---|---|
91
- | `notifyChannel(ns)` | `cca:notify:{ns}` | CHANNEL — coordinator publishes job completion. |
92
- | `notifyListKey(ns)` | `cca:notify:{ns}` | LIST — delivery queue (RPUSH/RPOP). Same key as channel (safe — Redis pub/sub and list namespaces are independent). |
93
- | `notifyLogKey(ns)` | `cca:notify-log:{ns}` | LIST — persistent audit log, capped at `CAP.NOTIFY_LOG` (100). |
94
- | `notifyPublishCommand(ns, payload)` | — | Returns a `redis-cli PUBLISH` shell command string. Useful for cron prompts. |
95
- | `chatLogKey(ns)` | `cca:chat:log:{ns}` | LIST — chat history, capped at `CAP.CHAT_LOG` (500), LIFO. |
96
- | `chatIncomingChannel(ns)` | `cca:chat:incoming:{ns}` | CHANNEL — UI → cc-tg. |
97
- | `chatOutgoingChannel(ns)` | `cca:chat:outgoing:{ns}` | CHANNEL — cc-tg → UI. |
98
-
99
- ### Meta-Agent
100
-
101
- | Builder | Pattern | Description |
102
- |---|---|---|
103
- | `metaKey(ns)` | `cca:meta:{ns}` | STRING (JSON) — MetaAgentInfo state, TTL 30d. |
104
- | `metaInputKey(ns)` | `cca:meta:{ns}:input` | LIST — input queue (RPUSH/RPOP). |
105
- | `metaAgentStatusKey(ns)` | `cca:meta-agent:status:{ns}` | STRING (JSON) — live status, TTL 7d. |
106
- | `META_AGENTS_INDEX` | `cca:meta:agents:index` | SET — canonical registry. |
107
-
108
- ### Wiki
109
-
110
- | Builder | Pattern | Description |
111
- |---|---|---|
112
- | `wikiKey(repoSlug)` | `cca:wiki:{repoSlug}` | HASH — wiki pages. Field = page name, value = markdown. |
113
- | `wikiUpdatedKey(repoSlug)` | `cca:wiki:{repoSlug}:updated` | STRING — ISO timestamp of last update. |
114
-
115
- ### Profiles
116
-
117
- | Builder | Pattern | Description |
118
- |---|---|---|
119
- | `profileKey(name)` | `cca:profile:{name}` | STRING (JSON) — saved Profile. |
120
- | `PROFILES_INDEX` | `cca:profiles:index` | SET — profile names. |
121
-
122
- ### Crons
123
-
124
- | Builder | Pattern | Description |
125
- |---|---|---|
126
- | `cronsKey(ns)` | `cca:crons:{ns}` | STRING (JSON array) — cron definitions. |
127
- | `deletedCronsKey(ns)` | `cca:deleted-crons:{ns}` | SET — tombstone IDs, TTL 7d. |
128
-
129
- ### Learnings
130
-
131
- | Builder | Pattern | Description |
132
- |---|---|---|
133
- | `learningsKey(ns)` | `cca:learnings:{ns}` | LIST — learnings (LPUSH, capped at `CAP.LEARNINGS` = 50), TTL 90d, LIFO. |
134
-
135
- ### Plans / Coordinator
136
-
137
- | Builder | Pattern | Description |
138
- |---|---|---|
139
- | `planKey(id)` | `cca:plan:{id}` | STRING (JSON) — PlanRecord, TTL 30d. |
140
- | `coordinatorPlanKey(jobId)` | `cca:coordinator:plan:{jobId}` | STRING — coordinator plan JSON. |
141
-
142
- ### Swarm
190
+ ### Shared / Utility Keys
143
191
 
144
- | Builder | Pattern | Description |
145
- |---|---|---|
146
- | `swarmKey(id)` | `cca:swarm:{id}` | STRING (JSON) SwarmRecord. |
147
- | `SWARM_REQUESTS_KEY` | `cca:swarm:requests` | LIST — task request queue (LPUSH). |
148
-
149
- ### Version / Token
192
+ | Builder / Constant | Pattern | Redis type | Description |
193
+ |---|---|---|---|
194
+ | `notifyChannel(ns)` | `cca:notify:{ns}` | CHANNEL | Legacy coordinator notify (use service-scoped builders for new code). |
195
+ | `notifyListKey(ns)` | `cca:notify:{ns}` | LIST | Same key delivery queue. |
196
+ | `notifyLogKey(ns)` | `cca:notify-log:{ns}` | LIST | Notification audit log, capped at 100, LIFO. |
197
+ | `notifyPublishCommand(ns, payload)` | — | — | Returns a `redis-cli PUBLISH` shell command string. Useful for cron prompts. |
198
+ | `planKey(id)` | `cca:plan:{id}` | STRING (JSON) | PlanRecord, TTL 30d. |
199
+ | `coordinatorPlanKey(jobId)` | `cca:coordinator:plan:{jobId}` | STRING | Coordinator plan JSON. |
200
+ | `profileKey(name)` | `cca:profile:{name}` | STRING (JSON) | Saved Profile. |
201
+ | `PROFILES_INDEX` | `cca:profiles:index` | SET | Profile names. |
202
+ | `cronsKey(ns)` | `cca:crons:{ns}` | STRING (JSON array) | Cron definitions. |
203
+ | `deletedCronsKey(ns)` | `cca:deleted-crons:{ns}` | SET | Tombstone IDs, TTL 7d. |
204
+ | `learningsKey(ns)` | `cca:learnings:{ns}` | LIST | Learnings (LPUSH, capped at 50), TTL 90d, LIFO. |
205
+ | `wikiKey(repoSlug)` | `cca:wiki:{repoSlug}` | HASH | Wiki pages. Field = page name, value = markdown. |
206
+ | `wikiUpdatedKey(repoSlug)` | `cca:wiki:{repoSlug}:updated` | STRING | ISO timestamp of last wiki update. |
207
+ | `swarmKey(id)` | `cca:swarm:{id}` | STRING (JSON) | SwarmRecord. |
208
+ | `SWARM_REQUESTS_KEY` | `cca:swarm:requests` | LIST | Swarm task request queue (LPUSH). |
209
+ | `CC_AGENT_VERSION_KEY` | `cca:meta:cc-agent:version` | STRING | Running cc-agent version. |
210
+ | `CC_TG_VERSION_KEY` | `cca:meta:cc-tg:version` | STRING | Running cc-tg version. |
211
+ | `TOKEN_INDEX_KEY` | `cca:token:index` | STRING | Token rotation index. |
212
+ | `VOICE_PENDING_KEY` | `voice:pending` | LIST | Transcription pending queue (cc-tg only). |
213
+ | `VOICE_FAILED_KEY` | `voice:failed` | LIST | Failure log, TTL 48h (cc-tg only). |
150
214
 
151
- | Constant | Value | Description |
152
- |---|---|---|
153
- | `CC_AGENT_VERSION_KEY` | `cca:meta:cc-agent:version` | STRING — running cc-agent version. |
154
- | `CC_TG_VERSION_KEY` | `cca:meta:cc-tg:version` | STRING — running cc-tg version. |
155
- | `TOKEN_INDEX_KEY` | `cca:token:index` | STRING — token rotation index. |
215
+ ### Service Ownership Constants
156
216
 
157
- ### Voice (cc-tg only)
217
+ ```typescript
218
+ CC_DISCORD_WORKSPACE_ROOT // "cc-discord-workspace"
219
+ // Usage: `${HOME}/${CC_DISCORD_WORKSPACE_ROOT}/{namespace}`
158
220
 
159
- | Constant | Value | Description |
160
- |---|---|---|
161
- | `VOICE_PENDING_KEY` | `voice:pending` | LIST — transcription pending queue. |
162
- | `VOICE_FAILED_KEY` | `voice:failed` | LIST — failure log, TTL 48h. |
221
+ CC_TG_WORKSPACE // "money-brain"
222
+ // Usage: `${HOME}/${CC_TG_WORKSPACE}`
223
+ ```
163
224
 
164
225
  ## Constants
165
226
 
166
227
  ```typescript
167
- TTL.JOB_SECONDS // 604800 (7 days)
168
- TTL.PLAN_SECONDS // 2592000 (30 days)
169
- TTL.LEARNINGS_SECONDS // 7776000 (90 days)
170
- TTL.VOICE_FAILED_SECONDS // 172800 (48 hours)
171
-
172
- CAP.NOTIFY_LOG // 100
173
- CAP.CHAT_LOG // 500
174
- CAP.LEARNINGS // 50
175
- CAP.EVENT_STREAM // 500
176
-
177
- TIMING.COORDINATOR_POLL_MS // 2000
178
- TIMING.DEPENDENCY_TICK_MS // 3000
179
- TIMING.INPUT_POLL_INTERVAL_MS // 3000
180
- TIMING.META_AGENT_FLUSH_DELAY_MS // 1500
228
+ TTL.JOB_SECONDS // 604800 (7 days)
229
+ TTL.PLAN_SECONDS // 2592000 (30 days)
230
+ TTL.LEARNINGS_SECONDS // 7776000 (90 days)
231
+ TTL.VOICE_FAILED_SECONDS // 172800 (48 hours)
232
+
233
+ CAP.NOTIFY_LOG // 100
234
+ CAP.CHAT_LOG // 500
235
+ CAP.LEARNINGS // 50
236
+ CAP.EVENT_STREAM // 500
237
+
238
+ TIMING.COORDINATOR_POLL_MS // 2000
239
+ TIMING.DEPENDENCY_TICK_MS // 3000
240
+ TIMING.INPUT_POLL_INTERVAL_MS // 3000
241
+ TIMING.META_AGENT_FLUSH_DELAY_MS // 1500
181
242
  ```
182
243
 
183
244
  ## Notification Routing
184
245
 
185
- Job-completion notifications flow through `cca:notify:{namespace}`. The namespace that
186
- receives the notification is determined by `spawning_namespace` on the spawn call:
187
-
188
- ```
189
- coordinator: targetNamespace = job.spawningNamespace ?? coordinator.namespace
190
- ```
191
-
192
- If `spawning_namespace` is absent the notification falls back to the coordinator's own
193
- namespace (typically `"money-brain"`), which is the source of the routing bug described
194
- below.
246
+ Each service subscribes to its own notify channel for job-completion notifications:
195
247
 
196
- ### Broken flow Discord jobs notify Telegram instead of Discord
197
-
198
- ```
199
- Discord #simorgh-mobile-app
200
- → cc-discord routeToMetaAgent("simorgh-mobile-app")
201
- → RPUSH cca:meta:simorgh-mobile-app:input
202
- → cc-agent meta-agent picks up message
203
- → spawn_agent { repoUrl, task } ← no spawning_namespace
204
- → Coordinator: targetNs = coordinator.namespace = "money-brain"
205
- → PUBLISH cca:notify:money-brain
206
- → cc-tg (subscribed to cca:notify:money-brain) → Telegram ✗
207
- ```
208
-
209
- Two bugs combine to produce this:
210
-
211
- | Bug | Repo | Description |
248
+ | Service | Notify key builder | Pattern |
212
249
  |---|---|---|
213
- | A | `cc-agent` | `spawn_agent` MCP handler does not auto-inject `spawning_namespace` when called from within a meta-agent context. cc-tg works around this by injecting it client-side; meta-agents cannot. |
214
- | B | `cc-discord` | The notifier subscribes only to `cca:notify:{CC_AGENT_NAMESPACE}` (one hardcoded namespace). Even after Bug A is fixed, cc-discord would not hear notifications on `cca:notify:simorgh-mobile-app`. |
250
+ | cc-discord | `discordNotify(ns)` | `cca:discord:notify:{ns}` |
251
+ | cc-tg | `tgNotify()` | `cca:tg:notify` |
215
252
 
216
- ### Correct flow after both fixes
253
+ Both use the same dual-purpose pattern: the coordinator PUBLISHes on the channel (pub/sub)
254
+ and also RPUSHes to the same key as a list (poll fallback). Redis pub/sub and list namespaces
255
+ are independent so this is safe.
217
256
 
218
- ```
219
- Discord #simorgh-mobile-app
220
- → cc-discord routeToMetaAgent("simorgh-mobile-app")
221
- → RPUSH cca:meta:simorgh-mobile-app:input
222
- → cc-agent meta-agent picks up message
223
- → spawn_agent { repoUrl, task }
224
- ← cc-agent MCP injects spawning_namespace = "simorgh-mobile-app"
225
- → Coordinator: targetNs = "simorgh-mobile-app"
226
- → PUBLISH cca:notify:simorgh-mobile-app
227
- → cc-discord (subscribed to cca:notify:simorgh-mobile-app)
228
- → Discord #simorgh-mobile-app ✓
229
- ```
230
-
231
- ### Redis keys involved
232
-
233
- | Step | Key / Channel | Builder |
234
- |---|---|---|
235
- | Input to meta-agent | `cca:meta:{ns}:input` | `metaInputKey(ns)` |
236
- | Job completion pub/sub | `cca:notify:{ns}` | `notifyChannel(ns)` |
237
- | Job completion list (5 s poll fallback) | `cca:notify:{ns}` | `notifyListKey(ns)` |
238
- | Notification audit log | `cca:notify-log:{ns}` | `notifyLogKey(ns)` |
257
+ ### Setting spawning_namespace on spawn calls
239
258
 
240
- ### Required fixes per repo
241
-
242
- **`gonzih/cc-agent`** (Bug A): When the `spawn_agent` MCP tool is called from within a
243
- running meta-agent context, the MCP handler should auto-inject
244
- `spawning_namespace: metaAgentNamespace` if the caller has not already set it. The
245
- meta-agent's namespace is known to cc-agent at the time of the call.
246
-
247
- **`gonzih/cc-discord`** (Bug B): The notifier must subscribe to `cca:notify:{ns}` for
248
- every namespace registered in `routedChannelIds`, and route incoming notifications to
249
- the corresponding Discord channel — not just the single default
250
- `DISCORD_NOTIFY_CHANNEL_ID`.
251
-
252
- ### Setting spawning_namespace on manual spawn calls
253
-
254
- Any caller that spawns jobs and wants notifications routed back to itself must set
255
- `spawning_namespace` on the `SpawnParams`:
259
+ Any caller spawning jobs via cc-agent must set `spawning_namespace` on `SpawnParams` so
260
+ the coordinator routes completion notifications to the right service:
256
261
 
257
262
  ```typescript
258
263
  import type { SpawnParams } from "@gonzih/cc-wire";
@@ -260,15 +265,34 @@ import type { SpawnParams } from "@gonzih/cc-wire";
260
265
  const params: SpawnParams = {
261
266
  repoUrl: "https://github.com/org/repo",
262
267
  task: "fix the build",
263
- spawning_namespace: "simorgh-mobile-app", // notifications routed here
268
+ spawning_namespace: "simorgh-mobile-app", // cc-agent uses this to route the notify
264
269
  };
265
270
  ```
266
271
 
272
+ ## Migration from v0.1.x
273
+
274
+ If you were using the old cc-agent-owned meta-agent keys, replace them:
275
+
276
+ | Old (deprecated) | New | Owner |
277
+ |---|---|---|
278
+ | `metaInputKey(ns)` → `cca:meta:{ns}:input` | `discordMetaInputKey(ns)` → `cca:discord:meta:{ns}:input` | cc-discord |
279
+ | `metaAgentStatusKey(ns)` → `cca:meta-agent:status:{ns}` | `discordMetaStatusKey(ns)` → `cca:discord:meta:{ns}:status` | cc-discord |
280
+ | `META_AGENTS_INDEX` → `cca:meta:agents:index` | — (cc-discord maintains its own registry) | cc-discord |
281
+ | `chatLogKey(ns)` → `cca:chat:log:{ns}` | `discordChatLog(ns)` → `cca:discord:chat:log:{ns}` | cc-discord |
282
+ | `chatOutgoingChannel(ns)` → `cca:chat:outgoing:{ns}` | `discordChatOutgoing(ns)` → `cca:discord:chat:outgoing:{ns}` | cc-discord |
283
+ | `chatIncomingChannel(ns)` → `cca:chat:incoming:{ns}` | `discordChatIncoming(ns)` → `cca:discord:chat:incoming:{ns}` | cc-discord |
284
+ | `notifyChannel(ns)` (Discord use) | `discordNotify(ns)` | cc-discord |
285
+ | `notifyChannel(ns)` / `notifyListKey(ns)` (tg use) | `tgNotify()` | cc-tg |
286
+
287
+ The deprecated exports remain in v0.2.x for migration; they will be removed in v0.3.x.
288
+
267
289
  ## Development
268
290
 
269
291
  ```sh
270
- npm run build # compile ESM + CJS
271
- npm test # run tests (Node built-in test runner via tsx)
292
+ npm run build # compile ESM + CJS
293
+ npm test # run all tests (channels: node --test; runtime: vitest)
294
+ npm run test:channels # key builder tests only (node --test + tsx)
295
+ npm run test:runtime # runtime tests only (vitest + ioredis-mock)
272
296
  ```
273
297
 
274
298
  ## License
@@ -12,13 +12,29 @@
12
12
  * All values match the exact strings used in the source repos as of 2026-05.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.TIMING = exports.CAP = exports.TTL = exports.swarmKey = exports.wikiUpdatedKey = exports.wikiKey = exports.deletedCronsKey = exports.cronsKey = exports.learningsKey = exports.metaAgentStatusKey = exports.metaInputKey = exports.metaKey = exports.chatOutgoingChannel = exports.chatIncomingChannel = exports.chatLogKey = exports.notifyPublishCommand = exports.notifyLogKey = exports.notifyListKey = exports.notifyChannel = exports.profileKey = exports.planKey = exports.coordinatorPlanKey = exports.jobDoneQueueKey = exports.jobDoneChannel = exports.jobOutputLiveChannel = exports.jobInputKey = exports.jobSignalKey = exports.jobOutputKey = exports.jobKey = exports.jobIndexKey = exports.SWARM_REQUESTS_KEY = exports.VOICE_FAILED_KEY = exports.VOICE_PENDING_KEY = exports.CC_TG_VERSION_KEY = exports.JOB_INDEX_PREFIX = exports.JOB_INDEX_GLOB = exports.CC_AGENT_VERSION_KEY = exports.TOKEN_INDEX_KEY = exports.PROFILES_INDEX = exports.META_AGENTS_INDEX = exports.COORDINATOR_GROUP = exports.EVENT_STREAM = void 0;
15
+ exports.swarmKey = exports.wikiUpdatedKey = exports.wikiKey = exports.deletedCronsKey = exports.cronsKey = exports.learningsKey = exports.tgNotify = exports.tgChatIncoming = exports.tgChatOutgoing = exports.discordNotify = exports.discordChatIncoming = exports.discordChatLog = exports.discordChatOutgoing = exports.discordMetaStatusKey = exports.discordMetaInputKey = exports.metaAgentStatusKey = exports.metaInputKey = exports.metaKey = exports.chatOutgoingChannel = exports.chatIncomingChannel = exports.chatLogKey = exports.notifyPublishCommand = exports.notifyLogKey = exports.notifyListKey = exports.notifyChannel = exports.profileKey = exports.planKey = exports.coordinatorPlanKey = exports.jobDoneQueueKey = exports.jobDoneChannel = exports.jobOutputLiveChannel = exports.jobInputKey = exports.jobSignalKey = exports.jobOutputKey = exports.jobKey = exports.jobIndexKey = exports.SWARM_REQUESTS_KEY = exports.VOICE_FAILED_KEY = exports.VOICE_PENDING_KEY = exports.CC_TG_VERSION_KEY = exports.JOB_INDEX_PREFIX = exports.JOB_INDEX_GLOB = exports.CC_AGENT_VERSION_KEY = exports.TOKEN_INDEX_KEY = exports.PROFILES_INDEX = exports.META_AGENTS_INDEX = exports.COORDINATOR_GROUP = exports.EVENT_STREAM = exports.CC_TG_WORKSPACE = exports.CC_DISCORD_WORKSPACE_ROOT = void 0;
16
+ exports.TIMING = exports.CAP = exports.TTL = void 0;
17
+ // ─── Service Ownership Constants ─────────────────────────────────────────────
18
+ /**
19
+ * Root directory (relative to HOME) where cc-discord checks out per-namespace
20
+ * workspace clones: `~/cc-discord-workspace/{namespace}`.
21
+ */
22
+ exports.CC_DISCORD_WORKSPACE_ROOT = "cc-discord-workspace";
23
+ /**
24
+ * Directory name (relative to HOME) of the money-brain repo used by cc-tg
25
+ * for its dedicated session: `~/money-brain`.
26
+ */
27
+ exports.CC_TG_WORKSPACE = "money-brain";
16
28
  // ─── Static Keys ─────────────────────────────────────────────────────────────
17
29
  /** Redis Stream written by cc-agent on every job status change. */
18
30
  exports.EVENT_STREAM = "cca:event-stream";
19
31
  /** Consumer group name used by the coordinator to read from EVENT_STREAM. */
20
32
  exports.COORDINATOR_GROUP = "coordinator";
21
- /** SET — canonical registry of meta-agent namespaces. */
33
+ /**
34
+ * SET — canonical registry of meta-agent namespaces.
35
+ * @deprecated cc-discord now maintains its own namespace registry.
36
+ * This key is no longer written by the cc-suite; kept for migration only.
37
+ */
22
38
  exports.META_AGENTS_INDEX = "cca:meta:agents:index";
23
39
  /** SET — index of saved profile names. */
24
40
  exports.PROFILES_INDEX = "cca:profiles:index";
@@ -118,12 +134,63 @@ exports.chatOutgoingChannel = chatOutgoingChannel;
118
134
  /** STRING (JSON) — MetaAgentInfo state, TTL 30 days. */
119
135
  const metaKey = (namespace) => `cca:meta:${namespace}`;
120
136
  exports.metaKey = metaKey;
121
- /** LIST — input queue for a meta-agent (RPUSH by cc-tg, RPOP by cc-agent). */
137
+ /**
138
+ * LIST — input queue for a meta-agent (RPUSH by cc-tg, RPOP by cc-agent).
139
+ * @deprecated Use `discordMetaInputKey(ns)` — cc-discord now owns namespace sessions directly.
140
+ */
122
141
  const metaInputKey = (namespace) => `cca:meta:${namespace}:input`;
123
142
  exports.metaInputKey = metaInputKey;
124
- /** STRING (JSON) — live meta-agent status (typing, tool, etc.), TTL 7 days. */
143
+ /**
144
+ * STRING (JSON) — live meta-agent status (typing, tool, etc.), TTL 7 days.
145
+ * @deprecated Use `discordMetaStatusKey(ns)` — cc-discord now owns namespace sessions directly.
146
+ */
125
147
  const metaAgentStatusKey = (namespace) => `cca:meta-agent:status:${namespace}`;
126
148
  exports.metaAgentStatusKey = metaAgentStatusKey;
149
+ // ─── cc-discord Keys (dynamic) ───────────────────────────────────────────────
150
+ //
151
+ // cc-discord owns all namespace-scoped Claude sessions. It reads from the
152
+ // meta input queue, writes status, and publishes/subscribes to chat/notify
153
+ // channels — all under the "cca:discord:" prefix to avoid collisions with
154
+ // the old cc-agent-owned keys.
155
+ /** LIST — input queue for a cc-discord-managed namespace session (RPUSH/RPOP). */
156
+ const discordMetaInputKey = (namespace) => `cca:discord:meta:${namespace}:input`;
157
+ exports.discordMetaInputKey = discordMetaInputKey;
158
+ /** STRING (JSON) — live status for a cc-discord-managed namespace session, TTL 7 days. */
159
+ const discordMetaStatusKey = (namespace) => `cca:discord:meta:${namespace}:status`;
160
+ exports.discordMetaStatusKey = discordMetaStatusKey;
161
+ /** CHANNEL — cc-discord → UI outgoing chat messages (pub/sub). */
162
+ const discordChatOutgoing = (namespace) => `cca:discord:chat:outgoing:${namespace}`;
163
+ exports.discordChatOutgoing = discordChatOutgoing;
164
+ /** LIST — cc-discord chat history (LPUSH capped at 500, LIFO). */
165
+ const discordChatLog = (namespace) => `cca:discord:chat:log:${namespace}`;
166
+ exports.discordChatLog = discordChatLog;
167
+ /** CHANNEL — UI/Discord → cc-discord incoming chat messages (pub/sub). */
168
+ const discordChatIncoming = (namespace) => `cca:discord:chat:incoming:${namespace}`;
169
+ exports.discordChatIncoming = discordChatIncoming;
170
+ /**
171
+ * CHANNEL + LIST — job-completion notifications for a Discord namespace.
172
+ *
173
+ * Same dual-purpose pattern as `notifyChannel`/`notifyListKey`: PUBLISH by
174
+ * coordinator (pub/sub) and RPUSH/RPOP delivery queue; Redis pub/sub and list
175
+ * namespaces are independent so the dual use is safe.
176
+ */
177
+ const discordNotify = (namespace) => `cca:discord:notify:${namespace}`;
178
+ exports.discordNotify = discordNotify;
179
+ // ─── cc-tg Keys (static) ─────────────────────────────────────────────────────
180
+ //
181
+ // cc-tg owns a single dedicated money-brain session with no namespace scoping.
182
+ /** CHANNEL — cc-tg → UI outgoing chat messages (pub/sub). */
183
+ const tgChatOutgoing = () => "cca:tg:chat:outgoing";
184
+ exports.tgChatOutgoing = tgChatOutgoing;
185
+ /** CHANNEL — UI/Telegram → cc-tg incoming chat messages (pub/sub). */
186
+ const tgChatIncoming = () => "cca:tg:chat:incoming";
187
+ exports.tgChatIncoming = tgChatIncoming;
188
+ /**
189
+ * CHANNEL + LIST — job-completion notifications for the cc-tg session.
190
+ * Same dual-purpose pattern (pub/sub + RPOP poll fallback).
191
+ */
192
+ const tgNotify = () => "cca:tg:notify";
193
+ exports.tgNotify = tgNotify;
127
194
  // ─── Learnings Keys (dynamic) ─────────────────────────────────────────────────
128
195
  /** LIST — learnings for a namespace (LPUSH capped at 50, LIFO), TTL 90 days. */
129
196
  const learningsKey = (namespace) => `cca:learnings:${namespace}`;
@@ -24,3 +24,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  __exportStar(require("./channels.cjs"), exports);
26
26
  __exportStar(require("./types.cjs"), exports);
27
+ __exportStar(require("./runtime.cjs"), exports);