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