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 +33 -0
- package/README.md +40 -3
- package/bin/setup.js +29 -1
- package/package.json +1 -1
- package/src/channel-hub.js +341 -0
- package/src/mcp-server.js +504 -2
- package/src/notebook-store.js +391 -0
- package/src/prompts.js +109 -0
- package/src/user-escalation.js +330 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notebook-store.js
|
|
3
|
+
* Cognition Layer: Shared knowledge scratchpad for worker collaboration.
|
|
4
|
+
*
|
|
5
|
+
* Provides a team wiki where any worker can write and read free-form markdown
|
|
6
|
+
* notes organized by category. Unlike artifacts (structured, versioned, immutable),
|
|
7
|
+
* notebook entries are informal, collaborative, and appendable — a worker can
|
|
8
|
+
* start a note and another worker can append to it.
|
|
9
|
+
*
|
|
10
|
+
* Storage structure:
|
|
11
|
+
* team/{teamName}/notebook/
|
|
12
|
+
* notebook-index.json - registry of all notes
|
|
13
|
+
* entries/{noteId}.jsonl - append-only entry log per note
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
|
|
20
|
+
const { atomicWriteJson, readJsonSafe } = require('./atomic-io');
|
|
21
|
+
const { acquireLock, releaseLock } = require('./file-lock');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Valid note categories.
|
|
25
|
+
* Exported so mcp-server.js can validate tool inputs.
|
|
26
|
+
*/
|
|
27
|
+
const NOTEBOOK_CATEGORIES = [
|
|
28
|
+
'research',
|
|
29
|
+
'decision',
|
|
30
|
+
'gotcha',
|
|
31
|
+
'pattern',
|
|
32
|
+
'plan',
|
|
33
|
+
'investigation',
|
|
34
|
+
'reference'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate a note ID.
|
|
39
|
+
* Must be lowercase, alphanumeric + hyphens only, max 80 chars.
|
|
40
|
+
* @param {string} noteId - Note ID to validate
|
|
41
|
+
* @throws {Error} If noteId is invalid
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
function _validateNoteId(noteId) {
|
|
45
|
+
if (!noteId || typeof noteId !== 'string') {
|
|
46
|
+
throw new Error('Note ID is required and must be a string');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (noteId.length > 80) {
|
|
50
|
+
throw new Error(`Note ID '${noteId}' exceeds 80 character limit`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!/^[a-z0-9-]+$/.test(noteId)) {
|
|
54
|
+
throw new Error(`Note ID '${noteId}' must be lowercase alphanumeric with hyphens only`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* NotebookStore manages a shared knowledge scratchpad for worker collaboration.
|
|
60
|
+
*
|
|
61
|
+
* Directory structure:
|
|
62
|
+
* team/{teamName}/notebook/
|
|
63
|
+
* notebook-index.json - note registry (locked for writes)
|
|
64
|
+
* entries/{noteId}.jsonl - append-only entry log per note
|
|
65
|
+
*/
|
|
66
|
+
class NotebookStore {
|
|
67
|
+
/**
|
|
68
|
+
* Create a new NotebookStore instance
|
|
69
|
+
* @param {string} teamName - Name of the team (default: 'default')
|
|
70
|
+
*/
|
|
71
|
+
constructor(teamName = 'default') {
|
|
72
|
+
const baseDir = path.join(os.homedir(), '.clearctx');
|
|
73
|
+
this.teamDir = path.join(baseDir, 'team', teamName);
|
|
74
|
+
this.notebookDir = path.join(this.teamDir, 'notebook');
|
|
75
|
+
this.entriesDir = path.join(this.notebookDir, 'entries');
|
|
76
|
+
this.indexPath = path.join(this.notebookDir, 'notebook-index.json');
|
|
77
|
+
this.locksDir = path.join(this.teamDir, 'locks');
|
|
78
|
+
|
|
79
|
+
this._ensureDirectories();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ensure all required directories exist
|
|
84
|
+
* @private
|
|
85
|
+
*/
|
|
86
|
+
_ensureDirectories() {
|
|
87
|
+
[this.notebookDir, this.entriesDir, this.locksDir].forEach(dir => {
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the JSONL file path for a note
|
|
96
|
+
* @param {string} noteId - The note ID
|
|
97
|
+
* @returns {string} Absolute path to the note's JSONL file
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
_entryPath(noteId) {
|
|
101
|
+
return path.join(this.entriesDir, `${noteId}.jsonl`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read and parse all JSONL entries for a note
|
|
106
|
+
* @param {string} noteId - The note ID
|
|
107
|
+
* @returns {Array} Parsed entry objects
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
_readEntries(noteId) {
|
|
111
|
+
const jsonlPath = this._entryPath(noteId);
|
|
112
|
+
let entries = [];
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
116
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
117
|
+
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
try {
|
|
120
|
+
entries.push(JSON.parse(line));
|
|
121
|
+
} catch (parseErr) {
|
|
122
|
+
// Skip corrupted lines
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
// File doesn't exist or can't be read — return empty
|
|
127
|
+
entries = [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Write a note or append to an existing one
|
|
135
|
+
* @param {string} noteId - Unique note ID (lowercase, alphanumeric + hyphens, max 80 chars)
|
|
136
|
+
* @param {Object} options - Write options
|
|
137
|
+
* @param {string} options.author - Session name writing the note
|
|
138
|
+
* @param {string} options.category - Note category (must be in NOTEBOOK_CATEGORIES)
|
|
139
|
+
* @param {string} options.title - Human-readable title
|
|
140
|
+
* @param {string} options.content - Markdown content
|
|
141
|
+
* @param {string[]} [options.tags=[]] - Tags for categorization
|
|
142
|
+
* @returns {Object} { noteId, entryId, appended: boolean }
|
|
143
|
+
*/
|
|
144
|
+
write(noteId, { author, category, title, content, tags = [] }) {
|
|
145
|
+
// Step 1: Validate inputs
|
|
146
|
+
_validateNoteId(noteId);
|
|
147
|
+
|
|
148
|
+
if (!author || typeof author !== 'string') {
|
|
149
|
+
throw new Error('Note author is required');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!NOTEBOOK_CATEGORIES.includes(category)) {
|
|
153
|
+
throw new Error(`Invalid category '${category}'. Must be one of: ${NOTEBOOK_CATEGORIES.join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!title || typeof title !== 'string') {
|
|
157
|
+
throw new Error('Note title is required');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!content || typeof content !== 'string') {
|
|
161
|
+
throw new Error('Note content is required');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Step 2: Build entry object
|
|
165
|
+
const entryId = `entry_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
166
|
+
const entry = {
|
|
167
|
+
entryId,
|
|
168
|
+
author,
|
|
169
|
+
content,
|
|
170
|
+
timestamp: new Date().toISOString()
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Step 3: Acquire lock on the notebook index
|
|
174
|
+
acquireLock(this.locksDir, 'notebook-index');
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Step 4: Read the current index
|
|
178
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
179
|
+
let appended = false;
|
|
180
|
+
|
|
181
|
+
if (index[noteId]) {
|
|
182
|
+
// Existing note — collaborative append
|
|
183
|
+
appended = true;
|
|
184
|
+
index[noteId].updatedAt = new Date().toISOString();
|
|
185
|
+
index[noteId].entryCount += 1;
|
|
186
|
+
|
|
187
|
+
// Merge new tags (no duplicates)
|
|
188
|
+
if (tags.length > 0) {
|
|
189
|
+
const existingTags = index[noteId].tags || [];
|
|
190
|
+
const merged = [...new Set([...existingTags, ...tags])];
|
|
191
|
+
index[noteId].tags = merged;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// New note — create index entry
|
|
195
|
+
appended = false;
|
|
196
|
+
index[noteId] = {
|
|
197
|
+
noteId,
|
|
198
|
+
category,
|
|
199
|
+
title,
|
|
200
|
+
creator: author,
|
|
201
|
+
createdAt: new Date().toISOString(),
|
|
202
|
+
updatedAt: new Date().toISOString(),
|
|
203
|
+
tags,
|
|
204
|
+
entryCount: 1
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Step 5: Save the index atomically
|
|
209
|
+
atomicWriteJson(this.indexPath, index);
|
|
210
|
+
|
|
211
|
+
// Step 6: Release index lock before JSONL append
|
|
212
|
+
releaseLock(this.locksDir, 'notebook-index');
|
|
213
|
+
|
|
214
|
+
// Step 7: Acquire per-note lock for JSONL append
|
|
215
|
+
acquireLock(this.locksDir, `notebook-${noteId}`);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Step 8: Append JSONL line
|
|
219
|
+
const jsonlPath = this._entryPath(noteId);
|
|
220
|
+
fs.appendFileSync(jsonlPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
221
|
+
|
|
222
|
+
// Step 9: Release the per-note lock
|
|
223
|
+
releaseLock(this.locksDir, `notebook-${noteId}`);
|
|
224
|
+
|
|
225
|
+
return { noteId, entryId, appended };
|
|
226
|
+
|
|
227
|
+
} catch (err) {
|
|
228
|
+
releaseLock(this.locksDir, `notebook-${noteId}`);
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Ensure index lock is released on error (may already be released)
|
|
234
|
+
releaseLock(this.locksDir, 'notebook-index');
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Read a note with all its entries
|
|
241
|
+
* @param {string} noteId - The note to read
|
|
242
|
+
* @returns {Object} { noteId, category, title, creator, createdAt, updatedAt, tags, entries: [...], entryCount }
|
|
243
|
+
*/
|
|
244
|
+
read(noteId) {
|
|
245
|
+
// Step 1: Validate note exists
|
|
246
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
247
|
+
if (!index[noteId]) {
|
|
248
|
+
throw new Error(`Note '${noteId}' does not exist`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Step 2: Read all JSONL entries
|
|
252
|
+
const entries = this._readEntries(noteId);
|
|
253
|
+
|
|
254
|
+
// Step 3: Assemble full note
|
|
255
|
+
const meta = index[noteId];
|
|
256
|
+
return {
|
|
257
|
+
noteId: meta.noteId,
|
|
258
|
+
category: meta.category,
|
|
259
|
+
title: meta.title,
|
|
260
|
+
creator: meta.creator,
|
|
261
|
+
createdAt: meta.createdAt,
|
|
262
|
+
updatedAt: meta.updatedAt,
|
|
263
|
+
tags: meta.tags || [],
|
|
264
|
+
entries,
|
|
265
|
+
entryCount: meta.entryCount
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Search notes by title, tags, and content
|
|
271
|
+
* @param {string} query - Search string (case-insensitive)
|
|
272
|
+
* @param {Object} [filters={}] - Optional filters
|
|
273
|
+
* @param {string} [filters.category] - Filter by category
|
|
274
|
+
* @param {string} [filters.author] - Filter by creator
|
|
275
|
+
* @param {string} [filters.tag] - Filter by tag
|
|
276
|
+
* @returns {Object} { query, results: [...], total }
|
|
277
|
+
*/
|
|
278
|
+
search(query, { category, author, tag } = {}) {
|
|
279
|
+
if (!query || typeof query !== 'string') {
|
|
280
|
+
throw new Error('Search query is required');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
284
|
+
const lowerQuery = query.toLowerCase();
|
|
285
|
+
const results = [];
|
|
286
|
+
const contentScanNeeded = [];
|
|
287
|
+
|
|
288
|
+
// Step 1: Search index first (title and tag matches)
|
|
289
|
+
for (const meta of Object.values(index)) {
|
|
290
|
+
// Apply filters
|
|
291
|
+
if (category && meta.category !== category) continue;
|
|
292
|
+
if (author && meta.creator !== author) continue;
|
|
293
|
+
if (tag && (!meta.tags || !meta.tags.includes(tag))) continue;
|
|
294
|
+
|
|
295
|
+
// Check title match
|
|
296
|
+
if (meta.title && meta.title.toLowerCase().includes(lowerQuery)) {
|
|
297
|
+
results.push({
|
|
298
|
+
noteId: meta.noteId,
|
|
299
|
+
category: meta.category,
|
|
300
|
+
title: meta.title,
|
|
301
|
+
creator: meta.creator,
|
|
302
|
+
matchType: 'title'
|
|
303
|
+
});
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check tag match
|
|
308
|
+
if (meta.tags && meta.tags.some(t => t.toLowerCase().includes(lowerQuery))) {
|
|
309
|
+
results.push({
|
|
310
|
+
noteId: meta.noteId,
|
|
311
|
+
category: meta.category,
|
|
312
|
+
title: meta.title,
|
|
313
|
+
creator: meta.creator,
|
|
314
|
+
matchType: 'tag'
|
|
315
|
+
});
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// No index match — queue for content scan
|
|
320
|
+
contentScanNeeded.push(meta);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Step 2: Scan JSONL files for content matches (only for unmatched notes)
|
|
324
|
+
for (const meta of contentScanNeeded) {
|
|
325
|
+
const entries = this._readEntries(meta.noteId);
|
|
326
|
+
|
|
327
|
+
const hasContentMatch = entries.some(entry =>
|
|
328
|
+
entry.content && entry.content.toLowerCase().includes(lowerQuery)
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (hasContentMatch) {
|
|
332
|
+
results.push({
|
|
333
|
+
noteId: meta.noteId,
|
|
334
|
+
category: meta.category,
|
|
335
|
+
title: meta.title,
|
|
336
|
+
creator: meta.creator,
|
|
337
|
+
matchType: 'content'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return { query, results, total: results.length };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* List notes from index with optional filters
|
|
347
|
+
* @param {Object} [options={}] - List options
|
|
348
|
+
* @param {string} [options.category] - Filter by category
|
|
349
|
+
* @param {string} [options.author] - Filter by creator
|
|
350
|
+
* @param {string} [options.tag] - Filter by tag
|
|
351
|
+
* @param {number} [options.limit=50] - Max notes to return
|
|
352
|
+
* @returns {Object} { notes: [...], total }
|
|
353
|
+
*/
|
|
354
|
+
list({ category, author, tag, limit = 50 } = {}) {
|
|
355
|
+
const index = readJsonSafe(this.indexPath, {});
|
|
356
|
+
|
|
357
|
+
// Convert index to array
|
|
358
|
+
let notes = Object.values(index);
|
|
359
|
+
|
|
360
|
+
// Apply filters
|
|
361
|
+
if (category) {
|
|
362
|
+
notes = notes.filter(n => n.category === category);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (author) {
|
|
366
|
+
notes = notes.filter(n => n.creator === author);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (tag) {
|
|
370
|
+
notes = notes.filter(n => n.tags && n.tags.includes(tag));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const total = notes.length;
|
|
374
|
+
|
|
375
|
+
// Sort by updatedAt descending (most recent first)
|
|
376
|
+
notes.sort((a, b) => {
|
|
377
|
+
const dateA = a.updatedAt || a.createdAt || '';
|
|
378
|
+
const dateB = b.updatedAt || b.createdAt || '';
|
|
379
|
+
return dateB.localeCompare(dateA);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Apply limit
|
|
383
|
+
if (limit && notes.length > limit) {
|
|
384
|
+
notes = notes.slice(0, limit);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { notes, total };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = { NotebookStore, NOTEBOOK_CATEGORIES };
|
package/src/prompts.js
CHANGED
|
@@ -141,6 +141,28 @@ mcp__multi-session__team_broadcast({ from: "${name}", content: "Completed: <what
|
|
|
141
141
|
| \`contract_start\` | Begin working on an assigned contract |
|
|
142
142
|
| \`contract_complete\` | Mark your contract as done |
|
|
143
143
|
|
|
144
|
+
### Channels (persistent topic-based discussion — use for decisions, discoveries, blockers):
|
|
145
|
+
| Tool | When to Use |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| \`channel_create\` | Create a new discussion channel (rarely needed — defaults exist) |
|
|
148
|
+
| \`channel_post\` | Post a message to a channel (\`#design-decisions\`, \`#blockers\`, \`#discoveries\`, \`#conventions\`, \`#questions\`) |
|
|
149
|
+
| \`channel_read\` | Read recent messages from a channel |
|
|
150
|
+
| \`channel_subscribe\` | Subscribe to a channel for updates |
|
|
151
|
+
| \`channel_list\` | List all available channels |
|
|
152
|
+
|
|
153
|
+
### Notebook (shared knowledge scratchpad — use for research, gotchas, patterns):
|
|
154
|
+
| Tool | When to Use |
|
|
155
|
+
|------|-------------|
|
|
156
|
+
| \`notebook_write\` | Write or append to a note (categories: research, decision, gotcha, pattern, plan, investigation, reference) |
|
|
157
|
+
| \`notebook_read\` | Read a note with all its entries |
|
|
158
|
+
| \`notebook_search\` | Search notes by keyword, category, or tag |
|
|
159
|
+
| \`notebook_list\` | List all notebook entries |
|
|
160
|
+
|
|
161
|
+
### Human Escalation (LAST RESORT — only when teammates + artifacts + channels can't resolve):
|
|
162
|
+
| Tool | When to Use |
|
|
163
|
+
|------|-------------|
|
|
164
|
+
| \`ask_user\` | Ask the human user a question when genuinely stuck on an ambiguous decision |
|
|
165
|
+
|
|
144
166
|
## RULES
|
|
145
167
|
|
|
146
168
|
IMPORTANT: Follow these rules strictly. Violating them causes coordination failures.
|
|
@@ -213,6 +235,66 @@ This is wrong because no one knows you're done, your work isn't discoverable via
|
|
|
213
235
|
- Do NOT publish artifacts for incomplete or draft work. Publish when a unit of work is DONE.
|
|
214
236
|
- Do NOT create contracts for tasks you can do yourself. Only create contracts for work that belongs to another session's domain.
|
|
215
237
|
|
|
238
|
+
## COGNITIVE PROTOCOL: Think Before You Code
|
|
239
|
+
|
|
240
|
+
You are not just a task executor — you are a thinking collaborator. Follow this protocol for every task:
|
|
241
|
+
|
|
242
|
+
### Phase 1: Orient (before writing any code)
|
|
243
|
+
- Read your inbox (\`team_check_inbox\`) — what context exists?
|
|
244
|
+
- Read shared artifacts (\`artifact_get\`) — what conventions, schemas, contracts exist?
|
|
245
|
+
- Read relevant channels (\`channel_read\` on \`#design-decisions\`, \`#conventions\`, \`#discoveries\`)
|
|
246
|
+
- Read the project notebook (\`notebook_list\`, \`notebook_read\`) — what has the team learned?
|
|
247
|
+
- Understand the WHY behind your task, not just the WHAT
|
|
248
|
+
|
|
249
|
+
### Phase 2: Think (research and analyze)
|
|
250
|
+
- If requirements are ambiguous, check channels and notebook first
|
|
251
|
+
- If still unclear, use \`team_ask\` to consult a teammate
|
|
252
|
+
- If genuinely stuck on a decision only a human can make, use \`ask_user\` (LAST RESORT)
|
|
253
|
+
- Document your research in the notebook (\`notebook_write\`, category: \`research\` or \`investigation\`)
|
|
254
|
+
|
|
255
|
+
### Phase 3: Plan (share your approach)
|
|
256
|
+
- Post your implementation plan to \`#design-decisions\` channel before coding
|
|
257
|
+
- If your plan affects other workers, post to relevant channels
|
|
258
|
+
- Write your plan to the notebook (\`notebook_write\`, category: \`plan\`)
|
|
259
|
+
- Wait briefly — if a teammate has concerns, they'll respond in the channel
|
|
260
|
+
|
|
261
|
+
### Phase 4: Execute (implement with awareness)
|
|
262
|
+
- Follow shared conventions from the conventions artifact
|
|
263
|
+
- When you discover something unexpected, post to \`#discoveries\` channel
|
|
264
|
+
- When you hit a blocker, post to \`#blockers\` channel immediately
|
|
265
|
+
- When you find a gotcha (something that could trip up others), write it to the notebook (category: \`gotcha\`)
|
|
266
|
+
|
|
267
|
+
### Phase 5: Verify (self-review)
|
|
268
|
+
- Review your work against the conventions artifact
|
|
269
|
+
- Check that your output matches expected formats
|
|
270
|
+
- Verify your changes don't break assumptions other workers depend on
|
|
271
|
+
|
|
272
|
+
### Phase 6: Deliver (publish and share)
|
|
273
|
+
- Publish your artifact (\`artifact_publish\`)
|
|
274
|
+
- Post a summary of what you built and any decisions you made to \`#design-decisions\`
|
|
275
|
+
- Document any patterns worth reusing (\`notebook_write\`, category: \`pattern\`)
|
|
276
|
+
- Broadcast completion (\`team_broadcast\`)
|
|
277
|
+
|
|
278
|
+
### Channel Usage Guide
|
|
279
|
+
|
|
280
|
+
| Channel | Purpose |
|
|
281
|
+
|---------|---------|
|
|
282
|
+
| \`#design-decisions\` | Post plans, architectural choices, approach changes |
|
|
283
|
+
| \`#blockers\` | When you're stuck and need help (check here to help others too) |
|
|
284
|
+
| \`#discoveries\` | Unexpected findings, API behaviors, edge cases |
|
|
285
|
+
| \`#conventions\` | Convention questions, proposed changes, clarifications |
|
|
286
|
+
| \`#questions\` | General questions for the team |
|
|
287
|
+
|
|
288
|
+
### When to Escalate to Human (\`ask_user\`)
|
|
289
|
+
|
|
290
|
+
ONLY use \`ask_user\` when ALL of these are true:
|
|
291
|
+
1. The question involves a genuine ambiguity (not a technical problem)
|
|
292
|
+
2. You've checked artifacts, conventions, channels, and notebook
|
|
293
|
+
3. You've asked teammates via \`team_ask\` and they couldn't resolve it
|
|
294
|
+
4. The decision has significant impact and guessing wrong would be costly
|
|
295
|
+
|
|
296
|
+
Examples: "Should this API be public or internal?", "Which payment provider to use?", "Is PII allowed in logs?"
|
|
297
|
+
|
|
216
298
|
## BLAST RADIUS AWARENESS
|
|
217
299
|
|
|
218
300
|
Before making any change, evaluate its impact on your teammates' work:
|
|
@@ -691,6 +773,10 @@ When all workers are done:
|
|
|
691
773
|
| List available skills | \`skill_list\` |
|
|
692
774
|
| Read a skill's content | \`skill_get\` |
|
|
693
775
|
| Spawn worker with skills | \`team_spawn\` with \`skills\` parameter |
|
|
776
|
+
| Monitor team discussions | \`channel_read\`, \`channel_list\` |
|
|
777
|
+
| Monitor team knowledge | \`notebook_read\`, \`notebook_list\` |
|
|
778
|
+
| Check pending user questions | \`user_list_pending\` |
|
|
779
|
+
| Answer a worker's question | \`user_respond\` |
|
|
694
780
|
| Clean up between runs | \`team_reset\` |
|
|
695
781
|
|
|
696
782
|
## WHAT GOES WRONG (And How to Avoid It)
|
|
@@ -817,6 +903,29 @@ When spawning workers, assign relevant expertise skills to improve output qualit
|
|
|
817
903
|
- devops → devops
|
|
818
904
|
- security → security
|
|
819
905
|
- fullstack → nodejs-backend, react-frontend, typescript
|
|
906
|
+
|
|
907
|
+
### Rule 9: Surface worker questions to the user
|
|
908
|
+
|
|
909
|
+
Workers can ask the human user questions via \`ask_user\` when genuinely stuck on ambiguous decisions.
|
|
910
|
+
|
|
911
|
+
- After spawning workers, periodically check for pending questions: use \`user_list_pending\` to see unanswered escalations
|
|
912
|
+
- When you see a pending question, present it to the user clearly with the worker's context
|
|
913
|
+
- After the user answers, relay it with \`user_respond\`
|
|
914
|
+
- Do NOT answer on behalf of the user — relay their actual response
|
|
915
|
+
- If multiple questions are pending, prioritize by priority level (urgent > high > normal > low)
|
|
916
|
+
|
|
917
|
+
### Monitoring Channels and Notebook
|
|
918
|
+
|
|
919
|
+
As orchestrator, you can monitor team collaboration through channels and the notebook:
|
|
920
|
+
|
|
921
|
+
| Tool | Purpose |
|
|
922
|
+
|------|---------|
|
|
923
|
+
| \`channel_list\` | See all active channels and subscriber counts |
|
|
924
|
+
| \`channel_read\` | Read channel messages to monitor discussions, decisions, and blockers |
|
|
925
|
+
| \`notebook_list\` | See all notebook entries by category |
|
|
926
|
+
| \`notebook_read\` | Read specific notes for research, decisions, or gotchas workers documented |
|
|
927
|
+
| \`user_list_pending\` | Check for unanswered worker questions that need human input |
|
|
928
|
+
| \`user_respond\` | Relay the human user's answer to a worker's question |
|
|
820
929
|
`;
|
|
821
930
|
}
|
|
822
931
|
|