yams-blackboard 0.1.1

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.
Files changed (4) hide show
  1. package/DESIGN.md +344 -0
  2. package/README.md +268 -0
  3. package/index.js +1082 -0
  4. package/package.json +34 -0
package/index.js ADDED
@@ -0,0 +1,1082 @@
1
+ // index.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+ import { z as z2 } from "zod";
4
+
5
+ // blackboard.ts
6
+ class YamsBlackboard {
7
+ $;
8
+ options;
9
+ sessionName;
10
+ sessionActive = false;
11
+ constructor($, options = {}) {
12
+ this.$ = $;
13
+ this.options = options;
14
+ this.sessionName = options.sessionName;
15
+ }
16
+ genId(prefix) {
17
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
18
+ }
19
+ nowISO() {
20
+ return new Date().toISOString();
21
+ }
22
+ sessionArg() {
23
+ return this.sessionName ? `--session ${this.sessionName}` : "";
24
+ }
25
+ shellEscape(s) {
26
+ return `'${s.replace(/'/g, "'\\''")}'`;
27
+ }
28
+ isBufferLike(value) {
29
+ return value !== null && typeof value === "object" && typeof value.toString === "function" && value.constructor?.name === "Buffer";
30
+ }
31
+ async shell(cmd) {
32
+ try {
33
+ const result = await this.$`sh -c ${cmd}`;
34
+ if (typeof result === "string") {
35
+ return result.trim();
36
+ }
37
+ if (this.isBufferLike(result)) {
38
+ return result.toString("utf-8").trim();
39
+ }
40
+ if (result && typeof result === "object") {
41
+ const output = result.stdout ?? result.output ?? result.text;
42
+ if (typeof output === "string") {
43
+ return output.trim();
44
+ }
45
+ if (this.isBufferLike(output)) {
46
+ return output.toString("utf-8").trim();
47
+ }
48
+ if (typeof output === "function") {
49
+ return (await output()).trim();
50
+ }
51
+ }
52
+ return String(result ?? "").trim();
53
+ } catch (e) {
54
+ throw new Error(`Shell command failed: ${e.message}`);
55
+ }
56
+ }
57
+ async yams(cmd) {
58
+ return this.shell(`yams ${cmd}`);
59
+ }
60
+ async yamsJson(cmd) {
61
+ const result = await this.yams(`${cmd} --json`);
62
+ try {
63
+ return JSON.parse(result);
64
+ } catch {
65
+ throw new Error(`Failed to parse YAMS JSON response: ${result}`);
66
+ }
67
+ }
68
+ async yamsStore(content, name, tags, extraArgs = "") {
69
+ const escaped = this.shellEscape(content);
70
+ const cmd = `echo ${escaped} | yams add - --name ${this.shellEscape(name)} --tags ${this.shellEscape(tags)} ${extraArgs}`;
71
+ return this.shell(cmd);
72
+ }
73
+ async startSession(name) {
74
+ this.sessionName = name || `opencode-${Date.now()}`;
75
+ await this.yams(`session_start --name "${this.sessionName}"`);
76
+ this.sessionActive = true;
77
+ return this.sessionName;
78
+ }
79
+ async stopSession() {
80
+ if (this.sessionName && this.sessionActive) {
81
+ await this.yams(`session_stop --name "${this.sessionName}"`);
82
+ this.sessionActive = false;
83
+ }
84
+ }
85
+ getSessionName() {
86
+ return this.sessionName;
87
+ }
88
+ async registerAgent(agent) {
89
+ const full = {
90
+ ...agent,
91
+ registered_at: this.nowISO(),
92
+ status: agent.status || "active"
93
+ };
94
+ const content = JSON.stringify(full, null, 2);
95
+ const tags = [
96
+ "agent",
97
+ ...agent.capabilities.map((c) => `capability:${c}`)
98
+ ].join(",");
99
+ await this.yamsStore(content, `agents/${agent.id}.json`, tags, this.sessionArg());
100
+ return full;
101
+ }
102
+ async getAgent(agentId) {
103
+ try {
104
+ const result = await this.yamsJson(`cat --name "agents/${agentId}.json"`);
105
+ return JSON.parse(result.content);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ async listAgents() {
111
+ try {
112
+ const result = await this.yamsJson(`list --tags "agent" --limit 100`);
113
+ const agents = [];
114
+ for (const doc of result.documents || []) {
115
+ try {
116
+ const content = await this.yams(`cat --name "${doc.name}"`);
117
+ agents.push(JSON.parse(content));
118
+ } catch {}
119
+ }
120
+ return agents;
121
+ } catch {
122
+ return [];
123
+ }
124
+ }
125
+ async updateAgentStatus(agentId, status) {
126
+ const agent = await this.getAgent(agentId);
127
+ if (agent) {
128
+ agent.status = status;
129
+ const content = JSON.stringify(agent, null, 2);
130
+ await this.yamsStore(content, `agents/${agentId}.json`, "agent");
131
+ }
132
+ }
133
+ findingToMarkdown(finding) {
134
+ const frontmatter = {
135
+ id: finding.id,
136
+ agent_id: finding.agent_id,
137
+ topic: finding.topic,
138
+ confidence: finding.confidence,
139
+ status: finding.status,
140
+ scope: finding.scope,
141
+ created_at: this.nowISO()
142
+ };
143
+ if (finding.severity)
144
+ frontmatter.severity = finding.severity;
145
+ if (finding.context_id)
146
+ frontmatter.context_id = finding.context_id;
147
+ if (finding.parent_id)
148
+ frontmatter.parent_id = finding.parent_id;
149
+ if (finding.references?.length)
150
+ frontmatter.references = finding.references;
151
+ if (finding.ttl)
152
+ frontmatter.ttl = finding.ttl;
153
+ if (finding.metadata)
154
+ frontmatter.metadata = finding.metadata;
155
+ const fm = Object.entries(frontmatter).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(`
156
+ `);
157
+ return `---
158
+ ${fm}
159
+ ---
160
+
161
+ # ${finding.title}
162
+
163
+ ${finding.content}
164
+ `;
165
+ }
166
+ buildFindingTags(finding) {
167
+ const tags = [
168
+ "finding",
169
+ `agent:${finding.agent_id}`,
170
+ `topic:${finding.topic}`,
171
+ `scope:${finding.scope || "persistent"}`,
172
+ `status:${finding.status || "published"}`
173
+ ];
174
+ if (finding.severity)
175
+ tags.push(`severity:${finding.severity}`);
176
+ if (finding.context_id)
177
+ tags.push(`ctx:${finding.context_id}`);
178
+ return tags.join(",");
179
+ }
180
+ async postFinding(input) {
181
+ const id = this.genId("f");
182
+ const finding = {
183
+ ...input,
184
+ id,
185
+ status: input.status || "published",
186
+ scope: input.scope || this.options.defaultScope || "persistent"
187
+ };
188
+ const md = this.findingToMarkdown(finding);
189
+ const tags = this.buildFindingTags(finding);
190
+ const name = `findings/${finding.topic}/${id}.md`;
191
+ await this.yamsStore(md, name, tags, this.sessionArg());
192
+ return finding;
193
+ }
194
+ async getFinding(findingId) {
195
+ try {
196
+ const result = await this.yams(`cat --name "findings/**/${findingId}.md"`);
197
+ const match = result.match(/^---\n([\s\S]*?)\n---\n\n# (.*?)\n\n([\s\S]*)$/);
198
+ if (!match)
199
+ return null;
200
+ const frontmatter = {};
201
+ match[1].split(`
202
+ `).forEach((line) => {
203
+ const [key, ...rest] = line.split(": ");
204
+ if (key && rest.length) {
205
+ try {
206
+ frontmatter[key] = JSON.parse(rest.join(": "));
207
+ } catch {
208
+ frontmatter[key] = rest.join(": ");
209
+ }
210
+ }
211
+ });
212
+ return {
213
+ ...frontmatter,
214
+ title: match[2],
215
+ content: match[3].trim()
216
+ };
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+ async queryFindings(query) {
222
+ const tags = ["finding"];
223
+ if (query.topic)
224
+ tags.push(`topic:${query.topic}`);
225
+ if (query.agent_id)
226
+ tags.push(`agent:${query.agent_id}`);
227
+ if (query.context_id)
228
+ tags.push(`ctx:${query.context_id}`);
229
+ if (query.status)
230
+ tags.push(`status:${query.status}`);
231
+ if (query.scope)
232
+ tags.push(`scope:${query.scope}`);
233
+ if (query.severity?.length) {
234
+ tags.push(`severity:${query.severity[0]}`);
235
+ }
236
+ try {
237
+ const result = await this.yamsJson(`list --tags "${tags.join(",")}" --limit ${query.limit} --offset ${query.offset} ${this.sessionArg()}`);
238
+ const findings = [];
239
+ for (const doc of result.documents || []) {
240
+ const finding = await this.getFinding(doc.name?.split("/").pop()?.replace(".md", "") || "");
241
+ if (finding) {
242
+ if (query.min_confidence && finding.confidence < query.min_confidence)
243
+ continue;
244
+ findings.push(finding);
245
+ }
246
+ }
247
+ return findings;
248
+ } catch {
249
+ return [];
250
+ }
251
+ }
252
+ async searchFindings(query, opts) {
253
+ const tags = opts?.topic ? `finding,topic:${opts.topic}` : "finding";
254
+ const limit = opts?.limit || 10;
255
+ try {
256
+ const result = await this.yamsJson(`search "${query}" --tags "${tags}" --limit ${limit} ${this.sessionArg()}`);
257
+ const findings = [];
258
+ for (const r of result.results || []) {
259
+ const id = r.path?.split("/").pop()?.replace(".md", "");
260
+ if (id) {
261
+ const finding = await this.getFinding(id);
262
+ if (finding)
263
+ findings.push(finding);
264
+ }
265
+ }
266
+ return findings;
267
+ } catch {
268
+ return [];
269
+ }
270
+ }
271
+ async acknowledgeFinding(findingId, agentId) {
272
+ await this.yams(`update --name "findings/**/${findingId}.md" --tags "status:acknowledged" --metadata '{"acknowledged_by":"${agentId}","acknowledged_at":"${this.nowISO()}"}'`);
273
+ }
274
+ async resolveFinding(findingId, resolvedBy, resolution) {
275
+ await this.yams(`update --name "findings/**/${findingId}.md" --tags "status:resolved" --metadata '{"resolved_by":"${resolvedBy}","resolution":"${resolution}","resolved_at":"${this.nowISO()}"}'`);
276
+ }
277
+ buildTaskTags(task) {
278
+ const tags = [
279
+ "task",
280
+ `type:${task.type}`,
281
+ `status:${task.status || "pending"}`,
282
+ `priority:${task.priority}`,
283
+ `creator:${task.created_by}`
284
+ ];
285
+ if (task.assigned_to)
286
+ tags.push(`assignee:${task.assigned_to}`);
287
+ if (task.context_id)
288
+ tags.push(`ctx:${task.context_id}`);
289
+ return tags.join(",");
290
+ }
291
+ async createTask(input) {
292
+ const id = this.genId("t");
293
+ const task = {
294
+ ...input,
295
+ id,
296
+ status: "pending",
297
+ priority: input.priority ?? 2
298
+ };
299
+ const content = JSON.stringify(task, null, 2);
300
+ const tags = this.buildTaskTags(task);
301
+ await this.yamsStore(content, `tasks/${id}.json`, tags, this.sessionArg());
302
+ return task;
303
+ }
304
+ async getTask(taskId) {
305
+ try {
306
+ const result = await this.yams(`cat --name "tasks/${taskId}.json"`);
307
+ return JSON.parse(result);
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+ async queryTasks(query) {
313
+ const tags = ["task"];
314
+ if (query.type)
315
+ tags.push(`type:${query.type}`);
316
+ if (query.status)
317
+ tags.push(`status:${query.status}`);
318
+ if (query.priority !== undefined)
319
+ tags.push(`priority:${query.priority}`);
320
+ if (query.created_by)
321
+ tags.push(`creator:${query.created_by}`);
322
+ if (query.assigned_to)
323
+ tags.push(`assignee:${query.assigned_to}`);
324
+ if (query.context_id)
325
+ tags.push(`ctx:${query.context_id}`);
326
+ try {
327
+ const result = await this.yamsJson(`list --tags "${tags.join(",")}" --limit ${query.limit} --offset ${query.offset} ${this.sessionArg()}`);
328
+ const tasks = [];
329
+ for (const doc of result.documents || []) {
330
+ const task = await this.getTask(doc.name?.replace("tasks/", "").replace(".json", "") || "");
331
+ if (task)
332
+ tasks.push(task);
333
+ }
334
+ return tasks;
335
+ } catch {
336
+ return [];
337
+ }
338
+ }
339
+ async getReadyTasks(agentCapabilities) {
340
+ const pending = await this.queryTasks({ status: "pending", limit: 100 });
341
+ const ready = [];
342
+ for (const task of pending) {
343
+ if (task.depends_on?.length) {
344
+ const deps = await Promise.all(task.depends_on.map((id) => this.getTask(id)));
345
+ const allComplete = deps.every((d) => d?.status === "completed");
346
+ if (!allComplete)
347
+ continue;
348
+ }
349
+ ready.push(task);
350
+ }
351
+ return ready.sort((a, b) => (a.priority ?? 2) - (b.priority ?? 2));
352
+ }
353
+ async claimTask(taskId, agentId) {
354
+ const task = await this.getTask(taskId);
355
+ if (!task || task.status !== "pending")
356
+ return null;
357
+ task.status = "claimed";
358
+ task.assigned_to = agentId;
359
+ task.claimed_at = this.nowISO();
360
+ const content = JSON.stringify(task, null, 2);
361
+ const tags = this.buildTaskTags(task);
362
+ await this.yamsStore(content, `tasks/${taskId}.json`, tags);
363
+ return task;
364
+ }
365
+ async updateTask(taskId, updates) {
366
+ const task = await this.getTask(taskId);
367
+ if (!task)
368
+ return null;
369
+ Object.assign(task, updates);
370
+ const content = JSON.stringify(task, null, 2);
371
+ const tags = this.buildTaskTags(task);
372
+ await this.yamsStore(content, `tasks/${taskId}.json`, tags);
373
+ return task;
374
+ }
375
+ async completeTask(taskId, results) {
376
+ return this.updateTask(taskId, {
377
+ status: "completed",
378
+ findings: results?.findings,
379
+ artifacts: results?.artifacts
380
+ });
381
+ }
382
+ async failTask(taskId, error) {
383
+ return this.updateTask(taskId, { status: "failed", error });
384
+ }
385
+ async createContext(id, name, description) {
386
+ const context = {
387
+ id,
388
+ name,
389
+ description,
390
+ findings: [],
391
+ tasks: [],
392
+ agents: [],
393
+ status: "active"
394
+ };
395
+ const content = JSON.stringify(context, null, 2);
396
+ await this.yamsStore(content, `contexts/${id}.json`, "context,status:active");
397
+ return context;
398
+ }
399
+ async getContext(contextId) {
400
+ try {
401
+ const result = await this.yams(`cat --name "contexts/${contextId}.json"`);
402
+ return JSON.parse(result);
403
+ } catch {
404
+ return null;
405
+ }
406
+ }
407
+ async getContextSummary(contextId) {
408
+ const findings = await this.queryFindings({ context_id: contextId, limit: 100 });
409
+ const tasks = await this.queryTasks({ context_id: contextId, limit: 100 });
410
+ const agents = await this.listAgents();
411
+ const activeAgents = agents.filter((a) => a.status === "active");
412
+ const highSeverity = findings.filter((f) => f.severity === "high" || f.severity === "critical");
413
+ const unresolved = findings.filter((f) => f.status !== "resolved");
414
+ const activeTasks = tasks.filter((t) => t.status === "working" || t.status === "claimed");
415
+ const blockedTasks = tasks.filter((t) => t.status === "blocked");
416
+ return `## Blackboard Summary (Context: ${contextId})
417
+
418
+ ### Agents Active (${activeAgents.length})
419
+ ${activeAgents.map((a) => `- ${a.id}: ${a.capabilities.join(", ")}`).join(`
420
+ `) || "- None"}
421
+
422
+ ### Key Findings (${findings.length} total, ${unresolved.length} unresolved)
423
+ ${highSeverity.slice(0, 5).map((f) => `- [${f.severity?.toUpperCase()}] ${f.title} (${f.agent_id}, ${f.confidence.toFixed(2)} confidence)`).join(`
424
+ `) || "- None"}
425
+ ${findings.length > 5 ? `- ... and ${findings.length - 5} more` : ""}
426
+
427
+ ### Tasks
428
+ ${activeTasks.map((t) => `- [${t.status.toUpperCase()}] ${t.title} (assigned: ${t.assigned_to || "unassigned"})`).join(`
429
+ `) || "- No active tasks"}
430
+ ${blockedTasks.length ? `
431
+ **Blocked (${blockedTasks.length}):**
432
+ ${blockedTasks.map((t) => `- ${t.title}`).join(`
433
+ `)}` : ""}
434
+
435
+ ### Unresolved Issues
436
+ ${unresolved.length ? `- ${unresolved.length} findings need resolution` : "- All findings resolved"}
437
+ ${blockedTasks.length ? `- ${blockedTasks.length} tasks blocked` : ""}
438
+ `;
439
+ }
440
+ async getConnections(entityPath, depth = 2) {
441
+ try {
442
+ const result = await this.yamsJson(`graph --name "${entityPath}" --depth ${depth}`);
443
+ return {
444
+ nodes: result.connected_nodes || [],
445
+ edges: []
446
+ };
447
+ } catch {
448
+ return { nodes: [], edges: [] };
449
+ }
450
+ }
451
+ async getStats() {
452
+ const agents = await this.listAgents();
453
+ const findings = await this.queryFindings({ limit: 1000 });
454
+ const tasks = await this.queryTasks({ limit: 1000 });
455
+ const findingsByTopic = {};
456
+ const findingsByStatus = {};
457
+ const findingsBySeverity = {};
458
+ for (const f of findings) {
459
+ findingsByTopic[f.topic] = (findingsByTopic[f.topic] || 0) + 1;
460
+ findingsByStatus[f.status] = (findingsByStatus[f.status] || 0) + 1;
461
+ if (f.severity)
462
+ findingsBySeverity[f.severity] = (findingsBySeverity[f.severity] || 0) + 1;
463
+ }
464
+ const tasksByStatus = {};
465
+ const tasksByType = {};
466
+ for (const t of tasks) {
467
+ tasksByStatus[t.status] = (tasksByStatus[t.status] || 0) + 1;
468
+ tasksByType[t.type] = (tasksByType[t.type] || 0) + 1;
469
+ }
470
+ return {
471
+ agents: agents.length,
472
+ findings: {
473
+ total: findings.length,
474
+ by_topic: findingsByTopic,
475
+ by_status: findingsByStatus,
476
+ by_severity: findingsBySeverity
477
+ },
478
+ tasks: {
479
+ total: tasks.length,
480
+ by_status: tasksByStatus,
481
+ by_type: tasksByType
482
+ },
483
+ contexts: 0
484
+ };
485
+ }
486
+ }
487
+
488
+ // types.ts
489
+ import { z } from "zod";
490
+ var AgentCardSchema = z.object({
491
+ id: z.string().min(1).describe("Unique agent identifier"),
492
+ name: z.string().min(1).describe("Human-readable name"),
493
+ capabilities: z.array(z.string()).min(1).describe("What this agent can do"),
494
+ version: z.string().optional(),
495
+ registered_at: z.string().datetime().optional(),
496
+ status: z.enum(["active", "idle", "offline"]).default("active")
497
+ });
498
+ var ReferenceSchema = z.object({
499
+ type: z.enum(["file", "url", "finding", "task", "symbol"]),
500
+ target: z.string().min(1).describe("Path, URL, ID, or symbol name"),
501
+ label: z.string().optional().describe("Human-readable label"),
502
+ line_start: z.number().int().positive().optional(),
503
+ line_end: z.number().int().positive().optional()
504
+ });
505
+ var FindingSeverity = z.enum(["info", "low", "medium", "high", "critical"]);
506
+ var FindingStatus = z.enum(["draft", "published", "acknowledged", "resolved", "rejected"]);
507
+ var FindingScope = z.enum(["session", "persistent"]);
508
+ var FindingTopic = z.enum([
509
+ "security",
510
+ "performance",
511
+ "bug",
512
+ "architecture",
513
+ "refactor",
514
+ "test",
515
+ "doc",
516
+ "dependency",
517
+ "accessibility",
518
+ "other"
519
+ ]);
520
+ var FindingSchema = z.object({
521
+ id: z.string().optional().describe("Auto-generated if not provided"),
522
+ agent_id: z.string().min(1).describe("Agent that produced this finding"),
523
+ topic: FindingTopic.describe("Category of the finding"),
524
+ title: z.string().min(1).max(200).describe("Brief summary"),
525
+ content: z.string().min(1).describe("Full details in markdown"),
526
+ confidence: z.number().min(0).max(1).default(0.8).describe("How certain the agent is"),
527
+ severity: FindingSeverity.optional().describe("Impact level"),
528
+ context_id: z.string().optional().describe("Groups related findings"),
529
+ references: z.array(ReferenceSchema).optional().describe("Links to code, docs, other findings"),
530
+ parent_id: z.string().optional().describe("For threaded/reply findings"),
531
+ status: FindingStatus.default("published"),
532
+ resolved_by: z.string().optional().describe("Agent that resolved this"),
533
+ resolution: z.string().optional().describe("How it was resolved"),
534
+ scope: FindingScope.default("persistent"),
535
+ ttl: z.number().int().positive().optional().describe("TTL in seconds for session-scoped"),
536
+ metadata: z.record(z.string()).optional()
537
+ });
538
+ var CreateFindingSchema = FindingSchema.omit({
539
+ id: true,
540
+ status: true,
541
+ resolved_by: true,
542
+ resolution: true
543
+ }).extend({
544
+ status: FindingStatus.optional()
545
+ });
546
+ var TaskType = z.enum(["analysis", "fix", "review", "test", "research", "synthesis"]);
547
+ var TaskStatus = z.enum([
548
+ "pending",
549
+ "claimed",
550
+ "working",
551
+ "blocked",
552
+ "review",
553
+ "completed",
554
+ "failed",
555
+ "cancelled"
556
+ ]);
557
+ var TaskPriority = z.union([
558
+ z.literal(0),
559
+ z.literal(1),
560
+ z.literal(2),
561
+ z.literal(3),
562
+ z.literal(4)
563
+ ]);
564
+ var ArtifactSchema = z.object({
565
+ name: z.string().min(1),
566
+ type: z.enum(["file", "data", "report"]),
567
+ path: z.string().optional().describe("YAMS path or local path"),
568
+ hash: z.string().optional().describe("YAMS content hash"),
569
+ mime_type: z.string().optional()
570
+ });
571
+ var TaskSchema = z.object({
572
+ id: z.string().optional().describe("Auto-generated if not provided"),
573
+ title: z.string().min(1).max(200).describe("What needs to be done"),
574
+ description: z.string().optional().describe("Detailed requirements"),
575
+ type: TaskType.describe("Kind of task"),
576
+ priority: TaskPriority.default(2).describe("0=critical, 4=backlog"),
577
+ status: TaskStatus.default("pending"),
578
+ created_by: z.string().min(1).describe("Agent that created the task"),
579
+ assigned_to: z.string().optional().describe("Agent currently working on it"),
580
+ claimed_at: z.string().datetime().optional(),
581
+ depends_on: z.array(z.string()).optional().describe("Task IDs that must complete first"),
582
+ blocks: z.array(z.string()).optional().describe("Task IDs waiting on this"),
583
+ findings: z.array(z.string()).optional().describe("Finding IDs produced"),
584
+ artifacts: z.array(ArtifactSchema).optional().describe("Output files/data"),
585
+ context_id: z.string().optional().describe("Groups related tasks"),
586
+ parent_task: z.string().optional().describe("For subtasks"),
587
+ error: z.string().optional().describe("Error message if failed"),
588
+ retry_count: z.number().int().nonnegative().optional(),
589
+ max_retries: z.number().int().nonnegative().optional(),
590
+ metadata: z.record(z.string()).optional()
591
+ });
592
+ var CreateTaskSchema = TaskSchema.omit({
593
+ id: true,
594
+ status: true,
595
+ assigned_to: true,
596
+ claimed_at: true,
597
+ findings: true,
598
+ artifacts: true,
599
+ error: true,
600
+ retry_count: true
601
+ });
602
+ var ContextStatus = z.enum(["active", "completed", "archived"]);
603
+ var ContextSchema = z.object({
604
+ id: z.string().min(1),
605
+ name: z.string().min(1).describe("Human-readable name"),
606
+ description: z.string().optional(),
607
+ findings: z.array(z.string()).default([]),
608
+ tasks: z.array(z.string()).default([]),
609
+ agents: z.array(z.string()).default([]),
610
+ status: ContextStatus.default("active"),
611
+ summary: z.string().optional().describe("AI-generated summary"),
612
+ key_findings: z.array(z.string()).optional().describe("Most important finding IDs")
613
+ });
614
+ var FindingQuerySchema = z.object({
615
+ topic: FindingTopic.optional(),
616
+ agent_id: z.string().optional(),
617
+ context_id: z.string().optional(),
618
+ status: FindingStatus.optional(),
619
+ severity: z.array(FindingSeverity).optional(),
620
+ min_confidence: z.number().min(0).max(1).optional(),
621
+ scope: FindingScope.optional(),
622
+ limit: z.number().int().positive().default(20),
623
+ offset: z.number().int().nonnegative().default(0)
624
+ });
625
+ var TaskQuerySchema = z.object({
626
+ type: TaskType.optional(),
627
+ status: TaskStatus.optional(),
628
+ priority: TaskPriority.optional(),
629
+ created_by: z.string().optional(),
630
+ assigned_to: z.string().optional(),
631
+ context_id: z.string().optional(),
632
+ limit: z.number().int().positive().default(20),
633
+ offset: z.number().int().nonnegative().default(0)
634
+ });
635
+
636
+ // index.ts
637
+ var YamsBlackboardPlugin = async ({ $, project, directory }) => {
638
+ let blackboard = new YamsBlackboard($, { defaultScope: "persistent" });
639
+ let currentContextId;
640
+ return {
641
+ "session.created": async () => {
642
+ const sessionName = await blackboard.startSession();
643
+ console.log(`[yams-blackboard] Session started: ${sessionName}`);
644
+ },
645
+ "session.compacted": async (input, output) => {
646
+ try {
647
+ const contextId = currentContextId || "default";
648
+ const summary = await blackboard.getContextSummary(contextId);
649
+ output.context.push(summary);
650
+ } catch (e) {
651
+ console.error("[yams-blackboard] Failed to generate compaction summary:", e);
652
+ }
653
+ },
654
+ tool: {
655
+ bb_register_agent: tool({
656
+ description: "Register an agent's identity and capabilities on the blackboard. Call this when starting work to announce your presence.",
657
+ args: {
658
+ id: z2.string().min(1).describe("Unique agent identifier (e.g., 'security-scanner')"),
659
+ name: z2.string().min(1).describe("Human-readable name"),
660
+ capabilities: z2.array(z2.string()).min(1).describe("List of capabilities (e.g., ['code-review', 'security-audit'])")
661
+ },
662
+ async execute(args) {
663
+ const agent = await blackboard.registerAgent({
664
+ id: args.id,
665
+ name: args.name,
666
+ capabilities: args.capabilities,
667
+ status: "active"
668
+ });
669
+ return `Agent registered: ${agent.id}
670
+ Capabilities: ${agent.capabilities.join(", ")}
671
+ Registered at: ${agent.registered_at}`;
672
+ }
673
+ }),
674
+ bb_list_agents: tool({
675
+ description: "List all registered agents and their capabilities",
676
+ args: {},
677
+ async execute() {
678
+ const agents = await blackboard.listAgents();
679
+ if (agents.length === 0) {
680
+ return "No agents registered yet.";
681
+ }
682
+ return agents.map((a) => `${a.id} (${a.status})
683
+ Name: ${a.name}
684
+ Capabilities: ${a.capabilities.join(", ")}`).join(`
685
+
686
+ `);
687
+ }
688
+ }),
689
+ bb_post_finding: tool({
690
+ description: "Post a finding to the shared blackboard for other agents to discover. Use this to share discoveries, observations, issues, or insights.",
691
+ args: {
692
+ agent_id: z2.string().min(1).describe("Your agent identifier"),
693
+ topic: FindingTopic.describe("Category: security, performance, bug, architecture, refactor, test, doc, dependency, accessibility, other"),
694
+ title: z2.string().min(1).max(200).describe("Brief summary of the finding"),
695
+ content: z2.string().min(1).describe("Full details in markdown"),
696
+ confidence: z2.number().min(0).max(1).optional().describe("Confidence level 0-1 (default: 0.8)"),
697
+ severity: FindingSeverity.optional().describe("Impact level: info, low, medium, high, critical"),
698
+ references: z2.array(z2.object({
699
+ type: z2.enum(["file", "url", "finding", "task", "symbol"]),
700
+ target: z2.string(),
701
+ label: z2.string().optional(),
702
+ line_start: z2.number().optional(),
703
+ line_end: z2.number().optional()
704
+ })).optional().describe("Related files, URLs, or other findings"),
705
+ context_id: z2.string().optional().describe("Group with related findings"),
706
+ parent_id: z2.string().optional().describe("Reply to another finding"),
707
+ scope: FindingScope.optional().describe("Persistence: 'persistent' (default) or 'session'"),
708
+ metadata: z2.record(z2.string()).optional().describe("Additional key-value metadata")
709
+ },
710
+ async execute(args) {
711
+ const finding = await blackboard.postFinding({
712
+ agent_id: args.agent_id,
713
+ topic: args.topic,
714
+ title: args.title,
715
+ content: args.content,
716
+ confidence: args.confidence ?? 0.8,
717
+ severity: args.severity,
718
+ references: args.references,
719
+ context_id: args.context_id || currentContextId,
720
+ parent_id: args.parent_id,
721
+ scope: args.scope ?? "persistent",
722
+ metadata: args.metadata
723
+ });
724
+ return `Finding posted: ${finding.id}
725
+ Topic: ${finding.topic}
726
+ Title: ${finding.title}
727
+ Confidence: ${finding.confidence}
728
+ ${finding.severity ? `Severity: ${finding.severity}` : ""}
729
+ ${finding.context_id ? `Context: ${finding.context_id}` : ""}`;
730
+ }
731
+ }),
732
+ bb_query_findings: tool({
733
+ description: "Query findings from the blackboard by topic, agent, severity, or context. Use to discover what other agents have found.",
734
+ args: {
735
+ topic: FindingTopic.optional().describe("Filter by topic"),
736
+ agent_id: z2.string().optional().describe("Filter by source agent"),
737
+ context_id: z2.string().optional().describe("Filter by context group"),
738
+ status: FindingStatus.optional().describe("Filter by status"),
739
+ severity: z2.array(FindingSeverity).optional().describe("Filter by severity levels"),
740
+ min_confidence: z2.number().min(0).max(1).optional().describe("Minimum confidence threshold"),
741
+ scope: FindingScope.optional().describe("Filter by persistence scope"),
742
+ limit: z2.number().int().positive().optional().describe("Max results (default: 20)")
743
+ },
744
+ async execute(args) {
745
+ const findings = await blackboard.queryFindings({
746
+ ...args,
747
+ context_id: args.context_id || currentContextId,
748
+ limit: args.limit ?? 20,
749
+ offset: 0
750
+ });
751
+ if (findings.length === 0) {
752
+ return "No findings match the query.";
753
+ }
754
+ return findings.map((f) => `[${f.id}] ${f.topic.toUpperCase()} | ${f.title}
755
+ Agent: ${f.agent_id} | Confidence: ${f.confidence.toFixed(2)}${f.severity ? ` | Severity: ${f.severity}` : ""}
756
+ Status: ${f.status}${f.context_id ? ` | Context: ${f.context_id}` : ""}`).join(`
757
+
758
+ `);
759
+ }
760
+ }),
761
+ bb_search_findings: tool({
762
+ description: "Search findings using natural language. Uses semantic search to find relevant findings.",
763
+ args: {
764
+ query: z2.string().min(1).describe("Natural language search query"),
765
+ topic: FindingTopic.optional().describe("Limit to specific topic"),
766
+ limit: z2.number().int().positive().optional().describe("Max results (default: 10)")
767
+ },
768
+ async execute(args) {
769
+ const findings = await blackboard.searchFindings(args.query, {
770
+ topic: args.topic,
771
+ limit: args.limit ?? 10
772
+ });
773
+ if (findings.length === 0) {
774
+ return "No findings match the search.";
775
+ }
776
+ return findings.map((f) => `[${f.id}] ${f.topic.toUpperCase()} | ${f.title}
777
+ ${f.content.slice(0, 200)}${f.content.length > 200 ? "..." : ""}`).join(`
778
+
779
+ `);
780
+ }
781
+ }),
782
+ bb_get_finding: tool({
783
+ description: "Get full details of a specific finding by ID",
784
+ args: {
785
+ finding_id: z2.string().min(1).describe("The finding ID")
786
+ },
787
+ async execute(args) {
788
+ const finding = await blackboard.getFinding(args.finding_id);
789
+ if (!finding) {
790
+ return `Finding not found: ${args.finding_id}`;
791
+ }
792
+ return `# ${finding.title}
793
+
794
+ **ID:** ${finding.id}
795
+ **Agent:** ${finding.agent_id}
796
+ **Topic:** ${finding.topic}
797
+ **Status:** ${finding.status}
798
+ **Confidence:** ${finding.confidence}
799
+ ${finding.severity ? `**Severity:** ${finding.severity}` : ""}
800
+ ${finding.context_id ? `**Context:** ${finding.context_id}` : ""}
801
+ ${finding.references?.length ? `**References:** ${finding.references.map((r) => `${r.type}:${r.target}`).join(", ")}` : ""}
802
+
803
+ ## Content
804
+
805
+ ${finding.content}`;
806
+ }
807
+ }),
808
+ bb_acknowledge_finding: tool({
809
+ description: "Mark a finding as acknowledged (you've seen it)",
810
+ args: {
811
+ finding_id: z2.string().min(1).describe("The finding ID to acknowledge"),
812
+ agent_id: z2.string().min(1).describe("Your agent ID")
813
+ },
814
+ async execute(args) {
815
+ await blackboard.acknowledgeFinding(args.finding_id, args.agent_id);
816
+ return `Finding ${args.finding_id} acknowledged by ${args.agent_id}`;
817
+ }
818
+ }),
819
+ bb_resolve_finding: tool({
820
+ description: "Mark a finding as resolved with an explanation",
821
+ args: {
822
+ finding_id: z2.string().min(1).describe("The finding ID to resolve"),
823
+ agent_id: z2.string().min(1).describe("Your agent ID"),
824
+ resolution: z2.string().min(1).describe("How the finding was resolved")
825
+ },
826
+ async execute(args) {
827
+ await blackboard.resolveFinding(args.finding_id, args.agent_id, args.resolution);
828
+ return `Finding ${args.finding_id} resolved by ${args.agent_id}: ${args.resolution}`;
829
+ }
830
+ }),
831
+ bb_create_task: tool({
832
+ description: "Create a new task on the blackboard for agents to claim and work on",
833
+ args: {
834
+ title: z2.string().min(1).max(200).describe("What needs to be done"),
835
+ description: z2.string().optional().describe("Detailed requirements"),
836
+ type: TaskType.describe("Kind of task: analysis, fix, review, test, research, synthesis"),
837
+ priority: TaskPriority.optional().describe("0=critical, 1=high, 2=medium, 3=low, 4=backlog"),
838
+ created_by: z2.string().min(1).describe("Your agent ID"),
839
+ depends_on: z2.array(z2.string()).optional().describe("Task IDs that must complete first"),
840
+ context_id: z2.string().optional().describe("Group with related tasks")
841
+ },
842
+ async execute(args) {
843
+ const task = await blackboard.createTask({
844
+ title: args.title,
845
+ description: args.description,
846
+ type: args.type,
847
+ priority: args.priority ?? 2,
848
+ created_by: args.created_by,
849
+ depends_on: args.depends_on,
850
+ context_id: args.context_id || currentContextId
851
+ });
852
+ return `Task created: ${task.id}
853
+ Title: ${task.title}
854
+ Type: ${task.type}
855
+ Priority: ${task.priority}
856
+ Status: ${task.status}
857
+ ${task.depends_on?.length ? `Depends on: ${task.depends_on.join(", ")}` : ""}`;
858
+ }
859
+ }),
860
+ bb_get_ready_tasks: tool({
861
+ description: "Get tasks that are ready to work on (pending, no unmet dependencies). Use this to find work.",
862
+ args: {
863
+ limit: z2.number().int().positive().optional().describe("Max results")
864
+ },
865
+ async execute(args) {
866
+ const tasks = await blackboard.getReadyTasks();
867
+ const limited = tasks.slice(0, args.limit || 10);
868
+ if (limited.length === 0) {
869
+ return "No tasks ready to work on.";
870
+ }
871
+ return limited.map((t) => `[${t.id}] P${t.priority} ${t.type.toUpperCase()} | ${t.title}
872
+ Created by: ${t.created_by}
873
+ ${t.description ? `Description: ${t.description.slice(0, 100)}...` : ""}`).join(`
874
+
875
+ `);
876
+ }
877
+ }),
878
+ bb_claim_task: tool({
879
+ description: "Claim a pending task to work on it",
880
+ args: {
881
+ task_id: z2.string().min(1).describe("The task ID to claim"),
882
+ agent_id: z2.string().min(1).describe("Your agent ID")
883
+ },
884
+ async execute(args) {
885
+ const task = await blackboard.claimTask(args.task_id, args.agent_id);
886
+ if (!task) {
887
+ return `Failed to claim task ${args.task_id}. It may not exist or is already claimed.`;
888
+ }
889
+ return `Task claimed: ${task.id}
890
+ Title: ${task.title}
891
+ Assigned to: ${task.assigned_to}
892
+ Status: ${task.status}`;
893
+ }
894
+ }),
895
+ bb_update_task: tool({
896
+ description: "Update the status of a task you're working on",
897
+ args: {
898
+ task_id: z2.string().min(1).describe("The task ID"),
899
+ status: TaskStatus.describe("New status: working, blocked, review"),
900
+ error: z2.string().optional().describe("Error message if blocked/failed")
901
+ },
902
+ async execute(args) {
903
+ const task = await blackboard.updateTask(args.task_id, {
904
+ status: args.status,
905
+ error: args.error
906
+ });
907
+ if (!task) {
908
+ return `Task not found: ${args.task_id}`;
909
+ }
910
+ return `Task ${task.id} updated to status: ${task.status}`;
911
+ }
912
+ }),
913
+ bb_complete_task: tool({
914
+ description: "Mark a task as completed, optionally with findings or artifacts",
915
+ args: {
916
+ task_id: z2.string().min(1).describe("The task ID"),
917
+ findings: z2.array(z2.string()).optional().describe("Finding IDs produced by this task")
918
+ },
919
+ async execute(args) {
920
+ const task = await blackboard.completeTask(args.task_id, {
921
+ findings: args.findings
922
+ });
923
+ if (!task) {
924
+ return `Task not found: ${args.task_id}`;
925
+ }
926
+ return `Task completed: ${task.id}
927
+ Title: ${task.title}
928
+ ${args.findings?.length ? `Findings: ${args.findings.join(", ")}` : ""}`;
929
+ }
930
+ }),
931
+ bb_fail_task: tool({
932
+ description: "Mark a task as failed with an error message",
933
+ args: {
934
+ task_id: z2.string().min(1).describe("The task ID"),
935
+ error: z2.string().min(1).describe("What went wrong")
936
+ },
937
+ async execute(args) {
938
+ const task = await blackboard.failTask(args.task_id, args.error);
939
+ if (!task) {
940
+ return `Task not found: ${args.task_id}`;
941
+ }
942
+ return `Task failed: ${task.id}
943
+ Error: ${args.error}`;
944
+ }
945
+ }),
946
+ bb_query_tasks: tool({
947
+ description: "Query tasks by type, status, priority, or assignee",
948
+ args: {
949
+ type: TaskType.optional().describe("Filter by task type"),
950
+ status: TaskStatus.optional().describe("Filter by status"),
951
+ priority: TaskPriority.optional().describe("Filter by priority"),
952
+ created_by: z2.string().optional().describe("Filter by creator"),
953
+ assigned_to: z2.string().optional().describe("Filter by assignee"),
954
+ context_id: z2.string().optional().describe("Filter by context"),
955
+ limit: z2.number().int().positive().optional().describe("Max results")
956
+ },
957
+ async execute(args) {
958
+ const tasks = await blackboard.queryTasks({
959
+ ...args,
960
+ context_id: args.context_id || currentContextId,
961
+ limit: args.limit ?? 20,
962
+ offset: 0
963
+ });
964
+ if (tasks.length === 0) {
965
+ return "No tasks match the query.";
966
+ }
967
+ return tasks.map((t) => `[${t.id}] P${t.priority} ${t.type} | ${t.title}
968
+ Status: ${t.status} | Created: ${t.created_by}${t.assigned_to ? ` | Assigned: ${t.assigned_to}` : ""}`).join(`
969
+
970
+ `);
971
+ }
972
+ }),
973
+ bb_create_context: tool({
974
+ description: "Create a new context to group related findings and tasks together",
975
+ args: {
976
+ id: z2.string().min(1).describe("Context identifier (e.g., 'security-audit-2025')"),
977
+ name: z2.string().min(1).describe("Human-readable name"),
978
+ description: z2.string().optional().describe("What this context is about"),
979
+ set_current: z2.boolean().optional().describe("Set as current context (default: true)")
980
+ },
981
+ async execute(args) {
982
+ const context = await blackboard.createContext(args.id, args.name, args.description);
983
+ if (args.set_current !== false) {
984
+ currentContextId = context.id;
985
+ }
986
+ return `Context created: ${context.id}
987
+ Name: ${context.name}
988
+ ${context.description ? `Description: ${context.description}` : ""}
989
+ ${args.set_current !== false ? "(Set as current context)" : ""}`;
990
+ }
991
+ }),
992
+ bb_get_context_summary: tool({
993
+ description: "Get a summary of a context including findings, tasks, and agents",
994
+ args: {
995
+ context_id: z2.string().optional().describe("Context ID (defaults to current)")
996
+ },
997
+ async execute(args) {
998
+ const contextId = args.context_id || currentContextId || "default";
999
+ return await blackboard.getContextSummary(contextId);
1000
+ }
1001
+ }),
1002
+ bb_set_context: tool({
1003
+ description: "Set the current working context for findings and tasks",
1004
+ args: {
1005
+ context_id: z2.string().min(1).describe("Context ID to set as current")
1006
+ },
1007
+ async execute(args) {
1008
+ currentContextId = args.context_id;
1009
+ return `Current context set to: ${args.context_id}`;
1010
+ }
1011
+ }),
1012
+ bb_recent_activity: tool({
1013
+ description: "Get recent findings and task updates across all topics",
1014
+ args: {
1015
+ limit: z2.number().int().positive().optional().describe("Max items (default: 10)")
1016
+ },
1017
+ async execute(args) {
1018
+ const limit = args.limit || 10;
1019
+ const findings = await blackboard.queryFindings({ limit });
1020
+ const tasks = await blackboard.queryTasks({ limit });
1021
+ const output = [`## Recent Activity
1022
+ `];
1023
+ if (findings.length > 0) {
1024
+ output.push("### Findings");
1025
+ output.push(findings.map((f) => `- [${f.topic}] ${f.title} (${f.agent_id})`).join(`
1026
+ `));
1027
+ }
1028
+ if (tasks.length > 0) {
1029
+ output.push(`
1030
+ ### Tasks`);
1031
+ output.push(tasks.map((t) => `- [${t.status}] ${t.title} (${t.type})`).join(`
1032
+ `));
1033
+ }
1034
+ return output.join(`
1035
+ `);
1036
+ }
1037
+ }),
1038
+ bb_stats: tool({
1039
+ description: "Get statistics about the blackboard (agents, findings, tasks)",
1040
+ args: {},
1041
+ async execute() {
1042
+ const stats = await blackboard.getStats();
1043
+ return `## Blackboard Statistics
1044
+
1045
+ ### Agents: ${stats.agents}
1046
+
1047
+ ### Findings: ${stats.findings.total}
1048
+ By Topic: ${Object.entries(stats.findings.by_topic).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}
1049
+ By Status: ${Object.entries(stats.findings.by_status).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}
1050
+ By Severity: ${Object.entries(stats.findings.by_severity).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}
1051
+
1052
+ ### Tasks: ${stats.tasks.total}
1053
+ By Status: ${Object.entries(stats.tasks.by_status).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}
1054
+ By Type: ${Object.entries(stats.tasks.by_type).map(([k, v]) => `${k}:${v}`).join(", ") || "none"}`;
1055
+ }
1056
+ }),
1057
+ bb_connections: tool({
1058
+ description: "Explore graph connections for a finding or document - see related code, findings, and concepts",
1059
+ args: {
1060
+ path: z2.string().min(1).describe("Path to the entity (e.g., 'findings/security/f-xxx.md')"),
1061
+ depth: z2.number().int().positive().optional().describe("Traversal depth (default: 2)")
1062
+ },
1063
+ async execute(args) {
1064
+ const graph = await blackboard.getConnections(args.path, args.depth ?? 2);
1065
+ if (graph.nodes.length === 0) {
1066
+ return "No connections found.";
1067
+ }
1068
+ return `## Connections for ${args.path}
1069
+
1070
+ Found ${graph.nodes.length} connected nodes:
1071
+
1072
+ ${JSON.stringify(graph.nodes, null, 2)}`;
1073
+ }
1074
+ })
1075
+ }
1076
+ };
1077
+ };
1078
+ var open_code_blackboard_default = YamsBlackboardPlugin;
1079
+ export {
1080
+ open_code_blackboard_default as default,
1081
+ YamsBlackboardPlugin
1082
+ };