@c-d-cc/reap 0.3.2 → 0.3.5
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/dist/cli.js +1 -1
- package/dist/templates/commands/reap.status.md +1 -1
- package/dist/templates/hooks/genome-loader.cjs +214 -0
- package/dist/templates/hooks/opencode-session-start.js +45 -131
- package/dist/templates/hooks/session-start.cjs +21 -152
- package/package.json +4 -2
- package/scripts/postinstall.cjs +56 -0
package/dist/cli.js
CHANGED
|
@@ -10147,7 +10147,7 @@ async function fixProject(projectRoot) {
|
|
|
10147
10147
|
|
|
10148
10148
|
// src/cli/index.ts
|
|
10149
10149
|
import { join as join8 } from "path";
|
|
10150
|
-
program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.3.
|
|
10150
|
+
program.name("reap").description("REAP — Recursive Evolutionary Autonomous Pipeline").version("0.3.5");
|
|
10151
10151
|
program.command("init").description("Initialize a new REAP project (Genesis)").argument("[project-name]", "Project name (defaults to current directory name)").option("-m, --mode <mode>", "Entry mode: greenfield, migration, adoption", "greenfield").option("-p, --preset <preset>", "Bootstrap with a genome preset (e.g., bun-hono-react)").action(async (projectName, options) => {
|
|
10152
10152
|
try {
|
|
10153
10153
|
const cwd = process.cwd();
|
|
@@ -47,7 +47,7 @@ Display a comprehensive overview of the current REAP project state.
|
|
|
47
47
|
### 6. Genome Health
|
|
48
48
|
- Quick check of `.reap/genome/` files:
|
|
49
49
|
- Any files that are still placeholder-only?
|
|
50
|
-
- Any files exceeding 100 lines
|
|
50
|
+
- Any files exceeding their line limit? (default: ~100 lines. For source-map.md, read the file's own header to find its adaptive line limit.)
|
|
51
51
|
- Is `domain/` empty (no rule files)?
|
|
52
52
|
|
|
53
53
|
## Output Format
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// REAP Genome Loader — shared logic for session-start hooks
|
|
2
|
+
// Used by session-start.cjs (Claude Code) and opencode-session-start.js (OpenCode)
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const L1_LIMIT = 500;
|
|
8
|
+
const L2_LIMIT = 200;
|
|
9
|
+
const L1_FILES = ['principles.md', 'conventions.md', 'constraints.md', 'source-map.md'];
|
|
10
|
+
const STAGE_COMMANDS = {
|
|
11
|
+
objective: '/reap.objective',
|
|
12
|
+
planning: '/reap.planning',
|
|
13
|
+
implementation: '/reap.implementation',
|
|
14
|
+
validation: '/reap.validation',
|
|
15
|
+
completion: '/reap.completion',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function readFile(p) {
|
|
19
|
+
try { return fs.readFileSync(p, 'utf-8'); } catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fileExists(p) {
|
|
23
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function dirExists(p) {
|
|
27
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function exec(cmd, opts) {
|
|
31
|
+
try { return execSync(cmd, { encoding: 'utf-8', timeout: 10000, ...opts }).trim(); } catch { return ''; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load Genome content (L1 core files + L2 domain files) with line budget.
|
|
36
|
+
* @param {string} genomeDir - path to .reap/genome/
|
|
37
|
+
* @returns {{ content: string, l1Lines: number }}
|
|
38
|
+
*/
|
|
39
|
+
function loadGenome(genomeDir) {
|
|
40
|
+
let content = '';
|
|
41
|
+
let l1Lines = 0;
|
|
42
|
+
|
|
43
|
+
// Check source-map.md header for custom line limit
|
|
44
|
+
const smPath = path.join(genomeDir, 'source-map.md');
|
|
45
|
+
let smLimit = null;
|
|
46
|
+
const smContent = readFile(smPath);
|
|
47
|
+
if (smContent) {
|
|
48
|
+
const limitMatch = smContent.match(/줄 수 한도:\s*~?(\d+)줄/);
|
|
49
|
+
if (limitMatch) smLimit = parseInt(limitMatch[1], 10);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const file of L1_FILES) {
|
|
53
|
+
const fileContent = readFile(path.join(genomeDir, file));
|
|
54
|
+
if (!fileContent) continue;
|
|
55
|
+
const lines = fileContent.split('\n').length;
|
|
56
|
+
const limit = (file === 'source-map.md' && smLimit) ? smLimit : L1_LIMIT;
|
|
57
|
+
l1Lines += lines;
|
|
58
|
+
if (l1Lines <= limit) {
|
|
59
|
+
content += `\n### ${file}\n${fileContent}\n`;
|
|
60
|
+
} else {
|
|
61
|
+
content += `\n### ${file} [TRUNCATED — L1 budget exceeded, read full file directly]\n${fileContent.split('\n').slice(0, 20).join('\n')}\n...\n`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// L2: domain/ files
|
|
66
|
+
const domainDir = path.join(genomeDir, 'domain');
|
|
67
|
+
if (dirExists(domainDir)) {
|
|
68
|
+
let l2Lines = 0;
|
|
69
|
+
let l2Overflow = false;
|
|
70
|
+
const domainFiles = fs.readdirSync(domainDir).filter(f => f.endsWith('.md')).sort();
|
|
71
|
+
for (const file of domainFiles) {
|
|
72
|
+
const fileContent = readFile(path.join(domainDir, file));
|
|
73
|
+
if (!fileContent) continue;
|
|
74
|
+
const lines = fileContent.split('\n').length;
|
|
75
|
+
l2Lines += lines;
|
|
76
|
+
if (!l2Overflow && l2Lines <= L2_LIMIT) {
|
|
77
|
+
content += `\n### domain/${file}\n${fileContent}\n`;
|
|
78
|
+
} else {
|
|
79
|
+
l2Overflow = true;
|
|
80
|
+
const firstLine = fileContent.split('\n').find(l => l.startsWith('>')) || fileContent.split('\n')[0];
|
|
81
|
+
content += `\n### domain/${file} [summary — read full file for details]\n${firstLine}\n`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { content, l1Lines };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse config.yml for strict mode and other settings.
|
|
91
|
+
* @param {string} configFile - path to .reap/config.yml
|
|
92
|
+
* @returns {{ strictMode: boolean, language: string, configContent: string|null }}
|
|
93
|
+
*/
|
|
94
|
+
function parseConfig(configFile) {
|
|
95
|
+
const configContent = readFile(configFile);
|
|
96
|
+
let strictMode = false;
|
|
97
|
+
let language = '';
|
|
98
|
+
if (configContent) {
|
|
99
|
+
strictMode = /^strict:\s*true/m.test(configContent);
|
|
100
|
+
const langMatch = configContent.match(/^language:\s*(.+)$/m);
|
|
101
|
+
if (langMatch) language = langMatch[1].trim();
|
|
102
|
+
}
|
|
103
|
+
return { strictMode, language, configContent };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse current.yml for generation state.
|
|
108
|
+
* @param {string} currentYml - path to .reap/life/current.yml
|
|
109
|
+
* @returns {{ genStage: string, genId: string, genGoal: string, generationContext: string, nextCmd: string }}
|
|
110
|
+
*/
|
|
111
|
+
function parseCurrentYml(currentYml) {
|
|
112
|
+
let genStage = 'none', genId = '', genGoal = '', generationContext = '';
|
|
113
|
+
const content = readFile(currentYml);
|
|
114
|
+
if (content && content.trim()) {
|
|
115
|
+
genId = (content.match(/^id:\s*(.+)/m) || [])[1] || '';
|
|
116
|
+
genGoal = (content.match(/^goal:\s*(.+)/m) || [])[1] || '';
|
|
117
|
+
genStage = (content.match(/^stage:\s*(.+)/m) || [])[1] || 'none';
|
|
118
|
+
if (genId && genStage !== 'none') {
|
|
119
|
+
generationContext = `Active Generation: ${genId} | Goal: ${genGoal} | Stage: ${genStage}`;
|
|
120
|
+
} else {
|
|
121
|
+
genStage = 'none';
|
|
122
|
+
generationContext = 'No active Generation. Run `/reap.start` to start one.';
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
generationContext = 'No active Generation. Run `/reap.start` to start one.';
|
|
126
|
+
}
|
|
127
|
+
const nextCmd = STAGE_COMMANDS[genStage] || '/reap.start';
|
|
128
|
+
return { genStage, genId, genGoal, generationContext, nextCmd };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Detect Genome staleness.
|
|
133
|
+
* @param {string} projectRoot
|
|
134
|
+
* @returns {{ genomeStaleWarning: string, commitsSince: number }}
|
|
135
|
+
*/
|
|
136
|
+
function detectStaleness(projectRoot) {
|
|
137
|
+
let genomeStaleWarning = '';
|
|
138
|
+
let commitsSince = 0;
|
|
139
|
+
if (dirExists(path.join(projectRoot, '.git'))) {
|
|
140
|
+
const lastGenomeCommit = exec(`git -C "${projectRoot}" log -1 --format="%H" -- ".reap/genome/"`);
|
|
141
|
+
if (lastGenomeCommit) {
|
|
142
|
+
commitsSince = parseInt(exec(`git -C "${projectRoot}" rev-list --count "${lastGenomeCommit}..HEAD" -- src/ tests/ package.json tsconfig.json scripts/`) || '0', 10);
|
|
143
|
+
if (commitsSince > 10) {
|
|
144
|
+
genomeStaleWarning = `WARNING: Genome may be stale — ${commitsSince} commits since last Genome update. Consider running /reap.sync to synchronize.`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { genomeStaleWarning, commitsSince };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build strict mode section for context injection.
|
|
154
|
+
* @param {boolean} strictMode
|
|
155
|
+
* @param {string} genStage
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function buildStrictSection(strictMode, genStage) {
|
|
159
|
+
if (!strictMode) return '';
|
|
160
|
+
if (genStage === 'implementation') {
|
|
161
|
+
return "\n\n## Strict Mode (ACTIVE — SCOPED MODIFICATION ALLOWED)\n<HARD-GATE>\nStrict mode is enabled. Code modification is ALLOWED only within the scope of the current Generation's plan.\n- You MUST read `.reap/life/02-planning.md` before writing any code.\n- You may ONLY modify files and modules listed in the plan's task list.\n- Changes outside the plan's scope are BLOCKED. If you discover out-of-scope work is needed, add it to the backlog instead of implementing it.\n- If the user explicitly requests to bypass strict mode (e.g., \"override\", \"bypass strict\"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>";
|
|
162
|
+
}
|
|
163
|
+
if (genStage === 'none') {
|
|
164
|
+
return "\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled and there is NO active Generation.\nYou MUST NOT write, edit, or create any source code files.\nAllowed actions: reading files, analyzing code, answering questions, running commands.\nTo start coding, the user must first run `/reap.start` and advance to the implementation stage.\nIf the user explicitly requests to bypass strict mode (e.g., \"override\", \"bypass strict\", \"just do it\"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>";
|
|
165
|
+
}
|
|
166
|
+
return `\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled. Current stage is '${genStage}', which is NOT the implementation stage.\nYou MUST NOT write, edit, or create any source code files.\nAllowed actions: reading files, analyzing code, answering questions, running commands, writing REAP artifacts.\nAdvance to the implementation stage via the REAP lifecycle to unlock code modification.\nIf the user explicitly requests to bypass strict mode (e.g., \"override\", \"bypass strict\", \"just do it\"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Build Genome health status for session init display.
|
|
171
|
+
* @param {object} params
|
|
172
|
+
* @returns {{ initLines: string[], severity: string }}
|
|
173
|
+
*/
|
|
174
|
+
function buildGenomeHealth({ l1Lines, genomeDir, configFile, genomeStaleWarning, commitsSince }) {
|
|
175
|
+
const issues = [];
|
|
176
|
+
let severity = 'ok';
|
|
177
|
+
if (l1Lines === 0) { issues.push('empty'); severity = 'danger'; }
|
|
178
|
+
for (const f of [...L1_FILES, 'domain/']) {
|
|
179
|
+
const check = f.endsWith('/') ? dirExists(path.join(genomeDir, f.slice(0, -1))) : fileExists(path.join(genomeDir, f));
|
|
180
|
+
if (!check) { issues.push(`missing ${f}`); severity = 'danger'; }
|
|
181
|
+
}
|
|
182
|
+
if (!fileExists(configFile)) { issues.push('no config.yml'); severity = 'danger'; }
|
|
183
|
+
if (genomeStaleWarning && commitsSince > 30) {
|
|
184
|
+
issues.push(`severely stale (${commitsSince} commits)`);
|
|
185
|
+
if (severity !== 'danger') severity = 'danger';
|
|
186
|
+
} else if (genomeStaleWarning) {
|
|
187
|
+
issues.push(`stale (${commitsSince} commits)`);
|
|
188
|
+
if (severity === 'ok') severity = 'warn';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const initLines = [];
|
|
192
|
+
if (severity === 'ok') initLines.push(`🟢 Genome — loaded (${l1Lines} lines), synced`);
|
|
193
|
+
else if (severity === 'warn') initLines.push(`🟡 Genome — ${issues.join(', ')}. /reap.sync`);
|
|
194
|
+
else initLines.push(`🔴 Genome — ${issues.join(', ')}. \`reap fix\` or /reap.sync`);
|
|
195
|
+
|
|
196
|
+
return { initLines, severity };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
L1_LIMIT,
|
|
201
|
+
L2_LIMIT,
|
|
202
|
+
L1_FILES,
|
|
203
|
+
STAGE_COMMANDS,
|
|
204
|
+
readFile,
|
|
205
|
+
fileExists,
|
|
206
|
+
dirExists,
|
|
207
|
+
exec,
|
|
208
|
+
loadGenome,
|
|
209
|
+
parseConfig,
|
|
210
|
+
parseCurrentYml,
|
|
211
|
+
detectStaleness,
|
|
212
|
+
buildStrictSection,
|
|
213
|
+
buildGenomeHealth,
|
|
214
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// REAP SessionStart plugin for OpenCode
|
|
2
2
|
// Injects REAP guide + Genome + current generation context into every OpenCode session
|
|
3
3
|
// Installed to ~/.config/opencode/plugins/reap-session-start.js
|
|
4
|
-
|
|
5
4
|
const { execSync } = require("child_process");
|
|
6
5
|
const fs = require("fs");
|
|
7
6
|
const path = require("path");
|
|
@@ -15,22 +14,41 @@ module.exports = async (ctx) => {
|
|
|
15
14
|
// Check if this is a REAP project
|
|
16
15
|
if (!fs.existsSync(reapDir)) return;
|
|
17
16
|
|
|
17
|
+
// Load shared genome-loader (try multiple locations)
|
|
18
|
+
let gl;
|
|
19
|
+
const loaderLocations = [
|
|
20
|
+
path.join(__dirname, "genome-loader.cjs"),
|
|
21
|
+
path.join(__dirname, "..", "hooks", "genome-loader.cjs"),
|
|
22
|
+
];
|
|
23
|
+
for (const loc of loaderLocations) {
|
|
24
|
+
if (fs.existsSync(loc)) {
|
|
25
|
+
gl = require(loc);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!gl) {
|
|
30
|
+
try {
|
|
31
|
+
const pkgPath = require.resolve("@c-d-cc/reap/package.json");
|
|
32
|
+
const pkgLoader = path.join(pkgPath, "..", "dist", "templates", "hooks", "genome-loader.cjs");
|
|
33
|
+
if (fs.existsSync(pkgLoader)) gl = require(pkgLoader);
|
|
34
|
+
} catch { /* package not found */ }
|
|
35
|
+
}
|
|
36
|
+
if (!gl) return; // Cannot load shared module, skip
|
|
37
|
+
|
|
18
38
|
// Auto-update check (with PATH resolution for non-shell environments)
|
|
19
39
|
let autoUpdateMessage = "";
|
|
20
40
|
try {
|
|
21
|
-
const
|
|
22
|
-
if (fs.existsSync(
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
if (autoUpdateMatch && autoUpdateMatch[1].trim() === "true") {
|
|
41
|
+
const configPath = path.join(reapDir, "config.yml");
|
|
42
|
+
if (fs.existsSync(configPath)) {
|
|
43
|
+
const { configContent } = gl.parseConfig(configPath);
|
|
44
|
+
if (configContent && /^autoUpdate:\s*true/m.test(configContent)) {
|
|
26
45
|
try {
|
|
27
|
-
// Resolve PATH: OpenCode plugin runs in Node.js context which may lack shell PATH
|
|
28
46
|
const userShell = process.env.SHELL || "/bin/bash";
|
|
29
47
|
const shellPath = execSync(`${userShell} -l -c 'echo $PATH' 2>/dev/null`, { encoding: "utf8" }).trim();
|
|
30
48
|
const execOpts = { encoding: "utf8", env: { ...process.env, PATH: shellPath || process.env.PATH } };
|
|
31
49
|
|
|
32
|
-
const installed =
|
|
33
|
-
const latest =
|
|
50
|
+
const installed = gl.exec("reap --version 2>/dev/null", execOpts);
|
|
51
|
+
const latest = gl.exec("npm view @c-d-cc/reap version 2>/dev/null", execOpts);
|
|
34
52
|
if (installed && latest && installed !== latest) {
|
|
35
53
|
execSync("npm update -g @c-d-cc/reap >/dev/null 2>&1", { ...execOpts, stdio: "ignore" });
|
|
36
54
|
execSync("reap update >/dev/null 2>&1", { ...execOpts, stdio: "ignore" });
|
|
@@ -41,14 +59,12 @@ module.exports = async (ctx) => {
|
|
|
41
59
|
}
|
|
42
60
|
} catch { /* config read failed, skip */ }
|
|
43
61
|
|
|
62
|
+
// Load REAP guide
|
|
44
63
|
const scriptDir = __dirname;
|
|
45
|
-
// Look for reap-guide.md relative to the package hooks dir
|
|
46
|
-
// The guide is installed alongside this plugin's source package
|
|
47
64
|
const guideLocations = [
|
|
48
65
|
path.join(scriptDir, "..", "hooks", "reap-guide.md"),
|
|
49
66
|
path.join(scriptDir, "reap-guide.md"),
|
|
50
67
|
];
|
|
51
|
-
|
|
52
68
|
let reapGuide = "";
|
|
53
69
|
for (const loc of guideLocations) {
|
|
54
70
|
if (fs.existsSync(loc)) {
|
|
@@ -56,134 +72,33 @@ module.exports = async (ctx) => {
|
|
|
56
72
|
break;
|
|
57
73
|
}
|
|
58
74
|
}
|
|
59
|
-
|
|
60
|
-
// If guide not found from package, try to get it via the reap CLI
|
|
61
75
|
if (!reapGuide) {
|
|
62
76
|
try {
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
];
|
|
67
|
-
for (const p of possiblePaths) {
|
|
68
|
-
if (fs.existsSync(p)) {
|
|
69
|
-
reapGuide = fs.readFileSync(p, "utf8");
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
77
|
+
const pkgPath = require.resolve("@c-d-cc/reap/package.json");
|
|
78
|
+
const guidePath = path.join(pkgPath, "..", "dist", "templates", "hooks", "reap-guide.md");
|
|
79
|
+
if (fs.existsSync(guidePath)) reapGuide = fs.readFileSync(guidePath, "utf8");
|
|
73
80
|
} catch { /* package not found */ }
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
//
|
|
83
|
+
// Load Genome via shared module
|
|
77
84
|
const genomeDir = path.join(reapDir, "genome");
|
|
78
|
-
|
|
79
|
-
const L1_LIMIT = 500;
|
|
80
|
-
const L2_LIMIT = 200;
|
|
81
|
-
let l1Lines = 0;
|
|
85
|
+
const { content: genomeContent } = gl.loadGenome(genomeDir);
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const lines = content.split("\n").length;
|
|
89
|
-
l1Lines += lines;
|
|
90
|
-
if (l1Lines <= L1_LIMIT) {
|
|
91
|
-
genomeContent += `\n### ${file}\n${content}\n`;
|
|
92
|
-
} else {
|
|
93
|
-
genomeContent += `\n### ${file} [TRUNCATED]\n${content.split("\n").slice(0, 20).join("\n")}\n...\n`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
87
|
+
// Parse config and generation state via shared module
|
|
88
|
+
const configFile = path.join(reapDir, "config.yml");
|
|
89
|
+
const currentYml = path.join(reapDir, "life", "current.yml");
|
|
90
|
+
const { strictMode, language } = gl.parseConfig(configFile);
|
|
91
|
+
const { genStage, generationContext, nextCmd } = gl.parseCurrentYml(currentYml);
|
|
97
92
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (fs.existsSync(domainDir)) {
|
|
101
|
-
let l2Lines = 0;
|
|
102
|
-
let l2Overflow = false;
|
|
103
|
-
const domainFiles = fs.readdirSync(domainDir).filter(f => f.endsWith(".md"));
|
|
104
|
-
for (const file of domainFiles) {
|
|
105
|
-
const filePath = path.join(domainDir, file);
|
|
106
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
107
|
-
const lines = content.split("\n").length;
|
|
108
|
-
l2Lines += lines;
|
|
109
|
-
if (!l2Overflow && l2Lines <= L2_LIMIT) {
|
|
110
|
-
genomeContent += `\n### domain/${file}\n${content}\n`;
|
|
111
|
-
} else {
|
|
112
|
-
l2Overflow = true;
|
|
113
|
-
const firstLine = content.split("\n").find(l => l.startsWith(">")) || content.split("\n")[0];
|
|
114
|
-
genomeContent += `\n### domain/${file} [summary]\n${firstLine}\n`;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
93
|
+
// Build strict mode section via shared module
|
|
94
|
+
const strictSection = gl.buildStrictSection(strictMode, genStage);
|
|
119
95
|
|
|
120
|
-
//
|
|
121
|
-
let strictMode = false;
|
|
122
|
-
let language = "";
|
|
123
|
-
const configPath = path.join(reapDir, "config.yml");
|
|
124
|
-
if (fs.existsSync(configPath)) {
|
|
125
|
-
const configContent = fs.readFileSync(configPath, "utf8");
|
|
126
|
-
const strictMatch = configContent.match(/^strict:\s*(.+)$/m);
|
|
127
|
-
if (strictMatch && strictMatch[1].trim() === "true") {
|
|
128
|
-
strictMode = true;
|
|
129
|
-
}
|
|
130
|
-
const langMatch = configContent.match(/^language:\s*(.+)$/m);
|
|
131
|
-
if (langMatch) {
|
|
132
|
-
language = langMatch[1].trim();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Read current.yml
|
|
137
|
-
const currentPath = path.join(reapDir, "life", "current.yml");
|
|
138
|
-
let genStage = "none";
|
|
139
|
-
let generationContext = "No active Generation. Run `/reap.start` to start one.";
|
|
140
|
-
|
|
141
|
-
if (fs.existsSync(currentPath)) {
|
|
142
|
-
const content = fs.readFileSync(currentPath, "utf8").trim();
|
|
143
|
-
if (content) {
|
|
144
|
-
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
145
|
-
const goalMatch = content.match(/^goal:\s*(.+)$/m);
|
|
146
|
-
const stageMatch = content.match(/^stage:\s*(.+)$/m);
|
|
147
|
-
if (idMatch && goalMatch && stageMatch) {
|
|
148
|
-
genStage = stageMatch[1].trim();
|
|
149
|
-
generationContext = `Active Generation: ${idMatch[1].trim()} | Goal: ${goalMatch[1].trim()} | Stage: ${genStage}`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Map stage to command
|
|
155
|
-
const stageCommandMap = {
|
|
156
|
-
objective: "/reap.objective",
|
|
157
|
-
planning: "/reap.planning",
|
|
158
|
-
implementation: "/reap.implementation",
|
|
159
|
-
validation: "/reap.validation",
|
|
160
|
-
completion: "/reap.completion",
|
|
161
|
-
};
|
|
162
|
-
const nextCmd = stageCommandMap[genStage] || "/reap.start";
|
|
163
|
-
|
|
164
|
-
// Build strict mode section
|
|
165
|
-
let strictSection = "";
|
|
166
|
-
if (strictMode) {
|
|
167
|
-
if (genStage === "implementation") {
|
|
168
|
-
strictSection = "\n\n## Strict Mode (ACTIVE — SCOPED MODIFICATION ALLOWED)\n<HARD-GATE>\nStrict mode is enabled. Code modification is ALLOWED only within the scope of the current Generation's plan.\n- You MUST read `.reap/life/02-planning.md` before writing any code.\n- You may ONLY modify files and modules listed in the plan's task list.\n- Changes outside the plan's scope are BLOCKED.\n</HARD-GATE>";
|
|
169
|
-
} else if (genStage === "none") {
|
|
170
|
-
strictSection = "\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled and there is NO active Generation.\nYou MUST NOT write, edit, or create any source code files.\nAllowed actions: reading files, analyzing code, answering questions, running commands.\nTo start coding, the user must first run `/reap.start` and advance to the implementation stage.\n</HARD-GATE>";
|
|
171
|
-
} else {
|
|
172
|
-
strictSection = `\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled. Current stage is '${genStage}', which is NOT the implementation stage.\nYou MUST NOT write, edit, or create any source code files.\n</HARD-GATE>`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Detect genome staleness
|
|
96
|
+
// Detect staleness via shared module
|
|
177
97
|
let staleSection = "";
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (commitsSince > 10) {
|
|
183
|
-
staleSection = `\n\n## Genome Staleness\nWARNING: Genome may be stale — ${commitsSince} commits since last Genome update. Consider running /reap.sync.`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
} catch { /* git not available or not a repo */ }
|
|
98
|
+
const staleness = gl.detectStaleness(projectRoot);
|
|
99
|
+
if (staleness.genomeStaleWarning) {
|
|
100
|
+
staleSection = `\n\n## Genome Staleness\n${staleness.genomeStaleWarning}\nIf the user wants to proceed without syncing, ask: "The Genome may be stale. Would you like to run /reap.sync now, or do it later?" and respect their choice.`;
|
|
101
|
+
}
|
|
187
102
|
|
|
188
103
|
// Build language instruction
|
|
189
104
|
let langSection = "";
|
|
@@ -199,7 +114,6 @@ module.exports = async (ctx) => {
|
|
|
199
114
|
|
|
200
115
|
const context = `<REAP_WORKFLOW>\n${reapGuide}\n\n---\n\n## Genome (Project Knowledge)\n${genomeContent}\n\n---\n\n## Current State\n${generationContext}${staleSection}${strictSection}${updateSection}${langSection}\n\n## Rules\n1. ALL development work MUST follow the REAP lifecycle.\n2. Before writing any code, check if a Generation is active and what stage it is in.\n3. If a Generation is active, use \`${nextCmd}\` to proceed with the current stage.\n4. If no Generation is active, use \`/reap.start\` to start a new one.\n5. Do NOT implement features outside of the REAP lifecycle unless explicitly asked.\n6. Genome is the authoritative knowledge source.\n</REAP_WORKFLOW>`;
|
|
201
116
|
|
|
202
|
-
// Inject context into session via return value
|
|
203
117
|
return { systemPrompt: context };
|
|
204
118
|
},
|
|
205
119
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// REAP SessionStart hook — injects REAP guide + Genome + current generation context
|
|
3
3
|
// Single Node.js process (replaces session-start.sh for better performance)
|
|
4
|
-
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
|
-
const
|
|
5
|
+
const gl = require('./genome-loader.cjs');
|
|
7
6
|
|
|
8
7
|
const startTime = Date.now();
|
|
9
8
|
let step = 0;
|
|
@@ -14,22 +13,6 @@ function log(msg) {
|
|
|
14
13
|
process.stderr.write(`[REAP ${step}/${totalSteps} +${Date.now() - startTime}ms] ${msg}\n`);
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
function readFile(p) {
|
|
18
|
-
try { return fs.readFileSync(p, 'utf-8'); } catch { return null; }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function fileExists(p) {
|
|
22
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function dirExists(p) {
|
|
26
|
-
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function exec(cmd) {
|
|
30
|
-
try { return execSync(cmd, { encoding: 'utf-8', timeout: 10000 }).trim(); } catch { return ''; }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
16
|
// Paths
|
|
34
17
|
const scriptDir = __dirname;
|
|
35
18
|
const projectRoot = process.cwd();
|
|
@@ -40,7 +23,7 @@ const currentYml = path.join(reapDir, 'life', 'current.yml');
|
|
|
40
23
|
const guideFile = path.join(scriptDir, 'reap-guide.md');
|
|
41
24
|
|
|
42
25
|
// Check REAP project
|
|
43
|
-
if (!dirExists(reapDir)) {
|
|
26
|
+
if (!gl.dirExists(reapDir)) {
|
|
44
27
|
process.stderr.write('[REAP] Not a REAP project, skipping\n');
|
|
45
28
|
process.exit(0);
|
|
46
29
|
}
|
|
@@ -49,15 +32,15 @@ if (!dirExists(reapDir)) {
|
|
|
49
32
|
log('Checking for updates...');
|
|
50
33
|
let autoUpdateMessage = '';
|
|
51
34
|
let updateAvailableMessage = '';
|
|
52
|
-
const configContent =
|
|
53
|
-
const installed = exec('reap --version');
|
|
54
|
-
const latest = exec('npm view @c-d-cc/reap version');
|
|
35
|
+
const { configContent } = gl.parseConfig(configFile);
|
|
36
|
+
const installed = gl.exec('reap --version');
|
|
37
|
+
const latest = gl.exec('npm view @c-d-cc/reap version');
|
|
55
38
|
if (installed && latest && installed !== latest) {
|
|
56
39
|
const autoUpdate = configContent ? /^autoUpdate:\s*true/m.test(configContent) : false;
|
|
57
40
|
if (autoUpdate) {
|
|
58
|
-
const updated = exec('npm update -g @c-d-cc/reap');
|
|
41
|
+
const updated = gl.exec('npm update -g @c-d-cc/reap');
|
|
59
42
|
if (updated !== null) {
|
|
60
|
-
exec('reap update');
|
|
43
|
+
gl.exec('reap update');
|
|
61
44
|
autoUpdateMessage = `REAP auto-updated: v${installed} → v${latest}`;
|
|
62
45
|
}
|
|
63
46
|
} else {
|
|
@@ -67,121 +50,29 @@ if (installed && latest && installed !== latest) {
|
|
|
67
50
|
|
|
68
51
|
// Step 2: Load REAP guide
|
|
69
52
|
log('Loading REAP guide...');
|
|
70
|
-
const reapGuide = readFile(guideFile) || '';
|
|
53
|
+
const reapGuide = gl.readFile(guideFile) || '';
|
|
71
54
|
|
|
72
55
|
// Step 3: Load Genome (tiered)
|
|
73
56
|
log('Loading Genome...');
|
|
74
|
-
const
|
|
75
|
-
const L2_LIMIT = 200;
|
|
76
|
-
let genomeContent = '';
|
|
77
|
-
let l1Lines = 0;
|
|
57
|
+
const { content: genomeContent, l1Lines } = gl.loadGenome(genomeDir);
|
|
78
58
|
|
|
79
|
-
|
|
80
|
-
for (const file of l1Files) {
|
|
81
|
-
const content = readFile(path.join(genomeDir, file));
|
|
82
|
-
if (!content) continue;
|
|
83
|
-
const lines = content.split('\n').length;
|
|
84
|
-
l1Lines += lines;
|
|
85
|
-
if (l1Lines <= L1_LIMIT) {
|
|
86
|
-
genomeContent += `\n### ${file}\n${content}\n`;
|
|
87
|
-
} else {
|
|
88
|
-
genomeContent += `\n### ${file} [TRUNCATED — L1 budget exceeded, read full file directly]\n${content.split('\n').slice(0, 20).join('\n')}\n...\n`;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// L2: domain/ files
|
|
93
|
-
const domainDir = path.join(genomeDir, 'domain');
|
|
94
|
-
if (dirExists(domainDir)) {
|
|
95
|
-
let l2Lines = 0;
|
|
96
|
-
let l2Overflow = false;
|
|
97
|
-
const domainFiles = fs.readdirSync(domainDir).filter(f => f.endsWith('.md')).sort();
|
|
98
|
-
for (const file of domainFiles) {
|
|
99
|
-
const content = readFile(path.join(domainDir, file));
|
|
100
|
-
if (!content) continue;
|
|
101
|
-
const lines = content.split('\n').length;
|
|
102
|
-
l2Lines += lines;
|
|
103
|
-
if (!l2Overflow && l2Lines <= L2_LIMIT) {
|
|
104
|
-
genomeContent += `\n### domain/${file}\n${content}\n`;
|
|
105
|
-
} else {
|
|
106
|
-
l2Overflow = true;
|
|
107
|
-
const firstLine = content.split('\n').find(l => l.startsWith('>')) || content.split('\n')[0];
|
|
108
|
-
genomeContent += `\n### domain/${file} [summary — read full file for details]\n${firstLine}\n`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Step 4: Check Genome & source-map sync
|
|
59
|
+
// Step 4: Check Genome staleness
|
|
114
60
|
log('Checking sync...');
|
|
115
|
-
|
|
116
|
-
let commitsSince = 0;
|
|
117
|
-
if (dirExists(path.join(projectRoot, '.git'))) {
|
|
118
|
-
const lastGenomeCommit = exec(`git -C "${projectRoot}" log -1 --format="%H" -- ".reap/genome/"`);
|
|
119
|
-
if (lastGenomeCommit) {
|
|
120
|
-
commitsSince = parseInt(exec(`git -C "${projectRoot}" rev-list --count "${lastGenomeCommit}..HEAD" -- src/ tests/ package.json tsconfig.json scripts/`) || '0', 10);
|
|
121
|
-
if (commitsSince > 10) {
|
|
122
|
-
genomeStaleWarning = `WARNING: Genome may be stale — ${commitsSince} commits since last Genome update. Consider running /reap.sync to synchronize.`;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let sourcemapDriftWarning = '';
|
|
128
|
-
let documented = 0, actual = 0;
|
|
129
|
-
const sourcemapFile = path.join(genomeDir, 'source-map.md');
|
|
130
|
-
const srcCoreDir = path.join(projectRoot, 'src', 'core');
|
|
131
|
-
if (fileExists(sourcemapFile) && dirExists(srcCoreDir)) {
|
|
132
|
-
const smContent = readFile(sourcemapFile) || '';
|
|
133
|
-
documented = (smContent.match(/Component\(/g) || []).length;
|
|
134
|
-
const coreEntries = fs.readdirSync(srcCoreDir);
|
|
135
|
-
actual = coreEntries.filter(e => e.endsWith('.ts')).length
|
|
136
|
-
+ coreEntries.filter(e => { try { return fs.statSync(path.join(srcCoreDir, e)).isDirectory(); } catch { return false; } }).length;
|
|
137
|
-
if (documented > 0 && actual > 0 && documented !== actual) {
|
|
138
|
-
sourcemapDriftWarning = `WARNING: source-map.md drift — ${documented} components documented, ${actual} core files found. Consider running /reap.sync.`;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
61
|
+
const { genomeStaleWarning, commitsSince } = gl.detectStaleness(projectRoot);
|
|
141
62
|
|
|
142
63
|
// Step 5: Read generation state
|
|
143
64
|
log('Reading generation state...');
|
|
144
|
-
const strictMode =
|
|
145
|
-
|
|
146
|
-
let genStage = 'none', genId = '', genGoal = '', generationContext = '';
|
|
147
|
-
const currentContent = readFile(currentYml);
|
|
148
|
-
if (currentContent && currentContent.trim()) {
|
|
149
|
-
genId = (currentContent.match(/^id:\s*(.+)/m) || [])[1] || '';
|
|
150
|
-
genGoal = (currentContent.match(/^goal:\s*(.+)/m) || [])[1] || '';
|
|
151
|
-
genStage = (currentContent.match(/^stage:\s*(.+)/m) || [])[1] || 'none';
|
|
152
|
-
if (genId && genStage !== 'none') {
|
|
153
|
-
generationContext = `Active Generation: ${genId} | Goal: ${genGoal} | Stage: ${genStage}`;
|
|
154
|
-
} else {
|
|
155
|
-
genStage = 'none';
|
|
156
|
-
generationContext = 'No active Generation. Run `/reap.start` to start one.';
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
generationContext = 'No active Generation. Run `/reap.start` to start one.';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const stageCommands = { objective: '/reap.objective', planning: '/reap.planning', implementation: '/reap.implementation', validation: '/reap.validation', completion: '/reap.completion' };
|
|
163
|
-
const nextCmd = stageCommands[genStage] || '/reap.start';
|
|
65
|
+
const { strictMode } = gl.parseConfig(configFile);
|
|
66
|
+
const { genStage, genId, generationContext, nextCmd } = gl.parseCurrentYml(currentYml);
|
|
164
67
|
|
|
165
68
|
// Build strict mode section
|
|
166
|
-
|
|
167
|
-
if (strictMode) {
|
|
168
|
-
if (genStage === 'implementation') {
|
|
169
|
-
strictSection = '\n\n## Strict Mode (ACTIVE — SCOPED MODIFICATION ALLOWED)\n<HARD-GATE>\nStrict mode is enabled. Code modification is ALLOWED only within the scope of the current Generation\'s plan.\n- You MUST read `.reap/life/02-planning.md` before writing any code.\n- You may ONLY modify files and modules listed in the plan\'s task list.\n- Changes outside the plan\'s scope are BLOCKED. If you discover out-of-scope work is needed, add it to the backlog instead of implementing it.\n- If the user explicitly requests to bypass strict mode (e.g., "override", "bypass strict"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>';
|
|
170
|
-
} else if (genStage === 'none') {
|
|
171
|
-
strictSection = '\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled and there is NO active Generation.\nYou MUST NOT write, edit, or create any source code files.\nAllowed actions: reading files, analyzing code, answering questions, running commands.\nTo start coding, the user must first run `/reap.start` and advance to the implementation stage.\nIf the user explicitly requests to bypass strict mode (e.g., "override", "bypass strict", "just do it"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>';
|
|
172
|
-
} else {
|
|
173
|
-
strictSection = `\n\n## Strict Mode (ACTIVE — CODE MODIFICATION BLOCKED)\n<HARD-GATE>\nStrict mode is enabled. Current stage is '${genStage}', which is NOT the implementation stage.\nYou MUST NOT write, edit, or create any source code files.\nAllowed actions: reading files, analyzing code, answering questions, running commands, writing REAP artifacts.\nAdvance to the implementation stage via the REAP lifecycle to unlock code modification.\nIf the user explicitly requests to bypass strict mode (e.g., "override", "bypass strict", "just do it"), you may proceed — but inform them that strict mode is being bypassed.\n</HARD-GATE>`;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
69
|
+
const strictSection = gl.buildStrictSection(strictMode, genStage);
|
|
176
70
|
|
|
177
71
|
// Build staleness section
|
|
178
72
|
let staleSection = '';
|
|
179
73
|
if (genomeStaleWarning) {
|
|
180
74
|
staleSection = `\n\n## Genome Staleness\n${genomeStaleWarning}\nIf the user wants to proceed without syncing, ask: "The Genome may be stale. Would you like to run /reap.sync now, or do it later?" and respect their choice.`;
|
|
181
75
|
}
|
|
182
|
-
if (sourcemapDriftWarning) {
|
|
183
|
-
staleSection += `\n${sourcemapDriftWarning}`;
|
|
184
|
-
}
|
|
185
76
|
|
|
186
77
|
// Build auto-update section
|
|
187
78
|
let updateSection = '';
|
|
@@ -189,38 +80,16 @@ if (autoUpdateMessage) {
|
|
|
189
80
|
updateSection = `\n\n## Auto-Update\n${autoUpdateMessage}. Tell the user: "${autoUpdateMessage}"`;
|
|
190
81
|
}
|
|
191
82
|
|
|
192
|
-
//
|
|
83
|
+
// Build session init display
|
|
193
84
|
const initLines = [];
|
|
194
85
|
if (autoUpdateMessage) initLines.push(`🟢 ${autoUpdateMessage}`);
|
|
195
86
|
|
|
196
|
-
// Genome
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
if (l1Lines === 0) { issues.push('empty'); severity = 'danger'; }
|
|
200
|
-
for (const f of [...l1Files, 'domain/']) {
|
|
201
|
-
const check = f.endsWith('/') ? dirExists(path.join(genomeDir, f.slice(0, -1))) : fileExists(path.join(genomeDir, f));
|
|
202
|
-
if (!check) { issues.push(`missing ${f}`); severity = 'danger'; }
|
|
203
|
-
}
|
|
204
|
-
if (!fileExists(configFile)) { issues.push('no config.yml'); severity = 'danger'; }
|
|
205
|
-
if (sourcemapDriftWarning) {
|
|
206
|
-
const diff = Math.abs(documented - actual);
|
|
207
|
-
issues.push(`source-map drift (${documented}→${actual})`);
|
|
208
|
-
if (diff > 3) { if (severity === 'ok') severity = 'danger'; }
|
|
209
|
-
else { if (severity === 'ok') severity = 'warn'; }
|
|
210
|
-
}
|
|
211
|
-
if (genomeStaleWarning && commitsSince > 30) {
|
|
212
|
-
issues.push(`severely stale (${commitsSince} commits)`);
|
|
213
|
-
if (severity !== 'danger') severity = 'danger';
|
|
214
|
-
} else if (genomeStaleWarning) {
|
|
215
|
-
issues.push(`stale (${commitsSince} commits)`);
|
|
216
|
-
if (severity === 'ok') severity = 'warn';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (severity === 'ok') initLines.push(`🟢 Genome — loaded (${l1Lines} lines), synced`);
|
|
220
|
-
else if (severity === 'warn') initLines.push(`🟡 Genome — ${issues.join(', ')}. /reap.sync`);
|
|
221
|
-
else initLines.push(`🔴 Genome — ${issues.join(', ')}. \`reap fix\` or /reap.sync`);
|
|
87
|
+
// Genome health
|
|
88
|
+
const health = gl.buildGenomeHealth({ l1Lines, genomeDir, configFile, genomeStaleWarning, commitsSince });
|
|
89
|
+
initLines.push(...health.initLines);
|
|
222
90
|
|
|
223
91
|
// Generation status
|
|
92
|
+
const currentContent = gl.readFile(currentYml);
|
|
224
93
|
if (currentContent && currentContent.trim()) {
|
|
225
94
|
if (!genId || genStage === 'none') {
|
|
226
95
|
initLines.push('🔴 Generation — current.yml corrupted. `reap fix`');
|
|
@@ -235,8 +104,8 @@ const initSummary = initLines.join('\n');
|
|
|
235
104
|
|
|
236
105
|
// Load session-init format template and render
|
|
237
106
|
const initFormatFile = path.join(scriptDir, 'session-init-format.md');
|
|
238
|
-
const initFormat = readFile(initFormatFile) || '{{SESSION_INIT_LINES}}';
|
|
239
|
-
const currentVersion = installed || exec('reap --version') || '?';
|
|
107
|
+
const initFormat = gl.readFile(initFormatFile) || '{{SESSION_INIT_LINES}}';
|
|
108
|
+
const currentVersion = installed || gl.exec('reap --version') || '?';
|
|
240
109
|
const updateBadge = updateAvailableMessage ? ` — ${updateAvailableMessage}` : '';
|
|
241
110
|
const sessionInitDisplay = initFormat
|
|
242
111
|
.replace('{{VERSION}}', currentVersion)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-d-cc/reap",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Recursive Evolutionary Autonomous Pipeline — AI and humans evolve software across generations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"reap": "dist/cli.js"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
|
-
"dist/"
|
|
26
|
+
"dist/",
|
|
27
|
+
"scripts/postinstall.cjs"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=18"
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"scripts": {
|
|
32
33
|
"dev": "bun run src/cli/index.ts",
|
|
33
34
|
"build": "node scripts/build.js",
|
|
35
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
34
36
|
"prepublishOnly": "npm run build",
|
|
35
37
|
"test": "bun test"
|
|
36
38
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall — install REAP slash commands to detected AI agents.
|
|
4
|
+
* Runs after `npm install -g @c-d-cc/reap`.
|
|
5
|
+
* Graceful: never fails npm install (always exits 0).
|
|
6
|
+
*/
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
const { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } = require("fs");
|
|
9
|
+
const { join, dirname } = require("path");
|
|
10
|
+
const { homedir } = require("os");
|
|
11
|
+
|
|
12
|
+
const AGENTS = [
|
|
13
|
+
{ name: "Claude Code", bin: "claude", commandsDir: join(homedir(), ".claude", "commands") },
|
|
14
|
+
{ name: "OpenCode", bin: "opencode", commandsDir: join(homedir(), ".config", "opencode", "commands") },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function isInstalled(bin) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`which ${bin}`, { stdio: "ignore" });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Resolve commands source: dist/templates/commands/ relative to this script
|
|
28
|
+
const commandsSource = join(dirname(__dirname), "dist", "templates", "commands");
|
|
29
|
+
if (!existsSync(commandsSource)) {
|
|
30
|
+
// During development or if dist not built yet, skip silently
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const commandFiles = readdirSync(commandsSource).filter(f => f.endsWith(".md"));
|
|
35
|
+
if (commandFiles.length === 0) process.exit(0);
|
|
36
|
+
|
|
37
|
+
let installed = 0;
|
|
38
|
+
for (const agent of AGENTS) {
|
|
39
|
+
if (!isInstalled(agent.bin)) continue;
|
|
40
|
+
|
|
41
|
+
mkdirSync(agent.commandsDir, { recursive: true });
|
|
42
|
+
for (const file of commandFiles) {
|
|
43
|
+
const src = readFileSync(join(commandsSource, file), "utf-8");
|
|
44
|
+
writeFileSync(join(agent.commandsDir, file), src);
|
|
45
|
+
}
|
|
46
|
+
installed++;
|
|
47
|
+
console.log(` reap: ${agent.name} — ${commandFiles.length} slash commands installed`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (installed === 0) {
|
|
51
|
+
console.log(" reap: no supported AI agents detected (claude, opencode). Run 'reap update' after installing one.");
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Graceful failure — never break npm install
|
|
55
|
+
console.warn(" reap: postinstall warning —", err.message);
|
|
56
|
+
}
|