@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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/bin/wheat.js +193 -0
- package/compiler/detect-sprints.js +319 -0
- package/compiler/generate-manifest.js +280 -0
- package/compiler/wheat-compiler.js +1229 -0
- package/lib/compiler.js +35 -0
- package/lib/connect.js +418 -0
- package/lib/disconnect.js +188 -0
- package/lib/guard.js +151 -0
- package/lib/index.js +14 -0
- package/lib/init.js +457 -0
- package/lib/install-prompt.js +186 -0
- package/lib/quickstart.js +276 -0
- package/lib/serve-mcp.js +509 -0
- package/lib/server.js +391 -0
- package/lib/stats.js +184 -0
- package/lib/status.js +135 -0
- package/lib/update.js +71 -0
- package/package.json +53 -0
- package/public/index.html +1798 -0
- package/templates/claude.md +122 -0
- package/templates/commands/blind-spot.md +47 -0
- package/templates/commands/brief.md +73 -0
- package/templates/commands/calibrate.md +39 -0
- package/templates/commands/challenge.md +72 -0
- package/templates/commands/connect.md +104 -0
- package/templates/commands/evaluate.md +80 -0
- package/templates/commands/feedback.md +60 -0
- package/templates/commands/handoff.md +53 -0
- package/templates/commands/init.md +68 -0
- package/templates/commands/merge.md +51 -0
- package/templates/commands/present.md +52 -0
- package/templates/commands/prototype.md +68 -0
- package/templates/commands/replay.md +61 -0
- package/templates/commands/research.md +73 -0
- package/templates/commands/resolve.md +42 -0
- package/templates/commands/status.md +56 -0
- package/templates/commands/witness.md +79 -0
- package/templates/explainer.html +343 -0
package/lib/guard.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wheat guard — PreToolUse hook for Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Blocks writes to output/ unless compilation is fresh and ready.
|
|
5
|
+
* Blocks malformed claims.json writes.
|
|
6
|
+
*
|
|
7
|
+
* Resolves all paths relative to the TARGET repo (via --dir or cwd),
|
|
8
|
+
* not the package directory.
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = allow
|
|
12
|
+
* 2 = block (with reason on stderr)
|
|
13
|
+
*
|
|
14
|
+
* Zero npm dependencies.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function loadConfig(dir) {
|
|
23
|
+
const configPath = path.join(dir, 'wheat.config.json');
|
|
24
|
+
const defaults = {
|
|
25
|
+
dirs: { output: 'output' },
|
|
26
|
+
compiler: { claims: 'claims.json', compilation: 'compilation.json' },
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
30
|
+
const config = JSON.parse(raw);
|
|
31
|
+
return {
|
|
32
|
+
dirs: { ...defaults.dirs, ...(config.dirs || {}) },
|
|
33
|
+
compiler: { ...defaults.compiler, ...(config.compiler || {}) },
|
|
34
|
+
};
|
|
35
|
+
} catch {
|
|
36
|
+
return defaults;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Guard logic ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
export function guard(dir, toolInput) {
|
|
43
|
+
const config = loadConfig(dir);
|
|
44
|
+
|
|
45
|
+
let input;
|
|
46
|
+
try {
|
|
47
|
+
input = JSON.parse(toolInput);
|
|
48
|
+
} catch {
|
|
49
|
+
// Not JSON — allow
|
|
50
|
+
return { allow: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const filePath = input.file_path || '';
|
|
54
|
+
const rel = path.relative(dir, filePath).split(path.sep).join('/');
|
|
55
|
+
|
|
56
|
+
// Guard 1: Writes to output/ require fresh compilation
|
|
57
|
+
if (rel.startsWith(config.dirs.output + '/') && !rel.endsWith('.gitkeep')) {
|
|
58
|
+
const compilationPath = path.join(dir, config.compiler.compilation);
|
|
59
|
+
const claimsPath = path.join(dir, config.compiler.claims);
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(compilationPath)) {
|
|
62
|
+
return {
|
|
63
|
+
allow: false,
|
|
64
|
+
reason: `BLOCKED: No ${config.compiler.compilation} found. Run "wheat compile" before generating output artifacts.\n` +
|
|
65
|
+
'The Wheat pipeline requires: claims.json -> compiler -> compilation.json -> artifact',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(claimsPath)) {
|
|
70
|
+
return {
|
|
71
|
+
allow: false,
|
|
72
|
+
reason: `BLOCKED: No ${config.compiler.claims} found. Run "wheat init" to bootstrap the sprint first.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const compilationMtime = fs.statSync(compilationPath).mtimeMs;
|
|
77
|
+
const claimsMtime = fs.statSync(claimsPath).mtimeMs;
|
|
78
|
+
|
|
79
|
+
if (claimsMtime > compilationMtime) {
|
|
80
|
+
return {
|
|
81
|
+
allow: false,
|
|
82
|
+
reason: `BLOCKED: ${config.compiler.compilation} is stale. Run "wheat compile" to recompile before generating output artifacts.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const compilation = JSON.parse(fs.readFileSync(compilationPath, 'utf8'));
|
|
88
|
+
if (compilation.status === 'blocked') {
|
|
89
|
+
const errors = (compilation.errors || []).map(e => ` - ${e.message}`).join('\n');
|
|
90
|
+
return {
|
|
91
|
+
allow: false,
|
|
92
|
+
reason: `BLOCKED: Compilation status is "blocked" — unresolved issues:\n${errors}\nFix these issues and recompile before generating output artifacts.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
allow: false,
|
|
98
|
+
reason: `BLOCKED: ${config.compiler.compilation} is corrupted. Run "wheat compile" to regenerate.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Guard 2: claims.json writes must maintain meta fields
|
|
104
|
+
if (rel === config.compiler.claims && input.content) {
|
|
105
|
+
try {
|
|
106
|
+
const newClaims = JSON.parse(input.content);
|
|
107
|
+
if (!newClaims.meta || !newClaims.meta.question) {
|
|
108
|
+
return {
|
|
109
|
+
allow: false,
|
|
110
|
+
reason: `BLOCKED: ${config.compiler.claims} must have meta.question set. Run "wheat init" first.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!newClaims.claims || !Array.isArray(newClaims.claims)) {
|
|
114
|
+
return {
|
|
115
|
+
allow: false,
|
|
116
|
+
reason: `BLOCKED: ${config.compiler.claims} must have a "claims" array.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Not valid JSON content — might be an Edit (partial), allow
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { allow: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── CLI handler ─────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
export async function run(dir, args) {
|
|
130
|
+
// Read tool input from stdin or first arg
|
|
131
|
+
let toolInput;
|
|
132
|
+
if (args[0] && !args[0].startsWith('--')) {
|
|
133
|
+
toolInput = args[0];
|
|
134
|
+
} else {
|
|
135
|
+
try {
|
|
136
|
+
// /dev/stdin is Unix-only; use fd 0 which Node resolves cross-platform
|
|
137
|
+
toolInput = fs.readFileSync(0, 'utf8');
|
|
138
|
+
} catch {
|
|
139
|
+
toolInput = '{}';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = guard(dir, toolInput);
|
|
144
|
+
|
|
145
|
+
if (!result.allow) {
|
|
146
|
+
process.stderr.write(result.reason);
|
|
147
|
+
process.exit(2);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grainulation/wheat — public API surface
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the main library modules for programmatic use.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { run as compile } from './compiler.js';
|
|
8
|
+
export { guard } from './guard.js';
|
|
9
|
+
export { run as serve } from './server.js';
|
|
10
|
+
export { run as connect } from './connect.js';
|
|
11
|
+
export { run as init } from './init.js';
|
|
12
|
+
export { run as status } from './status.js';
|
|
13
|
+
export { run as stats } from './stats.js';
|
|
14
|
+
export { run as update } from './update.js';
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wheat init — Bootstrap a research sprint in the target repo
|
|
3
|
+
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* 1. Interactive (default) — conversational readline session
|
|
6
|
+
* 2. Quick (--question "...") — skip conversation, seed from flags
|
|
7
|
+
* 3. Headless (--headless) — non-interactive, requires all flags
|
|
8
|
+
*
|
|
9
|
+
* Creates in the TARGET repo:
|
|
10
|
+
* - claims.json (seeded with constraint claims)
|
|
11
|
+
* - CLAUDE.md (sprint configuration for Claude Code)
|
|
12
|
+
* - .claude/commands/*.md (slash commands)
|
|
13
|
+
* - wheat.config.json (local config pointing back to package)
|
|
14
|
+
*
|
|
15
|
+
* Zero npm dependencies.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import readline from 'readline';
|
|
21
|
+
import { execFileSync } from 'child_process';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = path.dirname(__filename);
|
|
26
|
+
|
|
27
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Resolve a path relative to the target directory */
|
|
30
|
+
function target(dir, ...segments) {
|
|
31
|
+
return path.join(dir, ...segments);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Get the package root (where templates live) */
|
|
35
|
+
function packageRoot() {
|
|
36
|
+
return path.resolve(__dirname, '..');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ask a question and return the answer */
|
|
40
|
+
function ask(rl, question) {
|
|
41
|
+
return new Promise(resolve => {
|
|
42
|
+
rl.question(question, answer => resolve(answer.trim()));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Generate an ISO timestamp */
|
|
47
|
+
function now() {
|
|
48
|
+
return new Date().toISOString();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Parse --flag value pairs from args */
|
|
52
|
+
function parseFlags(args) {
|
|
53
|
+
const flags = {};
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
if (args[i].startsWith('--') && i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
56
|
+
flags[args[i].slice(2)] = args[i + 1];
|
|
57
|
+
i++;
|
|
58
|
+
} else if (args[i].startsWith('--')) {
|
|
59
|
+
flags[args[i].slice(2)] = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return flags;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Conversation ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
async function conversationalInit(dir) {
|
|
68
|
+
const rl = readline.createInterface({
|
|
69
|
+
input: process.stdin,
|
|
70
|
+
output: process.stdout,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(' \x1b[1m\x1b[33mwheat\x1b[0m — let\'s set up a research sprint');
|
|
75
|
+
console.log(' ─────────────────────────────────────────');
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(' Before you commit to anything big, let\'s figure out');
|
|
78
|
+
console.log(' what you actually need to know. Four questions.\n');
|
|
79
|
+
|
|
80
|
+
// Question
|
|
81
|
+
const question = await ask(rl,
|
|
82
|
+
' What question are you trying to answer?\n' +
|
|
83
|
+
' (The more specific, the better. "Should we migrate to X?" beats "what database?")\n\n' +
|
|
84
|
+
' > '
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!question) {
|
|
88
|
+
console.log('\n No question, no sprint. Come back when you have one.\n');
|
|
89
|
+
rl.close();
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log();
|
|
94
|
+
|
|
95
|
+
// Audience
|
|
96
|
+
const audienceRaw = await ask(rl,
|
|
97
|
+
' Who needs the answer?\n' +
|
|
98
|
+
' Could be your team, a VP, a client, or just yourself.\n\n' +
|
|
99
|
+
' > '
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
console.log();
|
|
103
|
+
|
|
104
|
+
// Constraints
|
|
105
|
+
const constraintsRaw = await ask(rl,
|
|
106
|
+
' Any constraints?\n' +
|
|
107
|
+
' Budget, timeline, tech stack, team size — whatever narrows the space.\n' +
|
|
108
|
+
' (Leave blank if none.)\n\n' +
|
|
109
|
+
' > '
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
console.log();
|
|
113
|
+
|
|
114
|
+
// Done criteria
|
|
115
|
+
const doneCriteria = await ask(rl,
|
|
116
|
+
' How will you know you\'re done?\n' +
|
|
117
|
+
' A recommendation? A prototype? A go/no-go? A deck for the meeting?\n\n' +
|
|
118
|
+
' > '
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
rl.close();
|
|
122
|
+
|
|
123
|
+
// Parse audience into array
|
|
124
|
+
const audience = audienceRaw
|
|
125
|
+
? audienceRaw.split(/[,;]/).map(s => s.trim()).filter(Boolean)
|
|
126
|
+
: ['self'];
|
|
127
|
+
|
|
128
|
+
// Parse constraints into individual items
|
|
129
|
+
const constraints = constraintsRaw
|
|
130
|
+
? constraintsRaw.split(/[.;]/).map(s => s.trim()).filter(s => s.length > 5)
|
|
131
|
+
: [];
|
|
132
|
+
|
|
133
|
+
return { question, audience, constraints, doneCriteria };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── File generation ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function buildClaims(meta, constraints) {
|
|
139
|
+
const claims = [];
|
|
140
|
+
const timestamp = now();
|
|
141
|
+
|
|
142
|
+
constraints.forEach((constraint, i) => {
|
|
143
|
+
claims.push({
|
|
144
|
+
id: `d${String(i + 1).padStart(3, '0')}`,
|
|
145
|
+
type: 'constraint',
|
|
146
|
+
topic: 'sprint-scope',
|
|
147
|
+
content: constraint,
|
|
148
|
+
source: { origin: 'stakeholder', artifact: null, connector: null },
|
|
149
|
+
evidence: 'stated',
|
|
150
|
+
status: 'active',
|
|
151
|
+
phase_added: 'define',
|
|
152
|
+
timestamp,
|
|
153
|
+
conflicts_with: [],
|
|
154
|
+
resolved_by: null,
|
|
155
|
+
tags: [],
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Add done-criteria as a constraint if provided
|
|
160
|
+
if (meta.doneCriteria) {
|
|
161
|
+
claims.push({
|
|
162
|
+
id: `d${String(constraints.length + 1).padStart(3, '0')}`,
|
|
163
|
+
type: 'constraint',
|
|
164
|
+
topic: 'done-criteria',
|
|
165
|
+
content: `Done looks like: ${meta.doneCriteria}`,
|
|
166
|
+
source: { origin: 'stakeholder', artifact: null, connector: null },
|
|
167
|
+
evidence: 'stated',
|
|
168
|
+
status: 'active',
|
|
169
|
+
phase_added: 'define',
|
|
170
|
+
timestamp,
|
|
171
|
+
conflicts_with: [],
|
|
172
|
+
resolved_by: null,
|
|
173
|
+
tags: ['done-criteria'],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
schema_version: '1.0',
|
|
179
|
+
meta: {
|
|
180
|
+
question: meta.question,
|
|
181
|
+
initiated: new Date().toISOString().split('T')[0],
|
|
182
|
+
audience: meta.audience,
|
|
183
|
+
phase: 'define',
|
|
184
|
+
connectors: [],
|
|
185
|
+
},
|
|
186
|
+
claims,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildClaudeMd(meta) {
|
|
191
|
+
const templatePath = path.join(packageRoot(), 'templates', 'claude.md');
|
|
192
|
+
let template;
|
|
193
|
+
try {
|
|
194
|
+
template = fs.readFileSync(templatePath, 'utf8');
|
|
195
|
+
} catch {
|
|
196
|
+
// Fallback if template is missing (shouldn't happen in installed package)
|
|
197
|
+
console.error(' Warning: CLAUDE.md template not found, using minimal template');
|
|
198
|
+
template = '# Wheat — Research Sprint\n\n## Sprint\n\n**Question:** {{QUESTION}}\n\n**Audience:** {{AUDIENCE}}\n\n**Constraints:**\n{{CONSTRAINTS}}\n\n**Done looks like:** {{DONE_CRITERIA}}\n';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const constraintList = meta.constraints.length > 0
|
|
202
|
+
? meta.constraints.map(c => `- ${c}`).join('\n')
|
|
203
|
+
: '- (none specified)';
|
|
204
|
+
|
|
205
|
+
return template
|
|
206
|
+
.replace(/\{\{QUESTION\}\}/g, meta.question)
|
|
207
|
+
.replace(/\{\{AUDIENCE\}\}/g, meta.audience.join(', '))
|
|
208
|
+
.replace(/\{\{CONSTRAINTS\}\}/g, constraintList)
|
|
209
|
+
.replace(/\{\{DONE_CRITERIA\}\}/g, meta.doneCriteria || 'A recommendation with evidence');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function copyCommands(dir) {
|
|
213
|
+
const srcDir = path.join(packageRoot(), 'templates', 'commands');
|
|
214
|
+
const destDir = target(dir, '.claude', 'commands');
|
|
215
|
+
|
|
216
|
+
// Create .claude/commands/ if it doesn't exist
|
|
217
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
218
|
+
|
|
219
|
+
let copied = 0;
|
|
220
|
+
try {
|
|
221
|
+
const files = fs.readdirSync(srcDir);
|
|
222
|
+
for (const file of files) {
|
|
223
|
+
if (!file.endsWith('.md')) continue;
|
|
224
|
+
const src = path.join(srcDir, file);
|
|
225
|
+
const dest = path.join(destDir, file);
|
|
226
|
+
|
|
227
|
+
// Don't overwrite existing commands (user may have customized)
|
|
228
|
+
if (fs.existsSync(dest)) {
|
|
229
|
+
console.log(` Skipped .claude/commands/${file} (already exists)`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fs.copyFileSync(src, dest);
|
|
234
|
+
copied++;
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.error(` Warning: could not copy commands: ${err.message}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return copied;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Git Hook Installation ─────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
function installGitHook(dir) {
|
|
246
|
+
// Find git root (might be different from target dir in monorepos)
|
|
247
|
+
let gitRoot;
|
|
248
|
+
try {
|
|
249
|
+
gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
250
|
+
cwd: dir, timeout: 5000, stdio: ['ignore', 'pipe', 'pipe'],
|
|
251
|
+
}).toString().trim();
|
|
252
|
+
} catch {
|
|
253
|
+
console.log(' \x1b[33m!\x1b[0m Not a git repo — skipping pre-commit hook');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const hooksDir = path.join(gitRoot, '.git', 'hooks');
|
|
258
|
+
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
259
|
+
|
|
260
|
+
// The hook snippet — runs wheat compile --check before allowing commits
|
|
261
|
+
// Uses Node for the check logic so it works on both Unix (sh) and Windows (Git Bash)
|
|
262
|
+
const WHEAT_MARKER = '# wheat-guard';
|
|
263
|
+
const escapedDir = dir.replace(/\\/g, '/'); // Normalize Windows backslashes for shell
|
|
264
|
+
const hookSnippet = `
|
|
265
|
+
${WHEAT_MARKER}
|
|
266
|
+
# Wheat pre-commit: verify claims compile before committing
|
|
267
|
+
if git diff --cached --name-only | grep -q 'claims.json'; then
|
|
268
|
+
npx --yes @grainulation/wheat compile --check --dir "${escapedDir}" 2>/dev/null
|
|
269
|
+
if [ $? -ne 0 ]; then
|
|
270
|
+
echo "wheat: claims.json has compilation errors. Run 'wheat compile --summary' to see details."
|
|
271
|
+
exit 1
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
`;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
if (fs.existsSync(hookPath)) {
|
|
278
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
279
|
+
if (existing.includes(WHEAT_MARKER)) {
|
|
280
|
+
console.log(' \x1b[34m-\x1b[0m pre-commit hook (already installed)');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Append to existing hook
|
|
284
|
+
fs.appendFileSync(hookPath, hookSnippet);
|
|
285
|
+
} else {
|
|
286
|
+
// Create new hook
|
|
287
|
+
fs.writeFileSync(hookPath, '#!/bin/sh\n' + hookSnippet);
|
|
288
|
+
fs.chmodSync(hookPath, 0o755);
|
|
289
|
+
}
|
|
290
|
+
console.log(' \x1b[32m+\x1b[0m .git/hooks/pre-commit (wheat guard)');
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.log(` \x1b[33m!\x1b[0m Could not install git hook: ${err.message}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
export async function run(dir, args) {
|
|
299
|
+
const flags = parseFlags(args);
|
|
300
|
+
|
|
301
|
+
// Check if sprint already exists
|
|
302
|
+
const claimsPath = target(dir, 'claims.json');
|
|
303
|
+
if (fs.existsSync(claimsPath) && !flags.force) {
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(' A sprint already exists in this directory (claims.json found).');
|
|
306
|
+
console.log(' Use --force to reinitialize, or run "wheat compile" to continue.');
|
|
307
|
+
console.log();
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let meta;
|
|
312
|
+
|
|
313
|
+
if (flags.headless) {
|
|
314
|
+
// ── Headless mode — all flags required ──
|
|
315
|
+
const missing = [];
|
|
316
|
+
if (!flags.question) missing.push('--question');
|
|
317
|
+
if (!flags.audience) missing.push('--audience');
|
|
318
|
+
if (!flags.constraints) missing.push('--constraints');
|
|
319
|
+
if (!flags.done) missing.push('--done');
|
|
320
|
+
if (missing.length > 0) {
|
|
321
|
+
console.error();
|
|
322
|
+
console.error(` --headless requires all flags: --question, --audience, --constraints, --done`);
|
|
323
|
+
console.error(` Missing: ${missing.join(', ')}`);
|
|
324
|
+
console.error();
|
|
325
|
+
console.error(' Example:');
|
|
326
|
+
console.error(' wheat init --headless \\');
|
|
327
|
+
console.error(' --question "Should we migrate to Postgres?" \\');
|
|
328
|
+
console.error(' --audience "Backend team" \\');
|
|
329
|
+
console.error(' --constraints "Must support zero-downtime; Budget under 10k" \\');
|
|
330
|
+
console.error(' --done "Migration plan with risk assessment and rollback strategy"');
|
|
331
|
+
console.error();
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
meta = {
|
|
335
|
+
question: flags.question,
|
|
336
|
+
audience: flags.audience.split(',').map(s => s.trim()),
|
|
337
|
+
constraints: flags.constraints.split(';').map(s => s.trim()).filter(Boolean),
|
|
338
|
+
doneCriteria: flags.done,
|
|
339
|
+
};
|
|
340
|
+
console.log();
|
|
341
|
+
console.log(' \x1b[1m\x1b[33mwheat\x1b[0m — headless sprint init');
|
|
342
|
+
console.log(' ─────────────────────────────────────────');
|
|
343
|
+
console.log(` Question: ${meta.question.slice(0, 70)}${meta.question.length > 70 ? '...' : ''}`);
|
|
344
|
+
console.log(` Audience: ${meta.audience.join(', ')}`);
|
|
345
|
+
console.log(` Constraints: ${meta.constraints.length}`);
|
|
346
|
+
console.log(` Done: ${meta.doneCriteria.slice(0, 70)}${meta.doneCriteria.length > 70 ? '...' : ''}`);
|
|
347
|
+
} else if (flags.question) {
|
|
348
|
+
// ── Quick mode — question pre-filled, prompt for the rest if missing ──
|
|
349
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
350
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
351
|
+
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(' \x1b[1m\x1b[33mwheat\x1b[0m — quick sprint init');
|
|
354
|
+
console.log(' ─────────────────────────────────────────');
|
|
355
|
+
console.log(` Question: ${flags.question}`);
|
|
356
|
+
console.log();
|
|
357
|
+
|
|
358
|
+
const audience = flags.audience
|
|
359
|
+
? flags.audience.split(',').map(s => s.trim())
|
|
360
|
+
: (await ask(' Who is this for? (comma-separated, default: self)\n > ')).split(',').map(s => s.trim()).filter(Boolean) || ['self'];
|
|
361
|
+
|
|
362
|
+
const constraints = flags.constraints
|
|
363
|
+
? flags.constraints.split(';').map(s => s.trim()).filter(Boolean)
|
|
364
|
+
: (await ask(' Any constraints? (semicolon-separated, or press Enter to skip)\n > ')).split(';').map(s => s.trim()).filter(Boolean);
|
|
365
|
+
|
|
366
|
+
const doneCriteria = flags.done
|
|
367
|
+
|| await ask(' What does "done" look like?\n > ');
|
|
368
|
+
|
|
369
|
+
rl.close();
|
|
370
|
+
|
|
371
|
+
meta = {
|
|
372
|
+
question: flags.question,
|
|
373
|
+
audience: audience.length ? audience : ['self'],
|
|
374
|
+
constraints,
|
|
375
|
+
doneCriteria,
|
|
376
|
+
};
|
|
377
|
+
} else {
|
|
378
|
+
// ── Interactive mode ──
|
|
379
|
+
meta = await conversationalInit(dir);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Build claims.json
|
|
383
|
+
const claims = buildClaims(meta, meta.constraints);
|
|
384
|
+
|
|
385
|
+
// Build CLAUDE.md
|
|
386
|
+
const claudeMd = buildClaudeMd(meta);
|
|
387
|
+
|
|
388
|
+
// Write files
|
|
389
|
+
console.log();
|
|
390
|
+
console.log(' \x1b[1mCreating sprint files...\x1b[0m');
|
|
391
|
+
console.log();
|
|
392
|
+
|
|
393
|
+
// claims.json (atomic write-then-rename)
|
|
394
|
+
const tmpClaims = claimsPath + '.tmp.' + process.pid;
|
|
395
|
+
fs.writeFileSync(tmpClaims, JSON.stringify(claims, null, 2) + '\n');
|
|
396
|
+
fs.renameSync(tmpClaims, claimsPath);
|
|
397
|
+
console.log(' \x1b[32m+\x1b[0m claims.json');
|
|
398
|
+
|
|
399
|
+
// CLAUDE.md
|
|
400
|
+
const claudePath = target(dir, 'CLAUDE.md');
|
|
401
|
+
fs.writeFileSync(claudePath, claudeMd);
|
|
402
|
+
console.log(' \x1b[32m+\x1b[0m CLAUDE.md');
|
|
403
|
+
|
|
404
|
+
// .claude/commands/
|
|
405
|
+
const copied = copyCommands(dir);
|
|
406
|
+
console.log(` \x1b[32m+\x1b[0m .claude/commands/ (${copied} commands installed)`);
|
|
407
|
+
|
|
408
|
+
// Create output directories
|
|
409
|
+
const dirs = ['output', 'research', 'prototypes', 'evidence'];
|
|
410
|
+
for (const d of dirs) {
|
|
411
|
+
const dirPath = target(dir, d);
|
|
412
|
+
if (!fs.existsSync(dirPath)) {
|
|
413
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
414
|
+
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
|
415
|
+
console.log(` \x1b[32m+\x1b[0m ${d}/`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Install git pre-commit hook (like husky — stakeholder insight)
|
|
420
|
+
installGitHook(dir);
|
|
421
|
+
|
|
422
|
+
// Summary
|
|
423
|
+
if (flags.json) {
|
|
424
|
+
console.log(JSON.stringify({
|
|
425
|
+
question: meta.question,
|
|
426
|
+
audience: meta.audience,
|
|
427
|
+
constraints: meta.constraints.length,
|
|
428
|
+
done_criteria: meta.doneCriteria || null,
|
|
429
|
+
claims_seeded: claims.claims.length,
|
|
430
|
+
files_created: ['claims.json', 'CLAUDE.md', '.claude/commands/'],
|
|
431
|
+
dir,
|
|
432
|
+
}));
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(' ─────────────────────────────────────────');
|
|
438
|
+
console.log(` \x1b[1m\x1b[33mSprint ready.\x1b[0m`);
|
|
439
|
+
console.log();
|
|
440
|
+
console.log(` Question: ${meta.question}`);
|
|
441
|
+
console.log(` Audience: ${meta.audience.join(', ')}`);
|
|
442
|
+
console.log(` Claims: ${claims.claims.length} constraint(s) seeded`);
|
|
443
|
+
console.log();
|
|
444
|
+
console.log(' Created:');
|
|
445
|
+
console.log(' claims.json Your evidence database');
|
|
446
|
+
console.log(' CLAUDE.md AI assistant configuration');
|
|
447
|
+
console.log(' .claude/commands/ 17 slash commands for Claude Code');
|
|
448
|
+
console.log(' output/ Where compiled artifacts land');
|
|
449
|
+
console.log();
|
|
450
|
+
console.log(' Next steps:');
|
|
451
|
+
console.log(' 1. Open Claude Code in this directory');
|
|
452
|
+
console.log(' 2. Run /research <topic> to start investigating');
|
|
453
|
+
console.log(' 3. The compiler validates as you go -- run wheat status to check health');
|
|
454
|
+
console.log();
|
|
455
|
+
console.log(' Trust the process. The evidence will compound.');
|
|
456
|
+
console.log();
|
|
457
|
+
}
|