@flavor-code/superharness 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/HARNESS.md +56 -0
  2. package/flavor-plugin.json +25 -0
  3. package/index.js +238 -0
  4. package/package.json +23 -0
  5. package/scripts/ralph-lib.ps1 +297 -0
  6. package/scripts/ralph-lib.sh +366 -0
  7. package/skills/brainstorm/SKILL.md +179 -0
  8. package/skills/brainstorm/scripts/layout.js +76 -0
  9. package/skills/brainstorm/scripts/mindmap.html +249 -0
  10. package/skills/brainstorm/scripts/server.cjs +208 -0
  11. package/skills/brainstorm/scripts/start-server.ps1 +57 -0
  12. package/skills/brainstorm/scripts/stop-server.ps1 +17 -0
  13. package/skills/finishing-a-development-branch/SKILL.md +112 -0
  14. package/skills/go/SKILL.md +169 -0
  15. package/skills/light/SKILL.md +85 -0
  16. package/skills/requesting-code-review/SKILL.md +103 -0
  17. package/skills/requesting-code-review/code-reviewer.md +168 -0
  18. package/skills/subagent-driven-development/SKILL.md +125 -0
  19. package/skills/systematic-debugging/SKILL.md +296 -0
  20. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  21. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  22. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  23. package/skills/systematic-debugging/find-polluter.sh +63 -0
  24. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  25. package/skills/test-driven-development/SKILL.md +371 -0
  26. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  27. package/skills/using-git-worktrees/SKILL.md +91 -0
  28. package/skills/verification-before-completion/SKILL.md +139 -0
  29. package/skills/writing-plans/SKILL.md +138 -0
@@ -0,0 +1,249 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Superharness Brainstorm — Mind Map</title>
6
+ <style>
7
+ :root { --bg: #1e1f26; --panel: #2a2c35; --text: #e8e8ef; --muted: #9aa0b0; }
8
+ * { box-sizing: border-box; }
9
+ body { margin: 0; font-family: system-ui, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); overflow: hidden; }
10
+ #topbar { position: fixed; top: 0; left: 0; right: 0; height: 48px; display: flex; align-items: center; gap: 12px; padding: 0 16px; background: var(--panel); border-bottom: 1px solid #3a3d4a; z-index: 10; }
11
+ #topic { font-weight: 600; font-size: 15px; }
12
+ .badge { font-size: 12px; padding: 2px 10px; border-radius: 10px; background: #3a3d4a; color: var(--muted); }
13
+ #status.designing { background: #4d3d1f; color: #e3c77b; }
14
+ #status.approved { background: #1f4d2e; color: #7be3a0; }
15
+ #conn.ok { background: #1f4d2e; color: #7be3a0; }
16
+ #conn.bad { background: #4d1f1f; color: #e37b7b; }
17
+ #canvas { position: absolute; left: 0; right: 0; top: 48px; bottom: 0; width: 100%; height: calc(100% - 48px); cursor: grab; }
18
+ #canvas.dragging { cursor: grabbing; }
19
+ .node { cursor: pointer; }
20
+ .node text { font-size: 13px; fill: var(--text); }
21
+ .node.rejected text { fill: var(--muted); text-decoration: line-through; }
22
+ .link { fill: none; stroke: #4a4e5e; stroke-width: 1.5; }
23
+ #hint { position: fixed; bottom: 10px; right: 14px; color: var(--muted); font-size: 12px; z-index: 10; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div id="topbar">
28
+ <span id="topic">等待快照…</span>
29
+ <span class="badge" id="status">exploring</span>
30
+ <span class="badge" id="rev">rev 0</span>
31
+ <span class="badge bad" id="conn">连接中…</span>
32
+ <button id="submitBtn" disabled style="margin-left:auto; background:#3d5afe; color:#fff; border:0; padding:6px 14px; border-radius:6px; cursor:pointer; font-size:13px;">提交 (0)</button>
33
+ </div>
34
+ <svg id="canvas"><g id="viewport"></g></svg>
35
+ <div id="hint">拖拽平移 · 滚轮缩放 · 双击空白复位 · 单击反馈选择 · 双击节点编辑</div>
36
+ <div id="editPanel" style="display:none; position:fixed; z-index:20; top:50%; left:50%; transform:translate(-50%,-50%); background:var(--panel); border:1px solid #3a3d4a; border-radius:10px; padding:16px; width:320px; box-shadow:0 8px 30px rgba(0,0,0,.5);">
37
+ <div style="font-size:13px; color:var(--muted); margin-bottom:6px;">编辑节点</div>
38
+ <input id="editLabel" type="text" placeholder="标签" style="width:100%; margin-bottom:8px; padding:6px 8px; background:var(--bg); color:var(--text); border:1px solid #3a3d4a; border-radius:6px;">
39
+ <textarea id="editNote" rows="4" placeholder="备注 (note)" style="width:100%; padding:6px 8px; background:var(--bg); color:var(--text); border:1px solid #3a3d4a; border-radius:6px; resize:vertical;"></textarea>
40
+ <div style="margin-top:10px; text-align:right;">
41
+ <button id="editCancel" style="background:#3a3d4a; color:var(--text); border:0; padding:6px 12px; border-radius:6px; cursor:pointer;">取消</button>
42
+ <button id="editSave" style="background:#2a9d8f; color:#fff; border:0; padding:6px 12px; border-radius:6px; cursor:pointer;">保存</button>
43
+ </div>
44
+ </div>
45
+ <script src="/layout.js"></script>
46
+ <script>
47
+ const SVG_NS = 'http://www.w3.org/2000/svg';
48
+ const KIND_COLORS = {
49
+ topic: '#3d5afe', question: '#8e6cf0', option: '#2a9d8f', decision: '#e9933a',
50
+ requirement: '#4fa3e3', risk: '#e35d6a', note: '#6c757d',
51
+ };
52
+ const svg = document.getElementById('canvas');
53
+ const viewport = document.getElementById('viewport');
54
+ let view = { x: 0, y: 0, k: 1 };
55
+ let currentRev = -1;
56
+ let lastSnap = null;
57
+ let selectedId = null;
58
+ const pendingEdits = {}; // id -> { label, note } saved-but-not-submitted
59
+ let editingId = null;
60
+ const panel = document.getElementById('editPanel');
61
+ const submitBtn = document.getElementById('submitBtn');
62
+
63
+ function refreshSubmit() {
64
+ const n = Object.keys(pendingEdits).length;
65
+ submitBtn.textContent = '提交 (' + n + ')';
66
+ submitBtn.disabled = n === 0;
67
+ }
68
+ function clearPending() {
69
+ for (const k of Object.keys(pendingEdits)) delete pendingEdits[k];
70
+ refreshSubmit();
71
+ }
72
+ function openEditor(n) {
73
+ editingId = n.id;
74
+ const pend = pendingEdits[n.id];
75
+ document.getElementById('editLabel').value = pend ? pend.label : n.label;
76
+ document.getElementById('editNote').value = pend ? (pend.note || '') : (n.note || '');
77
+ panel.style.display = 'block';
78
+ }
79
+ function closeEditor() { panel.style.display = 'none'; editingId = null; }
80
+ document.getElementById('editCancel').addEventListener('click', closeEditor);
81
+ document.getElementById('editSave').addEventListener('click', () => {
82
+ if (!editingId) return;
83
+ const label = document.getElementById('editLabel').value;
84
+ const note = document.getElementById('editNote').value;
85
+ pendingEdits[editingId] = { label, note };
86
+ fetch('/event', {
87
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ type: 'node:edit', id: editingId, label, note,
89
+ timestamp: Math.floor(Date.now() / 1000) }),
90
+ }).catch(() => {});
91
+ closeEditor();
92
+ refreshSubmit();
93
+ if (lastSnap) render(lastSnap);
94
+ });
95
+ submitBtn.addEventListener('click', () => {
96
+ fetch('/event', {
97
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
98
+ body: JSON.stringify({ type: 'submit', timestamp: Math.floor(Date.now() / 1000) }),
99
+ }).catch(() => {});
100
+ // Keep the optimistic overlay until the agent's merged snapshot lands (cleared in
101
+ // ws.onmessage). Just lock the button so the same batch can't be re-submitted.
102
+ submitBtn.disabled = true;
103
+ submitBtn.textContent = '已提交,等待合并…';
104
+ });
105
+
106
+ function applyView() {
107
+ viewport.setAttribute('transform', 'translate(' + view.x + ',' + view.y + ') scale(' + view.k + ')');
108
+ }
109
+ function resetView() {
110
+ view = { x: svg.clientWidth / 2, y: svg.clientHeight / 2, k: 1 };
111
+ applyView();
112
+ }
113
+
114
+ // --- pan ---
115
+ let drag = null;
116
+ svg.addEventListener('pointerdown', e => {
117
+ if (e.target.closest('.node')) return;
118
+ drag = { x: e.clientX, y: e.clientY };
119
+ svg.classList.add('dragging');
120
+ svg.setPointerCapture(e.pointerId);
121
+ });
122
+ svg.addEventListener('pointermove', e => {
123
+ if (!drag) return;
124
+ view.x += e.clientX - drag.x;
125
+ view.y += e.clientY - drag.y;
126
+ drag = { x: e.clientX, y: e.clientY };
127
+ applyView();
128
+ });
129
+ svg.addEventListener('pointerup', () => { drag = null; svg.classList.remove('dragging'); });
130
+ svg.addEventListener('dblclick', e => { if (!e.target.closest('.node')) resetView(); });
131
+
132
+ // --- zoom around the cursor ---
133
+ svg.addEventListener('wheel', e => {
134
+ e.preventDefault();
135
+ const factor = e.deltaY < 0 ? 1.15 : 1 / 1.15;
136
+ const k2 = Math.min(4, Math.max(0.2, view.k * factor));
137
+ const rect = svg.getBoundingClientRect();
138
+ const px = e.clientX - rect.left;
139
+ const py = e.clientY - rect.top;
140
+ view.x = px - (px - view.x) * (k2 / view.k);
141
+ view.y = py - (py - view.y) * (k2 / view.k);
142
+ view.k = k2;
143
+ applyView();
144
+ }, { passive: false });
145
+
146
+ function nodeWidth(label) {
147
+ let w = 0;
148
+ for (const ch of label) w += ch.charCodeAt(0) > 255 ? 13 : 7.5;
149
+ return Math.max(60, w + 24);
150
+ }
151
+
152
+ function el(name, attrs) {
153
+ const node = document.createElementNS(SVG_NS, name);
154
+ for (const [k, v] of Object.entries(attrs || {})) node.setAttribute(k, v);
155
+ return node;
156
+ }
157
+
158
+ function sendClick(n) {
159
+ selectedId = n.id;
160
+ fetch('/event', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({
164
+ type: 'node:click', id: n.id, label: n.label, kind: n.kind,
165
+ timestamp: Math.floor(Date.now() / 1000),
166
+ }),
167
+ }).catch(() => {});
168
+ if (lastSnap) render(lastSnap);
169
+ }
170
+
171
+ function render(snap) {
172
+ document.getElementById('topic').textContent = snap.topic || '(无主题)';
173
+ const st = document.getElementById('status');
174
+ st.textContent = snap.status;
175
+ st.className = 'badge ' + snap.status;
176
+ document.getElementById('rev').textContent = 'rev ' + snap.rev;
177
+ viewport.innerHTML = '';
178
+ if (!snap.root) return;
179
+ const { nodes, links } = MindmapLayout.layout(snap.root);
180
+ const byId = {};
181
+ nodes.forEach(n => { byId[n.id] = n; });
182
+ for (const l of links) {
183
+ const a = byId[l.from];
184
+ const b = byId[l.to];
185
+ if (!a || !b) continue;
186
+ const mx = (a.x + b.x) / 2;
187
+ viewport.appendChild(el('path', {
188
+ class: 'link',
189
+ d: 'M ' + a.x + ' ' + a.y + ' C ' + mx + ' ' + a.y + ', ' + mx + ' ' + b.y + ', ' + b.x + ' ' + b.y,
190
+ }));
191
+ }
192
+ for (const n of nodes) {
193
+ const pend = pendingEdits[n.id];
194
+ const g = el('g', { class: 'node ' + (n.state || 'open') });
195
+ const shownLabel = pend ? pend.label : n.label;
196
+ const label = (n.state === 'resolved' ? '✓ ' : '') + shownLabel;
197
+ const w = nodeWidth(label);
198
+ const h = 32;
199
+ const color = KIND_COLORS[n.kind] || KIND_COLORS.note;
200
+ const rect = el('rect', {
201
+ x: n.x - w / 2, y: n.y - h / 2, width: w, height: h, rx: 8,
202
+ fill: n.state === 'rejected' ? '#2a2c35' : color + '33',
203
+ stroke: n.state === 'chosen' ? '#ffd166' : color,
204
+ 'stroke-width': n.state === 'chosen' ? 3 : (n.id === selectedId ? 2.5 : 1.5),
205
+ });
206
+ if (n.id === selectedId) rect.setAttribute('stroke-dasharray', '4 2');
207
+ if (pend) { rect.setAttribute('stroke', '#7be3a0'); rect.setAttribute('stroke-dasharray', '5 3'); }
208
+ const text = el('text', { x: n.x, y: n.y + 4.5, 'text-anchor': 'middle' });
209
+ text.textContent = label;
210
+ g.appendChild(rect);
211
+ g.appendChild(text);
212
+ if (n.note) {
213
+ const tip = document.createElementNS(SVG_NS, 'title');
214
+ tip.textContent = n.note;
215
+ g.appendChild(tip);
216
+ }
217
+ g.addEventListener('click', () => sendClick(n));
218
+ g.addEventListener('dblclick', e => { e.stopPropagation(); openEditor(n); });
219
+ viewport.appendChild(g);
220
+ }
221
+ }
222
+
223
+ function connect() {
224
+ const conn = document.getElementById('conn');
225
+ const ws = new WebSocket('ws://' + location.host + '/');
226
+ ws.onopen = () => { conn.textContent = '已连接'; conn.className = 'badge ok'; };
227
+ ws.onmessage = ev => {
228
+ let snap;
229
+ try { snap = JSON.parse(ev.data); } catch { return; }
230
+ if (snap.type !== 'mindmap:snapshot' || snap.rev <= currentRev) return;
231
+ currentRev = snap.rev;
232
+ lastSnap = snap;
233
+ selectedId = null;
234
+ clearPending(); // authoritative snapshot now reflects any merged edits
235
+ render(snap);
236
+ };
237
+ ws.onclose = () => {
238
+ conn.textContent = '已断开,重连中…';
239
+ conn.className = 'badge bad';
240
+ setTimeout(connect, 2000);
241
+ };
242
+ ws.onerror = () => ws.close();
243
+ }
244
+
245
+ resetView();
246
+ connect();
247
+ </script>
248
+ </body>
249
+ </html>
@@ -0,0 +1,208 @@
1
+ // Superharness brainstorm mind-map server. Zero-dependency Node.
2
+ // Serves mindmap.html, watches content/mindmap.json and pushes snapshots to the
3
+ // browser over WebSocket; records browser interactions to state/events (JSONL).
4
+ const crypto = require('crypto');
5
+ const http = require('http');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ // ---------- config ----------
10
+ const SESSION_DIR = process.env.SUPERHARNESS_SESSION_DIR;
11
+ if (!SESSION_DIR) {
12
+ console.error('SUPERHARNESS_SESSION_DIR is required');
13
+ process.exit(1);
14
+ }
15
+ const CONTENT_DIR = path.join(SESSION_DIR, 'content');
16
+ const STATE_DIR = path.join(SESSION_DIR, 'state');
17
+ const SNAPSHOT_FILE = path.join(CONTENT_DIR, 'mindmap.json');
18
+ const EVENTS_FILE = path.join(STATE_DIR, 'events');
19
+ const EDITS_FILE = path.join(STATE_DIR, 'edits');
20
+ const INFO_FILE = path.join(STATE_DIR, 'server-info');
21
+ const STOPPED_FILE = path.join(STATE_DIR, 'server-stopped');
22
+ const PORT = Number(process.env.SUPERHARNESS_PORT) || (49152 + Math.floor(Math.random() * 16383));
23
+ const HOST = process.env.SUPERHARNESS_HOST || '127.0.0.1';
24
+ const IDLE_TIMEOUT_MS = Number(process.env.SUPERHARNESS_IDLE_TIMEOUT_MS) || 30 * 60 * 1000;
25
+ const IDLE_CHECK_MS = Math.min(5000, IDLE_TIMEOUT_MS);
26
+
27
+ fs.mkdirSync(CONTENT_DIR, { recursive: true });
28
+ fs.mkdirSync(STATE_DIR, { recursive: true });
29
+
30
+ let lastActivity = Date.now();
31
+ const touch = () => { lastActivity = Date.now(); };
32
+
33
+ // ---------- websocket protocol (RFC 6455) ----------
34
+ const OPCODES = { TEXT: 0x01, CLOSE: 0x08, PING: 0x09, PONG: 0x0A };
35
+ const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
36
+
37
+ function computeAcceptKey(clientKey) {
38
+ return crypto.createHash('sha1').update(clientKey + WS_MAGIC).digest('base64');
39
+ }
40
+
41
+ function encodeFrame(opcode, payload) {
42
+ const fin = 0x80;
43
+ const len = payload.length;
44
+ let header;
45
+ if (len < 126) {
46
+ header = Buffer.alloc(2);
47
+ header[0] = fin | opcode;
48
+ header[1] = len;
49
+ } else if (len < 65536) {
50
+ header = Buffer.alloc(4);
51
+ header[0] = fin | opcode;
52
+ header[1] = 126;
53
+ header.writeUInt16BE(len, 2);
54
+ } else {
55
+ header = Buffer.alloc(10);
56
+ header[0] = fin | opcode;
57
+ header[1] = 127;
58
+ header.writeBigUInt64BE(BigInt(len), 2);
59
+ }
60
+ return Buffer.concat([header, payload]);
61
+ }
62
+
63
+ function decodeFrame(buffer) {
64
+ if (buffer.length < 2) return null;
65
+ const opcode = buffer[0] & 0x0f;
66
+ const masked = (buffer[1] & 0x80) !== 0;
67
+ let payloadLen = buffer[1] & 0x7f;
68
+ let offset = 2;
69
+ if (!masked) throw new Error('Client frames must be masked');
70
+ if (payloadLen === 126) {
71
+ if (buffer.length < 4) return null;
72
+ payloadLen = buffer.readUInt16BE(2);
73
+ offset = 4;
74
+ } else if (payloadLen === 127) {
75
+ if (buffer.length < 10) return null;
76
+ payloadLen = Number(buffer.readBigUInt64BE(2));
77
+ offset = 10;
78
+ }
79
+ const dataOffset = offset + 4;
80
+ const totalLen = dataOffset + payloadLen;
81
+ if (buffer.length < totalLen) return null;
82
+ const mask = buffer.slice(offset, dataOffset);
83
+ const data = Buffer.alloc(payloadLen);
84
+ for (let i = 0; i < payloadLen; i++) data[i] = buffer[dataOffset + i] ^ mask[i % 4];
85
+ return { opcode, payload: data, bytesConsumed: totalLen };
86
+ }
87
+
88
+ const clients = new Set();
89
+ function broadcast(text) {
90
+ const frame = encodeFrame(OPCODES.TEXT, Buffer.from(text));
91
+ for (const socket of clients) socket.write(frame);
92
+ }
93
+
94
+ // ---------- snapshot ----------
95
+ const DEFAULT_SNAPSHOT = JSON.stringify({
96
+ type: 'mindmap:snapshot', rev: 0, topic: '', status: 'exploring', root: null,
97
+ });
98
+
99
+ function currentSnapshot() {
100
+ try { return fs.readFileSync(SNAPSHOT_FILE, 'utf-8'); }
101
+ catch { return DEFAULT_SNAPSHOT; }
102
+ }
103
+
104
+ // ---------- http ----------
105
+ function serveFile(res, name, type) {
106
+ res.writeHead(200, { 'Content-Type': type });
107
+ res.end(fs.readFileSync(path.join(__dirname, name)));
108
+ }
109
+
110
+ const server = http.createServer((req, res) => {
111
+ touch();
112
+ if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
113
+ serveFile(res, 'mindmap.html', 'text/html; charset=utf-8');
114
+ } else if (req.method === 'GET' && req.url === '/layout.js') {
115
+ serveFile(res, 'layout.js', 'application/javascript; charset=utf-8');
116
+ } else if (req.method === 'GET' && req.url === '/mindmap.json') {
117
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
118
+ res.end(currentSnapshot());
119
+ } else if (req.method === 'POST' && req.url === '/event') {
120
+ let body = '';
121
+ req.on('data', chunk => { body += chunk; });
122
+ req.on('end', () => {
123
+ try {
124
+ const msg = JSON.parse(body);
125
+ const file = (msg.type === 'node:edit' || msg.type === 'submit') ? EDITS_FILE : EVENTS_FILE;
126
+ fs.appendFileSync(file, body.trim() + '\n');
127
+ res.writeHead(204);
128
+ res.end();
129
+ } catch {
130
+ res.writeHead(400);
131
+ res.end('invalid JSON');
132
+ }
133
+ });
134
+ } else {
135
+ res.writeHead(404);
136
+ res.end('not found');
137
+ }
138
+ });
139
+
140
+ // ---------- websocket upgrade ----------
141
+ server.on('upgrade', (req, socket) => {
142
+ const key = req.headers['sec-websocket-key'];
143
+ if (!key) { socket.destroy(); return; }
144
+ socket.write(
145
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
146
+ 'Upgrade: websocket\r\nConnection: Upgrade\r\n' +
147
+ 'Sec-WebSocket-Accept: ' + computeAcceptKey(key) + '\r\n\r\n');
148
+ clients.add(socket);
149
+ touch();
150
+ socket.write(encodeFrame(OPCODES.TEXT, Buffer.from(currentSnapshot())));
151
+ let buf = Buffer.alloc(0);
152
+ socket.on('data', data => {
153
+ touch();
154
+ buf = Buffer.concat([buf, data]);
155
+ while (true) {
156
+ let frame;
157
+ try { frame = decodeFrame(buf); } catch { socket.destroy(); return; }
158
+ if (!frame) break;
159
+ buf = buf.slice(frame.bytesConsumed);
160
+ if (frame.opcode === OPCODES.CLOSE) { socket.end(); return; }
161
+ if (frame.opcode === OPCODES.PING) socket.write(encodeFrame(OPCODES.PONG, frame.payload));
162
+ }
163
+ });
164
+ const drop = () => clients.delete(socket);
165
+ socket.on('close', drop);
166
+ socket.on('error', drop);
167
+ });
168
+
169
+ // ---------- snapshot file watch ----------
170
+ let lastMtime = 0;
171
+ try { lastMtime = fs.statSync(SNAPSHOT_FILE).mtimeMs; } catch {}
172
+ setInterval(() => {
173
+ let stat;
174
+ try { stat = fs.statSync(SNAPSHOT_FILE); } catch { return; }
175
+ if (stat.mtimeMs === lastMtime) return;
176
+ lastMtime = stat.mtimeMs;
177
+ touch();
178
+ try { fs.writeFileSync(EVENTS_FILE, ''); } catch {}
179
+ broadcast(currentSnapshot());
180
+ }, 500).unref();
181
+
182
+ // ---------- lifecycle ----------
183
+ function shutdown(code) {
184
+ try { fs.writeFileSync(STOPPED_FILE, ''); } catch {}
185
+ try { fs.unlinkSync(INFO_FILE); } catch {}
186
+ process.exit(code);
187
+ }
188
+ process.on('SIGINT', () => shutdown(0));
189
+ process.on('SIGTERM', () => shutdown(0));
190
+
191
+ setInterval(() => {
192
+ if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown(0);
193
+ }, IDLE_CHECK_MS).unref();
194
+
195
+ server.listen(PORT, HOST, () => {
196
+ try { fs.unlinkSync(STOPPED_FILE); } catch {}
197
+ const urlHost = HOST === '127.0.0.1' ? 'localhost' : HOST;
198
+ const info = {
199
+ type: 'server-started',
200
+ port: PORT,
201
+ url: 'http://' + urlHost + ':' + PORT,
202
+ pid: process.pid,
203
+ content_dir: CONTENT_DIR,
204
+ state_dir: STATE_DIR,
205
+ };
206
+ fs.writeFileSync(INFO_FILE, JSON.stringify(info));
207
+ console.log(JSON.stringify(info));
208
+ });
@@ -0,0 +1,57 @@
1
+ # Starts the brainstorm mind-map server for a project.
2
+ # Creates <project>/<state-root>/superharness/brainstorm/<session-id>/{content,state}
3
+ # where <state-root> follows the host (.claude under Claude Code, .flavor under
4
+ # flavor-code), launches node server.cjs detached, waits for state/server-info,
5
+ # prints it and exits.
6
+ param(
7
+ [string]$ProjectDir = (Get-Location).Path,
8
+ [int]$TimeoutSec = 10
9
+ )
10
+
11
+ $ErrorActionPreference = 'Stop'
12
+
13
+ $node = Get-Command node -ErrorAction SilentlyContinue
14
+ if (-not $node) { Write-Error 'node not found on PATH - brainstorm mind map unavailable'; exit 1 }
15
+
16
+ # State root detection (mirrors ralph-lib Get-RalphStateRoot):
17
+ # $env:SUPERHARNESS_STATE_ROOT ('.claude' | '.flavor') forces a choice for unusual
18
+ # layouts; otherwise the install location of this script names the host, with a
19
+ # fallback to the project's .flavor install. Claude Code is the historical default.
20
+ $forced = $env:SUPERHARNESS_STATE_ROOT
21
+ if ($forced -eq '.flavor' -or $forced -eq 'flavor') {
22
+ $stateRoot = '.flavor'
23
+ } elseif ($forced -eq '.claude' -or $forced -eq 'claude') {
24
+ $stateRoot = '.claude'
25
+ } elseif ($PSScriptRoot -match '[/\\]\.flavor[/\\]plugins[/\\]superharness') {
26
+ $stateRoot = '.flavor'
27
+ } elseif (Test-Path (Join-Path $ProjectDir '.flavor\plugins\superharness')) {
28
+ $stateRoot = '.flavor'
29
+ } else {
30
+ $stateRoot = '.claude'
31
+ }
32
+
33
+ $sessionId = (Get-Date -Format 'yyyyMMdd-HHmmss') + '-' + $PID
34
+ $sessionDir = Join-Path $ProjectDir "$stateRoot\superharness\brainstorm\$sessionId"
35
+ New-Item -ItemType Directory -Force (Join-Path $sessionDir 'content') | Out-Null
36
+ New-Item -ItemType Directory -Force (Join-Path $sessionDir 'state') | Out-Null
37
+
38
+ $serverJs = Join-Path $PSScriptRoot 'server.cjs'
39
+ $env:SUPERHARNESS_SESSION_DIR = $sessionDir
40
+ try {
41
+ Start-Process -FilePath $node.Source -ArgumentList ('"' + $serverJs + '"') -WindowStyle Hidden
42
+ } finally {
43
+ Remove-Item Env:SUPERHARNESS_SESSION_DIR -ErrorAction SilentlyContinue
44
+ }
45
+
46
+ $infoPath = Join-Path $sessionDir 'state\server-info'
47
+ $deadline = (Get-Date).AddSeconds($TimeoutSec)
48
+ while ((Get-Date) -lt $deadline) {
49
+ if (Test-Path $infoPath) { break }
50
+ Start-Sleep -Milliseconds 200
51
+ }
52
+ if (-not (Test-Path $infoPath)) {
53
+ Write-Error "server did not start within $TimeoutSec seconds"
54
+ exit 1
55
+ }
56
+ Get-Content $infoPath -Raw -Encoding UTF8
57
+ exit 0
@@ -0,0 +1,17 @@
1
+ # Stops the brainstorm mind-map server for a session directory.
2
+ param(
3
+ [Parameter(Mandatory = $true)][string]$SessionDir
4
+ )
5
+
6
+ $ErrorActionPreference = 'SilentlyContinue'
7
+
8
+ $infoPath = Join-Path $SessionDir 'state\server-info'
9
+ if (Test-Path $infoPath) {
10
+ $info = Get-Content $infoPath -Raw | ConvertFrom-Json
11
+ if ($info.pid) { Stop-Process -Id $info.pid -Force -Confirm:$false }
12
+ Remove-Item $infoPath -Force
13
+ }
14
+ $stateDir = Join-Path $SessionDir 'state'
15
+ if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Force $stateDir | Out-Null }
16
+ New-Item -ItemType File -Force (Join-Path $stateDir 'server-stopped') | Out-Null
17
+ exit 0
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: finishing-a-development-branch
3
+ description: Use when work done in an isolated worktree or branch is complete, verified, and reviewed - merge the branch back, remove the worktree, delete the branch, and report. The closing half of using-git-worktrees (go Phase 5)
4
+ ---
5
+
6
+ # Finishing a Development Branch
7
+
8
+ ## Overview
9
+
10
+ Close out a task that was developed in an isolated worktree or branch: merge the
11
+ verified, reviewed work back into the main branch, remove the disposable
12
+ worktree, delete the merged branch, and leave the repository clean. This is the
13
+ counterpart of `superharness:using-git-worktrees` — that skill isolates the
14
+ work, this skill closes it out.
15
+
16
+ **Announce at start:** "Closing out the isolated workspace
17
+ (finishing-a-development-branch)."
18
+
19
+ **Preconditions — do NOT finish until all of these hold:**
20
+
21
+ 1. The full test suite passed in the worktree (`verification-before-completion`).
22
+ 2. The review pass is done and Critical/Important issues were fixed
23
+ (`requesting-code-review`).
24
+ 3. All work is committed on the feature branch (no uncommitted changes).
25
+
26
+ If any precondition is missing, go back and complete it first. Finishing is not
27
+ a way to skip verification.
28
+
29
+ ## Step 1 — Confirm where you are
30
+
31
+ ```bash
32
+ git rev-parse --is-inside-work-tree 2>/dev/null || { echo "Not a git repo - nothing to finish. Report and stop."; exit 0; }
33
+ git status --short
34
+ git branch --show-current
35
+ ```
36
+
37
+ - Not a git repo (or `go` worked in place, no branch was created): there is
38
+ nothing to merge or clean up. Skip straight to Step 5.
39
+ - `git status --short` shows uncommitted changes: commit them first (they went
40
+ through TDD already) or stop and ask — never merge a dirty worktree.
41
+ - `git rev-parse --show-superproject-working-tree` prints a path: you are in a
42
+ submodule; treat it as a normal repo.
43
+
44
+ ## Step 2 — Merge the branch back
45
+
46
+ Return to the main workspace and merge the feature branch. A fast-forward merge
47
+ is the normal case because the worktree branched off the current main and no
48
+ parallel work happened there:
49
+
50
+ ```bash
51
+ # from the MAIN workspace (git worktree list shows its path), not the worktree
52
+ git switch <main-branch>
53
+ git merge --ff-only <feature-branch>
54
+ ```
55
+
56
+ If `--ff-only` fails because main moved on, do a regular merge and resolve the
57
+ conflicts properly (this is a real engineering task — use
58
+ `superharness:systematic-debugging` if anything is unclear, never "just pick
59
+ ours"):
60
+
61
+ ```bash
62
+ git merge <feature-branch>
63
+ # resolve conflicts, then verify: run the FULL test suite again in the main workspace
64
+ ```
65
+
66
+ **After any merge, run the full test suite in the main workspace and paste the
67
+ real output.** The merge can change the result even when the branch was green.
68
+
69
+ ## Step 3 — Remove the worktree and branch
70
+
71
+ Once merged (fast-forward or resolved), clean up the disposable workspace:
72
+
73
+ ```bash
74
+ git worktree remove <worktree-path>
75
+ git branch -d <feature-branch> # -d only deletes merged branches; if it refuses, the branch was NOT merged - stop
76
+ ```
77
+
78
+ - If `git worktree remove` refuses because of uncommitted changes, deal with
79
+ those changes first. Do not `--force` unless the human explicitly confirms the
80
+ work there is disposable.
81
+ - If `git branch -d` refuses, the branch is not fully merged — investigate
82
+ before discarding. Never `-D` a branch with unreviewed work.
83
+ - Only remove the `.worktrees/` entry from `.gitignore` if the directory is now
84
+ gone (add/remove is optional; leaving the ignore line is harmless).
85
+
86
+ ## Step 4 — Leave the repo clean
87
+
88
+ ```bash
89
+ git worktree list # should show only the main workspace
90
+ git status --short # should be clean (or only expected files)
91
+ ```
92
+
93
+ ## Step 5 — Report
94
+
95
+ Report concisely:
96
+
97
+ - What was merged and into which branch (`<feature-branch>` -> `<main-branch>`)
98
+ - Merge method (fast-forward or merge commit) and whether any conflict resolution
99
+ happened
100
+ - Test command run in the main workspace after the merge and its real output
101
+ - Worktree/branch cleanup performed (or "worked in place — nothing to clean up")
102
+ - Push status: **you did not push** unless the human asked
103
+
104
+ ## Red Flags
105
+
106
+ | Thought | Reality |
107
+ |---------|---------|
108
+ | "It passed in the worktree, no need to re-run tests here" | The merge changes the tree. Run the full suite in the main workspace and show the output. |
109
+ | "The worktree has leftover changes, I'll just --force remove it" | Handle the changes first, or ask. --force discards work that may be unreviewed. |
110
+ | "git branch -d refused, but I know it's merged" | Then prove it — check `git log <main-branch>..<feature-branch>`. If it's really empty, -d succeeds. Refusal means it is not merged. |
111
+ | "I'll push to origin as part of finishing" | No. Pushing is a shared/external action — report the branch and let the human push. |
112
+ | "This repo isn't git, but I'll clean up anyway" | Nothing to clean up. Worked in place leaves no branch/worktree. Report and stop. |