@tini-works/inv-node 0.1.4

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 (2) hide show
  1. package/dist/cli.js +2051 -0
  2. package/package.json +25 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2051 @@
1
+ // @bun
2
+ // src/cli.ts
3
+ import * as readline from "readline";
4
+ import { writeFileSync } from "fs";
5
+ function generateInvConfig(input) {
6
+ return {
7
+ node: {
8
+ name: input.name,
9
+ vertical: input.vertical,
10
+ project: input.project,
11
+ owner: input.owner
12
+ },
13
+ server: {
14
+ url: input.serverUrl,
15
+ token: input.token
16
+ },
17
+ database: {
18
+ path: input.dbPath
19
+ }
20
+ };
21
+ }
22
+ function generateMcpConfig(configPath) {
23
+ return {
24
+ mcpServers: {
25
+ inventory: {
26
+ command: "bunx",
27
+ args: ["@tini-works/inv-node@latest", "serve", configPath]
28
+ }
29
+ }
30
+ };
31
+ }
32
+ var VERTICALS = ["pm", "design", "dev", "qa", "devops"];
33
+ function ask(rl, question, defaultValue) {
34
+ const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
35
+ return new Promise((resolve) => {
36
+ rl.question(prompt, (answer) => {
37
+ resolve(answer.trim() || defaultValue || "");
38
+ });
39
+ });
40
+ }
41
+ async function runWizard() {
42
+ const rl = readline.createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout
45
+ });
46
+ console.log("");
47
+ console.log("Inventory Node Setup");
48
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
49
+ console.log("");
50
+ const name = await ask(rl, "Node name");
51
+ const verticalInput = await ask(rl, `Vertical (${VERTICALS.join("/")})`, "dev");
52
+ if (!VERTICALS.includes(verticalInput)) {
53
+ console.error(`Invalid vertical: ${verticalInput}. Must be one of: ${VERTICALS.join(", ")}`);
54
+ rl.close();
55
+ process.exit(1);
56
+ }
57
+ const vertical = verticalInput;
58
+ const project = await ask(rl, "Project");
59
+ const owner = await ask(rl, "Owner");
60
+ console.log("");
61
+ const serverUrl = await ask(rl, "Server URL", "ws://localhost:8080/ws");
62
+ const token = await ask(rl, "Auth token");
63
+ console.log("");
64
+ const dbPath = await ask(rl, "Database path", "./inventory.db");
65
+ rl.close();
66
+ const invConfig = generateInvConfig({ name, vertical, project, owner, serverUrl, token, dbPath });
67
+ const mcpConfig = generateMcpConfig("./inv-config.json");
68
+ console.log("");
69
+ const invConfigPath = "./inv-config.json";
70
+ writeFileSync(invConfigPath, JSON.stringify(invConfig, null, 2) + `
71
+ `);
72
+ console.log(`Writing ${invConfigPath}... \u2713`);
73
+ const mcpConfigPath = "./.mcp.json";
74
+ writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + `
75
+ `);
76
+ console.log(`Writing ${mcpConfigPath}... \u2713`);
77
+ console.log("");
78
+ console.log("Setup complete! Start Claude Code:");
79
+ console.log(" claude");
80
+ console.log("");
81
+ }
82
+
83
+ // src/channel.ts
84
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
85
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
86
+ import {
87
+ ListToolsRequestSchema,
88
+ CallToolRequestSchema
89
+ } from "@modelcontextprotocol/sdk/types.js";
90
+ import { readFileSync, writeFileSync as writeFileSync2, existsSync } from "fs";
91
+
92
+ // src/logger.ts
93
+ class Logger {
94
+ name;
95
+ writer = (line) => process.stderr.write(line + `
96
+ `);
97
+ constructor(name) {
98
+ this.name = name;
99
+ }
100
+ setWriter(fn) {
101
+ this.writer = fn;
102
+ }
103
+ info(msg, data) {
104
+ this.log("info", msg, data);
105
+ }
106
+ warn(msg, data) {
107
+ this.log("warn", msg, data);
108
+ }
109
+ error(msg, data) {
110
+ this.log("error", msg, data);
111
+ }
112
+ log(level, msg, data) {
113
+ const entry = {
114
+ ts: new Date().toISOString(),
115
+ level,
116
+ logger: this.name,
117
+ msg,
118
+ ...data
119
+ };
120
+ this.writer(JSON.stringify(entry));
121
+ }
122
+ }
123
+
124
+ // src/store.ts
125
+ import { Database } from "bun:sqlite";
126
+ import { randomUUID as randomUUID2 } from "crypto";
127
+
128
+ // ../shared/src/types.ts
129
+ var UPSTREAM_VERTICALS = {
130
+ pm: [],
131
+ design: ["pm"],
132
+ dev: ["pm", "design"],
133
+ qa: ["dev"],
134
+ devops: ["dev", "qa"]
135
+ };
136
+ // ../shared/src/messages.ts
137
+ import { randomUUID } from "crypto";
138
+ function createEnvelope(fromNode, toNode, projectId, payload) {
139
+ return {
140
+ messageId: randomUUID(),
141
+ fromNode,
142
+ toNode,
143
+ projectId,
144
+ timestamp: new Date().toISOString(),
145
+ payload
146
+ };
147
+ }
148
+ function parseEnvelope(raw) {
149
+ const parsed = JSON.parse(raw);
150
+ if (!parsed.messageId || !parsed.payload?.type) {
151
+ throw new Error("Invalid envelope");
152
+ }
153
+ return parsed;
154
+ }
155
+ // src/store.ts
156
+ function mapNodeRow(row) {
157
+ return {
158
+ id: row.id,
159
+ name: row.name,
160
+ vertical: row.vertical,
161
+ project: row.project,
162
+ owner: row.owner,
163
+ isAI: row.is_ai === 1,
164
+ createdAt: row.created_at
165
+ };
166
+ }
167
+ function mapItemRow(row) {
168
+ return {
169
+ id: row.id,
170
+ nodeId: row.node_id,
171
+ kind: row.kind,
172
+ title: row.title,
173
+ body: row.body,
174
+ externalRef: row.external_ref,
175
+ state: row.state,
176
+ evidence: row.evidence,
177
+ confirmedBy: row.confirmed_by,
178
+ confirmedAt: row.confirmed_at,
179
+ version: row.version,
180
+ createdAt: row.created_at,
181
+ updatedAt: row.updated_at
182
+ };
183
+ }
184
+ function mapTraceRow(row) {
185
+ return {
186
+ id: row.id,
187
+ fromItemId: row.from_item_id,
188
+ fromNodeId: row.from_node_id,
189
+ toItemId: row.to_item_id,
190
+ toNodeId: row.to_node_id,
191
+ relation: row.relation,
192
+ confirmedBy: row.confirmed_by,
193
+ confirmedAt: row.confirmed_at,
194
+ createdAt: row.created_at
195
+ };
196
+ }
197
+ function mapSignalRow(row) {
198
+ return {
199
+ id: row.id,
200
+ kind: row.kind,
201
+ sourceItem: row.source_item,
202
+ sourceNode: row.source_node,
203
+ targetItem: row.target_item,
204
+ targetNode: row.target_node,
205
+ payload: row.payload,
206
+ processed: row.processed === 1,
207
+ createdAt: row.created_at
208
+ };
209
+ }
210
+ function mapTransitionRow(row) {
211
+ return {
212
+ id: row.id,
213
+ itemId: row.item_id,
214
+ kind: row.kind,
215
+ from: row.from_s,
216
+ to: row.to_s,
217
+ evidence: row.evidence,
218
+ reason: row.reason,
219
+ actor: row.actor,
220
+ timestamp: row.timestamp
221
+ };
222
+ }
223
+ function mapQueryRow(row) {
224
+ return {
225
+ id: row.id,
226
+ askerId: row.asker_id,
227
+ askerNode: row.asker_node,
228
+ question: row.question,
229
+ context: row.context,
230
+ targetNode: row.target_node,
231
+ resolved: row.resolved === 1,
232
+ createdAt: row.created_at
233
+ };
234
+ }
235
+ function mapQueryResponseRow(row) {
236
+ return {
237
+ id: row.id,
238
+ queryId: row.query_id,
239
+ responderId: row.responder_id,
240
+ nodeId: row.node_id,
241
+ answer: row.answer,
242
+ isAI: row.is_ai === 1,
243
+ createdAt: row.created_at
244
+ };
245
+ }
246
+ function mapPendingActionRow(row) {
247
+ return {
248
+ id: row.id,
249
+ messageType: row.message_type,
250
+ envelope: row.envelope,
251
+ summary: row.summary,
252
+ proposed: row.proposed,
253
+ status: row.status,
254
+ createdAt: row.created_at
255
+ };
256
+ }
257
+ function mapChangeRequestRow(row) {
258
+ return {
259
+ id: row.id,
260
+ proposerNode: row.proposer_node,
261
+ proposerId: row.proposer_id,
262
+ targetItemId: row.target_item_id,
263
+ description: row.description,
264
+ status: row.status,
265
+ createdAt: row.created_at,
266
+ updatedAt: row.updated_at
267
+ };
268
+ }
269
+ function mapVoteRow(row) {
270
+ return {
271
+ id: row.id,
272
+ crId: row.cr_id,
273
+ nodeId: row.node_id,
274
+ vertical: row.vertical,
275
+ approve: row.approve === 1,
276
+ reason: row.reason,
277
+ createdAt: row.created_at
278
+ };
279
+ }
280
+ function mapPairSessionRow(row) {
281
+ return {
282
+ id: row.id,
283
+ initiatorNode: row.initiator_node,
284
+ partnerNode: row.partner_node,
285
+ project: row.project,
286
+ status: row.status,
287
+ startedAt: row.started_at,
288
+ endedAt: row.ended_at
289
+ };
290
+ }
291
+ function mapChecklistItemRow(row) {
292
+ return {
293
+ id: row.id,
294
+ itemId: row.item_id,
295
+ text: row.text,
296
+ checked: row.checked === 1,
297
+ createdAt: row.created_at
298
+ };
299
+ }
300
+ function mapKindMappingRow(row) {
301
+ return {
302
+ id: row.id,
303
+ fromVertical: row.from_vertical,
304
+ fromKind: row.from_kind,
305
+ toVertical: row.to_vertical,
306
+ toKind: row.to_kind,
307
+ createdAt: row.created_at
308
+ };
309
+ }
310
+
311
+ class Store {
312
+ db;
313
+ constructor(dbPath) {
314
+ this.db = new Database(dbPath);
315
+ this.db.run("PRAGMA journal_mode = WAL;");
316
+ this.db.run("PRAGMA foreign_keys = ON;");
317
+ this.migrate();
318
+ }
319
+ close() {
320
+ this.db.close();
321
+ }
322
+ migrate() {
323
+ this.db.run(`
324
+ CREATE TABLE IF NOT EXISTS nodes (
325
+ id TEXT PRIMARY KEY,
326
+ name TEXT NOT NULL,
327
+ vertical TEXT NOT NULL,
328
+ project TEXT NOT NULL,
329
+ owner TEXT NOT NULL,
330
+ is_ai INTEGER DEFAULT 0,
331
+ created_at TEXT DEFAULT (datetime('now'))
332
+ )
333
+ `);
334
+ this.db.run(`
335
+ CREATE TABLE IF NOT EXISTS items (
336
+ id TEXT PRIMARY KEY,
337
+ node_id TEXT NOT NULL REFERENCES nodes(id),
338
+ kind TEXT NOT NULL,
339
+ title TEXT NOT NULL,
340
+ body TEXT DEFAULT '',
341
+ external_ref TEXT DEFAULT '',
342
+ state TEXT DEFAULT 'unverified',
343
+ evidence TEXT DEFAULT '',
344
+ confirmed_by TEXT DEFAULT '',
345
+ confirmed_at TEXT,
346
+ version INTEGER DEFAULT 1,
347
+ created_at TEXT DEFAULT (datetime('now')),
348
+ updated_at TEXT DEFAULT (datetime('now'))
349
+ )
350
+ `);
351
+ this.db.run(`
352
+ CREATE TABLE IF NOT EXISTS traces (
353
+ id TEXT PRIMARY KEY,
354
+ from_item_id TEXT NOT NULL REFERENCES items(id),
355
+ from_node_id TEXT NOT NULL REFERENCES nodes(id),
356
+ to_item_id TEXT NOT NULL REFERENCES items(id),
357
+ to_node_id TEXT NOT NULL REFERENCES nodes(id),
358
+ relation TEXT NOT NULL,
359
+ confirmed_by TEXT DEFAULT '',
360
+ confirmed_at TEXT,
361
+ created_at TEXT DEFAULT (datetime('now'))
362
+ )
363
+ `);
364
+ this.db.run(`
365
+ CREATE TABLE IF NOT EXISTS signals (
366
+ id TEXT PRIMARY KEY,
367
+ kind TEXT NOT NULL,
368
+ source_item TEXT NOT NULL,
369
+ source_node TEXT NOT NULL,
370
+ target_item TEXT NOT NULL,
371
+ target_node TEXT NOT NULL,
372
+ payload TEXT DEFAULT '',
373
+ processed INTEGER DEFAULT 0,
374
+ created_at TEXT DEFAULT (datetime('now'))
375
+ )
376
+ `);
377
+ this.db.run(`
378
+ CREATE TABLE IF NOT EXISTS transitions (
379
+ id TEXT PRIMARY KEY,
380
+ item_id TEXT NOT NULL REFERENCES items(id),
381
+ kind TEXT NOT NULL,
382
+ from_s TEXT NOT NULL,
383
+ to_s TEXT NOT NULL,
384
+ evidence TEXT DEFAULT '',
385
+ reason TEXT DEFAULT '',
386
+ actor TEXT NOT NULL,
387
+ timestamp TEXT DEFAULT (datetime('now'))
388
+ )
389
+ `);
390
+ this.db.run(`
391
+ CREATE TABLE IF NOT EXISTS queries (
392
+ id TEXT PRIMARY KEY,
393
+ asker_id TEXT NOT NULL,
394
+ asker_node TEXT NOT NULL,
395
+ question TEXT NOT NULL,
396
+ context TEXT DEFAULT '',
397
+ target_node TEXT DEFAULT '',
398
+ resolved INTEGER DEFAULT 0,
399
+ created_at TEXT DEFAULT (datetime('now'))
400
+ )
401
+ `);
402
+ this.db.run(`
403
+ CREATE TABLE IF NOT EXISTS query_responses (
404
+ id TEXT PRIMARY KEY,
405
+ query_id TEXT NOT NULL REFERENCES queries(id),
406
+ responder_id TEXT NOT NULL,
407
+ node_id TEXT NOT NULL,
408
+ answer TEXT NOT NULL,
409
+ is_ai INTEGER DEFAULT 0,
410
+ created_at TEXT DEFAULT (datetime('now'))
411
+ )
412
+ `);
413
+ this.db.run(`
414
+ CREATE TABLE IF NOT EXISTS pending_actions (
415
+ id TEXT PRIMARY KEY,
416
+ message_type TEXT NOT NULL,
417
+ envelope TEXT NOT NULL,
418
+ summary TEXT NOT NULL,
419
+ proposed TEXT NOT NULL,
420
+ status TEXT DEFAULT 'pending',
421
+ created_at TEXT DEFAULT (datetime('now'))
422
+ )
423
+ `);
424
+ this.db.run(`
425
+ CREATE TABLE IF NOT EXISTS change_requests (
426
+ id TEXT PRIMARY KEY,
427
+ proposer_node TEXT NOT NULL,
428
+ proposer_id TEXT NOT NULL,
429
+ target_item_id TEXT NOT NULL,
430
+ description TEXT NOT NULL,
431
+ status TEXT DEFAULT 'draft',
432
+ created_at TEXT DEFAULT (datetime('now')),
433
+ updated_at TEXT DEFAULT (datetime('now'))
434
+ )
435
+ `);
436
+ this.db.run(`
437
+ CREATE TABLE IF NOT EXISTS votes (
438
+ id TEXT PRIMARY KEY,
439
+ cr_id TEXT NOT NULL REFERENCES change_requests(id),
440
+ node_id TEXT NOT NULL,
441
+ vertical TEXT NOT NULL,
442
+ approve INTEGER NOT NULL,
443
+ reason TEXT DEFAULT '',
444
+ created_at TEXT DEFAULT (datetime('now')),
445
+ UNIQUE(cr_id, node_id)
446
+ )
447
+ `);
448
+ this.db.run(`
449
+ CREATE TABLE IF NOT EXISTS pair_sessions (
450
+ id TEXT PRIMARY KEY,
451
+ initiator_node TEXT NOT NULL,
452
+ partner_node TEXT NOT NULL,
453
+ project TEXT NOT NULL,
454
+ status TEXT DEFAULT 'pending',
455
+ started_at TEXT DEFAULT (datetime('now')),
456
+ ended_at TEXT
457
+ )
458
+ `);
459
+ this.db.run(`
460
+ CREATE TABLE IF NOT EXISTS checklists (
461
+ id TEXT PRIMARY KEY,
462
+ item_id TEXT NOT NULL,
463
+ text TEXT NOT NULL,
464
+ checked INTEGER DEFAULT 0,
465
+ created_at TEXT DEFAULT (datetime('now'))
466
+ )
467
+ `);
468
+ this.db.run(`
469
+ CREATE TABLE IF NOT EXISTS kind_mappings (
470
+ id TEXT PRIMARY KEY,
471
+ from_vertical TEXT NOT NULL,
472
+ from_kind TEXT NOT NULL,
473
+ to_vertical TEXT NOT NULL,
474
+ to_kind TEXT NOT NULL,
475
+ created_at TEXT DEFAULT (datetime('now')),
476
+ UNIQUE(from_vertical, from_kind, to_vertical)
477
+ )
478
+ `);
479
+ }
480
+ createNode(input) {
481
+ const id = randomUUID2();
482
+ const stmt = this.db.query(`INSERT INTO nodes (id, name, vertical, project, owner, is_ai)
483
+ VALUES (?, ?, ?, ?, ?, ?)
484
+ RETURNING *`);
485
+ const row = stmt.get(id, input.name, input.vertical, input.project, input.owner, input.isAI ? 1 : 0);
486
+ if (!row)
487
+ throw new Error("Failed to create node");
488
+ return mapNodeRow(row);
489
+ }
490
+ getNode(id) {
491
+ const row = this.db.query("SELECT * FROM nodes WHERE id = ?").get(id);
492
+ return row ? mapNodeRow(row) : null;
493
+ }
494
+ listNodes(project) {
495
+ const rows = this.db.query("SELECT * FROM nodes WHERE project = ?").all(project);
496
+ return rows.map(mapNodeRow);
497
+ }
498
+ createItem(input) {
499
+ const id = randomUUID2();
500
+ const stmt = this.db.query(`INSERT INTO items (id, node_id, kind, title, body, external_ref)
501
+ VALUES (?, ?, ?, ?, ?, ?)
502
+ RETURNING *`);
503
+ const row = stmt.get(id, input.nodeId, input.kind, input.title, input.body ?? "", input.externalRef ?? "");
504
+ if (!row)
505
+ throw new Error("Failed to create item");
506
+ return mapItemRow(row);
507
+ }
508
+ getItem(id) {
509
+ const row = this.db.query("SELECT * FROM items WHERE id = ?").get(id);
510
+ return row ? mapItemRow(row) : null;
511
+ }
512
+ listItems(nodeId) {
513
+ const rows = this.db.query("SELECT * FROM items WHERE node_id = ?").all(nodeId);
514
+ return rows.map(mapItemRow);
515
+ }
516
+ updateItemStatus(id, state, evidence, confirmedBy) {
517
+ const stmt = this.db.query(`UPDATE items
518
+ SET state = ?, evidence = ?, confirmed_by = ?,
519
+ confirmed_at = datetime('now'),
520
+ version = version + 1,
521
+ updated_at = datetime('now')
522
+ WHERE id = ?
523
+ RETURNING *`);
524
+ const row = stmt.get(state, evidence, confirmedBy, id);
525
+ if (!row)
526
+ throw new Error(`Item not found: ${id}`);
527
+ return mapItemRow(row);
528
+ }
529
+ findItemsByExternalRef(externalRef) {
530
+ const rows = this.db.query("SELECT * FROM items WHERE external_ref = ?").all(externalRef);
531
+ return rows.map(mapItemRow);
532
+ }
533
+ createTrace(input) {
534
+ const id = randomUUID2();
535
+ const stmt = this.db.query(`INSERT INTO traces (id, from_item_id, from_node_id, to_item_id, to_node_id, relation)
536
+ VALUES (?, ?, ?, ?, ?, ?)
537
+ RETURNING *`);
538
+ const row = stmt.get(id, input.fromItemId, input.fromNodeId, input.toItemId, input.toNodeId, input.relation);
539
+ if (!row)
540
+ throw new Error("Failed to create trace");
541
+ return mapTraceRow(row);
542
+ }
543
+ getItemTraces(itemId) {
544
+ const rows = this.db.query("SELECT * FROM traces WHERE from_item_id = ? OR to_item_id = ?").all(itemId, itemId);
545
+ return rows.map(mapTraceRow);
546
+ }
547
+ getDependentTraces(itemId) {
548
+ const rows = this.db.query("SELECT * FROM traces WHERE to_item_id = ?").all(itemId);
549
+ return rows.map(mapTraceRow);
550
+ }
551
+ getUpstreamTraces(itemId) {
552
+ const rows = this.db.query("SELECT * FROM traces WHERE from_item_id = ?").all(itemId);
553
+ return rows.map(mapTraceRow);
554
+ }
555
+ createSignal(input) {
556
+ const id = randomUUID2();
557
+ const stmt = this.db.query(`INSERT INTO signals (id, kind, source_item, source_node, target_item, target_node, payload)
558
+ VALUES (?, ?, ?, ?, ?, ?, ?)
559
+ RETURNING *`);
560
+ const row = stmt.get(id, input.kind, input.sourceItem, input.sourceNode, input.targetItem, input.targetNode, input.payload ?? "");
561
+ if (!row)
562
+ throw new Error("Failed to create signal");
563
+ return mapSignalRow(row);
564
+ }
565
+ markSignalProcessed(id) {
566
+ this.db.query("UPDATE signals SET processed = 1 WHERE id = ?").run(id);
567
+ }
568
+ recordTransition(input) {
569
+ const id = randomUUID2();
570
+ const stmt = this.db.query(`INSERT INTO transitions (id, item_id, kind, from_s, to_s, evidence, reason, actor)
571
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
572
+ RETURNING *`);
573
+ const row = stmt.get(id, input.itemId, input.kind, input.from, input.to, input.evidence ?? "", input.reason ?? "", input.actor);
574
+ if (!row)
575
+ throw new Error("Failed to record transition");
576
+ return mapTransitionRow(row);
577
+ }
578
+ getItemTransitions(itemId) {
579
+ const rows = this.db.query("SELECT * FROM transitions WHERE item_id = ? ORDER BY timestamp").all(itemId);
580
+ return rows.map(mapTransitionRow);
581
+ }
582
+ createQuery(input) {
583
+ const id = randomUUID2();
584
+ const stmt = this.db.query(`INSERT INTO queries (id, asker_id, asker_node, question, context, target_node)
585
+ VALUES (?, ?, ?, ?, ?, ?)
586
+ RETURNING *`);
587
+ const row = stmt.get(id, input.askerId, input.askerNode, input.question, input.context ?? "", input.targetNode ?? "");
588
+ if (!row)
589
+ throw new Error("Failed to create query");
590
+ return mapQueryRow(row);
591
+ }
592
+ createQueryResponse(input) {
593
+ const id = randomUUID2();
594
+ const stmt = this.db.query(`INSERT INTO query_responses (id, query_id, responder_id, node_id, answer, is_ai)
595
+ VALUES (?, ?, ?, ?, ?, ?)
596
+ RETURNING *`);
597
+ const row = stmt.get(id, input.queryId, input.responderId, input.nodeId, input.answer, input.isAI ? 1 : 0);
598
+ if (!row)
599
+ throw new Error("Failed to create query response");
600
+ return mapQueryResponseRow(row);
601
+ }
602
+ createPendingAction(input) {
603
+ const id = randomUUID2();
604
+ const stmt = this.db.query(`INSERT INTO pending_actions (id, message_type, envelope, summary, proposed)
605
+ VALUES (?, ?, ?, ?, ?)
606
+ RETURNING *`);
607
+ const row = stmt.get(id, input.messageType, input.envelope, input.summary, input.proposed);
608
+ if (!row)
609
+ throw new Error("Failed to create pending action");
610
+ return mapPendingActionRow(row);
611
+ }
612
+ listPendingActions() {
613
+ const rows = this.db.query("SELECT * FROM pending_actions WHERE status = 'pending' ORDER BY created_at").all();
614
+ return rows.map(mapPendingActionRow);
615
+ }
616
+ updatePendingActionStatus(id, status) {
617
+ this.db.query("UPDATE pending_actions SET status = ? WHERE id = ?").run(status, id);
618
+ }
619
+ createChangeRequest(input) {
620
+ const id = randomUUID2();
621
+ const stmt = this.db.query(`INSERT INTO change_requests (id, proposer_node, proposer_id, target_item_id, description)
622
+ VALUES (?, ?, ?, ?, ?)
623
+ RETURNING *`);
624
+ const row = stmt.get(id, input.proposerNode, input.proposerId, input.targetItemId, input.description);
625
+ if (!row)
626
+ throw new Error("Failed to create change request");
627
+ return mapChangeRequestRow(row);
628
+ }
629
+ getChangeRequest(id) {
630
+ const row = this.db.query("SELECT * FROM change_requests WHERE id = ?").get(id);
631
+ return row ? mapChangeRequestRow(row) : null;
632
+ }
633
+ updateChangeRequestStatus(id, status) {
634
+ const stmt = this.db.query(`UPDATE change_requests
635
+ SET status = ?, updated_at = datetime('now')
636
+ WHERE id = ?
637
+ RETURNING *`);
638
+ const row = stmt.get(status, id);
639
+ if (!row)
640
+ throw new Error(`Change request not found: ${id}`);
641
+ return mapChangeRequestRow(row);
642
+ }
643
+ listChangeRequests(status) {
644
+ if (status) {
645
+ const rows2 = this.db.query("SELECT * FROM change_requests WHERE status = ? ORDER BY created_at").all(status);
646
+ return rows2.map(mapChangeRequestRow);
647
+ }
648
+ const rows = this.db.query("SELECT * FROM change_requests ORDER BY created_at").all();
649
+ return rows.map(mapChangeRequestRow);
650
+ }
651
+ createVote(input) {
652
+ const id = randomUUID2();
653
+ const stmt = this.db.query(`INSERT INTO votes (id, cr_id, node_id, vertical, approve, reason)
654
+ VALUES (?, ?, ?, ?, ?, ?)
655
+ RETURNING *`);
656
+ const row = stmt.get(id, input.crId, input.nodeId, input.vertical, input.approve ? 1 : 0, input.reason);
657
+ if (!row)
658
+ throw new Error("Failed to create vote");
659
+ return mapVoteRow(row);
660
+ }
661
+ listVotes(crId) {
662
+ const rows = this.db.query("SELECT * FROM votes WHERE cr_id = ? ORDER BY created_at").all(crId);
663
+ return rows.map(mapVoteRow);
664
+ }
665
+ tallyVotes(crId) {
666
+ const votes = this.listVotes(crId);
667
+ let approved = 0;
668
+ let rejected = 0;
669
+ for (const vote of votes) {
670
+ if (vote.approve) {
671
+ approved++;
672
+ } else {
673
+ rejected++;
674
+ }
675
+ }
676
+ return { approved, rejected, total: votes.length };
677
+ }
678
+ createPairSession(input) {
679
+ const id = randomUUID2();
680
+ const stmt = this.db.query(`INSERT INTO pair_sessions (id, initiator_node, partner_node, project)
681
+ VALUES (?, ?, ?, ?)
682
+ RETURNING *`);
683
+ const row = stmt.get(id, input.initiatorNode, input.partnerNode, input.project);
684
+ if (!row)
685
+ throw new Error("Failed to create pair session");
686
+ return mapPairSessionRow(row);
687
+ }
688
+ getPairSession(id) {
689
+ const row = this.db.query("SELECT * FROM pair_sessions WHERE id = ?").get(id);
690
+ return row ? mapPairSessionRow(row) : null;
691
+ }
692
+ updatePairSessionStatus(id, status) {
693
+ if (status === "ended") {
694
+ const stmt2 = this.db.query(`UPDATE pair_sessions SET status = ?, ended_at = datetime('now') WHERE id = ? RETURNING *`);
695
+ const row2 = stmt2.get(status, id);
696
+ if (!row2)
697
+ throw new Error(`Pair session not found: ${id}`);
698
+ return mapPairSessionRow(row2);
699
+ }
700
+ const stmt = this.db.query(`UPDATE pair_sessions SET status = ? WHERE id = ? RETURNING *`);
701
+ const row = stmt.get(status, id);
702
+ if (!row)
703
+ throw new Error(`Pair session not found: ${id}`);
704
+ return mapPairSessionRow(row);
705
+ }
706
+ listPairSessions(nodeId) {
707
+ const rows = this.db.query("SELECT * FROM pair_sessions WHERE (initiator_node = ? OR partner_node = ?) AND status != 'ended' ORDER BY started_at").all(nodeId, nodeId);
708
+ return rows.map(mapPairSessionRow);
709
+ }
710
+ createChecklistItem(input) {
711
+ const id = randomUUID2();
712
+ const stmt = this.db.query(`INSERT INTO checklists (id, item_id, text)
713
+ VALUES (?, ?, ?)
714
+ RETURNING *`);
715
+ const row = stmt.get(id, input.itemId, input.text);
716
+ if (!row)
717
+ throw new Error("Failed to create checklist item");
718
+ return mapChecklistItemRow(row);
719
+ }
720
+ getChecklistItem(id) {
721
+ const row = this.db.query("SELECT * FROM checklists WHERE id = ?").get(id);
722
+ return row ? mapChecklistItemRow(row) : null;
723
+ }
724
+ updateChecklistItemChecked(id, checked) {
725
+ this.db.query("UPDATE checklists SET checked = ? WHERE id = ?").run(checked ? 1 : 0, id);
726
+ }
727
+ listChecklistItems(itemId) {
728
+ const rows = this.db.query("SELECT * FROM checklists WHERE item_id = ? ORDER BY created_at").all(itemId);
729
+ return rows.map(mapChecklistItemRow);
730
+ }
731
+ createKindMapping(input) {
732
+ const id = randomUUID2();
733
+ const stmt = this.db.query(`INSERT INTO kind_mappings (id, from_vertical, from_kind, to_vertical, to_kind)
734
+ VALUES (?, ?, ?, ?, ?)
735
+ RETURNING *`);
736
+ const row = stmt.get(id, input.fromVertical, input.fromKind, input.toVertical, input.toKind);
737
+ if (!row)
738
+ throw new Error("Failed to create kind mapping");
739
+ return mapKindMappingRow(row);
740
+ }
741
+ getMappedKind(fromVertical, fromKind, toVertical) {
742
+ const row = this.db.query("SELECT * FROM kind_mappings WHERE from_vertical = ? AND from_kind = ? AND to_vertical = ?").get(fromVertical, fromKind, toVertical);
743
+ return row ? row.to_kind : null;
744
+ }
745
+ listKindMappings() {
746
+ const rows = this.db.query("SELECT * FROM kind_mappings ORDER BY created_at").all();
747
+ return rows.map(mapKindMappingRow);
748
+ }
749
+ audit(nodeId) {
750
+ const node = this.getNode(nodeId);
751
+ if (!node)
752
+ throw new Error(`Node not found: ${nodeId}`);
753
+ const items = this.listItems(nodeId);
754
+ const unverified = [];
755
+ const proven = [];
756
+ const suspect = [];
757
+ const broke = [];
758
+ const orphans = [];
759
+ const missingUpstreamRefs = [];
760
+ const upstreamVerticals = UPSTREAM_VERTICALS[node.vertical];
761
+ for (const item of items) {
762
+ switch (item.state) {
763
+ case "unverified":
764
+ unverified.push(item.id);
765
+ break;
766
+ case "proven":
767
+ proven.push(item.id);
768
+ break;
769
+ case "suspect":
770
+ suspect.push(item.id);
771
+ break;
772
+ case "broke":
773
+ broke.push(item.id);
774
+ break;
775
+ }
776
+ const traces = this.getItemTraces(item.id);
777
+ if (traces.length === 0) {
778
+ orphans.push(item.id);
779
+ }
780
+ if (upstreamVerticals.length > 0) {
781
+ const incomingTraces = this.getDependentTraces(item.id);
782
+ const hasUpstreamTrace = incomingTraces.some((t) => {
783
+ const fromNode = this.getNode(t.fromNodeId);
784
+ return fromNode !== null && upstreamVerticals.includes(fromNode.vertical);
785
+ });
786
+ if (!hasUpstreamTrace) {
787
+ missingUpstreamRefs.push(item.id);
788
+ }
789
+ }
790
+ }
791
+ return {
792
+ nodeId,
793
+ totalItems: items.length,
794
+ unverified,
795
+ proven,
796
+ suspect,
797
+ broke,
798
+ orphans,
799
+ missingUpstreamRefs
800
+ };
801
+ }
802
+ }
803
+
804
+ // src/state.ts
805
+ class StateMachine {
806
+ rules = [
807
+ { from: "unverified", kind: "verify", to: "proven" },
808
+ { from: "proven", kind: "suspect", to: "suspect" },
809
+ { from: "suspect", kind: "re_verify", to: "proven" },
810
+ { from: "suspect", kind: "break", to: "broke" },
811
+ { from: "broke", kind: "fix", to: "proven" }
812
+ ];
813
+ canTransition(from, kind) {
814
+ return this.rules.some((r) => r.from === from && r.kind === kind);
815
+ }
816
+ apply(from, kind) {
817
+ const rule = this.rules.find((r) => r.from === from && r.kind === kind);
818
+ if (!rule) {
819
+ throw new Error(`Invalid transition: cannot apply "${kind}" from state "${from}"`);
820
+ }
821
+ return rule.to;
822
+ }
823
+ availableTransitions(from) {
824
+ return this.rules.filter((r) => r.from === from).map((r) => r.kind);
825
+ }
826
+ }
827
+
828
+ class CRStateMachine {
829
+ rules = [
830
+ { from: "draft", kind: "submit", to: "proposed" },
831
+ { from: "proposed", kind: "open_voting", to: "voting" },
832
+ { from: "voting", kind: "approve", to: "approved" },
833
+ { from: "voting", kind: "reject", to: "rejected" },
834
+ { from: "approved", kind: "apply", to: "applied" },
835
+ { from: "applied", kind: "archive", to: "archived" },
836
+ { from: "rejected", kind: "archive", to: "archived" }
837
+ ];
838
+ apply(from, kind) {
839
+ const rule = this.rules.find((r) => r.from === from && r.kind === kind);
840
+ if (!rule) {
841
+ throw new Error(`Invalid CR transition: cannot apply "${kind}" from status "${from}"`);
842
+ }
843
+ return rule.to;
844
+ }
845
+ }
846
+
847
+ // src/signal.ts
848
+ class SignalPropagator {
849
+ store;
850
+ sm;
851
+ constructor(store, sm) {
852
+ this.store = store;
853
+ this.sm = sm;
854
+ }
855
+ propagateChange(changedItemId) {
856
+ const visited = new Set;
857
+ const signals = [];
858
+ this.propagateRecursive(changedItemId, visited, signals);
859
+ return signals;
860
+ }
861
+ computeImpact(itemId) {
862
+ const visited = new Set;
863
+ const impacted = [];
864
+ this.collectImpactRecursive(itemId, visited, impacted);
865
+ return impacted;
866
+ }
867
+ propagateRecursive(itemId, visited, signals) {
868
+ visited.add(itemId);
869
+ const dependentTraces = this.store.getDependentTraces(itemId);
870
+ for (const trace of dependentTraces) {
871
+ const downstreamId = trace.fromItemId;
872
+ if (visited.has(downstreamId)) {
873
+ continue;
874
+ }
875
+ const downstreamItem = this.store.getItem(downstreamId);
876
+ if (!downstreamItem) {
877
+ continue;
878
+ }
879
+ if (downstreamItem.state === "proven" && this.sm.canTransition("proven", "suspect")) {
880
+ this.store.updateItemStatus(downstreamId, "suspect", "upstream-change", "signal-propagator");
881
+ this.store.recordTransition({
882
+ itemId: downstreamId,
883
+ kind: "suspect",
884
+ from: "proven",
885
+ to: "suspect",
886
+ evidence: "upstream-change",
887
+ reason: `Upstream item ${itemId} changed`,
888
+ actor: "signal-propagator"
889
+ });
890
+ const signal = this.store.createSignal({
891
+ kind: "change",
892
+ sourceItem: itemId,
893
+ sourceNode: trace.toNodeId,
894
+ targetItem: downstreamId,
895
+ targetNode: trace.fromNodeId,
896
+ payload: `Propagated change from ${itemId}`
897
+ });
898
+ signals.push(signal);
899
+ this.propagateRecursive(downstreamId, visited, signals);
900
+ }
901
+ }
902
+ }
903
+ collectImpactRecursive(itemId, visited, impacted) {
904
+ visited.add(itemId);
905
+ const dependentTraces = this.store.getDependentTraces(itemId);
906
+ for (const trace of dependentTraces) {
907
+ const downstreamId = trace.fromItemId;
908
+ if (visited.has(downstreamId)) {
909
+ continue;
910
+ }
911
+ const downstreamItem = this.store.getItem(downstreamId);
912
+ if (!downstreamItem) {
913
+ continue;
914
+ }
915
+ impacted.push(downstreamItem);
916
+ this.collectImpactRecursive(downstreamId, visited, impacted);
917
+ }
918
+ }
919
+ }
920
+
921
+ // src/engine.ts
922
+ class Engine {
923
+ store;
924
+ sm;
925
+ propagator;
926
+ crSm = new CRStateMachine;
927
+ constructor(store, sm, propagator) {
928
+ this.store = store;
929
+ this.sm = sm;
930
+ this.propagator = propagator;
931
+ }
932
+ registerNode(name, vertical, project, owner, isAI) {
933
+ return this.store.createNode({ name, vertical, project, owner, isAI });
934
+ }
935
+ getNode(id) {
936
+ const node = this.store.getNode(id);
937
+ if (!node)
938
+ throw new Error(`Node not found: ${id}`);
939
+ return node;
940
+ }
941
+ listNodes(project) {
942
+ return this.store.listNodes(project);
943
+ }
944
+ addItem(nodeId, kind, title, body, externalRef) {
945
+ const node = this.store.getNode(nodeId);
946
+ if (!node)
947
+ throw new Error(`Node not found: ${nodeId}`);
948
+ return this.store.createItem({ nodeId, kind, title, body, externalRef });
949
+ }
950
+ getItem(id) {
951
+ const item = this.store.getItem(id);
952
+ if (!item)
953
+ throw new Error(`Item not found: ${id}`);
954
+ return item;
955
+ }
956
+ listItems(nodeId) {
957
+ return this.store.listItems(nodeId);
958
+ }
959
+ addTrace(fromItemId, toItemId, relation, actor) {
960
+ const fromItem = this.store.getItem(fromItemId);
961
+ if (!fromItem)
962
+ throw new Error(`Item not found: ${fromItemId}`);
963
+ const toItem = this.store.getItem(toItemId);
964
+ if (!toItem)
965
+ throw new Error(`Item not found: ${toItemId}`);
966
+ return this.store.createTrace({
967
+ fromItemId,
968
+ fromNodeId: fromItem.nodeId,
969
+ toItemId,
970
+ toNodeId: toItem.nodeId,
971
+ relation
972
+ });
973
+ }
974
+ verifyItem(itemId, evidence, actor) {
975
+ const item = this.getItem(itemId);
976
+ let kind;
977
+ switch (item.state) {
978
+ case "unverified":
979
+ kind = "verify";
980
+ break;
981
+ case "suspect":
982
+ kind = "re_verify";
983
+ break;
984
+ case "broke":
985
+ kind = "fix";
986
+ break;
987
+ default:
988
+ throw new Error(`Cannot verify item in state "${item.state}": no valid transition`);
989
+ }
990
+ const newState = this.sm.apply(item.state, kind);
991
+ this.store.updateItemStatus(itemId, newState, evidence, actor);
992
+ this.store.recordTransition({
993
+ itemId,
994
+ kind,
995
+ from: item.state,
996
+ to: newState,
997
+ evidence,
998
+ actor
999
+ });
1000
+ return this.propagator.propagateChange(itemId);
1001
+ }
1002
+ markBroken(itemId, reason, actor) {
1003
+ const item = this.getItem(itemId);
1004
+ if (item.state !== "suspect") {
1005
+ throw new Error(`Cannot mark item as broken: item is in "${item.state}" state, must be "suspect"`);
1006
+ }
1007
+ const newState = this.sm.apply(item.state, "break");
1008
+ this.store.updateItemStatus(itemId, newState, reason, actor);
1009
+ this.store.recordTransition({
1010
+ itemId,
1011
+ kind: "break",
1012
+ from: item.state,
1013
+ to: newState,
1014
+ reason,
1015
+ actor
1016
+ });
1017
+ }
1018
+ propagateChange(itemId) {
1019
+ return this.propagator.propagateChange(itemId);
1020
+ }
1021
+ impact(itemId) {
1022
+ return this.propagator.computeImpact(itemId);
1023
+ }
1024
+ ask(askerId, askerNode, question, context, targetNode) {
1025
+ return this.store.createQuery({
1026
+ askerId,
1027
+ askerNode,
1028
+ question,
1029
+ context,
1030
+ targetNode
1031
+ });
1032
+ }
1033
+ respond(queryId, responderId, nodeId, answer, isAI) {
1034
+ return this.store.createQueryResponse({
1035
+ queryId,
1036
+ responderId,
1037
+ nodeId,
1038
+ answer,
1039
+ isAI
1040
+ });
1041
+ }
1042
+ audit(nodeId) {
1043
+ return this.store.audit(nodeId);
1044
+ }
1045
+ sweep(externalRef) {
1046
+ const matchedItems = this.store.findItemsByExternalRef(externalRef);
1047
+ const allSignals = [];
1048
+ const affectedItemIds = new Set;
1049
+ for (const item of matchedItems) {
1050
+ const signals = this.propagator.propagateChange(item.id);
1051
+ for (const signal of signals) {
1052
+ allSignals.push(signal);
1053
+ affectedItemIds.add(signal.targetItem);
1054
+ }
1055
+ }
1056
+ const affectedItems = [];
1057
+ for (const id of affectedItemIds) {
1058
+ const item = this.store.getItem(id);
1059
+ if (item) {
1060
+ affectedItems.push(item);
1061
+ }
1062
+ }
1063
+ return {
1064
+ triggerRef: externalRef,
1065
+ matchedItems,
1066
+ affectedItems,
1067
+ signalsCreated: allSignals.length
1068
+ };
1069
+ }
1070
+ createProposal(proposerNode, proposerId, targetItemId, description) {
1071
+ this.getItem(targetItemId);
1072
+ return this.store.createChangeRequest({ proposerNode, proposerId, targetItemId, description });
1073
+ }
1074
+ submitProposal(crId) {
1075
+ const cr = this.getChangeRequest(crId);
1076
+ this.crSm.apply(cr.status, "submit");
1077
+ return this.store.updateChangeRequestStatus(crId, "proposed");
1078
+ }
1079
+ openVoting(crId) {
1080
+ const cr = this.getChangeRequest(crId);
1081
+ this.crSm.apply(cr.status, "open_voting");
1082
+ return this.store.updateChangeRequestStatus(crId, "voting");
1083
+ }
1084
+ castVote(crId, nodeId, vertical, approve, reason) {
1085
+ const cr = this.getChangeRequest(crId);
1086
+ if (cr.status !== "voting") {
1087
+ throw new Error(`Cannot vote on CR in "${cr.status}" status`);
1088
+ }
1089
+ return this.store.createVote({ crId, nodeId, vertical, approve, reason });
1090
+ }
1091
+ resolveVoting(crId) {
1092
+ const cr = this.getChangeRequest(crId);
1093
+ if (cr.status !== "voting") {
1094
+ throw new Error(`Cannot resolve CR in "${cr.status}" status`);
1095
+ }
1096
+ const tally = this.store.tallyVotes(crId);
1097
+ let approved;
1098
+ if (tally.approved > tally.rejected) {
1099
+ approved = true;
1100
+ } else if (tally.rejected > tally.approved) {
1101
+ approved = false;
1102
+ } else {
1103
+ approved = this.tieBreak(cr);
1104
+ }
1105
+ const kind = approved ? "approve" : "reject";
1106
+ this.crSm.apply(cr.status, kind);
1107
+ return this.store.updateChangeRequestStatus(crId, approved ? "approved" : "rejected");
1108
+ }
1109
+ applyProposal(crId) {
1110
+ const cr = this.getChangeRequest(crId);
1111
+ this.crSm.apply(cr.status, "apply");
1112
+ const updated = this.store.updateChangeRequestStatus(crId, "applied");
1113
+ const signals = this.propagator.propagateChange(cr.targetItemId);
1114
+ return { cr: updated, signals };
1115
+ }
1116
+ archiveProposal(crId) {
1117
+ const cr = this.getChangeRequest(crId);
1118
+ this.crSm.apply(cr.status, "archive");
1119
+ return this.store.updateChangeRequestStatus(crId, "archived");
1120
+ }
1121
+ createChallenge(challengerNode, challengerId, targetItemId, reason) {
1122
+ this.getItem(targetItemId);
1123
+ return this.store.createChangeRequest({
1124
+ proposerNode: challengerNode,
1125
+ proposerId: challengerId,
1126
+ targetItemId,
1127
+ description: `Challenge: ${reason}`
1128
+ });
1129
+ }
1130
+ upholdChallenge(crId) {
1131
+ const cr = this.getChangeRequest(crId);
1132
+ if (cr.status !== "approved") {
1133
+ throw new Error(`Cannot uphold challenge in "${cr.status}" status \u2014 must be approved`);
1134
+ }
1135
+ const item = this.getItem(cr.targetItemId);
1136
+ if (item.state === "proven") {
1137
+ this.store.updateItemStatus(cr.targetItemId, "suspect", `Challenge upheld: ${cr.description}`, "challenge-system");
1138
+ this.store.recordTransition({
1139
+ itemId: cr.targetItemId,
1140
+ kind: "suspect",
1141
+ from: "proven",
1142
+ to: "suspect",
1143
+ evidence: cr.description,
1144
+ reason: "Challenge upheld by vote",
1145
+ actor: "challenge-system"
1146
+ });
1147
+ }
1148
+ this.crSm.apply(cr.status, "apply");
1149
+ const updated = this.store.updateChangeRequestStatus(crId, "applied");
1150
+ const signals = this.propagator.propagateChange(cr.targetItemId);
1151
+ return { cr: updated, signals };
1152
+ }
1153
+ dismissChallenge(crId) {
1154
+ const cr = this.getChangeRequest(crId);
1155
+ if (cr.status !== "rejected") {
1156
+ throw new Error(`Cannot dismiss challenge in "${cr.status}" status \u2014 must be rejected`);
1157
+ }
1158
+ this.crSm.apply(cr.status, "archive");
1159
+ return this.store.updateChangeRequestStatus(crId, "archived");
1160
+ }
1161
+ invitePair(initiatorNode, partnerNode, project) {
1162
+ this.getNode(initiatorNode);
1163
+ this.getNode(partnerNode);
1164
+ return this.store.createPairSession({ initiatorNode, partnerNode, project });
1165
+ }
1166
+ joinPair(sessionId) {
1167
+ const session = this.store.getPairSession(sessionId);
1168
+ if (!session)
1169
+ throw new Error(`Pair session not found: ${sessionId}`);
1170
+ if (session.status !== "pending")
1171
+ throw new Error(`Session is "${session.status}", cannot join`);
1172
+ return this.store.updatePairSessionStatus(sessionId, "active");
1173
+ }
1174
+ endPair(sessionId) {
1175
+ const session = this.store.getPairSession(sessionId);
1176
+ if (!session)
1177
+ throw new Error(`Pair session not found: ${sessionId}`);
1178
+ if (session.status !== "active")
1179
+ throw new Error(`Session is "${session.status}", cannot end`);
1180
+ return this.store.updatePairSessionStatus(sessionId, "ended");
1181
+ }
1182
+ listPairSessions(nodeId) {
1183
+ return this.store.listPairSessions(nodeId);
1184
+ }
1185
+ addChecklistItem(itemId, text) {
1186
+ this.getItem(itemId);
1187
+ return this.store.createChecklistItem({ itemId, text });
1188
+ }
1189
+ checkChecklistItem(checklistItemId) {
1190
+ this.store.updateChecklistItemChecked(checklistItemId, true);
1191
+ }
1192
+ uncheckChecklistItem(checklistItemId) {
1193
+ this.store.updateChecklistItemChecked(checklistItemId, false);
1194
+ }
1195
+ listChecklist(itemId) {
1196
+ return this.store.listChecklistItems(itemId);
1197
+ }
1198
+ addKindMapping(fromVertical, fromKind, toVertical, toKind) {
1199
+ return this.store.createKindMapping({ fromVertical, fromKind, toVertical, toKind });
1200
+ }
1201
+ getMappedKind(fromVertical, fromKind, toVertical) {
1202
+ return this.store.getMappedKind(fromVertical, fromKind, toVertical);
1203
+ }
1204
+ getChangeRequest(id) {
1205
+ const cr = this.store.getChangeRequest(id);
1206
+ if (!cr)
1207
+ throw new Error(`Change request not found: ${id}`);
1208
+ return cr;
1209
+ }
1210
+ tieBreak(cr) {
1211
+ const proposerNode = this.store.getNode(cr.proposerNode);
1212
+ if (!proposerNode)
1213
+ return false;
1214
+ const upstreams = UPSTREAM_VERTICALS[proposerNode.vertical];
1215
+ const votes = this.store.listVotes(cr.id);
1216
+ for (const vote of votes) {
1217
+ if (upstreams.includes(vote.vertical) && vote.approve) {
1218
+ return true;
1219
+ }
1220
+ }
1221
+ return false;
1222
+ }
1223
+ }
1224
+
1225
+ // src/event-bus.ts
1226
+ class EventBus {
1227
+ listeners = new Map;
1228
+ on(event, handler) {
1229
+ let handlers = this.listeners.get(event);
1230
+ if (!handlers) {
1231
+ handlers = new Set;
1232
+ this.listeners.set(event, handlers);
1233
+ }
1234
+ handlers.add(handler);
1235
+ }
1236
+ off(event, handler) {
1237
+ const handlers = this.listeners.get(event);
1238
+ if (handlers) {
1239
+ handlers.delete(handler);
1240
+ }
1241
+ }
1242
+ emit(event, data) {
1243
+ const handlers = this.listeners.get(event);
1244
+ if (!handlers)
1245
+ return;
1246
+ for (const handler of handlers) {
1247
+ handler(data);
1248
+ }
1249
+ }
1250
+ }
1251
+
1252
+ // src/ws-handlers.ts
1253
+ class WSHandlers {
1254
+ engine;
1255
+ store;
1256
+ eventBus;
1257
+ constructor(engine, store, eventBus) {
1258
+ this.engine = engine;
1259
+ this.store = store;
1260
+ this.eventBus = eventBus;
1261
+ }
1262
+ handle(envelope) {
1263
+ const { payload } = envelope;
1264
+ switch (payload.type) {
1265
+ case "signal_change":
1266
+ this.handleSignalChange(payload);
1267
+ break;
1268
+ case "sweep":
1269
+ this.handleSweep(payload);
1270
+ break;
1271
+ case "trace_resolve_request":
1272
+ this.handleTraceResolveRequest(payload);
1273
+ break;
1274
+ case "query_ask":
1275
+ this.handleQueryAsk(envelope, payload);
1276
+ break;
1277
+ case "query_respond":
1278
+ this.handleQueryRespond(payload);
1279
+ break;
1280
+ case "permission_request":
1281
+ case "permission_verdict":
1282
+ case "proposal_create":
1283
+ case "proposal_vote":
1284
+ case "proposal_result":
1285
+ case "challenge_create":
1286
+ case "pair_invite":
1287
+ case "pair_respond":
1288
+ case "pair_end":
1289
+ case "checklist_update":
1290
+ break;
1291
+ case "ack":
1292
+ break;
1293
+ case "error":
1294
+ break;
1295
+ }
1296
+ this.eventBus.emit(payload.type, payload);
1297
+ }
1298
+ handleSignalChange(payload) {
1299
+ try {
1300
+ this.engine.propagateChange(payload.itemId);
1301
+ } catch (err) {
1302
+ const message = err instanceof Error ? err.message : String(err);
1303
+ this.eventBus.emit("error", {
1304
+ type: "error",
1305
+ code: "SIGNAL_CHANGE_FAILED",
1306
+ message: `signal_change for item ${payload.itemId}: ${message}`
1307
+ });
1308
+ }
1309
+ }
1310
+ handleSweep(payload) {
1311
+ try {
1312
+ this.engine.sweep(payload.externalRef);
1313
+ } catch (err) {
1314
+ const message = err instanceof Error ? err.message : String(err);
1315
+ this.eventBus.emit("error", {
1316
+ type: "error",
1317
+ code: "SWEEP_FAILED",
1318
+ message: `sweep for ref ${payload.externalRef}: ${message}`
1319
+ });
1320
+ }
1321
+ }
1322
+ handleTraceResolveRequest(payload) {
1323
+ try {
1324
+ const item = this.engine.getItem(payload.itemId);
1325
+ this.eventBus.emit("trace_resolve_response", {
1326
+ type: "trace_resolve_response",
1327
+ itemId: item.id,
1328
+ title: item.title,
1329
+ kind: item.kind,
1330
+ state: item.state
1331
+ });
1332
+ } catch (err) {
1333
+ const message = err instanceof Error ? err.message : String(err);
1334
+ this.eventBus.emit("error", {
1335
+ type: "error",
1336
+ code: "TRACE_RESOLVE_FAILED",
1337
+ message: `trace_resolve_request for item ${payload.itemId}: ${message}`
1338
+ });
1339
+ }
1340
+ }
1341
+ handleQueryAsk(envelope, payload) {
1342
+ try {
1343
+ this.store.createQuery({
1344
+ askerId: payload.askerId,
1345
+ askerNode: envelope.fromNode,
1346
+ question: payload.question
1347
+ });
1348
+ } catch (err) {
1349
+ const message = err instanceof Error ? err.message : String(err);
1350
+ this.eventBus.emit("error", {
1351
+ type: "error",
1352
+ code: "QUERY_ASK_FAILED",
1353
+ message: `query_ask: ${message}`
1354
+ });
1355
+ }
1356
+ }
1357
+ handleQueryRespond(payload) {}
1358
+ }
1359
+
1360
+ // src/ws-client.ts
1361
+ class WSClient {
1362
+ config;
1363
+ ws = null;
1364
+ messageHandler = null;
1365
+ reconnectDelay = 1000;
1366
+ maxReconnectDelay = 30000;
1367
+ shouldReconnect = true;
1368
+ log = new Logger("ws-client");
1369
+ constructor(config) {
1370
+ this.config = config;
1371
+ }
1372
+ async connect() {
1373
+ return new Promise((resolve, reject) => {
1374
+ const url = `${this.config.serverUrl}?token=${this.config.token}`;
1375
+ this.ws = new WebSocket(url);
1376
+ this.ws.onopen = () => {
1377
+ this.reconnectDelay = 1000;
1378
+ resolve();
1379
+ };
1380
+ this.ws.onmessage = (event) => {
1381
+ if (!this.messageHandler)
1382
+ return;
1383
+ try {
1384
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
1385
+ const envelope = parseEnvelope(raw);
1386
+ this.messageHandler(envelope);
1387
+ } catch (err) {
1388
+ this.log.error("Failed to parse incoming message", { error: String(err) });
1389
+ }
1390
+ };
1391
+ this.ws.onclose = () => {
1392
+ this.ws = null;
1393
+ if (this.shouldReconnect) {
1394
+ setTimeout(() => {
1395
+ this.connect().catch((err) => {
1396
+ this.log.error("Reconnection failed", { error: String(err) });
1397
+ });
1398
+ }, this.reconnectDelay);
1399
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
1400
+ }
1401
+ };
1402
+ this.ws.onerror = (event) => {
1403
+ this.log.error("WebSocket error");
1404
+ if (!this.connected) {
1405
+ reject(new Error("WebSocket connection failed"));
1406
+ }
1407
+ };
1408
+ });
1409
+ }
1410
+ send(envelope) {
1411
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1412
+ throw new Error("WebSocket is not connected");
1413
+ }
1414
+ this.ws.send(JSON.stringify(envelope));
1415
+ }
1416
+ sendMessage(toNode, payload) {
1417
+ const envelope = createEnvelope(this.config.nodeId, toNode, this.config.projectId, payload);
1418
+ this.send(envelope);
1419
+ }
1420
+ broadcast(payload) {
1421
+ const envelope = createEnvelope(this.config.nodeId, "", this.config.projectId, payload);
1422
+ this.send(envelope);
1423
+ }
1424
+ onMessage(handler) {
1425
+ this.messageHandler = handler;
1426
+ }
1427
+ close() {
1428
+ this.shouldReconnect = false;
1429
+ if (this.ws) {
1430
+ this.ws.close();
1431
+ this.ws = null;
1432
+ }
1433
+ }
1434
+ get connected() {
1435
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
1436
+ }
1437
+ }
1438
+
1439
+ // src/config.ts
1440
+ function defaultConfig() {
1441
+ return {
1442
+ node: {
1443
+ id: "",
1444
+ name: "",
1445
+ vertical: "dev",
1446
+ project: "",
1447
+ owner: "",
1448
+ isAI: false
1449
+ },
1450
+ server: {
1451
+ url: "ws://localhost:8080/ws",
1452
+ token: ""
1453
+ },
1454
+ database: {
1455
+ path: "./inventory.db"
1456
+ },
1457
+ autonomy: {
1458
+ auto: ["signal_change", "trace_resolve_request", "sweep", "query_respond"],
1459
+ approval: ["proposal_vote", "challenge_respond", "pair_invite", "cr_create"]
1460
+ }
1461
+ };
1462
+ }
1463
+ function loadConfig(partial) {
1464
+ const cfg = defaultConfig();
1465
+ return deepMerge(cfg, partial);
1466
+ }
1467
+ function deepMerge(target, source) {
1468
+ const result = { ...target };
1469
+ for (const key of Object.keys(source)) {
1470
+ const srcVal = source[key];
1471
+ const tgtVal = target[key];
1472
+ if (srcVal !== null && typeof srcVal === "object" && !Array.isArray(srcVal) && tgtVal !== null && typeof tgtVal === "object" && !Array.isArray(tgtVal)) {
1473
+ result[key] = deepMerge(tgtVal, srcVal);
1474
+ } else {
1475
+ result[key] = srcVal;
1476
+ }
1477
+ }
1478
+ return result;
1479
+ }
1480
+
1481
+ // src/channel.ts
1482
+ var TOOL_DEFINITIONS = [
1483
+ {
1484
+ name: "inv_add_item",
1485
+ description: "Add a new inventory item. Requires kind and title. Optionally accepts body and externalRef.",
1486
+ inputSchema: {
1487
+ type: "object",
1488
+ properties: {
1489
+ kind: {
1490
+ type: "string",
1491
+ description: "Item kind",
1492
+ enum: [
1493
+ "adr",
1494
+ "api-spec",
1495
+ "data-model",
1496
+ "tech-design",
1497
+ "epic",
1498
+ "user-story",
1499
+ "prd",
1500
+ "screen-spec",
1501
+ "user-flow",
1502
+ "test-case",
1503
+ "test-plan",
1504
+ "runbook",
1505
+ "bug-report",
1506
+ "decision",
1507
+ "custom"
1508
+ ]
1509
+ },
1510
+ title: { type: "string", description: "Title of the item" },
1511
+ body: { type: "string", description: "Optional body/description" },
1512
+ externalRef: { type: "string", description: "Optional external reference (URL or ticket ID)" }
1513
+ },
1514
+ required: ["kind", "title"]
1515
+ }
1516
+ },
1517
+ {
1518
+ name: "inv_add_trace",
1519
+ description: "Create a trace link between two items. Relations: traced_from, matched_by, proven_by.",
1520
+ inputSchema: {
1521
+ type: "object",
1522
+ properties: {
1523
+ fromItemId: { type: "string", description: "Source item UUID" },
1524
+ toItemId: { type: "string", description: "Target item UUID" },
1525
+ relation: {
1526
+ type: "string",
1527
+ description: "Relation type",
1528
+ enum: ["traced_from", "matched_by", "proven_by"]
1529
+ }
1530
+ },
1531
+ required: ["fromItemId", "toItemId", "relation"]
1532
+ }
1533
+ },
1534
+ {
1535
+ name: "inv_verify",
1536
+ description: "Verify an item (unverified\u2192proven, suspect\u2192proven, broke\u2192proven). Requires evidence.",
1537
+ inputSchema: {
1538
+ type: "object",
1539
+ properties: {
1540
+ itemId: { type: "string", description: "Item UUID" },
1541
+ evidence: { type: "string", description: "Evidence for verification" }
1542
+ },
1543
+ required: ["itemId", "evidence"]
1544
+ }
1545
+ },
1546
+ {
1547
+ name: "inv_mark_broken",
1548
+ description: "Mark a suspect item as broken. Requires reason.",
1549
+ inputSchema: {
1550
+ type: "object",
1551
+ properties: {
1552
+ itemId: { type: "string", description: "Item UUID" },
1553
+ reason: { type: "string", description: "Reason for marking broken" }
1554
+ },
1555
+ required: ["itemId"]
1556
+ }
1557
+ },
1558
+ {
1559
+ name: "inv_audit",
1560
+ description: "Audit inventory health. Returns counts by state, orphans, missing upstream refs.",
1561
+ inputSchema: {
1562
+ type: "object",
1563
+ properties: {}
1564
+ }
1565
+ },
1566
+ {
1567
+ name: "inv_ask",
1568
+ description: "Ask a question to the inventory network. Optionally target a specific node.",
1569
+ inputSchema: {
1570
+ type: "object",
1571
+ properties: {
1572
+ question: { type: "string", description: "The question to ask" },
1573
+ targetNode: { type: "string", description: "Optional target node UUID" }
1574
+ },
1575
+ required: ["question"]
1576
+ }
1577
+ },
1578
+ {
1579
+ name: "inv_reply",
1580
+ description: "Send a reply message to a specific node through the inventory network.",
1581
+ inputSchema: {
1582
+ type: "object",
1583
+ properties: {
1584
+ message: { type: "string", description: "The reply message" },
1585
+ targetNode: { type: "string", description: "Target node UUID" }
1586
+ },
1587
+ required: ["message", "targetNode"]
1588
+ }
1589
+ },
1590
+ {
1591
+ name: "inv_proposal_create",
1592
+ description: "Create a change request proposal for an item. Starts as draft, submits, and opens for voting.",
1593
+ inputSchema: {
1594
+ type: "object",
1595
+ properties: {
1596
+ targetItemId: { type: "string", description: "Item UUID to propose changes for" },
1597
+ description: { type: "string", description: "Description of the proposed change" }
1598
+ },
1599
+ required: ["targetItemId", "description"]
1600
+ }
1601
+ },
1602
+ {
1603
+ name: "inv_proposal_vote",
1604
+ description: "Vote on a proposal that is in voting status.",
1605
+ inputSchema: {
1606
+ type: "object",
1607
+ properties: {
1608
+ crId: { type: "string", description: "Change request UUID" },
1609
+ approve: { type: "boolean", description: "true to approve, false to reject" },
1610
+ reason: { type: "string", description: "Reason for your vote" }
1611
+ },
1612
+ required: ["crId", "approve", "reason"]
1613
+ }
1614
+ },
1615
+ {
1616
+ name: "inv_challenge_create",
1617
+ description: "Challenge a proven item. Creates a CR that goes through voting. If upheld, item becomes suspect.",
1618
+ inputSchema: {
1619
+ type: "object",
1620
+ properties: {
1621
+ targetItemId: { type: "string", description: "Item UUID to challenge" },
1622
+ reason: { type: "string", description: "Reason for the challenge" }
1623
+ },
1624
+ required: ["targetItemId", "reason"]
1625
+ }
1626
+ },
1627
+ {
1628
+ name: "inv_challenge_respond",
1629
+ description: "Vote on an active challenge.",
1630
+ inputSchema: {
1631
+ type: "object",
1632
+ properties: {
1633
+ challengeId: { type: "string", description: "Challenge (CR) UUID" },
1634
+ approve: { type: "boolean", description: "true to uphold challenge, false to dismiss" },
1635
+ reason: { type: "string", description: "Reason" }
1636
+ },
1637
+ required: ["challengeId", "approve", "reason"]
1638
+ }
1639
+ },
1640
+ {
1641
+ name: "inv_pair_invite",
1642
+ description: "Invite another node to a pairing session.",
1643
+ inputSchema: {
1644
+ type: "object",
1645
+ properties: {
1646
+ targetNode: { type: "string", description: "Target node UUID to pair with" }
1647
+ },
1648
+ required: ["targetNode"]
1649
+ }
1650
+ },
1651
+ {
1652
+ name: "inv_pair_join",
1653
+ description: "Accept a pending pairing session invitation.",
1654
+ inputSchema: {
1655
+ type: "object",
1656
+ properties: {
1657
+ sessionId: { type: "string", description: "Pair session UUID" }
1658
+ },
1659
+ required: ["sessionId"]
1660
+ }
1661
+ },
1662
+ {
1663
+ name: "inv_pair_end",
1664
+ description: "End an active pairing session.",
1665
+ inputSchema: {
1666
+ type: "object",
1667
+ properties: {
1668
+ sessionId: { type: "string", description: "Pair session UUID" }
1669
+ },
1670
+ required: ["sessionId"]
1671
+ }
1672
+ },
1673
+ {
1674
+ name: "inv_pair_list",
1675
+ description: "List active pairing sessions for this node.",
1676
+ inputSchema: {
1677
+ type: "object",
1678
+ properties: {}
1679
+ }
1680
+ },
1681
+ {
1682
+ name: "inv_checklist_add",
1683
+ description: "Add a checklist item to an inventory item.",
1684
+ inputSchema: {
1685
+ type: "object",
1686
+ properties: {
1687
+ itemId: { type: "string", description: "Parent inventory item UUID" },
1688
+ text: { type: "string", description: "Checklist item text" }
1689
+ },
1690
+ required: ["itemId", "text"]
1691
+ }
1692
+ },
1693
+ {
1694
+ name: "inv_checklist_check",
1695
+ description: "Mark a checklist item as checked.",
1696
+ inputSchema: {
1697
+ type: "object",
1698
+ properties: {
1699
+ checklistItemId: { type: "string", description: "Checklist item UUID" }
1700
+ },
1701
+ required: ["checklistItemId"]
1702
+ }
1703
+ },
1704
+ {
1705
+ name: "inv_checklist_uncheck",
1706
+ description: "Uncheck a checklist item.",
1707
+ inputSchema: {
1708
+ type: "object",
1709
+ properties: {
1710
+ checklistItemId: { type: "string", description: "Checklist item UUID" }
1711
+ },
1712
+ required: ["checklistItemId"]
1713
+ }
1714
+ },
1715
+ {
1716
+ name: "inv_checklist_list",
1717
+ description: "List all checklist items for an inventory item.",
1718
+ inputSchema: {
1719
+ type: "object",
1720
+ properties: {
1721
+ itemId: { type: "string", description: "Parent inventory item UUID" }
1722
+ },
1723
+ required: ["itemId"]
1724
+ }
1725
+ }
1726
+ ];
1727
+ function buildToolHandlers(engine, config, wsClient) {
1728
+ return (name, args) => {
1729
+ const text = (s) => ({
1730
+ content: [{ type: "text", text: s }]
1731
+ });
1732
+ try {
1733
+ switch (name) {
1734
+ case "inv_add_item": {
1735
+ const item = engine.addItem(config.node.id, args.kind, args.title ?? "", args.body, args.externalRef);
1736
+ return text(JSON.stringify(item, null, 2));
1737
+ }
1738
+ case "inv_add_trace": {
1739
+ const trace = engine.addTrace(args.fromItemId ?? "", args.toItemId ?? "", args.relation, config.node.owner);
1740
+ return text(JSON.stringify(trace, null, 2));
1741
+ }
1742
+ case "inv_verify": {
1743
+ const signals = engine.verifyItem(args.itemId ?? "", args.evidence ?? "", config.node.owner);
1744
+ return text(JSON.stringify({
1745
+ verified: true,
1746
+ itemId: args.itemId,
1747
+ signalsPropagated: signals.length
1748
+ }, null, 2));
1749
+ }
1750
+ case "inv_mark_broken": {
1751
+ engine.markBroken(args.itemId ?? "", args.reason ?? "", config.node.owner);
1752
+ return text(JSON.stringify({
1753
+ markedBroken: true,
1754
+ itemId: args.itemId
1755
+ }, null, 2));
1756
+ }
1757
+ case "inv_audit": {
1758
+ const report = engine.audit(config.node.id);
1759
+ return text(JSON.stringify(report, null, 2));
1760
+ }
1761
+ case "inv_ask": {
1762
+ const question = args.question ?? "";
1763
+ const query = engine.ask(config.node.owner, config.node.id, question, undefined, args.targetNode);
1764
+ if (wsClient?.connected) {
1765
+ if (args.targetNode) {
1766
+ wsClient.sendMessage(args.targetNode, {
1767
+ type: "query_ask",
1768
+ question,
1769
+ askerId: config.node.id
1770
+ });
1771
+ } else {
1772
+ wsClient.broadcast({
1773
+ type: "query_ask",
1774
+ question,
1775
+ askerId: config.node.id
1776
+ });
1777
+ }
1778
+ }
1779
+ return text(JSON.stringify({
1780
+ queryId: query.id,
1781
+ broadcast: !args.targetNode,
1782
+ networkSent: wsClient?.connected ?? false
1783
+ }, null, 2));
1784
+ }
1785
+ case "inv_reply": {
1786
+ if (!wsClient?.connected) {
1787
+ return text(JSON.stringify({ error: "Not connected to server" }));
1788
+ }
1789
+ wsClient.sendMessage(args.targetNode ?? "", {
1790
+ type: "query_respond",
1791
+ answer: args.message ?? "",
1792
+ responderId: config.node.id
1793
+ });
1794
+ return text(JSON.stringify({
1795
+ sent: true,
1796
+ targetNode: args.targetNode
1797
+ }, null, 2));
1798
+ }
1799
+ case "inv_proposal_create": {
1800
+ const cr = engine.createProposal(config.node.id, config.node.owner, args.targetItemId ?? "", args.description ?? "");
1801
+ engine.submitProposal(cr.id);
1802
+ engine.openVoting(cr.id);
1803
+ if (wsClient?.connected) {
1804
+ wsClient.broadcast({
1805
+ type: "proposal_create",
1806
+ crId: cr.id,
1807
+ targetItemId: args.targetItemId ?? "",
1808
+ description: args.description ?? "",
1809
+ proposerNode: config.node.id
1810
+ });
1811
+ }
1812
+ return text(JSON.stringify({ crId: cr.id, status: "voting" }, null, 2));
1813
+ }
1814
+ case "inv_proposal_vote": {
1815
+ const nodeInfo = engine.getNode(config.node.id);
1816
+ const vote = engine.castVote(args.crId ?? "", config.node.id, nodeInfo.vertical, args.approve === "true" || args.approve === true, args.reason ?? "");
1817
+ if (wsClient?.connected) {
1818
+ wsClient.broadcast({
1819
+ type: "proposal_vote",
1820
+ crId: args.crId ?? "",
1821
+ approve: vote.approve,
1822
+ reason: args.reason ?? ""
1823
+ });
1824
+ }
1825
+ return text(JSON.stringify({ voted: true, crId: args.crId, approve: vote.approve }, null, 2));
1826
+ }
1827
+ case "inv_challenge_create": {
1828
+ const cr = engine.createChallenge(config.node.id, config.node.owner, args.targetItemId ?? "", args.reason ?? "");
1829
+ engine.submitProposal(cr.id);
1830
+ engine.openVoting(cr.id);
1831
+ if (wsClient?.connected) {
1832
+ wsClient.broadcast({
1833
+ type: "challenge_create",
1834
+ challengeId: cr.id,
1835
+ targetItemId: args.targetItemId ?? "",
1836
+ reason: args.reason ?? "",
1837
+ challengerNode: config.node.id
1838
+ });
1839
+ }
1840
+ return text(JSON.stringify({ challengeId: cr.id, status: "voting" }, null, 2));
1841
+ }
1842
+ case "inv_challenge_respond": {
1843
+ const nodeInfo = engine.getNode(config.node.id);
1844
+ const vote = engine.castVote(args.challengeId ?? "", config.node.id, nodeInfo.vertical, args.approve === "true" || args.approve === true, args.reason ?? "");
1845
+ if (wsClient?.connected) {
1846
+ wsClient.broadcast({
1847
+ type: "proposal_vote",
1848
+ crId: args.challengeId ?? "",
1849
+ approve: vote.approve,
1850
+ reason: args.reason ?? ""
1851
+ });
1852
+ }
1853
+ return text(JSON.stringify({ voted: true, challengeId: args.challengeId, approve: vote.approve }, null, 2));
1854
+ }
1855
+ case "inv_pair_invite": {
1856
+ const session = engine.invitePair(config.node.id, args.targetNode ?? "", config.node.project);
1857
+ if (wsClient?.connected) {
1858
+ wsClient.sendMessage(args.targetNode ?? "", {
1859
+ type: "pair_invite",
1860
+ sessionId: session.id,
1861
+ initiatorNode: config.node.id
1862
+ });
1863
+ }
1864
+ return text(JSON.stringify({ sessionId: session.id, status: "pending" }, null, 2));
1865
+ }
1866
+ case "inv_pair_join": {
1867
+ const session = engine.joinPair(args.sessionId ?? "");
1868
+ if (wsClient?.connected) {
1869
+ wsClient.sendMessage(session.initiatorNode, {
1870
+ type: "pair_respond",
1871
+ sessionId: session.id,
1872
+ accepted: true
1873
+ });
1874
+ }
1875
+ return text(JSON.stringify({ sessionId: session.id, status: "active" }, null, 2));
1876
+ }
1877
+ case "inv_pair_end": {
1878
+ const session = engine.endPair(args.sessionId ?? "");
1879
+ if (wsClient?.connected) {
1880
+ const partner = session.initiatorNode === config.node.id ? session.partnerNode : session.initiatorNode;
1881
+ wsClient.sendMessage(partner, {
1882
+ type: "pair_end",
1883
+ sessionId: session.id
1884
+ });
1885
+ }
1886
+ return text(JSON.stringify({ sessionId: session.id, status: "ended" }, null, 2));
1887
+ }
1888
+ case "inv_pair_list": {
1889
+ const sessions = engine.listPairSessions(config.node.id);
1890
+ return text(JSON.stringify(sessions, null, 2));
1891
+ }
1892
+ case "inv_checklist_add": {
1893
+ const cl = engine.addChecklistItem(args.itemId ?? "", args.text ?? "");
1894
+ return text(JSON.stringify(cl, null, 2));
1895
+ }
1896
+ case "inv_checklist_check": {
1897
+ engine.checkChecklistItem(args.checklistItemId ?? "");
1898
+ return text(JSON.stringify({ checked: true, checklistItemId: args.checklistItemId }, null, 2));
1899
+ }
1900
+ case "inv_checklist_uncheck": {
1901
+ engine.uncheckChecklistItem(args.checklistItemId ?? "");
1902
+ return text(JSON.stringify({ unchecked: true, checklistItemId: args.checklistItemId }, null, 2));
1903
+ }
1904
+ case "inv_checklist_list": {
1905
+ const items = engine.listChecklist(args.itemId ?? "");
1906
+ return text(JSON.stringify(items, null, 2));
1907
+ }
1908
+ default:
1909
+ return text(JSON.stringify({ error: `Unknown tool: ${name}` }));
1910
+ }
1911
+ } catch (err) {
1912
+ const message = err instanceof Error ? err.message : String(err);
1913
+ return text(JSON.stringify({ error: message }));
1914
+ }
1915
+ };
1916
+ }
1917
+ async function startChannelServer(configPath) {
1918
+ if (!existsSync(configPath)) {
1919
+ throw new Error(`Config file not found: ${configPath}`);
1920
+ }
1921
+ const raw = readFileSync(configPath, "utf-8");
1922
+ const partial = JSON.parse(raw);
1923
+ const config = loadConfig(partial);
1924
+ const store = new Store(config.database.path);
1925
+ const sm = new StateMachine;
1926
+ const propagator = new SignalPropagator(store, sm);
1927
+ const engine = new Engine(store, sm, propagator);
1928
+ if (!config.node.id) {
1929
+ const node = engine.registerNode(config.node.name || "unnamed-node", config.node.vertical, config.node.project || "default", config.node.owner || "local", config.node.isAI);
1930
+ config.node.id = node.id;
1931
+ const updatedRaw = JSON.parse(readFileSync(configPath, "utf-8"));
1932
+ if (!updatedRaw.node)
1933
+ updatedRaw.node = {};
1934
+ updatedRaw.node.id = node.id;
1935
+ writeFileSync2(configPath, JSON.stringify(updatedRaw, null, 2) + `
1936
+ `);
1937
+ }
1938
+ const eventBus = new EventBus;
1939
+ const wsHandlers = new WSHandlers(engine, store, eventBus);
1940
+ let wsClient = null;
1941
+ if (config.server.url && config.server.token) {
1942
+ wsClient = new WSClient({
1943
+ serverUrl: config.server.url,
1944
+ token: config.server.token,
1945
+ nodeId: config.node.id,
1946
+ projectId: config.node.project
1947
+ });
1948
+ wsClient.onMessage((envelope) => {
1949
+ wsHandlers.handle(envelope);
1950
+ });
1951
+ try {
1952
+ await wsClient.connect();
1953
+ } catch {
1954
+ wsClient = null;
1955
+ }
1956
+ }
1957
+ const mcp = new Server({ name: "inventory", version: "0.1.0" }, {
1958
+ capabilities: {
1959
+ experimental: {
1960
+ "claude/channel": {},
1961
+ "claude/channel/permission": {}
1962
+ },
1963
+ tools: {}
1964
+ },
1965
+ instructions: `You are connected to the inventory network as node "${config.node.name}" (${config.node.vertical}, project: ${config.node.project}, owner: ${config.node.owner}).
1966
+
1967
+ Events from the inventory network arrive as <channel source="inventory"> tags. These include:
1968
+ - signal_change: an item changed state
1969
+ - sweep: an external reference triggered a sweep
1970
+ - query_ask/query_respond: Q&A between nodes
1971
+ - permission_request/permission_verdict: approval workflows
1972
+ - proposal_create/proposal_vote/proposal_result: change request voting
1973
+ - challenge_create: a node challenges an item
1974
+ - pair_invite/pair_respond/pair_end: pairing session events
1975
+ - checklist_update: checklist item checked/unchecked
1976
+
1977
+ Use the inv_* tools to manage inventory, propose changes, vote, challenge items, pair with other nodes, and manage checklists.`
1978
+ });
1979
+ const handleTool = buildToolHandlers(engine, config, wsClient);
1980
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
1981
+ tools: TOOL_DEFINITIONS
1982
+ }));
1983
+ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
1984
+ const { name, arguments: args } = request.params;
1985
+ return handleTool(name, args ?? {});
1986
+ });
1987
+ const channelEvents = [
1988
+ "signal_change",
1989
+ "sweep",
1990
+ "query_ask",
1991
+ "query_respond",
1992
+ "permission_request",
1993
+ "permission_verdict",
1994
+ "proposal_create",
1995
+ "proposal_vote",
1996
+ "proposal_result",
1997
+ "challenge_create",
1998
+ "pair_invite",
1999
+ "pair_respond",
2000
+ "pair_end",
2001
+ "checklist_update",
2002
+ "error"
2003
+ ];
2004
+ for (const eventType of channelEvents) {
2005
+ eventBus.on(eventType, (data) => {
2006
+ mcp.notification({
2007
+ method: "notifications/claude/channel",
2008
+ params: {
2009
+ content: JSON.stringify(data),
2010
+ meta: { source: "inventory", eventType }
2011
+ }
2012
+ });
2013
+ });
2014
+ }
2015
+ const transport = new StdioServerTransport;
2016
+ await mcp.connect(transport);
2017
+ }
2018
+ if (false) {}
2019
+
2020
+ // src/index.ts
2021
+ async function main() {
2022
+ const command = process.argv[2];
2023
+ switch (command) {
2024
+ case "init":
2025
+ await runWizard();
2026
+ break;
2027
+ case "serve":
2028
+ await startChannelServer(process.argv[3] ?? "./inv-config.json");
2029
+ break;
2030
+ case undefined:
2031
+ await startChannelServer(process.argv[3] ?? "./inv-config.json");
2032
+ break;
2033
+ case "update":
2034
+ console.log("Clearing bunx cache for @tini-works/inv-node...");
2035
+ const proc = Bun.spawn(["bun", "pm", "cache", "rm"], { stdout: "inherit", stderr: "inherit" });
2036
+ await proc.exited;
2037
+ console.log("Cache cleared. Next run will fetch the latest version.");
2038
+ break;
2039
+ default:
2040
+ console.error(`Unknown command: ${command}`);
2041
+ console.error("Usage:");
2042
+ console.error(" inv-node init Set up a new node");
2043
+ console.error(" inv-node serve [config] Start MCP server");
2044
+ console.error(" inv-node update Update to latest version");
2045
+ process.exit(1);
2046
+ }
2047
+ }
2048
+ main().catch((err) => {
2049
+ console.error("Fatal error:", err);
2050
+ process.exit(1);
2051
+ });