clearctx 3.1.0 → 3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to clearctx will be documented in this file.
4
4
 
5
+ ## [3.2.0] - 2026-02-15
6
+ ### Added
7
+ - **Cognition Layer** — Think-Plan-Execute cognitive protocol for workers
8
+ - Communication Channels — 5 new tools: `channel_create`, `channel_post`, `channel_subscribe`, `channel_read`, `channel_list`
9
+ - Project Notebook — 4 new tools: `notebook_write`, `notebook_read`, `notebook_search`, `notebook_list`
10
+ - User Escalation — 2 new tools: `ask_user`, `user_respond`
11
+ - 6-phase cognitive protocol injected into worker system prompts
12
+ - 5 default channels: `#design-decisions`, `#blockers`, `#discoveries`, `#conventions`, `#questions`
13
+ - 7 notebook categories: research, decision, gotcha, pattern, plan, investigation, reference
14
+ - Orchestrator Rule 9: Surface worker questions to the user
15
+ - 3 new modules: `src/channel-hub.js`, `src/notebook-store.js`, `src/user-escalation.js`
16
+ - 15 new enforcement tests (73 total, 100% pass rate)
17
+ - Updated all 3 sources of truth (prompts.js, ORCHESTRATOR-CLAUDE.md, setup.js)
18
+
19
+ ### Changed
20
+ - Total MCP tools: 71 → 82
21
+ - Total tests: 58 → 73
22
+
23
+ ## [3.1.0] - 2026-02-15
24
+ ### Added
25
+ - Expertise skills system with 8 bundled domain skills: postgresql, nodejs-backend, react-frontend, testing-qa, api-design, devops, security, typescript
26
+ - 3 new MCP tools: skill_list, skill_get, skill_detect (68 → 71 tools)
27
+ - Auto-detection of relevant skills from task description keywords
28
+ - Role-based skill fallback in team_spawn (e.g., role "backend" → nodejs-backend + api-design)
29
+ - Skill injection via team_spawn `skills` parameter
30
+ - Rule 8: Use expertise skills for domain work (added to orchestrator guide)
31
+ - 9 new tests in Skills System group (49 → 58 tests)
32
+
33
+ ### Fixed
34
+ - ENAMETOOLONG on Windows when injecting skills: system prompts now written to temp files and passed via --append-system-prompt-file instead of CLI args
35
+ - Role-based fallback no longer triggers when skills: [] is explicitly passed (opt-out)
36
+ - getWorkerContext edge case: Worker Context as last section in SKILL.md now extracted correctly
37
+
5
38
  ## [3.0.0] - 2026-02-15
6
39
  ### BREAKING CHANGES
7
40
  - Renamed package from `claude-multi-session` to `clearctx`
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![license](https://img.shields.io/npm/l/clearctx.svg)](https://github.com/)
5
5
  [![node](https://img.shields.io/node/v/clearctx.svg)](https://nodejs.org)
6
6
 
7
- > Multi-session orchestration system for Claude Code CLI. Spawn parallel workers that coordinate via artifacts and phase gates. 68 MCP tools. v2.9.0.
7
+ > Multi-session orchestration system for Claude Code CLI. Spawn parallel workers that coordinate via artifacts and phase gates. 82 MCP tools. 8 bundled expertise skills. Cognition Layer. v3.2.0.
8
8
 
9
9
  ---
10
10
 
@@ -39,7 +39,7 @@ After installing, run the setup wizard to register the MCP server with Claude Co
39
39
  clearctx setup
40
40
  ```
41
41
 
42
- This adds 17 native tools to Claude Code (spawn_session, delegate_task, etc.) so Claude can manage sessions directly — no Bash commands needed.
42
+ This adds 82 native tools to Claude Code (spawn_session, delegate_task, skill_detect, etc.) so Claude can manage sessions directly — no Bash commands needed.
43
43
 
44
44
  **Non-interactive options:**
45
45
  ```bash
@@ -450,7 +450,7 @@ Add to your Claude Code MCP config (`~/.claude/settings.json` or project `.claud
450
450
  {
451
451
  "mcpServers": {
452
452
  "multi-session": {
453
- "command": "cms-mcp"
453
+ "command": "ctx-mcp"
454
454
  }
455
455
  }
456
456
  }
@@ -505,6 +505,28 @@ Once configured, Claude sees these tools:
505
505
  |------|-------------|
506
506
  | `batch_spawn` | Spawn multiple sessions in parallel |
507
507
 
508
+ **Expertise Skills:**
509
+ | Tool | Description |
510
+ |------|-------------|
511
+ | `skill_list` | List all available expertise skills |
512
+ | `skill_get` | Read a skill's full content (conventions, patterns, anti-patterns) |
513
+ | `skill_detect` | Auto-detect relevant skills from a task description |
514
+
515
+ **Cognition Layer:**
516
+ | Tool | Description |
517
+ |------|-------------|
518
+ | `channel_create` | Create a topic-based communication channel |
519
+ | `channel_post` | Post a message to a team channel |
520
+ | `channel_subscribe` | Subscribe a session to a channel |
521
+ | `channel_read` | Read messages from a channel |
522
+ | `channel_list` | List all channels with subscriber counts |
523
+ | `notebook_write` | Write or append to a shared notebook entry |
524
+ | `notebook_read` | Read a notebook entry with all entries |
525
+ | `notebook_search` | Search notebook by content, title, or tags |
526
+ | `notebook_list` | List notebook entries with filters |
527
+ | `ask_user` | Ask the human user a question (worker escalation) |
528
+ | `user_respond` | Relay the human's answer to a worker |
529
+
508
530
  ### How Claude Uses It
509
531
 
510
532
  With MCP configured, Claude can autonomously:
@@ -633,6 +655,21 @@ clearctx team-replay pre-graphql --overrides '{"build-auth-api":{"inputs":{"cont
633
655
 
634
656
  ---
635
657
 
658
+ ## Cognition Layer (v3.2.0)
659
+
660
+ Workers now follow a **Think-Plan-Execute cognitive protocol** that teaches them to orient before coding, research ambiguities, share plans before implementing, document discoveries, and self-review before delivering.
661
+
662
+ ### Communication Channels (5 tools)
663
+ Persistent topic-based discussions. 5 default channels: `#design-decisions`, `#blockers`, `#discoveries`, `#conventions`, `#questions`. Workers post plans and discoveries to channels so teammates stay informed without orchestrator relay.
664
+
665
+ ### Project Notebook (4 tools)
666
+ Shared knowledge scratchpad with 7 categories: research, decision, gotcha, pattern, plan, investigation, reference. Multiple workers can append to the same note, enabling collaborative knowledge building.
667
+
668
+ ### User Escalation (2 tools)
669
+ Workers can ask the human user questions when genuinely stuck on ambiguous decisions that can't be resolved by checking artifacts, conventions, channels, notebook, or asking teammates. The orchestrator relays answers.
670
+
671
+ ---
672
+
636
673
  ## Layer 1: Chat (8 MCP Tools)
637
674
 
638
675
  Sessions can communicate directly without routing through the orchestrator.
package/bin/setup.js CHANGED
@@ -205,6 +205,30 @@ When spawning workers, assign relevant expertise skills to improve output qualit
205
205
  - security → security
206
206
  - fullstack → nodejs-backend, react-frontend, typescript
207
207
 
208
+ ### Rule 9: Surface worker questions to the user
209
+
210
+ Workers can ask the human user questions via \`ask_user\` when genuinely stuck on ambiguous decisions that can't be resolved by checking artifacts, conventions, channels, notebook, or asking teammates.
211
+
212
+ - After spawning workers, periodically check for pending questions: use \`user_list_pending\` to see unanswered escalations
213
+ - When you see a pending question, present it to the user clearly with the worker's context
214
+ - After the user answers, relay it with \`user_respond\`
215
+ - Do NOT answer on behalf of the user — relay their actual response
216
+ - If multiple questions are pending, prioritize by priority level (urgent > high > normal > low)
217
+
218
+ ## Cognition Layer (v3.2.0)
219
+
220
+ Workers now follow a **Think-Plan-Execute cognitive protocol** that teaches them to orient before coding, research ambiguities, share plans in channels before implementing, document discoveries in the notebook, and self-review before delivering. This reduces convention mismatches, duplicate work, and silent failures.
221
+
222
+ Three new subsystems support this:
223
+
224
+ | Category | Tools | Purpose |
225
+ |----------|-------|---------|
226
+ | **Channels** | \`channel_create\`, \`channel_post\`, \`channel_read\`, \`channel_subscribe\`, \`channel_list\` | Persistent topic-based discussion. Default channels: \`#design-decisions\`, \`#blockers\`, \`#discoveries\`, \`#conventions\`, \`#questions\` |
227
+ | **Notebook** | \`notebook_write\`, \`notebook_read\`, \`notebook_search\`, \`notebook_list\` | Shared knowledge scratchpad for research, gotchas, patterns, plans, and investigations |
228
+ | **User Escalation** | \`ask_user\`, \`user_respond\` | Worker-to-human escalation for genuinely ambiguous decisions. Workers ask, orchestrator relays answer |
229
+
230
+ **Orchestrator role:** Monitor channels (\`channel_read\`) and notebook (\`notebook_list\`, \`notebook_read\`) to stay informed. Check \`user_list_pending\` for unanswered worker questions and relay answers with \`user_respond\`. You do NOT need to post to channels yourself — workers handle their own collaboration.
231
+
208
232
  ## Auto-Behaviors (v2.7.0)
209
233
 
210
234
  These happen automatically — no action needed from you or the workers:
@@ -215,7 +239,7 @@ These happen automatically — no action needed from you or the workers:
215
239
  - **Convention completeness check**: \`phase_gate\` warns if \`shared-conventions\` artifact is missing or has incomplete fields
216
240
  - **Skills System**: When you provide a \`role\` to \`team_spawn\` without explicit \`skills\`, the system auto-detects relevant expertise skills based on the role (e.g., "database" → postgresql, "backend" → nodejs-backend + api-design). You can override this by passing explicit \`skills\` array.
217
241
 
218
- ## Quick Reference (71 tools)
242
+ ## Quick Reference (82 tools)
219
243
 
220
244
  | You want to... | Use this tool |
221
245
  |----------------|---------------|
@@ -234,6 +258,10 @@ These happen automatically — no action needed from you or the workers:
234
258
  | List available skills | \`skill_list\` |
235
259
  | Read a skill's content | \`skill_get\` |
236
260
  | Spawn worker with skills | \`team_spawn\` with \`skills\` parameter |
261
+ | Monitor team discussions | \`channel_read\`, \`channel_list\` |
262
+ | Monitor shared knowledge | \`notebook_read\`, \`notebook_list\` |
263
+ | Check pending user questions | \`user_list_pending\` |
264
+ | Relay user's answer to worker | \`user_respond\` |
237
265
  | Clean up between runs | \`team_reset\` |
238
266
 
239
267
  ### When NOT to Delegate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clearctx",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Multi-session orchestrator for Claude Code CLI — spawn, control, pause, resume, and send multiple inputs to Claude Code sessions programmatically",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,341 @@
1
+ /**
2
+ * channel-hub.js
3
+ * Cognition Layer: Topic-based persistent channels for worker collaboration.
4
+ *
5
+ * Provides Slack-like channels where workers can discuss, share findings,
6
+ * and debate approaches. Each channel is an append-only JSONL message log
7
+ * with a shared index tracking channel metadata and subscriptions.
8
+ *
9
+ * Storage structure:
10
+ * team/{teamName}/channels/
11
+ * channels-index.json - registry of all channels
12
+ * {channelName}.jsonl - append-only message log per channel
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+
19
+ const { atomicWriteJson, readJsonSafe } = require('./atomic-io');
20
+ const { acquireLock, releaseLock } = require('./file-lock');
21
+
22
+ /**
23
+ * Default channels auto-created for new teams.
24
+ * Exported so mcp-server.js can create them on team_spawn if they don't exist.
25
+ */
26
+ const DEFAULT_CHANNELS = [
27
+ 'design-decisions',
28
+ 'blockers',
29
+ 'discoveries',
30
+ 'conventions',
31
+ 'questions'
32
+ ];
33
+
34
+ /**
35
+ * Validate a channel name.
36
+ * Must be lowercase, alphanumeric + hyphens only, max 50 chars.
37
+ * @param {string} name - Channel name to validate
38
+ * @throws {Error} If name is invalid
39
+ * @private
40
+ */
41
+ function _validateChannelName(name) {
42
+ if (!name || typeof name !== 'string') {
43
+ throw new Error('Channel name is required and must be a string');
44
+ }
45
+
46
+ if (name.length > 50) {
47
+ throw new Error(`Channel name '${name}' exceeds 50 character limit`);
48
+ }
49
+
50
+ if (!/^[a-z0-9-]+$/.test(name)) {
51
+ throw new Error(`Channel name '${name}' must be lowercase alphanumeric with hyphens only`);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * ChannelHub manages topic-based persistent channels for worker collaboration.
57
+ *
58
+ * Directory structure:
59
+ * team/{teamName}/channels/
60
+ * channels-index.json - channel registry (locked for writes)
61
+ * {channelName}.jsonl - append-only message log per channel
62
+ */
63
+ class ChannelHub {
64
+ /**
65
+ * Create a new ChannelHub instance
66
+ * @param {string} teamName - Name of the team (default: 'default')
67
+ */
68
+ constructor(teamName = 'default') {
69
+ const baseDir = path.join(os.homedir(), '.clearctx');
70
+ this.teamDir = path.join(baseDir, 'team', teamName);
71
+ this.channelsDir = path.join(this.teamDir, 'channels');
72
+ this.indexPath = path.join(this.channelsDir, 'channels-index.json');
73
+ this.locksDir = path.join(this.teamDir, 'locks');
74
+
75
+ this._ensureDirectories();
76
+ }
77
+
78
+ /**
79
+ * Ensure all required directories exist
80
+ * @private
81
+ */
82
+ _ensureDirectories() {
83
+ [this.channelsDir, this.locksDir].forEach(dir => {
84
+ if (!fs.existsSync(dir)) {
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Get the JSONL file path for a channel
92
+ * @param {string} channelName - The channel name
93
+ * @returns {string} Absolute path to the channel's JSONL file
94
+ * @private
95
+ */
96
+ _channelPath(channelName) {
97
+ return path.join(this.channelsDir, `${channelName}.jsonl`);
98
+ }
99
+
100
+ /**
101
+ * Create a new channel
102
+ * @param {string} channelName - Unique channel name (lowercase, alphanumeric + hyphens, max 50 chars)
103
+ * @param {Object} options - Channel options
104
+ * @param {string} options.description - Human-readable description
105
+ * @param {string} options.creator - Session name that created the channel
106
+ * @returns {Object} { channelName, created: true }
107
+ */
108
+ createChannel(channelName, { description, creator }) {
109
+ // Step 1: Validate channel name
110
+ _validateChannelName(channelName);
111
+
112
+ if (!creator || typeof creator !== 'string') {
113
+ throw new Error('Channel creator is required');
114
+ }
115
+
116
+ // Step 2: Acquire lock on the channels index
117
+ acquireLock(this.locksDir, 'channels-index');
118
+
119
+ try {
120
+ // Step 3: Read the current index
121
+ const index = readJsonSafe(this.indexPath, {});
122
+
123
+ // Step 4: Check for duplicates
124
+ if (index[channelName]) {
125
+ throw new Error(`Channel '${channelName}' already exists`);
126
+ }
127
+
128
+ // Step 5: Add channel to index
129
+ index[channelName] = {
130
+ name: channelName,
131
+ description: description || '',
132
+ creator,
133
+ createdAt: new Date().toISOString(),
134
+ subscribers: [creator]
135
+ };
136
+
137
+ // Step 6: Save the index atomically
138
+ atomicWriteJson(this.indexPath, index);
139
+
140
+ // Step 7: Create empty JSONL file
141
+ const jsonlPath = this._channelPath(channelName);
142
+ if (!fs.existsSync(jsonlPath)) {
143
+ fs.writeFileSync(jsonlPath, '', 'utf-8');
144
+ }
145
+
146
+ // Step 8: Release the lock
147
+ releaseLock(this.locksDir, 'channels-index');
148
+
149
+ return { channelName, created: true };
150
+
151
+ } catch (err) {
152
+ releaseLock(this.locksDir, 'channels-index');
153
+ throw err;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Post a message to a channel
159
+ * @param {string} channelName - Target channel name
160
+ * @param {Object} options - Message options
161
+ * @param {string} options.from - Sender session name
162
+ * @param {string} options.content - Message content
163
+ * @param {Object} [options.metadata] - Optional metadata
164
+ * @returns {Object} { messageId, channelName, posted: true }
165
+ */
166
+ postMessage(channelName, { from, content, metadata }) {
167
+ // Step 1: Validate channel exists
168
+ const index = readJsonSafe(this.indexPath, {});
169
+ if (!index[channelName]) {
170
+ throw new Error(`Channel '${channelName}' does not exist`);
171
+ }
172
+
173
+ if (!from || typeof from !== 'string') {
174
+ throw new Error('Message sender (from) is required');
175
+ }
176
+
177
+ if (!content || typeof content !== 'string') {
178
+ throw new Error('Message content is required');
179
+ }
180
+
181
+ // Step 2: Build message object
182
+ const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
183
+ const message = {
184
+ id: messageId,
185
+ channelName,
186
+ from,
187
+ content,
188
+ metadata: metadata || null,
189
+ timestamp: new Date().toISOString()
190
+ };
191
+
192
+ // Step 3: Acquire lock for safe concurrent append
193
+ acquireLock(this.locksDir, `channel-${channelName}`);
194
+
195
+ try {
196
+ // Step 4: Append JSONL line
197
+ const jsonlPath = this._channelPath(channelName);
198
+ fs.appendFileSync(jsonlPath, JSON.stringify(message) + '\n', 'utf-8');
199
+
200
+ // Step 5: Release the lock
201
+ releaseLock(this.locksDir, `channel-${channelName}`);
202
+
203
+ return { messageId, channelName, posted: true };
204
+
205
+ } catch (err) {
206
+ releaseLock(this.locksDir, `channel-${channelName}`);
207
+ throw err;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Subscribe a session to a channel
213
+ * @param {string} channelName - Channel to subscribe to
214
+ * @param {string} sessionName - Session name to subscribe
215
+ * @returns {Object} { channelName, subscriber: sessionName, subscribed: true }
216
+ */
217
+ subscribe(channelName, sessionName) {
218
+ if (!sessionName || typeof sessionName !== 'string') {
219
+ throw new Error('Session name is required for subscription');
220
+ }
221
+
222
+ // Acquire lock on the channels index
223
+ acquireLock(this.locksDir, 'channels-index');
224
+
225
+ try {
226
+ const index = readJsonSafe(this.indexPath, {});
227
+
228
+ if (!index[channelName]) {
229
+ throw new Error(`Channel '${channelName}' does not exist`);
230
+ }
231
+
232
+ // Add subscriber (no duplicates)
233
+ if (!index[channelName].subscribers.includes(sessionName)) {
234
+ index[channelName].subscribers.push(sessionName);
235
+ atomicWriteJson(this.indexPath, index);
236
+ }
237
+
238
+ releaseLock(this.locksDir, 'channels-index');
239
+
240
+ return { channelName, subscriber: sessionName, subscribed: true };
241
+
242
+ } catch (err) {
243
+ releaseLock(this.locksDir, 'channels-index');
244
+ throw err;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Read messages from a channel
250
+ * @param {string} channelName - Channel to read from
251
+ * @param {Object} [options={}] - Read options
252
+ * @param {number} [options.limit=50] - Max messages to return (returns last N)
253
+ * @param {string} [options.after] - Only return messages after this ISO timestamp
254
+ * @param {string} [options.subscriber] - If provided, track last-read timestamp in index
255
+ * @returns {Object} { channelName, messages: [...], total }
256
+ */
257
+ readMessages(channelName, { limit = 50, after, subscriber } = {}) {
258
+ // Step 1: Validate channel exists
259
+ const index = readJsonSafe(this.indexPath, {});
260
+ if (!index[channelName]) {
261
+ throw new Error(`Channel '${channelName}' does not exist`);
262
+ }
263
+
264
+ // Step 2: Read JSONL file and parse each line
265
+ const jsonlPath = this._channelPath(channelName);
266
+ let messages = [];
267
+
268
+ try {
269
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
270
+ const lines = content.split('\n').filter(line => line.trim());
271
+
272
+ for (const line of lines) {
273
+ try {
274
+ messages.push(JSON.parse(line));
275
+ } catch (parseErr) {
276
+ // Skip corrupted lines
277
+ }
278
+ }
279
+ } catch (err) {
280
+ // File doesn't exist or can't be read — return empty
281
+ messages = [];
282
+ }
283
+
284
+ const total = messages.length;
285
+
286
+ // Step 3: Filter by 'after' timestamp if provided
287
+ if (after) {
288
+ messages = messages.filter(msg => msg.timestamp > after);
289
+ }
290
+
291
+ // Step 4: Apply limit (return last N messages)
292
+ if (limit && messages.length > limit) {
293
+ messages = messages.slice(-limit);
294
+ }
295
+
296
+ // Step 5: Track last-read timestamp if subscriber provided
297
+ if (subscriber) {
298
+ acquireLock(this.locksDir, 'channels-index');
299
+
300
+ try {
301
+ const freshIndex = readJsonSafe(this.indexPath, {});
302
+
303
+ if (freshIndex[channelName]) {
304
+ if (!freshIndex[channelName].lastRead) {
305
+ freshIndex[channelName].lastRead = {};
306
+ }
307
+ freshIndex[channelName].lastRead[subscriber] = new Date().toISOString();
308
+ atomicWriteJson(this.indexPath, freshIndex);
309
+ }
310
+
311
+ releaseLock(this.locksDir, 'channels-index');
312
+ } catch (err) {
313
+ releaseLock(this.locksDir, 'channels-index');
314
+ // Non-critical — don't fail the read
315
+ }
316
+ }
317
+
318
+ return { channelName, messages, total };
319
+ }
320
+
321
+ /**
322
+ * List all channels with subscriber counts
323
+ * @returns {Object} { channels: [...] }
324
+ */
325
+ listChannels() {
326
+ const index = readJsonSafe(this.indexPath, {});
327
+
328
+ const channels = Object.values(index).map(channel => ({
329
+ name: channel.name,
330
+ description: channel.description,
331
+ creator: channel.creator,
332
+ createdAt: channel.createdAt,
333
+ subscriberCount: channel.subscribers ? channel.subscribers.length : 0,
334
+ subscribers: channel.subscribers || []
335
+ }));
336
+
337
+ return { channels };
338
+ }
339
+ }
340
+
341
+ module.exports = { ChannelHub, DEFAULT_CHANNELS };