create-harness-vibe-coding 0.8.17 → 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 +22 -0
- package/README-CN.md +2 -0
- package/README.md +3 -0
- package/package.json +5 -2
- package/src/generator.js +613 -97
- package/src/index.js +556 -309
- package/src/prompts.js +18 -0
- 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-command-create.md +58 -0
- package/templates/common/.claude/commands/wf-help.md +5 -0
- package/templates/common/.claude/commands/wf-task-archive.md +26 -0
- package/templates/common/.claude/commands/wf-task-list.md +26 -0
- package/templates/common/.claude/commands/wf-task-record.md +24 -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-agents-docs/SKILL.md +15 -30
- 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-command-create/SKILL.md +37 -0
- package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
- package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
- package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
- package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
- 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 +105 -47
- package/templates/common/.opencode/commands/wf-command-create.md +61 -0
- package/templates/common/.opencode/commands/wf-help.md +5 -0
- package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
- package/templates/common/.opencode/commands/wf-task-list.md +29 -0
- package/templates/common/.opencode/commands/wf-task-record.md +27 -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 +8 -6
- package/templates/common/Harness/MEMORY.md +11 -0
- package/templates/common/Harness/README.md +21 -38
- 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 +142 -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 +949 -26
- package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
- package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
- 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/guides/SETUP.md +8 -0
- package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
- package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
- package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
- package/templates/common/Harness/specs/runtime/subagents.md +6 -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/specs/workflows/WF-MAX.md +5 -0
- package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
- 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/STATE.json +6 -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
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# /wf-command-create
|
|
2
|
+
|
|
3
|
+
Create or modify a Harness wf-* command surface atomically. Do not invoke a skill or start WF mode.
|
|
4
|
+
|
|
5
|
+
## Classification
|
|
6
|
+
|
|
7
|
+
DIRECT command. It is a Harness maintenance command: it must create or resume a task capsule, then update every command surface declared by `Harness/specs/runtime/command-surface.json`. Never loads `Harness/MEMORY.md` as part of command routing.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
/wf-command-create <wf-command-id> [--direct|--workflow] [--task <task-id>]
|
|
12
|
+
|
|
13
|
+
- Command id must be `wf` or `wf-*` kebab-case.
|
|
14
|
+
- Use an existing task capsule when `--task` is supplied or when the active task clearly matches.
|
|
15
|
+
- Otherwise create a `task-<verb>-<noun>[-detail]` capsule with `node Harness/scripts/task-state.mjs record <task-id> --create --apply --json`.
|
|
16
|
+
- Record the command id, classification, and validation evidence in the task capsule.
|
|
17
|
+
|
|
18
|
+
## Required Surface Checklist
|
|
19
|
+
|
|
20
|
+
Update all applicable files before returning:
|
|
21
|
+
|
|
22
|
+
- `Harness/specs/runtime/command-surface.json`
|
|
23
|
+
- `.claude/commands/<id>.md`
|
|
24
|
+
- `.opencode/commands/<id>.md`
|
|
25
|
+
- `.claude/skills/<id>/SKILL.md` when Codex compatibility or workflow routing is needed
|
|
26
|
+
- `.agents/skills/<id>/SKILL.md` mirror when a Claude skill exists
|
|
27
|
+
- Matching `templates/common/...` files
|
|
28
|
+
- `CLAUDE.md` and `templates/common/CLAUDE.md`
|
|
29
|
+
- `.claude/rules/ecc/common.md` and template mirror
|
|
30
|
+
- `.claude/commands/wf-help.md`, `.opencode/commands/wf-help.md`, and template mirrors
|
|
31
|
+
- `Harness/README.md`, `Harness/MEMORY.md`, and template mirrors
|
|
32
|
+
- `Harness/scripts/validate-harness.mjs` and template mirror
|
|
33
|
+
- `Harness/scripts/wf-remove.mjs` and template mirror
|
|
34
|
+
- `tests/generator.test.js`, `tests/anti-drift.test.js`, and `tests/validate-harness.test.js`
|
|
35
|
+
- `Harness/.harness-version`, `templates/common/.harness-version`, and ownership manifests via `node scripts/build-version.mjs`
|
|
36
|
+
|
|
37
|
+
## Validation
|
|
38
|
+
|
|
39
|
+
Run, at minimum:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm test
|
|
43
|
+
node Harness/scripts/validate-harness.mjs
|
|
44
|
+
node templates/common/Harness/scripts/validate-harness.mjs
|
|
45
|
+
node Harness/scripts/context-budget.mjs --json
|
|
46
|
+
node scripts/build-version.mjs --check
|
|
47
|
+
npm run check:mirrors
|
|
48
|
+
git diff --check
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If a check is blocked by an existing unrelated repository issue, record the command, failure, and reason in the task capsule.
|
|
52
|
+
|
|
53
|
+
## Return
|
|
54
|
+
|
|
55
|
+
- Command id and classification.
|
|
56
|
+
- Task capsule path.
|
|
57
|
+
- Files changed by surface category.
|
|
58
|
+
- Verification results.
|
|
@@ -19,6 +19,11 @@ which returns this same table without loading `Harness/MEMORY.md` or entering WF
|
|
|
19
19
|
| `/wf-readme <task>` | workflow skill | `/wf-readme polish quickstart` | Preserve, merge, or improve README docs without trampling existing project documentation. |
|
|
20
20
|
| `/wf-update` | direct command | `/wf-update` | Check/apply Harness scaffold updates with safe file classification and conflict handling. |
|
|
21
21
|
| `/wf-remove` | workflow skill | `/wf-remove` | Safely remove Harness files while preserving project/user data unless explicitly purged. |
|
|
22
|
+
| `/wf-task-record <task-id>` | direct command | `/wf-task-record my-feature --create` | Record user intent into a task capsule (wraps task-state.mjs). |
|
|
23
|
+
| `/wf-task-list` | direct command | `/wf-task-list` | List all task capsules with status, phase, and dependencies. |
|
|
24
|
+
| `/wf-task-archive [--apply]` | direct command | `/wf-task-archive --apply` | Archive completed task capsules (dry-run by default). |
|
|
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. |
|
|
22
27
|
|
|
23
28
|
Source of truth: `Harness/README.md#Skill Commands` plus installed skills under
|
|
24
29
|
`.claude/skills/` (Claude Code and OpenCode adapters) or `.agents/skills/`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# /wf-task-archive
|
|
2
|
+
|
|
3
|
+
Archive completed/obsolete task capsules to `Harness/tasks/_archive/`. Do not invoke a skill or start WF mode.
|
|
4
|
+
|
|
5
|
+
## Classification
|
|
6
|
+
|
|
7
|
+
DIRECT command. Wraps `node Harness/scripts/task-state.mjs archive`. Never loads Harness/MEMORY.md.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
/wf-task-archive [--dry-run] [--apply] [--task <id>] [--keep <n>]
|
|
12
|
+
|
|
13
|
+
- Defaults to dry-run (`--dry-run`) without `--apply`.
|
|
14
|
+
- With `--apply`: moves eligible task directories into yearly subdirectories under `_archive/`.
|
|
15
|
+
- With `--task <id>`: targets a specific task for archive eligibility check.
|
|
16
|
+
- With `--keep <n>`: keeps N most recent non-archived task capsules (default 5).
|
|
17
|
+
- Tasks with active, blocked, in_progress, running, pending, or needs-user-decision status are never auto-archived.
|
|
18
|
+
|
|
19
|
+
## Execution
|
|
20
|
+
|
|
21
|
+
Run: `node Harness/scripts/task-state.mjs archive [--dry-run] [--apply] [--task <id>] [--keep <n>] [--json]`
|
|
22
|
+
|
|
23
|
+
## Return
|
|
24
|
+
|
|
25
|
+
- JSON output with archive plan: scanned, archiveable, toArchive, kept, skipped counts and per-task results.
|
|
26
|
+
- If the command fails, report the error and do not retry.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# /wf-task-list
|
|
2
|
+
|
|
3
|
+
List Harness task capsules with state, phase, status, and dependency info. Do not invoke a skill or start WF mode.
|
|
4
|
+
|
|
5
|
+
## Classification
|
|
6
|
+
|
|
7
|
+
DIRECT command. Wraps `node Harness/scripts/task-state.mjs list --json`. Never loads Harness/MEMORY.md.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
/wf-task-list [--json]
|
|
12
|
+
|
|
13
|
+
- By default, returns a human-readable listing of all task capsules.
|
|
14
|
+
- With --json, returns structured JSON output.
|
|
15
|
+
- Shows active, open, blocked, verified tasks with dependency info.
|
|
16
|
+
|
|
17
|
+
## Execution
|
|
18
|
+
|
|
19
|
+
Run: `node Harness/scripts/task-state.mjs list --json`
|
|
20
|
+
|
|
21
|
+
## Return
|
|
22
|
+
|
|
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).
|
|
26
|
+
- If the command fails, report the error and do not retry.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# /wf-task-record
|
|
2
|
+
|
|
3
|
+
Record a task into a Harness task capsule. Do not invoke a skill or start WF mode.
|
|
4
|
+
|
|
5
|
+
## Classification
|
|
6
|
+
|
|
7
|
+
DIRECT command. Wraps `node Harness/scripts/task-state.mjs record`. Never loads Harness/MEMORY.md.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
/wf-task-record [<task-id>] [--title <slug>] [--note <text>] [--context <text>] [--new] [--create] [--status <status>] [--mode <mode>] [--text "description"]
|
|
12
|
+
|
|
13
|
+
- With <task-id>: record to that task (existing semantics).
|
|
14
|
+
- Without <task-id>: --title/--note/--context required. Deterministically matches open tasks; updates if unique match found; creates new if no match; fails if ambiguous unless --new.
|
|
15
|
+
- Never uses LLM or embeddings — slug + keyword overlap only.
|
|
16
|
+
|
|
17
|
+
## Execution
|
|
18
|
+
|
|
19
|
+
Run: `node Harness/scripts/task-state.mjs record [<task-id>] [--title <slug>] [--note <text>] [--context <text>] [--new] [--create] [--apply] [--text "..."] [--status <status>] [--mode <mode>] [--json]`
|
|
20
|
+
|
|
21
|
+
## Return
|
|
22
|
+
|
|
23
|
+
- Output of `node Harness/scripts/task-state.mjs record ... --json`
|
|
24
|
+
- 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.
|