@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 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.2");
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 autoConfigPath = path.join(reapDir, "config.yml");
22
- if (fs.existsSync(autoConfigPath)) {
23
- const configRaw = fs.readFileSync(autoConfigPath, "utf8");
24
- const autoUpdateMatch = configRaw.match(/^autoUpdate:\s*(.+)$/m);
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 = execSync("reap --version 2>/dev/null", execOpts).trim();
33
- const latest = execSync("npm view @c-d-cc/reap version 2>/dev/null", execOpts).trim();
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
- // Find the package hooks dir by looking up from the plugin location
64
- const possiblePaths = [
65
- path.join(require.resolve("@c-d-cc/reap/package.json"), "..", "dist", "templates", "hooks", "reap-guide.md"),
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
- // Read Genome files
83
+ // Load Genome via shared module
77
84
  const genomeDir = path.join(reapDir, "genome");
78
- let genomeContent = "";
79
- const L1_LIMIT = 500;
80
- const L2_LIMIT = 200;
81
- let l1Lines = 0;
85
+ const { content: genomeContent } = gl.loadGenome(genomeDir);
82
86
 
83
- if (fs.existsSync(genomeDir)) {
84
- for (const file of ["principles.md", "conventions.md", "constraints.md"]) {
85
- const filePath = path.join(genomeDir, file);
86
- if (fs.existsSync(filePath)) {
87
- const content = fs.readFileSync(filePath, "utf8");
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
- // L2: domain/ files
99
- const domainDir = path.join(genomeDir, "domain");
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
- // Read config (strict mode + language)
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
- try {
179
- const lastCommit = execSync(`git -C "${projectRoot}" log -1 --format="%H" -- ".reap/genome/" 2>/dev/null`, { encoding: "utf8" }).trim();
180
- if (lastCommit) {
181
- const commitsSince = parseInt(execSync(`git -C "${projectRoot}" rev-list --count "${lastCommit}..HEAD" 2>/dev/null`, { encoding: "utf8" }).trim(), 10);
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 { execSync } = require('child_process');
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 = readFile(configFile);
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 L1_LIMIT = 500;
75
- const L2_LIMIT = 200;
76
- let genomeContent = '';
77
- let l1Lines = 0;
57
+ const { content: genomeContent, l1Lines } = gl.loadGenome(genomeDir);
78
58
 
79
- const l1Files = ['principles.md', 'conventions.md', 'constraints.md', 'source-map.md'];
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
- let genomeStaleWarning = '';
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 = configContent ? /^strict:\s*true/m.test(configContent) : false;
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
- let strictSection = '';
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
- // Write session init log
83
+ // Build session init display
193
84
  const initLines = [];
194
85
  if (autoUpdateMessage) initLines.push(`🟢 ${autoUpdateMessage}`);
195
86
 
196
- // Genome status (single line)
197
- const issues = [];
198
- let severity = 'ok';
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.2",
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
+ }