@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.
- package/README.ja.md +96 -10
- package/README.ko.md +96 -10
- package/README.md +96 -10
- package/README.zh-CN.md +96 -10
- package/dist/cli.js +212 -112
- package/dist/templates/artifacts/01-objective.md +17 -0
- package/dist/templates/artifacts/merge/01-detect.md +18 -0
- package/dist/templates/artifacts/merge/02-mate.md +12 -0
- package/dist/templates/artifacts/merge/03-merge.md +15 -0
- package/dist/templates/artifacts/merge/04-sync.md +18 -0
- package/dist/templates/artifacts/merge/05-validation.md +11 -0
- package/dist/templates/artifacts/merge/06-completion.md +13 -0
- package/dist/templates/brainstorm/frame.html +125 -0
- package/dist/templates/brainstorm/server.cjs +306 -0
- package/dist/templates/brainstorm/spec-reviewer-prompt.md +52 -0
- package/dist/templates/brainstorm/start-server.sh +67 -0
- package/dist/templates/brainstorm/visual-companion-guide.md +120 -0
- package/dist/templates/commands/reap.evolve.md +7 -0
- package/dist/templates/commands/reap.merge.completion.md +20 -0
- package/dist/templates/commands/reap.merge.detect.md +20 -0
- package/dist/templates/commands/reap.merge.evolve.md +28 -0
- package/dist/templates/commands/reap.merge.mate.md +27 -0
- package/dist/templates/commands/reap.merge.md +47 -0
- package/dist/templates/commands/reap.merge.merge.md +22 -0
- package/dist/templates/commands/reap.merge.start.md +21 -0
- package/dist/templates/commands/reap.merge.sync.md +32 -0
- package/dist/templates/commands/reap.merge.validation.md +25 -0
- package/dist/templates/commands/reap.next.md +14 -1
- package/dist/templates/commands/reap.objective.md +105 -9
- package/dist/templates/commands/reap.pull.md +51 -0
- package/dist/templates/commands/reap.push.md +18 -0
- package/dist/templates/hooks/genome-loader.cjs +34 -12
- package/dist/templates/hooks/opencode-session-start.js +2 -2
- package/dist/templates/hooks/reap-guide.md +1 -1
- package/dist/templates/hooks/session-start.cjs +2 -2
- package/package.json +1 -1
|
@@ -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
|
+
유저가 명시적으로 거부한 경우에만 스킵한다.
|