@grainulation/wheat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/bin/wheat.js +193 -0
  4. package/compiler/detect-sprints.js +319 -0
  5. package/compiler/generate-manifest.js +280 -0
  6. package/compiler/wheat-compiler.js +1229 -0
  7. package/lib/compiler.js +35 -0
  8. package/lib/connect.js +418 -0
  9. package/lib/disconnect.js +188 -0
  10. package/lib/guard.js +151 -0
  11. package/lib/index.js +14 -0
  12. package/lib/init.js +457 -0
  13. package/lib/install-prompt.js +186 -0
  14. package/lib/quickstart.js +276 -0
  15. package/lib/serve-mcp.js +509 -0
  16. package/lib/server.js +391 -0
  17. package/lib/stats.js +184 -0
  18. package/lib/status.js +135 -0
  19. package/lib/update.js +71 -0
  20. package/package.json +53 -0
  21. package/public/index.html +1798 -0
  22. package/templates/claude.md +122 -0
  23. package/templates/commands/blind-spot.md +47 -0
  24. package/templates/commands/brief.md +73 -0
  25. package/templates/commands/calibrate.md +39 -0
  26. package/templates/commands/challenge.md +72 -0
  27. package/templates/commands/connect.md +104 -0
  28. package/templates/commands/evaluate.md +80 -0
  29. package/templates/commands/feedback.md +60 -0
  30. package/templates/commands/handoff.md +53 -0
  31. package/templates/commands/init.md +68 -0
  32. package/templates/commands/merge.md +51 -0
  33. package/templates/commands/present.md +52 -0
  34. package/templates/commands/prototype.md +68 -0
  35. package/templates/commands/replay.md +61 -0
  36. package/templates/commands/research.md +73 -0
  37. package/templates/commands/resolve.md +42 -0
  38. package/templates/commands/status.md +56 -0
  39. package/templates/commands/witness.md +79 -0
  40. package/templates/explainer.html +343 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * wheat serve-mcp -- Local MCP server for Claude Code
3
+ *
4
+ * Exposes wheat compiler tools and resources over stdio transport
5
+ * using the MCP JSON-RPC protocol. Zero npm dependencies.
6
+ *
7
+ * Tools:
8
+ * wheat/compile -- Run the compiler, return status + warnings
9
+ * wheat/add-claim -- Append a typed claim to claims.json
10
+ * wheat/resolve -- Adjudicate a specific conflict
11
+ * wheat/search -- Query claims by topic, type, evidence tier
12
+ * wheat/status -- Return compilation summary
13
+ *
14
+ * Resources:
15
+ * wheat://compilation -- Current compilation.json
16
+ * wheat://claims -- Current claims.json
17
+ * wheat://brief -- Latest brief (output/brief.html)
18
+ *
19
+ * Protocol: MCP over stdio (JSON-RPC 2.0, newline-delimited)
20
+ *
21
+ * Install:
22
+ * claude mcp add wheat -- npx @grainulation/wheat mcp
23
+ *
24
+ * Zero npm dependencies.
25
+ */
26
+
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import readline from 'node:readline';
30
+ import { execSync } from 'node:child_process';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = path.dirname(__filename);
35
+
36
+ // --- Constants ---------------------------------------------------------------
37
+
38
+ const SERVER_NAME = 'wheat';
39
+ const SERVER_VERSION = '1.0.0';
40
+ const PROTOCOL_VERSION = '2024-11-05';
41
+
42
+ const VALID_TYPES = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
43
+ const VALID_EVIDENCE = ['stated', 'web', 'documented', 'tested', 'production'];
44
+
45
+ // --- JSON-RPC helpers --------------------------------------------------------
46
+
47
+ function jsonRpcResponse(id, result) {
48
+ return JSON.stringify({ jsonrpc: '2.0', id, result });
49
+ }
50
+
51
+ function jsonRpcError(id, code, message) {
52
+ return JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } });
53
+ }
54
+
55
+ // --- Paths -------------------------------------------------------------------
56
+
57
+ function resolvePaths(dir) {
58
+ return {
59
+ claims: path.join(dir, 'claims.json'),
60
+ compilation: path.join(dir, 'compilation.json'),
61
+ brief: path.join(dir, 'output', 'brief.html'),
62
+ compiler: path.join(dir, 'wheat-compiler.js'),
63
+ };
64
+ }
65
+
66
+ // --- Tool implementations ----------------------------------------------------
67
+
68
+ function toolCompile(dir) {
69
+ const paths = resolvePaths(dir);
70
+
71
+ if (!fs.existsSync(paths.claims)) {
72
+ return { status: 'error', message: 'No claims.json found. Run wheat init first.' };
73
+ }
74
+
75
+ // Find compiler -- check local dir, then package compiler/
76
+ let compilerPath = paths.compiler;
77
+ if (!fs.existsSync(compilerPath)) {
78
+ compilerPath = path.join(__dirname, '..', 'compiler', 'wheat-compiler.js');
79
+ }
80
+ if (!fs.existsSync(compilerPath)) {
81
+ return { status: 'error', message: 'Compiler not found. Ensure wheat-compiler.js is in the project root.' };
82
+ }
83
+
84
+ try {
85
+ const output = execSync(`node "${compilerPath}" --summary --dir "${dir}"`, {
86
+ cwd: dir,
87
+ encoding: 'utf8',
88
+ timeout: 30000,
89
+ stdio: ['pipe', 'pipe', 'pipe'],
90
+ });
91
+ return { status: 'ok', output: output.trim() };
92
+ } catch (err) {
93
+ return { status: 'error', output: (err.stdout || '').trim(), error: (err.stderr || '').trim() };
94
+ }
95
+ }
96
+
97
+ function toolAddClaim(dir, args) {
98
+ const paths = resolvePaths(dir);
99
+ if (!fs.existsSync(paths.claims)) {
100
+ return { status: 'error', message: 'No claims.json found. Run wheat init first.' };
101
+ }
102
+
103
+ const { id, type, topic, content, evidence, tags } = args;
104
+
105
+ // Validate
106
+ if (!id || !type || !topic || !content) {
107
+ return { status: 'error', message: 'Required fields: id, type, topic, content' };
108
+ }
109
+ if (!VALID_TYPES.includes(type)) {
110
+ return { status: 'error', message: `Invalid type "${type}". Valid: ${VALID_TYPES.join(', ')}` };
111
+ }
112
+ if (evidence && !VALID_EVIDENCE.includes(evidence)) {
113
+ return { status: 'error', message: `Invalid evidence "${evidence}". Valid: ${VALID_EVIDENCE.join(', ')}` };
114
+ }
115
+
116
+ const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
117
+
118
+ // Check for duplicate ID
119
+ if (data.claims.some(c => c.id === id)) {
120
+ return { status: 'error', message: `Claim ID "${id}" already exists.` };
121
+ }
122
+
123
+ const claim = {
124
+ id,
125
+ type,
126
+ topic,
127
+ content,
128
+ source: { origin: 'mcp', artifact: null, connector: null },
129
+ evidence: evidence || 'stated',
130
+ status: 'active',
131
+ phase_added: data.meta.phase || 'research',
132
+ timestamp: new Date().toISOString(),
133
+ conflicts_with: [],
134
+ resolved_by: null,
135
+ tags: tags || [],
136
+ };
137
+
138
+ data.claims.push(claim);
139
+ fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + '\n');
140
+
141
+ return { status: 'ok', message: `Claim ${id} added.`, claim };
142
+ }
143
+
144
+ function toolResolve(dir, args) {
145
+ const paths = resolvePaths(dir);
146
+ if (!fs.existsSync(paths.claims)) {
147
+ return { status: 'error', message: 'No claims.json found.' };
148
+ }
149
+
150
+ const { winner, loser, reason } = args;
151
+ if (!winner || !loser) {
152
+ return { status: 'error', message: 'Required fields: winner, loser' };
153
+ }
154
+
155
+ const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
156
+ const winnerClaim = data.claims.find(c => c.id === winner);
157
+ const loserClaim = data.claims.find(c => c.id === loser);
158
+
159
+ if (!winnerClaim) return { status: 'error', message: `Claim "${winner}" not found.` };
160
+ if (!loserClaim) return { status: 'error', message: `Claim "${loser}" not found.` };
161
+
162
+ // Clear conflict references
163
+ winnerClaim.conflicts_with = (winnerClaim.conflicts_with || []).filter(cid => cid !== loser);
164
+ loserClaim.conflicts_with = [];
165
+ loserClaim.status = 'superseded';
166
+ loserClaim.resolved_by = winner;
167
+
168
+ fs.writeFileSync(paths.claims, JSON.stringify(data, null, 2) + '\n');
169
+
170
+ return {
171
+ status: 'ok',
172
+ message: `Resolved: ${winner} wins over ${loser}${reason ? ` (${reason})` : ''}.`,
173
+ winner: winnerClaim.id,
174
+ loser: loserClaim.id,
175
+ };
176
+ }
177
+
178
+ function toolSearch(dir, args) {
179
+ const paths = resolvePaths(dir);
180
+ if (!fs.existsSync(paths.claims)) {
181
+ return { status: 'error', message: 'No claims.json found.' };
182
+ }
183
+
184
+ const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
185
+ let results = data.claims.filter(c => c.status === 'active');
186
+
187
+ if (args.topic) {
188
+ results = results.filter(c => c.topic === args.topic);
189
+ }
190
+ if (args.type) {
191
+ results = results.filter(c => c.type === args.type);
192
+ }
193
+ if (args.evidence) {
194
+ results = results.filter(c => c.evidence === args.evidence);
195
+ }
196
+ if (args.query) {
197
+ const q = args.query.toLowerCase();
198
+ results = results.filter(c => c.content.toLowerCase().includes(q));
199
+ }
200
+
201
+ return {
202
+ status: 'ok',
203
+ count: results.length,
204
+ claims: results.map(c => ({
205
+ id: c.id,
206
+ type: c.type,
207
+ topic: c.topic,
208
+ evidence: c.evidence,
209
+ content: c.content.slice(0, 200) + (c.content.length > 200 ? '...' : ''),
210
+ })),
211
+ };
212
+ }
213
+
214
+ function toolStatus(dir) {
215
+ const paths = resolvePaths(dir);
216
+
217
+ if (!fs.existsSync(paths.claims)) {
218
+ return { status: 'no_sprint', message: 'No claims.json found. Run wheat init to start a sprint.' };
219
+ }
220
+
221
+ const data = JSON.parse(fs.readFileSync(paths.claims, 'utf8'));
222
+ const claims = data.claims || [];
223
+ const active = claims.filter(c => c.status === 'active');
224
+ const conflicted = claims.filter(c =>
225
+ c.status === 'conflicted' ||
226
+ (c.conflicts_with && c.conflicts_with.length > 0 && c.status === 'active')
227
+ );
228
+ const topics = [...new Set(active.map(c => c.topic))];
229
+ const types = {};
230
+ active.forEach(c => { types[c.type] = (types[c.type] || 0) + 1; });
231
+
232
+ let compilationStatus = 'unknown';
233
+ if (fs.existsSync(paths.compilation)) {
234
+ try {
235
+ const comp = JSON.parse(fs.readFileSync(paths.compilation, 'utf8'));
236
+ compilationStatus = comp.status || 'unknown';
237
+ } catch { /* ignore */ }
238
+ }
239
+
240
+ return {
241
+ status: 'ok',
242
+ question: data.meta.question,
243
+ phase: data.meta.phase,
244
+ total_claims: claims.length,
245
+ active_claims: active.length,
246
+ conflicted_claims: conflicted.length,
247
+ topics: topics.length,
248
+ type_distribution: types,
249
+ compilation_status: compilationStatus,
250
+ };
251
+ }
252
+
253
+ // --- Tool & Resource definitions ---------------------------------------------
254
+
255
+ const TOOLS = [
256
+ {
257
+ name: 'wheat/compile',
258
+ description: 'Run the wheat compiler on claims.json. Returns compilation status, warnings, and errors.',
259
+ inputSchema: {
260
+ type: 'object',
261
+ properties: {},
262
+ },
263
+ },
264
+ {
265
+ name: 'wheat/add-claim',
266
+ description: 'Append a typed claim to claims.json. Validates type, evidence tier, and checks for duplicate IDs.',
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ id: { type: 'string', description: 'Claim ID (e.g., r001, x001, d001)' },
271
+ type: { type: 'string', enum: VALID_TYPES, description: 'Claim type' },
272
+ topic: { type: 'string', description: 'Topic slug (e.g., database-migration)' },
273
+ content: { type: 'string', description: 'The claim content -- specific, verifiable finding' },
274
+ evidence: { type: 'string', enum: VALID_EVIDENCE, description: 'Evidence tier (default: stated)' },
275
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags' },
276
+ },
277
+ required: ['id', 'type', 'topic', 'content'],
278
+ },
279
+ },
280
+ {
281
+ name: 'wheat/resolve',
282
+ description: 'Resolve a conflict between two claims. The winner stays active; the loser is superseded.',
283
+ inputSchema: {
284
+ type: 'object',
285
+ properties: {
286
+ winner: { type: 'string', description: 'ID of the winning claim' },
287
+ loser: { type: 'string', description: 'ID of the losing claim' },
288
+ reason: { type: 'string', description: 'Optional reason for the resolution' },
289
+ },
290
+ required: ['winner', 'loser'],
291
+ },
292
+ },
293
+ {
294
+ name: 'wheat/search',
295
+ description: 'Search active claims by topic, type, evidence tier, or free-text query.',
296
+ inputSchema: {
297
+ type: 'object',
298
+ properties: {
299
+ topic: { type: 'string', description: 'Filter by topic slug' },
300
+ type: { type: 'string', enum: VALID_TYPES, description: 'Filter by claim type' },
301
+ evidence: { type: 'string', enum: VALID_EVIDENCE, description: 'Filter by evidence tier' },
302
+ query: { type: 'string', description: 'Free-text search in claim content' },
303
+ },
304
+ },
305
+ },
306
+ {
307
+ name: 'wheat/status',
308
+ description: 'Get sprint status: question, phase, claim counts, topic count, compilation status.',
309
+ inputSchema: {
310
+ type: 'object',
311
+ properties: {},
312
+ },
313
+ },
314
+ ];
315
+
316
+ const RESOURCES = [
317
+ {
318
+ uri: 'wheat://compilation',
319
+ name: 'Compilation Output',
320
+ description: 'Current compilation.json -- the checked, certified output from the wheat compiler.',
321
+ mimeType: 'application/json',
322
+ },
323
+ {
324
+ uri: 'wheat://claims',
325
+ name: 'Claims Data',
326
+ description: 'Current claims.json -- all typed claims in the sprint.',
327
+ mimeType: 'application/json',
328
+ },
329
+ {
330
+ uri: 'wheat://brief',
331
+ name: 'Decision Brief',
332
+ description: 'Latest compiled brief (output/brief.html) -- self-contained HTML.',
333
+ mimeType: 'text/html',
334
+ },
335
+ ];
336
+
337
+ // --- Request handler ---------------------------------------------------------
338
+
339
+ function handleRequest(dir, method, params, id) {
340
+ switch (method) {
341
+ case 'initialize':
342
+ return jsonRpcResponse(id, {
343
+ protocolVersion: PROTOCOL_VERSION,
344
+ capabilities: { tools: {}, resources: {} },
345
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
346
+ });
347
+
348
+ case 'notifications/initialized':
349
+ // No response needed for notifications
350
+ return null;
351
+
352
+ case 'tools/list':
353
+ return jsonRpcResponse(id, { tools: TOOLS });
354
+
355
+ case 'tools/call': {
356
+ const toolName = params.name;
357
+ const toolArgs = params.arguments || {};
358
+ let result;
359
+
360
+ switch (toolName) {
361
+ case 'wheat/compile':
362
+ result = toolCompile(dir);
363
+ break;
364
+ case 'wheat/add-claim':
365
+ result = toolAddClaim(dir, toolArgs);
366
+ break;
367
+ case 'wheat/resolve':
368
+ result = toolResolve(dir, toolArgs);
369
+ break;
370
+ case 'wheat/search':
371
+ result = toolSearch(dir, toolArgs);
372
+ break;
373
+ case 'wheat/status':
374
+ result = toolStatus(dir);
375
+ break;
376
+ default:
377
+ return jsonRpcError(id, -32601, `Unknown tool: ${toolName}`);
378
+ }
379
+
380
+ const isError = result.status === 'error';
381
+ return jsonRpcResponse(id, {
382
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
383
+ isError,
384
+ });
385
+ }
386
+
387
+ case 'resources/list':
388
+ return jsonRpcResponse(id, { resources: RESOURCES });
389
+
390
+ case 'resources/read': {
391
+ const uri = params.uri;
392
+ const paths = resolvePaths(dir);
393
+ let filePath, mimeType;
394
+
395
+ switch (uri) {
396
+ case 'wheat://compilation':
397
+ filePath = paths.compilation;
398
+ mimeType = 'application/json';
399
+ break;
400
+ case 'wheat://claims':
401
+ filePath = paths.claims;
402
+ mimeType = 'application/json';
403
+ break;
404
+ case 'wheat://brief':
405
+ filePath = paths.brief;
406
+ mimeType = 'text/html';
407
+ break;
408
+ default:
409
+ return jsonRpcError(id, -32602, `Unknown resource: ${uri}`);
410
+ }
411
+
412
+ if (!fs.existsSync(filePath)) {
413
+ return jsonRpcResponse(id, {
414
+ contents: [{ uri, mimeType, text: `Resource not found: ${filePath}` }],
415
+ });
416
+ }
417
+
418
+ const text = fs.readFileSync(filePath, 'utf8');
419
+ return jsonRpcResponse(id, {
420
+ contents: [{ uri, mimeType, text }],
421
+ });
422
+ }
423
+
424
+ case 'ping':
425
+ return jsonRpcResponse(id, {});
426
+
427
+ default:
428
+ // Ignore unknown notifications (no id = notification)
429
+ if (id === undefined || id === null) return null;
430
+ return jsonRpcError(id, -32601, `Method not found: ${method}`);
431
+ }
432
+ }
433
+
434
+ // --- Stdio transport ---------------------------------------------------------
435
+
436
+ function startServer(dir) {
437
+ const rl = readline.createInterface({
438
+ input: process.stdin,
439
+ output: process.stdout,
440
+ terminal: false,
441
+ });
442
+
443
+ // Disable output buffering on stdout
444
+ if (process.stdout._handle && process.stdout._handle.setBlocking) {
445
+ process.stdout._handle.setBlocking(true);
446
+ }
447
+
448
+ rl.on('line', (line) => {
449
+ if (!line.trim()) return;
450
+
451
+ let msg;
452
+ try {
453
+ msg = JSON.parse(line);
454
+ } catch {
455
+ const resp = jsonRpcError(null, -32700, 'Parse error');
456
+ process.stdout.write(resp + '\n');
457
+ return;
458
+ }
459
+
460
+ const response = handleRequest(dir, msg.method, msg.params || {}, msg.id);
461
+
462
+ // Notifications don't get responses
463
+ if (response !== null) {
464
+ process.stdout.write(response + '\n');
465
+ }
466
+ });
467
+
468
+ rl.on('close', () => {
469
+ process.exit(0);
470
+ });
471
+
472
+ // Log to stderr (stdout is reserved for JSON-RPC)
473
+ process.stderr.write(`wheat MCP server v${SERVER_VERSION} ready on stdio\n`);
474
+ process.stderr.write(` Target directory: ${dir}\n`);
475
+ process.stderr.write(` Tools: ${TOOLS.length} | Resources: ${RESOURCES.length}\n`);
476
+ }
477
+
478
+ // --- CLI handler -------------------------------------------------------------
479
+
480
+ export async function run(dir, args) {
481
+ if (args.includes('--help') || args.includes('-h')) {
482
+ console.log(`wheat mcp -- Local MCP server for Claude Code
483
+
484
+ Usage:
485
+ wheat mcp [--dir <path>]
486
+
487
+ Install in Claude Code:
488
+ claude mcp add wheat -- npx @grainulation/wheat mcp
489
+
490
+ Tools exposed:
491
+ wheat/compile Run the compiler
492
+ wheat/add-claim Append a typed claim
493
+ wheat/resolve Resolve a conflict
494
+ wheat/search Search claims
495
+ wheat/status Sprint status
496
+
497
+ Resources exposed:
498
+ wheat://compilation compilation.json
499
+ wheat://claims claims.json
500
+ wheat://brief output/brief.html
501
+
502
+ Protocol: MCP over stdio (JSON-RPC 2.0, newline-delimited)`);
503
+ return;
504
+ }
505
+
506
+ startServer(dir);
507
+ }
508
+
509
+ export { startServer, handleRequest, TOOLS, RESOURCES };