@c-d-cc/reap 0.4.0 → 0.6.1

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.
Files changed (36) hide show
  1. package/README.ja.md +96 -10
  2. package/README.ko.md +96 -10
  3. package/README.md +96 -10
  4. package/README.zh-CN.md +96 -10
  5. package/dist/cli.js +212 -112
  6. package/dist/templates/artifacts/01-objective.md +17 -0
  7. package/dist/templates/artifacts/merge/01-detect.md +18 -0
  8. package/dist/templates/artifacts/merge/02-mate.md +12 -0
  9. package/dist/templates/artifacts/merge/03-merge.md +15 -0
  10. package/dist/templates/artifacts/merge/04-sync.md +18 -0
  11. package/dist/templates/artifacts/merge/05-validation.md +11 -0
  12. package/dist/templates/artifacts/merge/06-completion.md +13 -0
  13. package/dist/templates/brainstorm/frame.html +125 -0
  14. package/dist/templates/brainstorm/server.cjs +306 -0
  15. package/dist/templates/brainstorm/spec-reviewer-prompt.md +52 -0
  16. package/dist/templates/brainstorm/start-server.sh +67 -0
  17. package/dist/templates/brainstorm/visual-companion-guide.md +120 -0
  18. package/dist/templates/commands/reap.evolve.md +7 -0
  19. package/dist/templates/commands/reap.merge.completion.md +20 -0
  20. package/dist/templates/commands/reap.merge.detect.md +20 -0
  21. package/dist/templates/commands/reap.merge.evolve.md +28 -0
  22. package/dist/templates/commands/reap.merge.mate.md +27 -0
  23. package/dist/templates/commands/reap.merge.md +47 -0
  24. package/dist/templates/commands/reap.merge.merge.md +22 -0
  25. package/dist/templates/commands/reap.merge.start.md +21 -0
  26. package/dist/templates/commands/reap.merge.sync.md +32 -0
  27. package/dist/templates/commands/reap.merge.validation.md +25 -0
  28. package/dist/templates/commands/reap.next.md +14 -1
  29. package/dist/templates/commands/reap.objective.md +105 -9
  30. package/dist/templates/commands/reap.pull.md +51 -0
  31. package/dist/templates/commands/reap.push.md +18 -0
  32. package/dist/templates/hooks/genome-loader.cjs +34 -12
  33. package/dist/templates/hooks/opencode-session-start.js +2 -2
  34. package/dist/templates/hooks/reap-guide.md +1 -1
  35. package/dist/templates/hooks/session-start.cjs +2 -2
  36. package/package.json +1 -1
@@ -0,0 +1,13 @@
1
+ # Completion
2
+
3
+ ## Summary
4
+
5
+
6
+ ## Genome Changes Applied
7
+
8
+
9
+ ## Lessons Learned
10
+
11
+
12
+ ## Backlog (Genome Modifications Discovered)
13
+ None
@@ -0,0 +1,125 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>REAP Brainstorm</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f1117;
10
+ --surface: #1a1d27;
11
+ --border: #2a2d3a;
12
+ --text: #e1e4ed;
13
+ --text-muted: #8b8fa3;
14
+ --accent: #6c8cff;
15
+ --accent-soft: rgba(108,140,255,0.12);
16
+ --success: #4ade80;
17
+ --warning: #fbbf24;
18
+ --radius: 8px;
19
+ }
20
+
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ line-height: 1.6;
28
+ padding: 2rem;
29
+ max-width: 960px;
30
+ margin: 0 auto;
31
+ }
32
+
33
+ h2 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: var(--text); }
34
+ h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; color: var(--text-muted); }
35
+ .subtitle { color: var(--text-muted); font-size: 0.95rem; }
36
+ .section { margin-bottom: 2rem; }
37
+ .label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.5rem; }
38
+
39
+ /* Options: single/multi select */
40
+ .options { display: flex; flex-direction: column; gap: 0.5rem; }
41
+ .option {
42
+ padding: 1rem 1.25rem;
43
+ border: 1px solid var(--border);
44
+ border-radius: var(--radius);
45
+ cursor: pointer;
46
+ transition: all 0.15s;
47
+ background: var(--surface);
48
+ }
49
+ .option:hover { border-color: var(--accent); background: var(--accent-soft); }
50
+ .option.selected { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent); }
51
+
52
+ /* Cards: visual design cards */
53
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; }
54
+ .card {
55
+ padding: 1.25rem;
56
+ border: 1px solid var(--border);
57
+ border-radius: var(--radius);
58
+ cursor: pointer;
59
+ transition: all 0.15s;
60
+ background: var(--surface);
61
+ }
62
+ .card:hover { border-color: var(--accent); transform: translateY(-2px); }
63
+ .card.selected { border-color: var(--accent); background: var(--accent-soft); }
64
+ .card h3 { margin-bottom: 0.5rem; }
65
+
66
+ /* Mockup containers */
67
+ .mockup {
68
+ border: 1px solid var(--border);
69
+ border-radius: var(--radius);
70
+ overflow: hidden;
71
+ background: var(--surface);
72
+ }
73
+ .mockup-header {
74
+ padding: 0.5rem 1rem;
75
+ background: var(--border);
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.5rem;
79
+ }
80
+ .mockup-header::before {
81
+ content: '';
82
+ display: inline-flex;
83
+ gap: 4px;
84
+ width: 48px;
85
+ height: 12px;
86
+ background: radial-gradient(circle at 6px 6px, #ff5f56 5px, transparent 5px),
87
+ radial-gradient(circle at 22px 6px, #ffbd2e 5px, transparent 5px),
88
+ radial-gradient(circle at 38px 6px, #27c93f 5px, transparent 5px);
89
+ }
90
+ .mockup-body { padding: 1.25rem; }
91
+
92
+ /* Split comparison */
93
+ .split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
94
+ @media (max-width: 600px) { .split { grid-template-columns: 1fr; } }
95
+
96
+ /* Pros and cons */
97
+ .pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
98
+ .pros-cons .pros { color: var(--success); }
99
+ .pros-cons .cons { color: var(--warning); }
100
+ .pros-cons ul { list-style: none; padding: 0; }
101
+ .pros-cons li { padding: 0.25rem 0; }
102
+ .pros-cons .pros li::before { content: '+ '; font-weight: bold; }
103
+ .pros-cons .cons li::before { content: '- '; font-weight: bold; }
104
+
105
+ /* Mock UI elements */
106
+ .mock-nav { height: 48px; background: var(--border); border-radius: var(--radius) var(--radius) 0 0; display: flex; align-items: center; padding: 0 1rem; }
107
+ .mock-sidebar { background: var(--surface); border-right: 1px solid var(--border); padding: 1rem; min-height: 200px; }
108
+ .mock-content { padding: 1rem; flex: 1; }
109
+ .mock-button { display: inline-block; padding: 0.5rem 1rem; background: var(--accent); border-radius: var(--radius); color: white; font-size: 0.85rem; cursor: pointer; }
110
+ .mock-input { display: block; width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 0.9rem; }
111
+ .placeholder { background: var(--border); border-radius: 4px; height: 1em; margin: 0.25rem 0; }
112
+
113
+ /* Table for trade-offs */
114
+ table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
115
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid var(--border); }
116
+ th { color: var(--text-muted); font-weight: 500; font-size: 0.85rem; }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ {{CONTENT}}
121
+ <script>
122
+ {{WS_SCRIPT}}
123
+ </script>
124
+ </body>
125
+ </html>
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ // REAP Visual Companion Server
3
+ // Zero-dependency HTTP + WebSocket server using Node.js built-in modules only.
4
+ // Serves HTML fragments from a screen directory, auto-wraps in frame template,
5
+ // watches for file changes and pushes updates via WebSocket.
6
+
7
+ const http = require('http');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+ const url = require('url');
12
+
13
+ // --- Configuration ---
14
+ const PORT = parseInt(process.env.BRAINSTORM_PORT || '3210', 10);
15
+ const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
16
+ const URL_HOST = process.env.BRAINSTORM_URL_HOST || `http://${HOST}:${PORT}`;
17
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
18
+
19
+ // Screen directory: where HTML fragments are written by the AI agent
20
+ const SCREEN_DIR = process.env.BRAINSTORM_DIR || path.join(process.cwd(), '.reap', 'brainstorm');
21
+ const SERVER_INFO_FILE = path.join(SCREEN_DIR, '.server-info');
22
+ const SERVER_STOPPED_FILE = path.join(SCREEN_DIR, '.server-stopped');
23
+ const EVENTS_FILE = path.join(SCREEN_DIR, '.events');
24
+
25
+ // Frame template path (same directory as this script)
26
+ const FRAME_TEMPLATE_PATH = path.join(__dirname, 'frame.html');
27
+
28
+ // --- State ---
29
+ let idleTimer = null;
30
+ const wsClients = new Set();
31
+
32
+ // --- Helpers ---
33
+
34
+ function ensureDir(dir) {
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ }
39
+
40
+ function resetIdleTimer() {
41
+ if (idleTimer) clearTimeout(idleTimer);
42
+ idleTimer = setTimeout(() => {
43
+ console.log('[brainstorm] Idle timeout reached. Shutting down.');
44
+ shutdown();
45
+ }, IDLE_TIMEOUT_MS);
46
+ }
47
+
48
+ function shutdown() {
49
+ try {
50
+ fs.writeFileSync(SERVER_STOPPED_FILE, new Date().toISOString());
51
+ if (fs.existsSync(SERVER_INFO_FILE)) fs.unlinkSync(SERVER_INFO_FILE);
52
+ } catch (_) { /* best effort */ }
53
+ process.exit(0);
54
+ }
55
+
56
+ function getNewestHtmlFile() {
57
+ ensureDir(SCREEN_DIR);
58
+ const files = fs.readdirSync(SCREEN_DIR)
59
+ .filter(f => f.endsWith('.html') && !f.startsWith('.'))
60
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtimeMs }))
61
+ .sort((a, b) => b.mtime - a.mtime);
62
+ return files.length > 0 ? files[0].name : null;
63
+ }
64
+
65
+ function loadFrame() {
66
+ if (fs.existsSync(FRAME_TEMPLATE_PATH)) {
67
+ return fs.readFileSync(FRAME_TEMPLATE_PATH, 'utf-8');
68
+ }
69
+ return '<!DOCTYPE html><html><head><meta charset="utf-8"><title>REAP Brainstorm</title></head><body>{{CONTENT}}<script>{{WS_SCRIPT}}</script></body></html>';
70
+ }
71
+
72
+ function wrapInFrame(content) {
73
+ const frame = loadFrame();
74
+ const wsScript = `
75
+ (function() {
76
+ var ws = new WebSocket('ws://' + location.host + '/ws');
77
+ ws.onmessage = function(e) {
78
+ var data = JSON.parse(e.data);
79
+ if (data.type === 'reload') location.reload();
80
+ };
81
+ ws.onclose = function() { setTimeout(function() { location.reload(); }, 2000); };
82
+
83
+ document.addEventListener('click', function(e) {
84
+ var el = e.target.closest('[data-choice]');
85
+ if (!el) return;
86
+ var container = el.closest('.options, .cards');
87
+ var isMulti = container && container.hasAttribute('data-multiselect');
88
+ if (!isMulti) {
89
+ container.querySelectorAll('[data-choice]').forEach(function(s) { s.classList.remove('selected'); });
90
+ }
91
+ el.classList.toggle('selected');
92
+ var event = {
93
+ type: 'click',
94
+ choice: el.getAttribute('data-choice'),
95
+ text: el.textContent.trim().substring(0, 200),
96
+ timestamp: Math.floor(Date.now() / 1000)
97
+ };
98
+ ws.send(JSON.stringify(event));
99
+ });
100
+ })();`;
101
+
102
+ // Check if content is a full HTML document
103
+ if (content.trim().startsWith('<!DOCTYPE') || content.trim().startsWith('<html')) {
104
+ return content;
105
+ }
106
+ return frame.replace('{{CONTENT}}', content).replace('{{WS_SCRIPT}}', wsScript);
107
+ }
108
+
109
+ // --- WebSocket (RFC 6455 minimal implementation) ---
110
+
111
+ function parseWsFrame(buffer) {
112
+ if (buffer.length < 2) return null;
113
+ const secondByte = buffer[1];
114
+ const masked = (secondByte & 0x80) !== 0;
115
+ let payloadLen = secondByte & 0x7f;
116
+ let offset = 2;
117
+
118
+ if (payloadLen === 126) {
119
+ if (buffer.length < 4) return null;
120
+ payloadLen = buffer.readUInt16BE(2);
121
+ offset = 4;
122
+ } else if (payloadLen === 127) {
123
+ if (buffer.length < 10) return null;
124
+ payloadLen = Number(buffer.readBigUInt64BE(2));
125
+ offset = 10;
126
+ }
127
+
128
+ let maskKey = null;
129
+ if (masked) {
130
+ if (buffer.length < offset + 4) return null;
131
+ maskKey = buffer.slice(offset, offset + 4);
132
+ offset += 4;
133
+ }
134
+
135
+ if (buffer.length < offset + payloadLen) return null;
136
+
137
+ let payload = buffer.slice(offset, offset + payloadLen);
138
+ if (masked && maskKey) {
139
+ for (let i = 0; i < payload.length; i++) {
140
+ payload[i] ^= maskKey[i & 3];
141
+ }
142
+ }
143
+
144
+ const opcode = buffer[0] & 0x0f;
145
+ return { opcode, payload, totalLength: offset + payloadLen };
146
+ }
147
+
148
+ function createWsFrame(data) {
149
+ const payload = Buffer.from(data, 'utf-8');
150
+ const len = payload.length;
151
+ let header;
152
+ if (len < 126) {
153
+ header = Buffer.alloc(2);
154
+ header[0] = 0x81; // FIN + text
155
+ header[1] = len;
156
+ } else if (len < 65536) {
157
+ header = Buffer.alloc(4);
158
+ header[0] = 0x81;
159
+ header[1] = 126;
160
+ header.writeUInt16BE(len, 2);
161
+ } else {
162
+ header = Buffer.alloc(10);
163
+ header[0] = 0x81;
164
+ header[1] = 127;
165
+ header.writeBigUInt64BE(BigInt(len), 2);
166
+ }
167
+ return Buffer.concat([header, payload]);
168
+ }
169
+
170
+ function broadcastWs(message) {
171
+ const frame = createWsFrame(JSON.stringify(message));
172
+ for (const client of wsClients) {
173
+ try { client.write(frame); } catch (_) { wsClients.delete(client); }
174
+ }
175
+ }
176
+
177
+ // --- HTTP Server ---
178
+
179
+ const server = http.createServer((req, res) => {
180
+ resetIdleTimer();
181
+ const parsed = url.parse(req.url, true);
182
+ const pathname = parsed.pathname;
183
+
184
+ // Serve newest HTML file
185
+ if (pathname === '/') {
186
+ const newest = getNewestHtmlFile();
187
+ if (!newest) {
188
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
189
+ res.end(wrapInFrame('<div style="display:flex;align-items:center;justify-content:center;min-height:60vh"><p style="color:#888;font-size:1.2em;">Waiting for content...</p></div>'));
190
+ return;
191
+ }
192
+ const content = fs.readFileSync(path.join(SCREEN_DIR, newest), 'utf-8');
193
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
194
+ res.end(wrapInFrame(content));
195
+ return;
196
+ }
197
+
198
+ // Serve specific files from screen directory
199
+ if (pathname.startsWith('/files/')) {
200
+ const filename = path.basename(pathname);
201
+ const filePath = path.join(SCREEN_DIR, filename);
202
+ if (fs.existsSync(filePath)) {
203
+ const ext = path.extname(filename).toLowerCase();
204
+ const mimeTypes = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
205
+ res.writeHead(200, { 'Content-Type': (mimeTypes[ext] || 'application/octet-stream') + '; charset=utf-8' });
206
+ res.end(fs.readFileSync(filePath));
207
+ return;
208
+ }
209
+ }
210
+
211
+ res.writeHead(404);
212
+ res.end('Not Found');
213
+ });
214
+
215
+ // WebSocket upgrade
216
+ server.on('upgrade', (req, socket) => {
217
+ if (req.url !== '/ws') { socket.destroy(); return; }
218
+
219
+ const key = req.headers['sec-websocket-key'];
220
+ const accept = crypto.createHash('sha1')
221
+ .update(key + '258EAFA5-E914-47DA-95CA-5AB5DC085B11')
222
+ .digest('base64');
223
+
224
+ socket.write(
225
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
226
+ 'Upgrade: websocket\r\n' +
227
+ 'Connection: Upgrade\r\n' +
228
+ `Sec-WebSocket-Accept: ${accept}\r\n\r\n`
229
+ );
230
+
231
+ wsClients.add(socket);
232
+ let buffer = Buffer.alloc(0);
233
+
234
+ socket.on('data', (data) => {
235
+ resetIdleTimer();
236
+ buffer = Buffer.concat([buffer, data]);
237
+ while (true) {
238
+ const frame = parseWsFrame(buffer);
239
+ if (!frame) break;
240
+ buffer = buffer.slice(frame.totalLength);
241
+
242
+ if (frame.opcode === 0x08) { // close
243
+ wsClients.delete(socket);
244
+ socket.end();
245
+ return;
246
+ }
247
+ if (frame.opcode === 0x09) { // ping
248
+ const pong = Buffer.alloc(2);
249
+ pong[0] = 0x8a; pong[1] = 0;
250
+ socket.write(pong);
251
+ continue;
252
+ }
253
+ if (frame.opcode === 0x01) { // text
254
+ try {
255
+ const event = frame.payload.toString('utf-8');
256
+ fs.appendFileSync(EVENTS_FILE, event + '\n');
257
+ } catch (_) { /* best effort */ }
258
+ }
259
+ }
260
+ });
261
+
262
+ socket.on('close', () => wsClients.delete(socket));
263
+ socket.on('error', () => wsClients.delete(socket));
264
+ });
265
+
266
+ // --- File Watcher ---
267
+
268
+ function startWatcher() {
269
+ ensureDir(SCREEN_DIR);
270
+ let debounce = null;
271
+ try {
272
+ fs.watch(SCREEN_DIR, (eventType, filename) => {
273
+ if (!filename || filename.startsWith('.') || !filename.endsWith('.html')) return;
274
+ if (debounce) clearTimeout(debounce);
275
+ debounce = setTimeout(() => {
276
+ // Clear events file when new HTML is pushed
277
+ try { fs.writeFileSync(EVENTS_FILE, ''); } catch (_) {}
278
+ broadcastWs({ type: 'reload' });
279
+ }, 100);
280
+ });
281
+ } catch (err) {
282
+ console.error('[brainstorm] File watcher error:', err.message);
283
+ }
284
+ }
285
+
286
+ // --- Startup ---
287
+
288
+ ensureDir(SCREEN_DIR);
289
+
290
+ // Remove stale stopped marker
291
+ if (fs.existsSync(SERVER_STOPPED_FILE)) {
292
+ fs.unlinkSync(SERVER_STOPPED_FILE);
293
+ }
294
+
295
+ server.listen(PORT, HOST, () => {
296
+ const info = { url: URL_HOST, port: PORT, pid: process.pid, startedAt: new Date().toISOString() };
297
+ fs.writeFileSync(SERVER_INFO_FILE, JSON.stringify(info, null, 2));
298
+ console.log(`[brainstorm] Visual Companion running at ${URL_HOST}`);
299
+ console.log(`[brainstorm] Screen directory: ${SCREEN_DIR}`);
300
+ console.log(`[brainstorm] Idle timeout: 30 minutes`);
301
+ resetIdleTimer();
302
+ startWatcher();
303
+ });
304
+
305
+ process.on('SIGINT', shutdown);
306
+ process.on('SIGTERM', shutdown);
@@ -0,0 +1,52 @@
1
+ # Spec Document Review
2
+
3
+ You are a spec-document-reviewer subagent. Your job is to review the REAP Objective artifact (`01-objective.md`) for quality issues that would cause problems during planning and implementation.
4
+
5
+ ## What to Check
6
+
7
+ | Category | What to Look For |
8
+ |----------|------------------|
9
+ | Completeness | TODOs, placeholders, "TBD", incomplete sections, missing completion criteria |
10
+ | Consistency | Internal contradictions, conflicting requirements, mismatched scope vs requirements |
11
+ | Clarity | Requirements ambiguous enough to cause someone to build the wrong thing |
12
+ | Scope | Focused enough for a single generation — not covering multiple independent subsystems |
13
+ | YAGNI | Unrequested features, over-engineering, unnecessary complexity |
14
+ | Verifiability | Completion criteria that cannot be objectively verified (vague: "improve", "better") |
15
+
16
+ ## Calibration
17
+
18
+ Only flag issues that would cause **real problems** during planning or implementation.
19
+
20
+ **Flag these:**
21
+ - Missing sections that would block planning
22
+ - Contradictions between requirements
23
+ - Ambiguous requirements with multiple valid interpretations
24
+ - Scope too large for a single generation
25
+
26
+ **Do NOT flag:**
27
+ - Minor wording improvements
28
+ - Stylistic preferences
29
+ - Suggestions for additional nice-to-have features
30
+ - Formatting issues
31
+
32
+ ## Output Format
33
+
34
+ ```
35
+ ## Spec Review
36
+
37
+ **Status:** Approved | Issues Found
38
+
39
+ **Issues (if any):**
40
+ - [Section]: [specific issue] — [why it matters for planning]
41
+
42
+ **Recommendations (advisory, do not block approval):**
43
+ - [suggestions for improvement]
44
+ ```
45
+
46
+ ## Important
47
+
48
+ - Read the full `01-objective.md` before starting the review
49
+ - Cross-reference requirements against completion criteria — every criterion should map to at least one FR
50
+ - Check that exclusions are explicitly stated
51
+ - Verify that FR numbering is consistent (FR-001, FR-002, ...)
52
+ - Maximum 3 review iterations — if issues persist after 3 rounds, escalate to the human
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env bash
2
+ # REAP Visual Companion — Server Start Script
3
+ # Usage: start-server.sh [--project-dir /path/to/project] [--port 3210] [--foreground]
4
+
5
+ set -e
6
+
7
+ PROJECT_DIR="$(pwd)"
8
+ PORT="${BRAINSTORM_PORT:-3210}"
9
+ FOREGROUND=false
10
+
11
+ while [[ $# -gt 0 ]]; do
12
+ case "$1" in
13
+ --project-dir) PROJECT_DIR="$2"; shift 2 ;;
14
+ --port) PORT="$2"; shift 2 ;;
15
+ --foreground) FOREGROUND=true; shift ;;
16
+ *) shift ;;
17
+ esac
18
+ done
19
+
20
+ SCREEN_DIR="${PROJECT_DIR}/.reap/brainstorm"
21
+ SERVER_INFO="${SCREEN_DIR}/.server-info"
22
+ SERVER_STOPPED="${SCREEN_DIR}/.server-stopped"
23
+
24
+ # Find server.cjs relative to this script
25
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
26
+ SERVER_JS="${SCRIPT_DIR}/server.cjs"
27
+
28
+ # Ensure screen directory exists
29
+ mkdir -p "${SCREEN_DIR}"
30
+
31
+ # Remove stale stopped marker
32
+ rm -f "${SERVER_STOPPED}"
33
+
34
+ # Check if already running
35
+ if [ -f "${SERVER_INFO}" ]; then
36
+ PID=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('${SERVER_INFO}','utf-8')).pid)}catch(e){console.log('')}")
37
+ if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then
38
+ echo "[brainstorm] Server already running (PID: ${PID})"
39
+ cat "${SERVER_INFO}"
40
+ exit 0
41
+ fi
42
+ # Stale info file
43
+ rm -f "${SERVER_INFO}"
44
+ fi
45
+
46
+ export BRAINSTORM_PORT="${PORT}"
47
+ export BRAINSTORM_DIR="${SCREEN_DIR}"
48
+
49
+ if [ "${FOREGROUND}" = true ]; then
50
+ exec node "${SERVER_JS}"
51
+ else
52
+ nohup node "${SERVER_JS}" > "${SCREEN_DIR}/.server.log" 2>&1 &
53
+ NOHUP_PID=$!
54
+
55
+ # Wait for server-info to appear (max 5 seconds)
56
+ for i in $(seq 1 50); do
57
+ if [ -f "${SERVER_INFO}" ]; then
58
+ echo "[brainstorm] Server started."
59
+ cat "${SERVER_INFO}"
60
+ exit 0
61
+ fi
62
+ sleep 0.1
63
+ done
64
+
65
+ echo "[brainstorm] Warning: server may have failed to start. Check ${SCREEN_DIR}/.server.log"
66
+ exit 1
67
+ fi
@@ -0,0 +1,120 @@
1
+ # Visual Companion Guide
2
+
3
+ > REAP Objective 단계에서 비주얼 컴패니언을 사용하는 가이드.
4
+ > `reap.objective` 슬래시 커맨드가 이 파일을 참조한다.
5
+
6
+ ## 비주얼 컴패니언이란
7
+
8
+ 로컬 Node.js 서버를 통해 브라우저에 목업, 다이어그램, 비교 카드 등을 표시하여 설계 논의를 시각적으로 보조하는 도구.
9
+ 외부 의존 없이 Node.js 내장 모듈만 사용한다.
10
+
11
+ ## 제안 시점
12
+
13
+ Objective Step 5(Goal + Spec Definition) 진입 시, 시각적 질문이 예상되면 컴패니언을 제안한다.
14
+ 제안 메시지는 **독립 메시지**로 보내야 한다 (다른 질문과 합치지 않는다):
15
+
16
+ > "이번 설계에서 목업이나 다이어그램으로 보여드리면 이해하기 쉬운 부분이 있을 수 있습니다.
17
+ > 브라우저에서 시각 자료를 보여드릴 수 있는 비주얼 컴패니언을 사용할까요?
18
+ > (로컬 서버를 띄워 브라우저에서 확인하는 방식입니다)"
19
+
20
+ 유저가 거부하면 터미널 전용으로 진행한다.
21
+
22
+ ## 브라우저 vs 터미널 판단 규칙
23
+
24
+ 각 질문마다 판단: **유저가 읽는 것보다 보는 것이 이해에 도움이 되는가?**
25
+
26
+ ### 브라우저 사용
27
+ - UI 목업, 와이어프레임, 레이아웃
28
+ - 아키텍처 다이어그램, 시스템 구성도, 데이터 흐름 맵
29
+ - 나란히 비교 (레이아웃, 색상, 디자인 방향)
30
+ - 디자인 폴리시 (느낌, 간격, 비주얼 위계)
31
+ - 공간 관계 (상태 머신, 플로우차트, ERD를 다이어그램으로)
32
+
33
+ ### 터미널 사용
34
+ - 요구사항, 범위 질문 ("X는 무슨 뜻인가요?")
35
+ - 개념적 A/B/C 선택 (텍스트로 설명 가능한 접근법)
36
+ - 트레이드오프 목록, 비교표
37
+ - 기술 결정 (API 설계, 데이터 모델링, 아키텍처 접근)
38
+ - 명확화 질문 (답이 시각적 선호가 아닌 말)
39
+
40
+ ### 핵심 테스트
41
+
42
+ UI 관련 질문이라도 자동으로 비주얼은 아니다.
43
+ - "어떤 종류의 마법사를 원하시나요?" → 개념적 → **터미널**
44
+ - "어떤 마법사 레이아웃이 좋으세요?" → 시각적 → **브라우저**
45
+
46
+ ## 서버 기동
47
+
48
+ ```bash
49
+ # 프로젝트 루트에서 실행
50
+ bash .reap/brainstorm/start-server.sh
51
+ # 또는 직접
52
+ node .reap/brainstorm/server.cjs
53
+ ```
54
+
55
+ - `BRAINSTORM_PORT` 환경 변수로 포트 변경 (기본: 3210)
56
+ - `BRAINSTORM_DIR` 환경 변수로 스크린 디렉토리 변경 (기본: `.reap/brainstorm/`)
57
+
58
+ ## 서버 상태 확인
59
+
60
+ - `.reap/brainstorm/.server-info` — 서버 실행 중이면 JSON 존재 (url, port, pid)
61
+ - `.reap/brainstorm/.server-stopped` — 서버가 종료되면 생성됨
62
+ - 서버가 종료된 상태에서 재기동 필요: `start-server.sh` 재실행
63
+
64
+ ## HTML 작성 규칙
65
+
66
+ 1. `.reap/brainstorm/` 디렉토리에 HTML 파일을 Write 도구로 작성
67
+ 2. 시맨틱 파일명 사용 (`architecture.html`, `layout-options.html`)
68
+ 3. 파일명 재사용 금지 (수정 시 `layout-v2.html` 사용)
69
+ 4. **Content fragment 기본** — `<!DOCTYPE` 없이 본문만 작성하면 프레임 템플릿이 자동 래핑
70
+ 5. 전체 HTML 제어가 필요한 경우만 full document 작성
71
+
72
+ ## 사용 가능한 CSS 클래스
73
+
74
+ | 클래스 | 용도 |
75
+ |--------|------|
76
+ | `.options` + `.option[data-choice]` | A/B/C 단일 선택 |
77
+ | `.options[data-multiselect]` | 다중 선택 |
78
+ | `.cards` + `.card[data-choice]` | 비주얼 디자인 카드 |
79
+ | `.mockup` + `.mockup-header` + `.mockup-body` | 목업 컨테이너 |
80
+ | `.split` | 나란히 비교 |
81
+ | `.pros-cons` + `.pros` + `.cons` | 장단점 |
82
+ | `.mock-nav`, `.mock-sidebar`, `.mock-content` | 목업 UI 요소 |
83
+ | `.mock-button`, `.mock-input` | 목업 인터랙티브 요소 |
84
+ | `.placeholder` | 플레이스홀더 블록 |
85
+ | `table`, `h2`, `h3`, `.subtitle`, `.section`, `.label` | 타이포그래피 |
86
+
87
+ ## 이벤트 읽기
88
+
89
+ 유저가 브라우저에서 `[data-choice]` 요소를 클릭하면 WebSocket을 통해 `.events` 파일에 JSON Lines로 기록된다:
90
+
91
+ ```json
92
+ {"type":"click","choice":"a","text":"Option A","timestamp":1706000101}
93
+ ```
94
+
95
+ - 터미널 메시지가 주 피드백 채널
96
+ - `.events` 파일은 보조 인터랙션 데이터
97
+ - 새 HTML 파일 푸시 시 `.events`는 자동 초기화
98
+
99
+ ## 턴 기반 흐름
100
+
101
+ 1. HTML 파일을 Write 도구로 작성
102
+ 2. 유저에게 URL 안내 + 간략한 텍스트 설명 → **턴 종료**
103
+ 3. 다음 턴에서 `.events` 파일 읽기 (유저 인터랙션 확인)
104
+ 4. 터미널 메시지 + `.events`를 종합하여 다음 단계 진행
105
+ 5. 터미널로 돌아갈 때 대기 화면 푸시:
106
+ ```html
107
+ <div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
108
+ <p class="subtitle">터미널에서 계속 진행 중...</p>
109
+ </div>
110
+ ```
111
+
112
+ ## 조건부 실행
113
+
114
+ 비주얼 컴패니언은 brainstorming이 활성화된 경우에만 제안된다.
115
+ brainstorming 자체가 목표 복잡도에 따라 조건부로 실행되므로, 단순 태스크(bugfix, config, docs-only)에서는 비주얼 컴패니언도 제안되지 않는다.
116
+
117
+ ## evolve 모드에서의 동작
118
+
119
+ `/reap.evolve`의 Autonomous Override가 활성화되어 있어도, brainstorming 진입 시 비주얼 컴패니언 제안은 수행한다.
120
+ 유저가 명시적으로 거부한 경우에만 스킵한다.