create-harness-vibe-coding 0.8.18 → 0.8.19

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 (128) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -0
  3. package/package.json +5 -2
  4. package/src/index.js +383 -267
  5. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  6. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  7. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  8. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  9. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  10. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  11. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  12. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  13. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  14. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  15. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  16. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  17. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  18. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  19. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  20. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  21. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  22. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  23. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  24. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  25. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  26. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  27. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  28. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  29. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  30. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  31. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  32. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  33. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  34. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  35. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  36. package/src/ui/dist/index.html +18 -0
  37. package/src/ui/index.html +17 -0
  38. package/src/ui/package.json +33 -0
  39. package/src/ui/pnpm-lock.yaml +1582 -0
  40. package/src/ui/pnpm-workspace.yaml +2 -0
  41. package/src/ui/src/App.tsx +86 -0
  42. package/src/ui/src/api.ts +93 -0
  43. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  44. package/src/ui/src/components/Footer.tsx +69 -0
  45. package/src/ui/src/components/Header.tsx +175 -0
  46. package/src/ui/src/components/LoadingView.tsx +22 -0
  47. package/src/ui/src/components/RolesRoute.tsx +169 -0
  48. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  49. package/src/ui/src/components/TaskList.tsx +388 -0
  50. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  51. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  52. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  53. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  54. package/src/ui/src/index.css +281 -0
  55. package/src/ui/src/main.tsx +11 -0
  56. package/src/ui/src/types.ts +108 -0
  57. package/src/ui/tsconfig.json +21 -0
  58. package/src/ui/vite.config.ts +19 -0
  59. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  60. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  61. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  62. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  63. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  64. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  65. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  66. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  67. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  68. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  69. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  70. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  71. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  72. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  73. package/src/wf-ui-server/a2a-store.mjs +296 -0
  74. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  75. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  76. package/src/wf-ui-server/runtime-config.mjs +153 -0
  77. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  78. package/src/wf-ui-server/security.mjs +67 -0
  79. package/src/wf-ui-server/server.mjs +849 -0
  80. package/src/wf-ui-server/session-registry.mjs +204 -0
  81. package/src/wf-ui-server/settings.mjs +100 -0
  82. package/src/wf-ui-server/task-parser.mjs +133 -0
  83. package/src/wf-ui-server/terminal-store.mjs +225 -0
  84. package/src/wf-ui-server/token.mjs +41 -0
  85. package/src/wf-ui-server/ws-events.mjs +354 -0
  86. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  87. package/templates/common/.claude/commands/wf-help.md +1 -0
  88. package/templates/common/.claude/commands/wf-task-list.md +2 -0
  89. package/templates/common/.claude/commands/wf-ui.md +26 -0
  90. package/templates/common/.claude/commands/wf-update.md +54 -9
  91. package/templates/common/.claude/rules/ecc/common.md +1 -1
  92. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  93. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  94. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  95. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  96. package/templates/common/.harness-version +63 -39
  97. package/templates/common/.opencode/commands/wf-help.md +1 -0
  98. package/templates/common/.opencode/commands/wf-task-list.md +2 -0
  99. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  100. package/templates/common/.opencode/commands/wf-update.md +54 -9
  101. package/templates/common/CLAUDE.md +1 -1
  102. package/templates/common/Harness/MEMORY.md +2 -0
  103. package/templates/common/Harness/README.md +6 -4
  104. package/templates/common/Harness/a2a/role-graph.json +79 -0
  105. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  106. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  107. package/templates/common/Harness/ownership.manifest.json +57 -2
  108. package/templates/common/Harness/scripts/README.md +134 -0
  109. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  110. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  111. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  112. package/templates/common/Harness/scripts/task-state.mjs +556 -23
  113. package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
  114. package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
  115. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  116. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  117. package/templates/common/Harness/settings.json +35 -0
  118. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
  119. package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
  120. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  121. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  122. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  123. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  124. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  125. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  126. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  127. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  128. package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
@@ -0,0 +1,354 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseTaskList } from './task-parser.mjs';
5
+ import { validateToken } from './token.mjs';
6
+
7
+ const KEEPALIVE_INTERVAL = 15000;
8
+ const PING_TIMEOUT = 5000;
9
+ const TICK_MS = Math.min(KEEPALIVE_INTERVAL, PING_TIMEOUT) / 2;
10
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
11
+
12
+ // ── Frame helpers ──
13
+
14
+ /**
15
+ * Write an unmasked text frame (opcode 0x1) to a socket.
16
+ * @param {import('net').Socket} socket
17
+ * @param {string} text
18
+ * @param {number} [opcode=0x1]
19
+ */
20
+ function sendFrame(socket, text, opcode = 0x1) {
21
+ const data = Buffer.from(text, 'utf8');
22
+ const len = data.length;
23
+ let frame;
24
+ if (len < 126) {
25
+ frame = Buffer.alloc(2 + len);
26
+ frame[0] = 0x80 | opcode;
27
+ frame[1] = len;
28
+ data.copy(frame, 2);
29
+ } else if (len < 65536) {
30
+ frame = Buffer.alloc(4 + len);
31
+ frame[0] = 0x80 | opcode;
32
+ frame[1] = 126;
33
+ frame.writeUInt16BE(len, 2);
34
+ data.copy(frame, 4);
35
+ } else {
36
+ frame = Buffer.alloc(10 + len);
37
+ frame[0] = 0x80 | opcode;
38
+ frame[1] = 127;
39
+ frame.writeBigUInt64BE(BigInt(len), 2);
40
+ data.copy(frame, 10);
41
+ }
42
+ socket.write(frame);
43
+ }
44
+
45
+ /**
46
+ * Send a close frame with the given status code.
47
+ * @param {import('net').Socket} socket
48
+ * @param {number} [code=1000]
49
+ */
50
+ function sendCloseFrame(socket, code = 1000) {
51
+ const payload = Buffer.alloc(2);
52
+ payload.writeUInt16BE(code, 0);
53
+ const header = Buffer.alloc(2);
54
+ header[0] = 0x88; // FIN + close
55
+ header[1] = 2;
56
+ socket.write(Buffer.concat([header, payload]));
57
+ }
58
+
59
+ /**
60
+ * Parse incoming WebSocket frames from a buffer of raw bytes.
61
+ * Handles masked client frames by unmasking the payload.
62
+ * @param {Buffer} buffer
63
+ * @returns {{ frames: Array<{ opcode: number, payload: Buffer }>, remainder: Buffer }}
64
+ */
65
+ function parseFrames(buffer) {
66
+ const frames = [];
67
+ let offset = 0;
68
+ while (offset + 2 <= buffer.length) {
69
+ const b1 = buffer[offset];
70
+ const opcode = b1 & 0x0F;
71
+ const b2 = buffer[offset + 1];
72
+ const masked = (b2 & 0x80) !== 0;
73
+ let payloadLen = b2 & 0x7F;
74
+ let headerLen = 2;
75
+ if (payloadLen === 126) {
76
+ if (offset + 4 > buffer.length) break;
77
+ payloadLen = buffer.readUInt16BE(offset + 2);
78
+ headerLen = 4;
79
+ } else if (payloadLen === 127) {
80
+ if (offset + 10 > buffer.length) break;
81
+ payloadLen = Number(buffer.readBigUInt64BE(offset + 2));
82
+ headerLen = 10;
83
+ }
84
+ const maskLen = masked ? 4 : 0;
85
+ if (offset + headerLen + maskLen + payloadLen > buffer.length) break;
86
+ offset += headerLen;
87
+ let payload;
88
+ if (masked) {
89
+ const mask = buffer.slice(offset, offset + 4);
90
+ offset += 4;
91
+ payload = Buffer.allocUnsafe(payloadLen);
92
+ for (let i = 0; i < payloadLen; i++) {
93
+ payload[i] = buffer[offset + i] ^ mask[i % 4];
94
+ }
95
+ } else {
96
+ payload = buffer.slice(offset, offset + payloadLen);
97
+ }
98
+ offset += payloadLen;
99
+ frames.push({ opcode, payload });
100
+ }
101
+ return { frames, remainder: buffer.slice(offset) };
102
+ }
103
+
104
+ /**
105
+ * Send a short HTTP error JSON response, then close the socket.
106
+ * @param {import('net').Socket} sock
107
+ * @param {number} statusCode
108
+ * @param {string} message
109
+ */
110
+ function sendHttpError(sock, statusCode, message) {
111
+ const body = JSON.stringify({ error: { code: statusCode === 401 ? 'UNAUTHORIZED' : 'ERROR', message } });
112
+ sock.write(
113
+ `HTTP/1.1 ${statusCode} ${statusCode === 401 ? 'Unauthorized' : 'Error'}\r\n` +
114
+ 'Content-Type: application/json\r\n' +
115
+ `Content-Length: ${body.length}\r\n` +
116
+ 'Connection: close\r\n\r\n' + body
117
+ );
118
+ sock.end();
119
+ }
120
+
121
+ // ── Public API ──
122
+
123
+ /**
124
+ * Attach a WebSocket /ws/events endpoint to an existing http.Server.
125
+ *
126
+ * On upgrade: validates `?token=` query param, upgrades to WS, sends
127
+ * `server.connected` handshake. Listens to `Harness/tasks/` for changes
128
+ * and broadcasts `task.updated` to all connected clients. Sends `ping`
129
+ * keepalive every 15s and force-closes clients that don't pong within 5s.
130
+ *
131
+ * @param {import('http').Server} httpServer
132
+ * @param {string} expectedToken - The token clients must present.
133
+ * @param {string} projectRoot - Absolute path to the project root.
134
+ * @param {{ keepaliveInterval?: number, pingTimeout?: number }} [options]
135
+ * @returns {{ wss: null, seq: number, close: () => Promise<void> }}
136
+ */
137
+ export function attachEventsWs(httpServer, expectedToken, projectRoot, options = {}) {
138
+ const keepaliveInterval = options.keepaliveInterval || KEEPALIVE_INTERVAL;
139
+ const pingTimeoutMs = options.pingTimeout || PING_TIMEOUT;
140
+ const tickMs = Math.min(keepaliveInterval, pingTimeoutMs) / 2;
141
+
142
+ const clients = new Set();
143
+ let seq = 0;
144
+ let watcher = null;
145
+ let watchTimeout = null;
146
+ let keepaliveTimer = null;
147
+
148
+ // ── wire upgrade handler ──
149
+ httpServer.on('upgrade', (req, socket, head) => {
150
+ const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`);
151
+ if (url.pathname !== '/ws/events') {
152
+ // Not an events path — let another handler process it
153
+ return;
154
+ }
155
+
156
+ if (!validateToken(url.searchParams.get('token'), expectedToken)) {
157
+ sendHttpError(socket, 401, 'Invalid or missing token');
158
+ return;
159
+ }
160
+
161
+ // WS upgrade handshake
162
+ const key = req.headers['sec-websocket-key'];
163
+ const digest = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
164
+ socket.write(
165
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
166
+ 'Upgrade: websocket\r\n' +
167
+ 'Connection: Upgrade\r\n' +
168
+ 'Sec-WebSocket-Accept: ' + digest + '\r\n\r\n'
169
+ );
170
+
171
+ const connectionId = crypto.randomBytes(16).toString('hex');
172
+ const client = {
173
+ socket,
174
+ connectionId,
175
+ lastActivity: Date.now(),
176
+ waitingPong: false,
177
+ pingSentAt: 0,
178
+ };
179
+ clients.add(client);
180
+
181
+ // Send handshake
182
+ seq = 0;
183
+ sendFrame(socket, JSON.stringify({
184
+ type: 'server.connected',
185
+ seq: 0,
186
+ ts: new Date().toISOString(),
187
+ connectionId,
188
+ }));
189
+
190
+ // Read incoming WS frames
191
+ let buffer = Buffer.alloc(0);
192
+ if (head && head.length > 0) buffer = Buffer.concat([buffer, head]);
193
+
194
+ socket.on('data', (chunk) => {
195
+ buffer = Buffer.concat([buffer, chunk]);
196
+ client.lastActivity = Date.now();
197
+
198
+ const result = parseFrames(buffer);
199
+ buffer = result.remainder;
200
+ for (const frame of result.frames) {
201
+ if (frame.opcode === 0x9) {
202
+ // Ping from client — respond with pong
203
+ sendFrame(socket, '', 0xA);
204
+ } else if (frame.opcode === 0xA) {
205
+ // Pong received — clear waiting flag
206
+ client.waitingPong = false;
207
+ } else if (frame.opcode === 0x8) {
208
+ // Close frame — echo back
209
+ sendCloseFrame(socket, 1000);
210
+ clients.delete(client);
211
+ socket.end();
212
+ return;
213
+ }
214
+ }
215
+ });
216
+
217
+ socket.on('close', () => {
218
+ clients.delete(client);
219
+ });
220
+
221
+ socket.on('error', () => {
222
+ clients.delete(client);
223
+ });
224
+ });
225
+
226
+ // ── file watcher ──
227
+ const tasksRoot = path.join(projectRoot, 'Harness', 'tasks');
228
+ if (fs.existsSync(tasksRoot)) {
229
+ try {
230
+ watcher = fs.watch(tasksRoot, { recursive: true }, () => {
231
+ if (watchTimeout) clearTimeout(watchTimeout);
232
+ watchTimeout = setTimeout(() => {
233
+ seq++;
234
+ const payload = JSON.stringify({
235
+ type: 'task.updated',
236
+ seq,
237
+ ts: new Date().toISOString(),
238
+ taskId: null,
239
+ payload: { taskCount: parseTaskList(tasksRoot).length },
240
+ });
241
+ for (const c of clients) {
242
+ try { sendFrame(c.socket, payload); } catch { /* ignore */ }
243
+ }
244
+ }, 200);
245
+ });
246
+ } catch { /* watcher unavailable — degrade gracefully */ }
247
+ }
248
+
249
+ // ── keepalive timer ──
250
+ keepaliveTimer = setInterval(() => {
251
+ const now = Date.now();
252
+ for (const c of [...clients]) {
253
+ try {
254
+ if (c.waitingPong) {
255
+ if (now - c.pingSentAt >= pingTimeoutMs) {
256
+ sendCloseFrame(c.socket, 1001);
257
+ c.socket.end();
258
+ clients.delete(c);
259
+ }
260
+ } else if (now - c.lastActivity >= keepaliveInterval) {
261
+ sendFrame(c.socket, '', 0x9);
262
+ c.waitingPong = true;
263
+ c.pingSentAt = now;
264
+ }
265
+ } catch {
266
+ clients.delete(c);
267
+ }
268
+ }
269
+ }, Math.max(tickMs, 100));
270
+
271
+ // ── close ──
272
+ function close() {
273
+ return new Promise((resolve) => {
274
+ if (keepaliveTimer) clearInterval(keepaliveTimer);
275
+ if (watchTimeout) clearTimeout(watchTimeout);
276
+ if (watcher) {
277
+ try { watcher.close(); } catch { /* ignore */ }
278
+ }
279
+ for (const c of [...clients]) {
280
+ try { sendCloseFrame(c.socket, 1001); c.socket.end(); } catch { /* ignore */ }
281
+ }
282
+ clients.clear();
283
+ resolve();
284
+ });
285
+ }
286
+
287
+ // ── public broadcast helpers (invoked externally) ──
288
+
289
+ /**
290
+ * Broadcast an arbitrary type+payload to all connected WebSocket clients.
291
+ * @param {string} type - Event type name
292
+ * @param {object} payload - Data fields (type and seq will be overridden)
293
+ */
294
+ function broadcastAll(type, payload) {
295
+ seq++;
296
+ const msg = JSON.stringify({ type, ...payload, seq, ts: new Date().toISOString() });
297
+ for (const c of [...clients]) {
298
+ try { sendFrame(c.socket, msg); } catch { /* ignore */ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Broadcast a peer.started event to all connected WS clients.
304
+ * @param {string} peerId
305
+ * @param {string} runtime
306
+ * @param {string} taskId
307
+ */
308
+ function broadcastPeerStartedFn(peerId, runtime, taskId) {
309
+ broadcastAll('peer.started', { peerId, runtime, taskId });
310
+ }
311
+
312
+ /**
313
+ * Broadcast a peer.blocked event to all connected WS clients.
314
+ * @param {string} peerId
315
+ * @param {string} reason
316
+ */
317
+ function broadcastPeerBlockedFn(peerId, reason) {
318
+ broadcastAll('peer.blocked', { peerId, reason });
319
+ }
320
+
321
+ /**
322
+ * Broadcast a session.state event to all connected WS clients.
323
+ * @param {string} sessionId
324
+ * @param {string} state
325
+ */
326
+ function broadcastSessionStateFn(sessionId, state) {
327
+ broadcastAll('session.state', { sessionId, state });
328
+ }
329
+
330
+ /**
331
+ * Get the current sequence number.
332
+ * @returns {number}
333
+ */
334
+ function getSeq() { return seq; }
335
+
336
+ /**
337
+ * Increment the sequence number and return the new value.
338
+ * @returns {number}
339
+ */
340
+ function incrementSeq() { return ++seq; }
341
+
342
+ // ── return ──
343
+ return {
344
+ wss: null,
345
+ get seq() { return seq; },
346
+ close,
347
+ broadcast: broadcastAll,
348
+ broadcastPeerStarted: broadcastPeerStartedFn,
349
+ broadcastPeerBlocked: broadcastPeerBlockedFn,
350
+ broadcastSessionState: broadcastSessionStateFn,
351
+ getSeq,
352
+ incrementSeq,
353
+ };
354
+ }