@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.
- package/LICENSE +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +47 -36
- package/compiler/detect-sprints.js +108 -66
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +763 -471
- 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
|
}
|
|
@@ -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
|
|
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
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(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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 {
|
|
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() {
|
|
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(
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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,
|
|
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 = [
|
|
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,
|
|
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 {
|
|
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 ||
|
|
226
|
-
const isArchived = phase ===
|
|
227
|
-
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/");
|
|
228
247
|
|
|
229
248
|
// Compute status
|
|
230
249
|
let status;
|
|
231
250
|
if (isArchived) {
|
|
232
|
-
status =
|
|
251
|
+
status = "archived";
|
|
233
252
|
} else if (isExample) {
|
|
234
|
-
status =
|
|
253
|
+
status = "example";
|
|
235
254
|
} else {
|
|
236
|
-
status =
|
|
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 ===
|
|
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 ===
|
|
276
|
-
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");
|
|
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
|
|
282
|
-
|
|
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 =
|
|
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 !==
|
|
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
|
|
307
|
-
|
|
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 =
|
|
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 ===
|
|
318
|
-
if (b.status ===
|
|
319
|
-
const dateA = a.last_git_activity
|
|
320
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
351
|
-
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;
|
|
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(
|
|
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(
|
|
398
|
+
if (args.includes("--active")) {
|
|
363
399
|
if (result.active) {
|
|
364
400
|
console.log(result.active.path);
|
|
365
401
|
} else {
|
|
366
|
-
console.error(
|
|
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(
|
|
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 ===
|
|
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(
|
|
384
|
-
|
|
385
|
-
|
|
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(
|
|
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(
|
|
436
|
+
console.log("No active sprint detected.");
|
|
395
437
|
}
|
|
396
438
|
}
|