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.
- package/CHANGELOG.md +13 -0
- package/README.md +1 -0
- package/package.json +5 -2
- package/src/index.js +383 -267
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
- package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
- package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
- package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
- package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
- package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
- package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
- package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
- package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
- package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
- package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
- package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
- package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
- package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
- package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
- package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
- package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
- package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
- package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
- package/src/ui/dist/index.html +18 -0
- package/src/ui/index.html +17 -0
- package/src/ui/package.json +33 -0
- package/src/ui/pnpm-lock.yaml +1582 -0
- package/src/ui/pnpm-workspace.yaml +2 -0
- package/src/ui/src/App.tsx +86 -0
- package/src/ui/src/api.ts +93 -0
- package/src/ui/src/components/AgentsRoute.tsx +503 -0
- package/src/ui/src/components/Footer.tsx +69 -0
- package/src/ui/src/components/Header.tsx +175 -0
- package/src/ui/src/components/LoadingView.tsx +22 -0
- package/src/ui/src/components/RolesRoute.tsx +169 -0
- package/src/ui/src/components/SettingsRoute.tsx +166 -0
- package/src/ui/src/components/TaskList.tsx +388 -0
- package/src/ui/src/components/TerminalDrawer.tsx +611 -0
- package/src/ui/src/components/WorkflowRoute.tsx +670 -0
- package/src/ui/src/hooks/useReducedMotion.ts +17 -0
- package/src/ui/src/hooks/useServerConnection.ts +114 -0
- package/src/ui/src/index.css +281 -0
- package/src/ui/src/main.tsx +11 -0
- package/src/ui/src/types.ts +108 -0
- package/src/ui/tsconfig.json +21 -0
- package/src/ui/vite.config.ts +19 -0
- package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
- package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
- package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
- package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
- package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
- package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
- package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
- package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
- package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
- package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
- package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
- package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
- package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
- package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
- package/src/wf-ui-server/a2a-store.mjs +296 -0
- package/src/wf-ui-server/peer-capsule.mjs +213 -0
- package/src/wf-ui-server/pty-adapter.mjs +155 -0
- package/src/wf-ui-server/runtime-config.mjs +153 -0
- package/src/wf-ui-server/runtime-detector.mjs +374 -0
- package/src/wf-ui-server/security.mjs +67 -0
- package/src/wf-ui-server/server.mjs +849 -0
- package/src/wf-ui-server/session-registry.mjs +204 -0
- package/src/wf-ui-server/settings.mjs +100 -0
- package/src/wf-ui-server/task-parser.mjs +133 -0
- package/src/wf-ui-server/terminal-store.mjs +225 -0
- package/src/wf-ui-server/token.mjs +41 -0
- package/src/wf-ui-server/ws-events.mjs +354 -0
- package/src/wf-ui-server/ws-terminal.mjs +473 -0
- package/templates/common/.claude/commands/wf-help.md +1 -0
- package/templates/common/.claude/commands/wf-task-list.md +2 -0
- package/templates/common/.claude/commands/wf-ui.md +26 -0
- package/templates/common/.claude/commands/wf-update.md +54 -9
- package/templates/common/.claude/rules/ecc/common.md +1 -1
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
- package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
- package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
- package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
- package/templates/common/.harness-version +63 -39
- package/templates/common/.opencode/commands/wf-help.md +1 -0
- package/templates/common/.opencode/commands/wf-task-list.md +2 -0
- package/templates/common/.opencode/commands/wf-ui.md +26 -0
- package/templates/common/.opencode/commands/wf-update.md +54 -9
- package/templates/common/CLAUDE.md +1 -1
- package/templates/common/Harness/MEMORY.md +2 -0
- package/templates/common/Harness/README.md +6 -4
- package/templates/common/Harness/a2a/role-graph.json +79 -0
- package/templates/common/Harness/a2a/runtime-registry.json +5 -0
- package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
- package/templates/common/Harness/ownership.manifest.json +57 -2
- package/templates/common/Harness/scripts/README.md +134 -0
- package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
- package/templates/common/Harness/scripts/context-budget.mjs +1 -1
- package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
- package/templates/common/Harness/scripts/task-state.mjs +556 -23
- package/templates/common/Harness/scripts/validate-harness.mjs +229 -19
- package/templates/common/Harness/scripts/wf-remove.mjs +8 -3
- package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
- package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
- package/templates/common/Harness/settings.json +35 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +7 -2
- package/templates/common/Harness/specs/runtime/command-surface.json +14 -0
- package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
- package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
- package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
- package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
- package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
- package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
- package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
- package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
- package/templates/common/Harness/tasks/_template/NOTES.md +0 -3
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { validateToken } from './token.mjs';
|
|
3
|
+
|
|
4
|
+
const KEEPALIVE_INTERVAL = 15000;
|
|
5
|
+
const PING_TIMEOUT = 5000;
|
|
6
|
+
const TICK_MS = Math.min(KEEPALIVE_INTERVAL, PING_TIMEOUT) / 2;
|
|
7
|
+
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
8
|
+
|
|
9
|
+
// ── PTY process registry ──
|
|
10
|
+
// Maps sessionId to ptyProcess for writing input, resizing, or killing.
|
|
11
|
+
/** @type {Map<string, object>} */
|
|
12
|
+
const ptyRegistry = new Map();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register a PTY process for a session so ws-terminal can write/resize/kill it.
|
|
16
|
+
* @param {string} sessionId
|
|
17
|
+
* @param {object} ptyProcess
|
|
18
|
+
*/
|
|
19
|
+
export function registerPtyProcess(sessionId, ptyProcess) {
|
|
20
|
+
ptyRegistry.set(sessionId, ptyProcess);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Unregister a PTY process when the session ends.
|
|
25
|
+
* @param {string} sessionId
|
|
26
|
+
*/
|
|
27
|
+
export function unregisterPtyProcess(sessionId) {
|
|
28
|
+
ptyRegistry.delete(sessionId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function writePtyInput(sessionId, data) {
|
|
32
|
+
const ptyProcess = ptyRegistry.get(sessionId);
|
|
33
|
+
if (!ptyProcess || typeof ptyProcess.write !== 'function') return false;
|
|
34
|
+
ptyProcess.write(data || '');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function killPtyProcess(sessionId) {
|
|
39
|
+
const ptyProcess = ptyRegistry.get(sessionId);
|
|
40
|
+
if (!ptyProcess || typeof ptyProcess.kill !== 'function') return false;
|
|
41
|
+
ptyProcess.kill();
|
|
42
|
+
unregisterPtyProcess(sessionId);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Frame helpers ──
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write an unmasked text frame (opcode 0x1) to a socket.
|
|
50
|
+
* @param {import('net').Socket} socket
|
|
51
|
+
* @param {string} text
|
|
52
|
+
* @param {number} [opcode=0x1]
|
|
53
|
+
*/
|
|
54
|
+
function sendFrame(socket, text, opcode = 0x1) {
|
|
55
|
+
const data = Buffer.from(text, 'utf8');
|
|
56
|
+
const len = data.length;
|
|
57
|
+
let frame;
|
|
58
|
+
if (len < 126) {
|
|
59
|
+
frame = Buffer.alloc(2 + len);
|
|
60
|
+
frame[0] = 0x80 | opcode;
|
|
61
|
+
frame[1] = len;
|
|
62
|
+
data.copy(frame, 2);
|
|
63
|
+
} else if (len < 65536) {
|
|
64
|
+
frame = Buffer.alloc(4 + len);
|
|
65
|
+
frame[0] = 0x80 | opcode;
|
|
66
|
+
frame[1] = 126;
|
|
67
|
+
frame.writeUInt16BE(len, 2);
|
|
68
|
+
data.copy(frame, 4);
|
|
69
|
+
} else {
|
|
70
|
+
frame = Buffer.alloc(10 + len);
|
|
71
|
+
frame[0] = 0x80 | opcode;
|
|
72
|
+
frame[1] = 127;
|
|
73
|
+
frame.writeBigUInt64BE(BigInt(len), 2);
|
|
74
|
+
data.copy(frame, 10);
|
|
75
|
+
}
|
|
76
|
+
socket.write(frame);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send a close frame with the given status code.
|
|
81
|
+
* @param {import('net').Socket} socket
|
|
82
|
+
* @param {number} [code=1000]
|
|
83
|
+
*/
|
|
84
|
+
function sendCloseFrame(socket, code = 1000) {
|
|
85
|
+
const payload = Buffer.alloc(2);
|
|
86
|
+
payload.writeUInt16BE(code, 0);
|
|
87
|
+
const header = Buffer.alloc(2);
|
|
88
|
+
header[0] = 0x88;
|
|
89
|
+
header[1] = 2;
|
|
90
|
+
socket.write(Buffer.concat([header, payload]));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parse incoming WebSocket frames from a buffer of raw bytes.
|
|
95
|
+
* Handles masked client frames by unmasking the payload.
|
|
96
|
+
* @param {Buffer} buffer
|
|
97
|
+
* @returns {{ frames: Array<{ opcode: number, payload: Buffer }>, remainder: Buffer }}
|
|
98
|
+
*/
|
|
99
|
+
function parseFrames(buffer) {
|
|
100
|
+
const frames = [];
|
|
101
|
+
let offset = 0;
|
|
102
|
+
while (offset + 2 <= buffer.length) {
|
|
103
|
+
const b1 = buffer[offset];
|
|
104
|
+
const opcode = b1 & 0x0F;
|
|
105
|
+
const b2 = buffer[offset + 1];
|
|
106
|
+
const masked = (b2 & 0x80) !== 0;
|
|
107
|
+
let payloadLen = b2 & 0x7F;
|
|
108
|
+
let headerLen = 2;
|
|
109
|
+
if (payloadLen === 126) {
|
|
110
|
+
if (offset + 4 > buffer.length) break;
|
|
111
|
+
payloadLen = buffer.readUInt16BE(offset + 2);
|
|
112
|
+
headerLen = 4;
|
|
113
|
+
} else if (payloadLen === 127) {
|
|
114
|
+
if (offset + 10 > buffer.length) break;
|
|
115
|
+
payloadLen = Number(buffer.readBigUInt64BE(offset + 2));
|
|
116
|
+
headerLen = 10;
|
|
117
|
+
}
|
|
118
|
+
const maskLen = masked ? 4 : 0;
|
|
119
|
+
if (offset + headerLen + maskLen + payloadLen > buffer.length) break;
|
|
120
|
+
offset += headerLen;
|
|
121
|
+
let payload;
|
|
122
|
+
if (masked) {
|
|
123
|
+
const mask = buffer.slice(offset, offset + 4);
|
|
124
|
+
offset += 4;
|
|
125
|
+
payload = Buffer.allocUnsafe(payloadLen);
|
|
126
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
127
|
+
payload[i] = buffer[offset + i] ^ mask[i % 4];
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
payload = buffer.slice(offset, offset + payloadLen);
|
|
131
|
+
}
|
|
132
|
+
offset += payloadLen;
|
|
133
|
+
frames.push({ opcode, payload });
|
|
134
|
+
}
|
|
135
|
+
return { frames, remainder: buffer.slice(offset) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Send a short HTTP error JSON response, then close the socket.
|
|
140
|
+
* @param {import('net').Socket} sock
|
|
141
|
+
* @param {number} statusCode
|
|
142
|
+
* @param {string} message
|
|
143
|
+
*/
|
|
144
|
+
function sendHttpError(sock, statusCode, message) {
|
|
145
|
+
let code;
|
|
146
|
+
if (statusCode === 401) code = 'UNAUTHORIZED';
|
|
147
|
+
else if (statusCode === 404) code = 'NOT_FOUND';
|
|
148
|
+
else code = 'ERROR';
|
|
149
|
+
const body = JSON.stringify({ error: { code, message } });
|
|
150
|
+
const reason = statusCode === 401 ? 'Unauthorized' : statusCode === 404 ? 'Not Found' : 'Error';
|
|
151
|
+
sock.write(
|
|
152
|
+
`HTTP/1.1 ${statusCode} ${reason}\r\n` +
|
|
153
|
+
'Content-Type: application/json\r\n' +
|
|
154
|
+
`Content-Length: ${body.length}\r\n` +
|
|
155
|
+
'Connection: close\r\n\r\n' + body
|
|
156
|
+
);
|
|
157
|
+
sock.end();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Public API ──
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Attach a WebSocket /ws/terminal/:sessionId endpoint to an existing http.Server.
|
|
164
|
+
*
|
|
165
|
+
* On upgrade: validates `?token=` query param, validates sessionId exists in
|
|
166
|
+
* registry, performs WS upgrade, sends `session:state` handshake, and
|
|
167
|
+
* broadcasts PTY data to all WS clients for that session.
|
|
168
|
+
*
|
|
169
|
+
* Default mode is watch (read-only). pty:input is rejected unless
|
|
170
|
+
* session.attachMode === true.
|
|
171
|
+
*
|
|
172
|
+
* @param {import('http').Server} httpServer
|
|
173
|
+
* @param {string} expectedToken - The token clients must present.
|
|
174
|
+
* @param {import('./session-registry.mjs').SessionRegistry} sessionRegistry
|
|
175
|
+
* @param {{ keepaliveInterval?: number, pingTimeout?: number }} [options]
|
|
176
|
+
* @returns {{ close: () => Promise<void>, broadcastToSession: (sessionId: string, msg: object) => void }}
|
|
177
|
+
*/
|
|
178
|
+
export function attachTerminalWs(httpServer, expectedToken, sessionRegistry, options = {}) {
|
|
179
|
+
const keepaliveInterval = options.keepaliveInterval || KEEPALIVE_INTERVAL;
|
|
180
|
+
const pingTimeoutMs = options.pingTimeout || PING_TIMEOUT;
|
|
181
|
+
const tickMs = Math.min(keepaliveInterval, pingTimeoutMs) / 2;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Map<sessionId, Set<{ socket, connectionId, lastActivity, waitingPong, pingSentAt }>>
|
|
185
|
+
*/
|
|
186
|
+
const sessionClients = new Map();
|
|
187
|
+
let keepaliveTimer = null;
|
|
188
|
+
|
|
189
|
+
// ── wire upgrade handler ──
|
|
190
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
191
|
+
const url = new URL(req.url, `http://${req.headers.host || '127.0.0.1'}`);
|
|
192
|
+
|
|
193
|
+
// Match /ws/terminal/:sessionId
|
|
194
|
+
const match = url.pathname.match(/^\/ws\/terminal\/([^/]+)$/);
|
|
195
|
+
if (!match) {
|
|
196
|
+
// Not a terminal path — let another handler process it
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const sessionId = match[1];
|
|
201
|
+
|
|
202
|
+
if (!validateToken(url.searchParams.get('token'), expectedToken)) {
|
|
203
|
+
sendHttpError(socket, 401, 'Invalid or missing token');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate session exists
|
|
208
|
+
const session = sessionRegistry.get(sessionId);
|
|
209
|
+
if (!session) {
|
|
210
|
+
sendHttpError(socket, 404, `Session "${sessionId}" not found`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// WS upgrade handshake
|
|
215
|
+
const key = req.headers['sec-websocket-key'];
|
|
216
|
+
const digest = crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
|
|
217
|
+
socket.write(
|
|
218
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
219
|
+
'Upgrade: websocket\r\n' +
|
|
220
|
+
'Connection: Upgrade\r\n' +
|
|
221
|
+
'Sec-WebSocket-Accept: ' + digest + '\r\n\r\n'
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const connectionId = crypto.randomBytes(16).toString('hex');
|
|
225
|
+
const client = {
|
|
226
|
+
socket,
|
|
227
|
+
connectionId,
|
|
228
|
+
lastActivity: Date.now(),
|
|
229
|
+
waitingPong: false,
|
|
230
|
+
pingSentAt: 0,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Add to session's client set
|
|
234
|
+
if (!sessionClients.has(sessionId)) {
|
|
235
|
+
sessionClients.set(sessionId, new Set());
|
|
236
|
+
}
|
|
237
|
+
sessionClients.get(sessionId).add(client);
|
|
238
|
+
|
|
239
|
+
// Increment wsClientCount
|
|
240
|
+
sessionRegistry.update(sessionId, { wsClientCount: (session.wsClientCount || 0) + 1 });
|
|
241
|
+
|
|
242
|
+
// Send session:state handshake
|
|
243
|
+
sendFrame(socket, JSON.stringify({
|
|
244
|
+
type: 'session:state',
|
|
245
|
+
sessionId,
|
|
246
|
+
state: session.status || 'starting',
|
|
247
|
+
}));
|
|
248
|
+
|
|
249
|
+
// Read incoming WS frames
|
|
250
|
+
let buffer = Buffer.alloc(0);
|
|
251
|
+
if (head && head.length > 0) buffer = Buffer.concat([buffer, head]);
|
|
252
|
+
|
|
253
|
+
socket.on('data', (chunk) => {
|
|
254
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
255
|
+
client.lastActivity = Date.now();
|
|
256
|
+
|
|
257
|
+
const result = parseFrames(buffer);
|
|
258
|
+
buffer = result.remainder;
|
|
259
|
+
for (const frame of result.frames) {
|
|
260
|
+
if (frame.opcode === 0x9) {
|
|
261
|
+
// Ping from client — respond with pong
|
|
262
|
+
sendFrame(socket, '', 0xA);
|
|
263
|
+
} else if (frame.opcode === 0xA) {
|
|
264
|
+
// Pong received
|
|
265
|
+
client.waitingPong = false;
|
|
266
|
+
} else if (frame.opcode === 0x8) {
|
|
267
|
+
// Close frame — echo back
|
|
268
|
+
sendCloseFrame(socket, 1000);
|
|
269
|
+
cleanupClient(sessionId, client);
|
|
270
|
+
socket.end();
|
|
271
|
+
return;
|
|
272
|
+
} else if (frame.opcode === 0x1) {
|
|
273
|
+
// Text frame — parse as JSON
|
|
274
|
+
try {
|
|
275
|
+
const msg = JSON.parse(frame.payload.toString('utf8'));
|
|
276
|
+
handleMessage(sessionId, msg, socket);
|
|
277
|
+
} catch {
|
|
278
|
+
// Ignore malformed JSON
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
socket.on('close', () => {
|
|
285
|
+
cleanupClient(sessionId, client);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
socket.on('end', () => {
|
|
289
|
+
cleanupClient(sessionId, client);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
socket.on('error', () => {
|
|
293
|
+
cleanupClient(sessionId, client);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Handle a parsed message frame for a session.
|
|
299
|
+
* @param {string} sessionId
|
|
300
|
+
* @param {object} msg
|
|
301
|
+
* @param {import('net').Socket} socket
|
|
302
|
+
*/
|
|
303
|
+
function handleMessage(sessionId, msg, socket) {
|
|
304
|
+
const session = sessionRegistry.get(sessionId);
|
|
305
|
+
if (!session) return;
|
|
306
|
+
|
|
307
|
+
switch (msg.type) {
|
|
308
|
+
case 'pty:resize': {
|
|
309
|
+
const cols = typeof msg.cols === 'number' ? msg.cols : session.cols;
|
|
310
|
+
const rows = typeof msg.rows === 'number' ? msg.rows : session.rows;
|
|
311
|
+
if (cols === session.cols && rows === session.rows) break;
|
|
312
|
+
sessionRegistry.update(sessionId, { cols, rows });
|
|
313
|
+
|
|
314
|
+
// If a real PTY process exists, resize it
|
|
315
|
+
const ptyProcess = ptyRegistry.get(sessionId);
|
|
316
|
+
if (ptyProcess && typeof ptyProcess.resize === 'function') {
|
|
317
|
+
ptyProcess.resize(cols, rows);
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
case 'pty:input': {
|
|
323
|
+
if (!session.attachMode) {
|
|
324
|
+
sendFrame(socket, JSON.stringify({
|
|
325
|
+
type: 'session:error',
|
|
326
|
+
sessionId,
|
|
327
|
+
message: 'Input rejected: session is in watch mode. Enable attach mode to send input.',
|
|
328
|
+
}));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const ptyProcess = ptyRegistry.get(sessionId);
|
|
333
|
+
if (ptyProcess && typeof ptyProcess.write === 'function') {
|
|
334
|
+
// Write input to the real PTY process
|
|
335
|
+
ptyProcess.write(msg.data || '');
|
|
336
|
+
if (typeof options.onTerminalInput === 'function') {
|
|
337
|
+
options.onTerminalInput(session, msg.data || '');
|
|
338
|
+
}
|
|
339
|
+
sendFrame(socket, JSON.stringify({
|
|
340
|
+
type: 'pty:input-accepted',
|
|
341
|
+
sessionId,
|
|
342
|
+
}));
|
|
343
|
+
} else {
|
|
344
|
+
// No PTY attached — inform the client
|
|
345
|
+
sendFrame(socket, JSON.stringify({
|
|
346
|
+
type: 'session:error',
|
|
347
|
+
sessionId,
|
|
348
|
+
message: 'No PTY process attached to this session. Cannot send input.',
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
case 'control:attach-mode': {
|
|
355
|
+
const attachMode = Boolean(msg.attachMode);
|
|
356
|
+
sessionRegistry.update(sessionId, { attachMode });
|
|
357
|
+
if (typeof options.onAttachModeChange === 'function') {
|
|
358
|
+
options.onAttachModeChange(sessionRegistry.get(sessionId), attachMode);
|
|
359
|
+
}
|
|
360
|
+
sendFrame(socket, JSON.stringify({
|
|
361
|
+
type: 'session:state',
|
|
362
|
+
sessionId,
|
|
363
|
+
state: session.status || 'running',
|
|
364
|
+
attachMode,
|
|
365
|
+
}));
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'control:stop': {
|
|
370
|
+
killPtyProcess(sessionId);
|
|
371
|
+
|
|
372
|
+
sendFrame(socket, JSON.stringify({
|
|
373
|
+
type: 'session:state',
|
|
374
|
+
sessionId,
|
|
375
|
+
state: 'exited',
|
|
376
|
+
}));
|
|
377
|
+
sessionRegistry.update(sessionId, { status: 'exited' });
|
|
378
|
+
if (typeof options.onSessionState === 'function') {
|
|
379
|
+
options.onSessionState(sessionRegistry.get(sessionId), 'exited');
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
default:
|
|
385
|
+
// Unknown frame type — ignore
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Remove a client from a session's client set and decrement wsClientCount.
|
|
392
|
+
* @param {string} sessionId
|
|
393
|
+
* @param {object} client
|
|
394
|
+
*/
|
|
395
|
+
function cleanupClient(sessionId, client) {
|
|
396
|
+
// Guard against double-cleanup when both 'error' and 'close' fire
|
|
397
|
+
if (client._cleanedUp) return;
|
|
398
|
+
client._cleanedUp = true;
|
|
399
|
+
|
|
400
|
+
const clients = sessionClients.get(sessionId);
|
|
401
|
+
if (clients) {
|
|
402
|
+
clients.delete(client);
|
|
403
|
+
if (clients.size === 0) {
|
|
404
|
+
sessionClients.delete(sessionId);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const session = sessionRegistry.get(sessionId);
|
|
409
|
+
if (session) {
|
|
410
|
+
const count = Math.max(0, (session.wsClientCount || 1) - 1);
|
|
411
|
+
sessionRegistry.update(sessionId, { wsClientCount: count });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Broadcast a message to all WS clients for a specific session.
|
|
417
|
+
* @param {string} sessionId
|
|
418
|
+
* @param {object} msg - Plain object to JSON-serialize and send.
|
|
419
|
+
*/
|
|
420
|
+
function broadcastToSession(sessionId, msg) {
|
|
421
|
+
const clients = sessionClients.get(sessionId);
|
|
422
|
+
if (!clients || clients.size === 0) return;
|
|
423
|
+
|
|
424
|
+
const payload = JSON.stringify(msg);
|
|
425
|
+
for (const c of clients) {
|
|
426
|
+
try {
|
|
427
|
+
sendFrame(c.socket, payload);
|
|
428
|
+
} catch {
|
|
429
|
+
/* ignore disconnected */
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── keepalive timer ──
|
|
435
|
+
keepaliveTimer = setInterval(() => {
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
for (const [, clients] of sessionClients) {
|
|
438
|
+
for (const c of [...clients]) {
|
|
439
|
+
try {
|
|
440
|
+
if (c.waitingPong) {
|
|
441
|
+
if (now - c.pingSentAt >= pingTimeoutMs) {
|
|
442
|
+
sendCloseFrame(c.socket, 1001);
|
|
443
|
+
c.socket.end();
|
|
444
|
+
clients.delete(c);
|
|
445
|
+
}
|
|
446
|
+
} else if (now - c.lastActivity >= keepaliveInterval) {
|
|
447
|
+
sendFrame(c.socket, '', 0x9);
|
|
448
|
+
c.waitingPong = true;
|
|
449
|
+
c.pingSentAt = now;
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
clients.delete(c);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}, Math.max(tickMs, 100));
|
|
457
|
+
|
|
458
|
+
// ── close ──
|
|
459
|
+
function close() {
|
|
460
|
+
return new Promise((resolve) => {
|
|
461
|
+
if (keepaliveTimer) clearInterval(keepaliveTimer);
|
|
462
|
+
for (const [, clients] of sessionClients) {
|
|
463
|
+
for (const c of [...clients]) {
|
|
464
|
+
try { sendCloseFrame(c.socket, 1001); c.socket.end(); } catch { /* ignore */ }
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
sessionClients.clear();
|
|
468
|
+
resolve();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return { close, broadcastToSession };
|
|
473
|
+
}
|
|
@@ -23,6 +23,7 @@ which returns this same table without loading `Harness/MEMORY.md` or entering WF
|
|
|
23
23
|
| `/wf-task-list` | direct command | `/wf-task-list` | List all task capsules with status, phase, and dependencies. |
|
|
24
24
|
| `/wf-task-archive [--apply]` | direct command | `/wf-task-archive --apply` | Archive completed task capsules (dry-run by default). |
|
|
25
25
|
| `/wf-command-create <wf-command-id>` | direct command | `/wf-command-create wf-report --direct` | Create or modify wf-* command surfaces atomically from the command registry. |
|
|
26
|
+
| `/wf-ui` | direct command | `/wf-ui` | Start the local Harness backend and browser control panel. |
|
|
26
27
|
|
|
27
28
|
Source of truth: `Harness/README.md#Skill Commands` plus installed skills under
|
|
28
29
|
`.claude/skills/` (Claude Code and OpenCode adapters) or `.agents/skills/`
|
|
@@ -21,4 +21,6 @@ Run: `node Harness/scripts/task-state.mjs list --json`
|
|
|
21
21
|
## Return
|
|
22
22
|
|
|
23
23
|
- JSON output of all task capsules with status, phase, dependencies, and archive eligibility.
|
|
24
|
+
- Includes a `graph` section: roots, dependency edges, block edges, and orphaned dependency warnings.
|
|
25
|
+
- Human-readable mode renders the graph with ASCII arrows: `from ──▶ to` (depends) and `from ▸▸ to` (blocks).
|
|
24
26
|
- If the command fails, report the error and do not retry.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Open the Harness browser control panel for task capsules, workflow graphs, agent peers, settings, and terminal observability
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /wf-ui
|
|
6
|
+
|
|
7
|
+
Start the Harness browser control panel directly. This is a direct command.
|
|
8
|
+
Do not invoke a skill, do not start WF mode, and do not dispatch agents.
|
|
9
|
+
|
|
10
|
+
Run from the project root:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
create-harness-vibe-coding wf-ui --project . --host 127.0.0.1 --port 0 --open
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If the global binary is not available, use the package fallback:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx create-harness-vibe-coding@0.8.19 wf-ui --project . --host 127.0.0.1 --port 0 --open
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Rules:
|
|
23
|
+
- Bind only to `127.0.0.1`.
|
|
24
|
+
- Use port `0` unless the user asks for a fixed port.
|
|
25
|
+
- Leave the server running until the user stops it.
|
|
26
|
+
- Return the local URL printed by the command.
|
|
@@ -22,7 +22,23 @@ targeted inspection.
|
|
|
22
22
|
|
|
23
23
|
## Flow
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Scope resolution is deterministic:
|
|
26
|
+
|
|
27
|
+
- Current project has `Harness/scripts/wf-update-check.mjs`: update the project
|
|
28
|
+
install from that script.
|
|
29
|
+
- A global runtime is discoverable from `Harness/.harness-version.globalDir`,
|
|
30
|
+
`HARNESS_GLOBAL_HOME`, or the default `~/.harness/create-harness-vibe-coding`:
|
|
31
|
+
update that runtime too, then sync its Claude/Codex/OpenCode host-global
|
|
32
|
+
copies.
|
|
33
|
+
- Current project has no Harness install: update only the global runtime and
|
|
34
|
+
host-global copies. Do not scaffold or modify the project unless the user
|
|
35
|
+
explicitly asks to install Harness there.
|
|
36
|
+
`Harness/scripts/wf-update-runner.mjs` implements this multi-scope routing.
|
|
37
|
+
Older installs that do not have the runner fall back to
|
|
38
|
+
`Harness/scripts/wf-update-check.mjs`.
|
|
39
|
+
|
|
40
|
+
1. Run `node Harness/scripts/wf-update-runner.mjs --json` first when present;
|
|
41
|
+
otherwise run `node Harness/scripts/wf-update-check.mjs --json`. Use the
|
|
26
42
|
`agent` block as the action plan. Preserve `agent.releaseHighlights` for the
|
|
27
43
|
user-facing update summary. Current updaters try npm
|
|
28
44
|
`create-harness-vibe-coding@latest` first, then the canonical GitHub source
|
|
@@ -33,9 +49,11 @@ targeted inspection.
|
|
|
33
49
|
starter files may be created, and checksum-matching legacy architecture can
|
|
34
50
|
move to `Harness/project/architecture.md`. Harness/README.md is merge-tier,
|
|
35
51
|
not PRESERVE.
|
|
36
|
-
3. If `agent.safeApplyCommand` is present, run
|
|
37
|
-
adopted metadata-only files
|
|
38
|
-
|
|
52
|
+
3. If `agent.safeApplyCommand` is present, run the runner apply path to apply
|
|
53
|
+
SAFE, NEW, and adopted metadata-only files across every discovered scope
|
|
54
|
+
before spending AI time on conflicts. Default multi-scope command:
|
|
55
|
+
`node Harness/scripts/wf-update-runner.mjs --apply-safe --json`. Single-scope
|
|
56
|
+
fallback command: `node Harness/scripts/wf-update-check.mjs --apply-safe`.
|
|
39
57
|
Framework-owned templates, commands, skills, agents, and scripts are
|
|
40
58
|
script-owned and should be overwritten by the updater after checksum
|
|
41
59
|
validation.
|
|
@@ -53,11 +71,38 @@ targeted inspection.
|
|
|
53
71
|
`--accept-local <file>`, `--accept-merged <file>`, or
|
|
54
72
|
`--accept-template <file>`; do not hand-edit `Harness/.harness-version`.
|
|
55
73
|
Ask the user only when the intent is ambiguous.
|
|
56
|
-
7. Run `node Harness/scripts/wf-update-
|
|
57
|
-
conflicts have script-recorded decisions.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
7. Run `node Harness/scripts/wf-update-runner.mjs --finalize --json` after all
|
|
75
|
+
conflicts have script-recorded decisions. Single-scope fallback:
|
|
76
|
+
`node Harness/scripts/wf-update-check.mjs --finalize`. Use strict `--apply` only when
|
|
77
|
+
the JSON plan has zero conflicts.
|
|
78
|
+
8. After update, sync and validate all machine surfaces:
|
|
79
|
+
```
|
|
80
|
+
node Harness/scripts/sync-host-global.mjs --json
|
|
81
|
+
node Harness/scripts/sync-host-global.mjs --apply --json
|
|
82
|
+
node Harness/scripts/validate-harness.mjs
|
|
83
|
+
node Harness/scripts/validate-harness.mjs --manifest-audit
|
|
84
|
+
node Harness/scripts/scan-clean.mjs --json
|
|
85
|
+
```
|
|
86
|
+
`sync-host-global.mjs` is a no-op for project-only installs. For global
|
|
87
|
+
installs it compares host-global files against the runtime sources listed in
|
|
88
|
+
`.harness-version.hostGlobal.targets`; missing or Harness-marked stale copies
|
|
89
|
+
are repaired by script, while user-looking files stay conflicts.
|
|
90
|
+
9. If the update reports "Already up to date" but files are missing (e.g.,
|
|
91
|
+
new commands not showing up on a platform), run:
|
|
92
|
+
```
|
|
93
|
+
node Harness/scripts/wf-update-runner.mjs --repair --json
|
|
94
|
+
node Harness/scripts/wf-update-runner.mjs --repair --apply-safe --json
|
|
95
|
+
node Harness/scripts/wf-update-runner.mjs --repair --finalize --json
|
|
96
|
+
node Harness/scripts/wf-update-check.mjs --repair --json
|
|
97
|
+
node Harness/scripts/wf-update-check.mjs --repair --apply-safe
|
|
98
|
+
node Harness/scripts/wf-update-check.mjs --repair --finalize
|
|
99
|
+
node Harness/scripts/sync-host-global.mjs --apply --json
|
|
100
|
+
node Harness/scripts/validate-harness.mjs --manifest-audit
|
|
101
|
+
```
|
|
102
|
+
`--repair` bypasses the version check and forces a full file diff against
|
|
103
|
+
the latest remote template. This catches files that were missed during a
|
|
104
|
+
previous partial update where the version was bumped but not all files
|
|
105
|
+
were written.
|
|
61
106
|
|
|
62
107
|
Codex users without a direct command surface: use `$wf-update` (skill path) or
|
|
63
108
|
`node Harness/scripts/wf-update-check.mjs`.
|
|
@@ -8,7 +8,7 @@ alwaysApply: true
|
|
|
8
8
|
## Context
|
|
9
9
|
|
|
10
10
|
- Start with `CLAUDE.md`. When `Harness/` exists, also read `Harness/memory/startup-hints.md` (L2 lightweight digest, not full router).
|
|
11
|
-
- When the user explicitly invokes a workflow command (`/wf-*`, `$wf-*`, or `/skills wf-*`), excluding `/wf-help`, `$wf-help`, `/skills wf-help`, `/wf-update`, `$wf-update`, `/skills wf-update`, `/wf-task-record`, `$wf-task-record`, `/skills wf-task-record`, `/wf-task-list`, `$wf-task-list`, `/skills wf-task-list`, `/wf-task-archive`, `$wf-task-archive`, `/skills wf-task-archive`, `/wf-command-create`, `$wf-command-create`,
|
|
11
|
+
- When the user explicitly invokes a workflow command (`/wf-*`, `$wf-*`, or `/skills wf-*`), excluding `/wf-help`, `$wf-help`, `/skills wf-help`, `/wf-update`, `$wf-update`, `/skills wf-update`, `/wf-task-record`, `$wf-task-record`, `/skills wf-task-record`, `/wf-task-list`, `$wf-task-list`, `/skills wf-task-list`, `/wf-task-archive`, `$wf-task-archive`, `/skills wf-task-archive`, `/wf-command-create`, `$wf-command-create`, `/skills wf-command-create`, `/wf-ui`, `$wf-ui`, and `/skills wf-ui`, load `Harness/MEMORY.md` and `Harness/README.md`.
|
|
12
12
|
- For simple single-step tasks without `/wf-*`, operate in direct mode: skip the Harness router and execute directly.
|
|
13
13
|
- Do not bulk-read `Harness/`. Load by router trigger.
|
|
14
14
|
- Keep `Harness/tasks/<task-id>/PROGRESS.md` and `Harness/tasks/<task-id>/PLAN.md` current when work has multiple steps, files, or agents.
|
|
@@ -48,7 +48,7 @@ that auto scanning costs more than it helps.
|
|
|
48
48
|
obligations are covered, two different confirmation strategies are empty,
|
|
49
49
|
and unresolved uncertainty is recorded.
|
|
50
50
|
2. CEO never edits production source. CEO may write only
|
|
51
|
-
`Harness/tasks/
|
|
51
|
+
`Harness/tasks/continuous/PLAN.md` and `Harness/tasks/continuous/PROGRESS.md`.
|
|
52
52
|
3. Build a project profile each W0 cycle and dispatch only the selected probes;
|
|
53
53
|
invoke oracle and spark searchers when evidence justifies them.
|
|
54
54
|
4. One accepted finding per cycle: <=3 files and <=50 changed lines. Larger
|
|
@@ -61,8 +61,8 @@ that auto scanning costs more than it helps.
|
|
|
61
61
|
below 3.
|
|
62
62
|
8. Intent Checkpoint is adaptive: 2 -> 5 -> 10 cycles, exactly two questions.
|
|
63
63
|
9. Record compact evidence per cycle; do not paste full logs or transcripts.
|
|
64
|
-
10. A bounded test tick still creates or updates `Harness/tasks/
|
|
65
|
-
and `Harness/tasks/
|
|
64
|
+
10. A bounded test tick still creates or updates `Harness/tasks/continuous/PLAN.md`
|
|
65
|
+
and `Harness/tasks/continuous/PROGRESS.md`; missing auto capsule evidence is a
|
|
66
66
|
failed cycle record.
|
|
67
67
|
|
|
68
68
|
## Loop
|
|
@@ -44,5 +44,5 @@ replaying search transcripts.
|
|
|
44
44
|
6. Value reflection is required every cycle: source, why it matters, deviation,
|
|
45
45
|
evidence, and milestone progress.
|
|
46
46
|
|
|
47
|
-
Active roadmap: `Harness/tasks/
|
|
48
|
-
Per-cycle evidence: `Harness/tasks/
|
|
47
|
+
Active roadmap: `Harness/tasks/continuous/SPARK-ROADMAP.md`.
|
|
48
|
+
Per-cycle evidence: `Harness/tasks/continuous/PROGRESS.md`.
|