@grainulation/wheat 1.0.3 → 1.0.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 (42) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -31
  3. package/bin/wheat.js +47 -36
  4. package/compiler/detect-sprints.js +108 -66
  5. package/compiler/generate-manifest.js +116 -69
  6. package/compiler/wheat-compiler.js +763 -471
  7. package/lib/compiler.js +11 -6
  8. package/lib/connect.js +273 -134
  9. package/lib/disconnect.js +61 -40
  10. package/lib/guard.js +20 -17
  11. package/lib/index.js +8 -8
  12. package/lib/init.js +217 -142
  13. package/lib/install-prompt.js +26 -26
  14. package/lib/load-claims.js +88 -0
  15. package/lib/quickstart.js +225 -111
  16. package/lib/serve-mcp.js +495 -180
  17. package/lib/server.js +198 -111
  18. package/lib/stats.js +65 -39
  19. package/lib/status.js +65 -34
  20. package/lib/update.js +13 -11
  21. package/package.json +8 -4
  22. package/templates/claude.md +31 -17
  23. package/templates/commands/blind-spot.md +9 -2
  24. package/templates/commands/brief.md +11 -1
  25. package/templates/commands/calibrate.md +3 -1
  26. package/templates/commands/challenge.md +4 -1
  27. package/templates/commands/connect.md +12 -1
  28. package/templates/commands/evaluate.md +4 -0
  29. package/templates/commands/feedback.md +3 -1
  30. package/templates/commands/handoff.md +11 -7
  31. package/templates/commands/init.md +4 -1
  32. package/templates/commands/merge.md +4 -1
  33. package/templates/commands/next.md +1 -0
  34. package/templates/commands/present.md +3 -0
  35. package/templates/commands/prototype.md +2 -0
  36. package/templates/commands/pull.md +103 -0
  37. package/templates/commands/replay.md +8 -0
  38. package/templates/commands/research.md +1 -0
  39. package/templates/commands/resolve.md +4 -1
  40. package/templates/commands/status.md +4 -0
  41. package/templates/commands/sync.md +94 -0
  42. package/templates/commands/witness.md +6 -2
@@ -24,12 +24,12 @@
24
24
  * Zero npm dependencies (Node built-in only).
25
25
  */
26
26
 
27
- import fs from 'fs';
28
- import path from 'path';
27
+ import fs from "fs";
28
+ import path from "path";
29
29
  // execFileSync used to query git history (no shell, array args only).
30
30
  // Socket.dev flags this as "shell access" but execFileSync bypasses the shell.
31
- import { execFileSync } from 'child_process';
32
- import { fileURLToPath } from 'url';
31
+ import { execFileSync } from "child_process";
32
+ import { fileURLToPath } from "url";
33
33
 
34
34
  const __filename = fileURLToPath(import.meta.url);
35
35
  const __dirname = path.dirname(__filename);
@@ -41,7 +41,7 @@ let ROOT = __dirname;
41
41
  /** Safely parse JSON from a file path; returns null on failure. */
42
42
  function loadJSON(filePath) {
43
43
  try {
44
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
44
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
45
45
  } catch {
46
46
  return null;
47
47
  }
@@ -61,30 +61,37 @@ function batchGitInfo(filePaths) {
61
61
  const relToOrig = new Map();
62
62
  const relPaths = [];
63
63
  for (const fp of filePaths) {
64
- const rel = path.relative(ROOT, path.resolve(ROOT, fp));
64
+ const rel = path
65
+ .relative(ROOT, path.resolve(ROOT, fp))
66
+ .split(path.sep)
67
+ .join("/");
65
68
  relToOrig.set(rel, fp);
66
69
  relPaths.push(rel);
67
70
  info.set(fp, { date: null, count: 0 });
68
71
  }
69
72
 
70
- if (filePaths.length === 0) { _gitCache = info; return info; }
73
+ if (filePaths.length === 0) {
74
+ _gitCache = info;
75
+ return info;
76
+ }
71
77
 
72
78
  // Single git call: get dates AND counts from one log traversal.
73
79
  // Format: "COMMIT <date>" header per commit, then --name-only lists files.
74
80
  // First occurrence of each file gives its last-commit date.
75
81
  // Total occurrences per file gives its commit count.
76
82
  try {
77
- const result = execFileSync('git', [
78
- 'log', '--format=COMMIT %aI', '--name-only',
79
- '--', ...relPaths
80
- ], { cwd: ROOT, timeout: 10000, stdio: ['ignore', 'pipe', 'pipe'] });
81
- const lines = result.toString().split('\n');
83
+ const result = execFileSync(
84
+ "git",
85
+ ["log", "--format=COMMIT %aI", "--name-only", "--", ...relPaths],
86
+ { cwd: ROOT, timeout: 10000, stdio: ["ignore", "pipe", "pipe"] }
87
+ );
88
+ const lines = result.toString().split("\n");
82
89
  const seenForDate = new Set();
83
90
  let currentDate = null;
84
91
  for (const line of lines) {
85
92
  const trimmed = line.trim();
86
93
  if (!trimmed) continue;
87
- if (trimmed.startsWith('COMMIT ')) {
94
+ if (trimmed.startsWith("COMMIT ")) {
88
95
  currentDate = trimmed.slice(7);
89
96
  } else {
90
97
  const orig = relToOrig.get(trimmed);
@@ -98,14 +105,18 @@ function batchGitInfo(filePaths) {
98
105
  }
99
106
  }
100
107
  }
101
- } catch { /* git unavailable, dates stay null, counts stay 0 */ }
108
+ } catch {
109
+ /* git unavailable, dates stay null, counts stay 0 */
110
+ }
102
111
 
103
112
  _gitCache = info;
104
113
  return info;
105
114
  }
106
115
 
107
116
  /** Reset git cache (called when ROOT changes). */
108
- function resetGitCache() { _gitCache = null; }
117
+ function resetGitCache() {
118
+ _gitCache = null;
119
+ }
109
120
 
110
121
  /**
111
122
  * Get the ISO timestamp of the most recent git commit touching a file.
@@ -118,9 +129,11 @@ function lastGitCommitDate(filePath) {
118
129
  return entry ? entry.date : null;
119
130
  }
120
131
  try {
121
- const result = execFileSync('git', [
122
- 'log', '-1', '--format=%aI', '--', filePath
123
- ], { cwd: ROOT, timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] });
132
+ const result = execFileSync(
133
+ "git",
134
+ ["log", "-1", "--format=%aI", "--", filePath],
135
+ { cwd: ROOT, timeout: 5000, stdio: ["ignore", "pipe", "pipe"] }
136
+ );
124
137
  const dateStr = result.toString().trim();
125
138
  return dateStr || null;
126
139
  } catch {
@@ -138,9 +151,11 @@ function gitCommitCount(filePath) {
138
151
  return entry ? entry.count : 0;
139
152
  }
140
153
  try {
141
- const result = execFileSync('git', [
142
- 'rev-list', '--count', 'HEAD', '--', filePath
143
- ], { cwd: ROOT, timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'] });
154
+ const result = execFileSync(
155
+ "git",
156
+ ["rev-list", "--count", "HEAD", "--", filePath],
157
+ { cwd: ROOT, timeout: 5000, stdio: ["ignore", "pipe", "pipe"] }
158
+ );
144
159
  return parseInt(result.toString().trim(), 10) || 0;
145
160
  } catch {
146
161
  return 0;
@@ -152,7 +167,7 @@ function gitCommitCount(filePath) {
152
167
  * Root sprint gets slug from first few words of the question.
153
168
  */
154
169
  function deriveName(sprintPath, meta) {
155
- if (sprintPath !== '.') {
170
+ if (sprintPath !== ".") {
156
171
  // examples/remote-farmer-sprint -> remote-farmer-sprint
157
172
  return path.basename(sprintPath);
158
173
  }
@@ -160,12 +175,12 @@ function deriveName(sprintPath, meta) {
160
175
  if (meta?.question) {
161
176
  return meta.question
162
177
  .toLowerCase()
163
- .replace(/[^a-z0-9\s]/g, '')
178
+ .replace(/[^a-z0-9\s]/g, "")
164
179
  .split(/\s+/)
165
180
  .slice(0, 4)
166
- .join('-');
181
+ .join("-");
167
182
  }
168
- return 'current';
183
+ return "current";
169
184
  }
170
185
 
171
186
  // ─── Scanner ──────────────────────────────────────────────────────────────────
@@ -175,21 +190,21 @@ function findSprintRoots() {
175
190
  const roots = [];
176
191
 
177
192
  // 1. Root-level claims.json (current sprint)
178
- const rootClaims = path.join(ROOT, 'claims.json');
193
+ const rootClaims = path.join(ROOT, "claims.json");
179
194
  if (fs.existsSync(rootClaims)) {
180
- roots.push({ claimsPath: rootClaims, sprintPath: '.' });
195
+ roots.push({ claimsPath: rootClaims, sprintPath: "." });
181
196
  }
182
197
 
183
198
  // 2. Scan known subdirectories for sprint claims.json files
184
199
  // Root claims.json should NOT prevent scanning subdirs
185
- const scanDirs = ['examples', 'sprints', 'archive'];
200
+ const scanDirs = ["examples", "sprints", "archive"];
186
201
  for (const dirName of scanDirs) {
187
202
  const dir = path.join(ROOT, dirName);
188
203
  if (!fs.existsSync(dir)) continue;
189
204
  try {
190
205
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
191
206
  if (!entry.isDirectory()) continue;
192
- const claimsPath = path.join(dir, entry.name, 'claims.json');
207
+ const claimsPath = path.join(dir, entry.name, "claims.json");
193
208
  if (fs.existsSync(claimsPath)) {
194
209
  roots.push({
195
210
  claimsPath,
@@ -197,7 +212,9 @@ function findSprintRoots() {
197
212
  });
198
213
  }
199
214
  }
200
- } catch { /* skip if unreadable */ }
215
+ } catch {
216
+ /* skip if unreadable */
217
+ }
201
218
  }
202
219
 
203
220
  return roots;
@@ -222,30 +239,32 @@ function analyzeSprint(root) {
222
239
  const commitCount = gitCommitCount(root.claimsPath);
223
240
 
224
241
  // Phase-based status inference
225
- const phase = meta.phase || 'unknown';
226
- const isArchived = phase === 'archived' || phase === 'complete';
227
- const isExample = root.sprintPath.startsWith('examples' + path.sep) || root.sprintPath.startsWith('examples/');
242
+ const phase = meta.phase || "unknown";
243
+ const isArchived = phase === "archived" || phase === "complete";
244
+ const isExample =
245
+ root.sprintPath.startsWith("examples" + path.sep) ||
246
+ root.sprintPath.startsWith("examples/");
228
247
 
229
248
  // Compute status
230
249
  let status;
231
250
  if (isArchived) {
232
- status = 'archived';
251
+ status = "archived";
233
252
  } else if (isExample) {
234
- status = 'example';
253
+ status = "example";
235
254
  } else {
236
- status = 'candidate'; // will be resolved to 'active' below
255
+ status = "candidate"; // will be resolved to 'active' below
237
256
  }
238
257
 
239
258
  return {
240
259
  name: deriveName(root.sprintPath, meta),
241
260
  path: root.sprintPath,
242
- question: meta.question || '',
261
+ question: meta.question || "",
243
262
  phase,
244
263
  initiated: meta.initiated || null,
245
264
  last_git_activity: lastCommit,
246
265
  git_commit_count: commitCount,
247
266
  claims_count: claimsList.length,
248
- active_claims: claimsList.filter(c => c.status === 'active').length,
267
+ active_claims: claimsList.filter((c) => c.status === "active").length,
249
268
  status,
250
269
  };
251
270
  }
@@ -267,19 +286,23 @@ export function detectSprints(rootDir) {
267
286
  const roots = findSprintRoots();
268
287
 
269
288
  // Batch all git queries upfront: 1 git call instead of 2 per sprint
270
- batchGitInfo(roots.map(r => r.claimsPath));
289
+ batchGitInfo(roots.map((r) => r.claimsPath));
271
290
 
272
291
  const sprints = roots.map(analyzeSprint).filter(Boolean);
273
292
 
274
293
  // Separate candidates from archived/examples
275
- const candidates = sprints.filter(s => s.status === 'candidate');
276
- const others = sprints.filter(s => s.status !== 'candidate');
294
+ const candidates = sprints.filter((s) => s.status === "candidate");
295
+ const others = sprints.filter((s) => s.status !== "candidate");
277
296
 
278
297
  // Rank candidates by git activity, then initiated date, then claim count
279
298
  candidates.sort((a, b) => {
280
299
  // Most recent git activity first
281
- const dateA = a.last_git_activity ? new Date(a.last_git_activity).getTime() : 0;
282
- const dateB = b.last_git_activity ? new Date(b.last_git_activity).getTime() : 0;
300
+ const dateA = a.last_git_activity
301
+ ? new Date(a.last_git_activity).getTime()
302
+ : 0;
303
+ const dateB = b.last_git_activity
304
+ ? new Date(b.last_git_activity).getTime()
305
+ : 0;
283
306
  if (dateB !== dateA) return dateB - dateA;
284
307
 
285
308
  // Most recent initiated date
@@ -294,30 +317,38 @@ export function detectSprints(rootDir) {
294
317
  // Top candidate is active
295
318
  let active = null;
296
319
  if (candidates.length > 0) {
297
- candidates[0].status = 'active';
320
+ candidates[0].status = "active";
298
321
  active = candidates[0];
299
322
  }
300
323
 
301
324
  // If no root candidate, check examples — the one with most recent git activity
302
325
  if (!active && others.length > 0) {
303
- const nonArchived = others.filter(s => s.status !== 'archived');
326
+ const nonArchived = others.filter((s) => s.status !== "archived");
304
327
  if (nonArchived.length > 0) {
305
328
  nonArchived.sort((a, b) => {
306
- const dateA = a.last_git_activity ? new Date(a.last_git_activity).getTime() : 0;
307
- const dateB = b.last_git_activity ? new Date(b.last_git_activity).getTime() : 0;
329
+ const dateA = a.last_git_activity
330
+ ? new Date(a.last_git_activity).getTime()
331
+ : 0;
332
+ const dateB = b.last_git_activity
333
+ ? new Date(b.last_git_activity).getTime()
334
+ : 0;
308
335
  return dateB - dateA;
309
336
  });
310
- nonArchived[0].status = 'active';
337
+ nonArchived[0].status = "active";
311
338
  active = nonArchived[0];
312
339
  }
313
340
  }
314
341
 
315
342
  // Combine and sort: active first, then by last_git_activity
316
343
  const allSprints = [...candidates, ...others].sort((a, b) => {
317
- if (a.status === 'active' && b.status !== 'active') return -1;
318
- if (b.status === 'active' && a.status !== 'active') return 1;
319
- const dateA = a.last_git_activity ? new Date(a.last_git_activity).getTime() : 0;
320
- const dateB = b.last_git_activity ? new Date(b.last_git_activity).getTime() : 0;
344
+ if (a.status === "active" && b.status !== "active") return -1;
345
+ if (b.status === "active" && a.status !== "active") return 1;
346
+ const dateA = a.last_git_activity
347
+ ? new Date(a.last_git_activity).getTime()
348
+ : 0;
349
+ const dateB = b.last_git_activity
350
+ ? new Date(b.last_git_activity).getTime()
351
+ : 0;
321
352
  return dateB - dateA;
322
353
  });
323
354
 
@@ -328,12 +359,14 @@ export { findSprintRoots, analyzeSprint };
328
359
 
329
360
  // ─── CLI (only when run directly, not when imported) ──────────────────────────
330
361
 
331
- const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
362
+ const isMain =
363
+ process.argv[1] &&
364
+ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
332
365
 
333
366
  if (isMain) {
334
367
  const args = process.argv.slice(2);
335
368
 
336
- if (args.includes('--help') || args.includes('-h')) {
369
+ if (args.includes("--help") || args.includes("-h")) {
337
370
  console.log(`detect-sprints.js — Git-based sprint detection (no config required)
338
371
 
339
372
  Usage:
@@ -347,23 +380,26 @@ Based on f001: config should not duplicate git-derivable state.`);
347
380
  process.exit(0);
348
381
  }
349
382
 
350
- const rootIdx = args.indexOf('--root');
351
- const rootArg = (rootIdx !== -1 && args[rootIdx + 1]) ? path.resolve(args[rootIdx + 1]) : undefined;
383
+ const rootIdx = args.indexOf("--root");
384
+ const rootArg =
385
+ rootIdx !== -1 && args[rootIdx + 1]
386
+ ? path.resolve(args[rootIdx + 1])
387
+ : undefined;
352
388
 
353
389
  const t0 = performance.now();
354
390
  const result = detectSprints(rootArg);
355
391
  const elapsed = (performance.now() - t0).toFixed(1);
356
392
 
357
- if (args.includes('--json')) {
393
+ if (args.includes("--json")) {
358
394
  console.log(JSON.stringify(result, null, 2));
359
395
  process.exit(0);
360
396
  }
361
397
 
362
- if (args.includes('--active')) {
398
+ if (args.includes("--active")) {
363
399
  if (result.active) {
364
400
  console.log(result.active.path);
365
401
  } else {
366
- console.error('No active sprint detected.');
402
+ console.error("No active sprint detected.");
367
403
  process.exit(1);
368
404
  }
369
405
  process.exit(0);
@@ -371,26 +407,32 @@ Based on f001: config should not duplicate git-derivable state.`);
371
407
 
372
408
  // Human-readable output
373
409
  console.log(`Sprint Detection (${elapsed}ms)`);
374
- console.log('='.repeat(50));
410
+ console.log("=".repeat(50));
375
411
  console.log(`Found ${result.sprints.length} sprint(s)\n`);
376
412
 
377
413
  for (const sprint of result.sprints) {
378
- const icon = sprint.status === 'active' ? '>>>' : ' ';
414
+ const icon = sprint.status === "active" ? ">>>" : " ";
379
415
  const statusTag = sprint.status.toUpperCase().padEnd(8);
380
416
  console.log(`${icon} [${statusTag}] ${sprint.name}`);
381
417
  console.log(` Path: ${sprint.path}`);
382
418
  console.log(` Phase: ${sprint.phase}`);
383
- console.log(` Claims: ${sprint.claims_count} total, ${sprint.active_claims} active`);
384
- console.log(` Initiated: ${sprint.initiated || 'unknown'}`);
385
- console.log(` Last git: ${sprint.last_git_activity || 'untracked'}`);
419
+ console.log(
420
+ ` Claims: ${sprint.claims_count} total, ${sprint.active_claims} active`
421
+ );
422
+ console.log(` Initiated: ${sprint.initiated || "unknown"}`);
423
+ console.log(` Last git: ${sprint.last_git_activity || "untracked"}`);
386
424
  console.log(` Commits: ${sprint.git_commit_count}`);
387
- console.log(` Question: ${sprint.question.slice(0, 80)}${sprint.question.length > 80 ? '...' : ''}`);
425
+ console.log(
426
+ ` Question: ${sprint.question.slice(0, 80)}${
427
+ sprint.question.length > 80 ? "..." : ""
428
+ }`
429
+ );
388
430
  console.log();
389
431
  }
390
432
 
391
433
  if (result.active) {
392
434
  console.log(`Active sprint: ${result.active.path} (${result.active.name})`);
393
435
  } else {
394
- console.log('No active sprint detected.');
436
+ console.log("No active sprint detected.");
395
437
  }
396
438
  }