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.
Files changed (150) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +3 -0
  4. package/package.json +5 -2
  5. package/src/generator.js +613 -97
  6. package/src/index.js +556 -309
  7. package/src/prompts.js +18 -0
  8. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js +17 -0
  9. package/src/ui/dist/assets/AgentsRoute-CuSKYmFu.js.map +1 -0
  10. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js +7 -0
  11. package/src/ui/dist/assets/RolesRoute-DOzTYVRm.js.map +1 -0
  12. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js +12 -0
  13. package/src/ui/dist/assets/SettingsRoute-DPU7jSQm.js.map +1 -0
  14. package/src/ui/dist/assets/TaskList-BN4r8s1i.js +50 -0
  15. package/src/ui/dist/assets/TaskList-BN4r8s1i.js.map +1 -0
  16. package/src/ui/dist/assets/TerminalDrawer-6GBZ9nXN.css +32 -0
  17. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js +74 -0
  18. package/src/ui/dist/assets/TerminalDrawer-Cej4PxsH.js.map +1 -0
  19. package/src/ui/dist/assets/WorkflowRoute-BnuhLJ6X.css +1 -0
  20. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js +39 -0
  21. package/src/ui/dist/assets/WorkflowRoute-C-clKe-j.js.map +1 -0
  22. package/src/ui/dist/assets/index-BOVYWntB.js +117 -0
  23. package/src/ui/dist/assets/index-BOVYWntB.js.map +1 -0
  24. package/src/ui/dist/assets/index-DaUObq0H.css +1 -0
  25. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js +7 -0
  26. package/src/ui/dist/assets/maximize-2-LNrr_rKr.js.map +1 -0
  27. package/src/ui/dist/assets/plus-QuTJyoT3.js +7 -0
  28. package/src/ui/dist/assets/plus-QuTJyoT3.js.map +1 -0
  29. package/src/ui/dist/assets/proxy-D0vUbds5.js +2 -0
  30. package/src/ui/dist/assets/proxy-D0vUbds5.js.map +1 -0
  31. package/src/ui/dist/assets/refresh-cw-JNmExijd.js +7 -0
  32. package/src/ui/dist/assets/refresh-cw-JNmExijd.js.map +1 -0
  33. package/src/ui/dist/assets/terminal-BBQvnOAK.js +7 -0
  34. package/src/ui/dist/assets/terminal-BBQvnOAK.js.map +1 -0
  35. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js +7 -0
  36. package/src/ui/dist/assets/useReducedMotion-BqmH0Tf7.js.map +1 -0
  37. package/src/ui/dist/assets/x-DoIOGAJz.js +7 -0
  38. package/src/ui/dist/assets/x-DoIOGAJz.js.map +1 -0
  39. package/src/ui/dist/index.html +18 -0
  40. package/src/ui/index.html +17 -0
  41. package/src/ui/package.json +33 -0
  42. package/src/ui/pnpm-lock.yaml +1582 -0
  43. package/src/ui/pnpm-workspace.yaml +2 -0
  44. package/src/ui/src/App.tsx +86 -0
  45. package/src/ui/src/api.ts +93 -0
  46. package/src/ui/src/components/AgentsRoute.tsx +503 -0
  47. package/src/ui/src/components/Footer.tsx +69 -0
  48. package/src/ui/src/components/Header.tsx +175 -0
  49. package/src/ui/src/components/LoadingView.tsx +22 -0
  50. package/src/ui/src/components/RolesRoute.tsx +169 -0
  51. package/src/ui/src/components/SettingsRoute.tsx +166 -0
  52. package/src/ui/src/components/TaskList.tsx +388 -0
  53. package/src/ui/src/components/TerminalDrawer.tsx +611 -0
  54. package/src/ui/src/components/WorkflowRoute.tsx +670 -0
  55. package/src/ui/src/hooks/useReducedMotion.ts +17 -0
  56. package/src/ui/src/hooks/useServerConnection.ts +114 -0
  57. package/src/ui/src/index.css +281 -0
  58. package/src/ui/src/main.tsx +11 -0
  59. package/src/ui/src/types.ts +108 -0
  60. package/src/ui/tsconfig.json +21 -0
  61. package/src/ui/vite.config.ts +19 -0
  62. package/src/wf-ui-server/__tests__/a2a-store.test.mjs +79 -0
  63. package/src/wf-ui-server/__tests__/peer-capsule.test.mjs +268 -0
  64. package/src/wf-ui-server/__tests__/pty-adapter.test.mjs +70 -0
  65. package/src/wf-ui-server/__tests__/runtime-config.test.mjs +43 -0
  66. package/src/wf-ui-server/__tests__/runtime-detector.test.mjs +90 -0
  67. package/src/wf-ui-server/__tests__/security.test.mjs +59 -0
  68. package/src/wf-ui-server/__tests__/server.integration.test.mjs +150 -0
  69. package/src/wf-ui-server/__tests__/session-registry.test.mjs +238 -0
  70. package/src/wf-ui-server/__tests__/settings.test.mjs +135 -0
  71. package/src/wf-ui-server/__tests__/task-parser.test.mjs +200 -0
  72. package/src/wf-ui-server/__tests__/terminal-store.test.mjs +138 -0
  73. package/src/wf-ui-server/__tests__/token.test.mjs +48 -0
  74. package/src/wf-ui-server/__tests__/ws-events.integration.test.mjs +419 -0
  75. package/src/wf-ui-server/__tests__/ws-terminal.integration.test.mjs +383 -0
  76. package/src/wf-ui-server/a2a-store.mjs +296 -0
  77. package/src/wf-ui-server/peer-capsule.mjs +213 -0
  78. package/src/wf-ui-server/pty-adapter.mjs +155 -0
  79. package/src/wf-ui-server/runtime-config.mjs +153 -0
  80. package/src/wf-ui-server/runtime-detector.mjs +374 -0
  81. package/src/wf-ui-server/security.mjs +67 -0
  82. package/src/wf-ui-server/server.mjs +849 -0
  83. package/src/wf-ui-server/session-registry.mjs +204 -0
  84. package/src/wf-ui-server/settings.mjs +100 -0
  85. package/src/wf-ui-server/task-parser.mjs +133 -0
  86. package/src/wf-ui-server/terminal-store.mjs +225 -0
  87. package/src/wf-ui-server/token.mjs +41 -0
  88. package/src/wf-ui-server/ws-events.mjs +354 -0
  89. package/src/wf-ui-server/ws-terminal.mjs +473 -0
  90. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  91. package/templates/common/.claude/commands/wf-help.md +5 -0
  92. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  93. package/templates/common/.claude/commands/wf-task-list.md +26 -0
  94. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  95. package/templates/common/.claude/commands/wf-ui.md +26 -0
  96. package/templates/common/.claude/commands/wf-update.md +54 -9
  97. package/templates/common/.claude/rules/ecc/common.md +1 -1
  98. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  99. package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -3
  100. package/templates/common/.claude/skills/wf-auto-spark/SKILL.md +2 -2
  101. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  102. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  103. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  104. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  105. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  106. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  107. package/templates/common/.claude/skills/wf-ui/SKILL.md +78 -0
  108. package/templates/common/.claude/skills/wf-update/SKILL.md +55 -58
  109. package/templates/common/.harness-version +105 -47
  110. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  111. package/templates/common/.opencode/commands/wf-help.md +5 -0
  112. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  113. package/templates/common/.opencode/commands/wf-task-list.md +29 -0
  114. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  115. package/templates/common/.opencode/commands/wf-ui.md +26 -0
  116. package/templates/common/.opencode/commands/wf-update.md +54 -9
  117. package/templates/common/CLAUDE.md +8 -6
  118. package/templates/common/Harness/MEMORY.md +11 -0
  119. package/templates/common/Harness/README.md +21 -38
  120. package/templates/common/Harness/a2a/role-graph.json +79 -0
  121. package/templates/common/Harness/a2a/runtime-registry.json +5 -0
  122. package/templates/common/Harness/a2a/skills/terminal-control.json +15 -0
  123. package/templates/common/Harness/ownership.manifest.json +142 -2
  124. package/templates/common/Harness/scripts/README.md +134 -0
  125. package/templates/common/Harness/scripts/a2a-terminal.mjs +191 -0
  126. package/templates/common/Harness/scripts/context-budget.mjs +1 -1
  127. package/templates/common/Harness/scripts/sync-host-global.mjs +278 -0
  128. package/templates/common/Harness/scripts/task-state.mjs +949 -26
  129. package/templates/common/Harness/scripts/validate-harness.mjs +637 -62
  130. package/templates/common/Harness/scripts/wf-remove.mjs +42 -5
  131. package/templates/common/Harness/scripts/wf-update-check.mjs +37 -9
  132. package/templates/common/Harness/scripts/wf-update-runner.mjs +325 -0
  133. package/templates/common/Harness/settings.json +35 -0
  134. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  135. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  136. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +16 -5
  137. package/templates/common/Harness/specs/runtime/command-surface.json +229 -0
  138. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  139. package/templates/common/Harness/specs/workflows/WF-AUTO-SPARK.md +8 -8
  140. package/templates/common/Harness/specs/workflows/WF-AUTO.md +12 -12
  141. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  142. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  143. package/templates/common/Harness/tasks/_template/NAMING.md +16 -16
  144. package/templates/common/Harness/tasks/_template/PLAN.md +17 -60
  145. package/templates/common/Harness/tasks/_template/PROBLEM.md +18 -0
  146. package/templates/common/Harness/tasks/_template/PROGRESS.md +9 -14
  147. package/templates/common/Harness/tasks/_template/REFERENCES.md +26 -0
  148. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
  149. package/templates/common/Harness/tasks/_template/ARTIFACTS.md +0 -3
  150. 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.