agileflow 3.4.2 → 3.4.3

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 (37) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/lib/drivers/claude-driver.ts +1 -1
  4. package/lib/lazy-require.js +1 -1
  5. package/package.json +1 -1
  6. package/scripts/agent-loop.js +290 -230
  7. package/scripts/check-sessions.js +116 -0
  8. package/scripts/lib/quality-gates.js +35 -8
  9. package/scripts/lib/signal-detectors.js +0 -13
  10. package/scripts/lib/team-events.js +1 -1
  11. package/scripts/lib/tmux-audit-monitor.js +2 -1
  12. package/src/core/commands/ads/audit.md +19 -3
  13. package/src/core/commands/code/accessibility.md +22 -6
  14. package/src/core/commands/code/api.md +22 -6
  15. package/src/core/commands/code/architecture.md +22 -6
  16. package/src/core/commands/code/completeness.md +22 -6
  17. package/src/core/commands/code/legal.md +22 -6
  18. package/src/core/commands/code/logic.md +22 -6
  19. package/src/core/commands/code/performance.md +22 -6
  20. package/src/core/commands/code/security.md +22 -6
  21. package/src/core/commands/code/test.md +22 -6
  22. package/src/core/commands/ideate/features.md +5 -4
  23. package/src/core/commands/ideate/new.md +8 -7
  24. package/src/core/commands/seo/audit.md +21 -5
  25. package/lib/claude-cli-bridge.js +0 -215
  26. package/lib/dashboard-automations.js +0 -130
  27. package/lib/dashboard-git.js +0 -254
  28. package/lib/dashboard-inbox.js +0 -64
  29. package/lib/dashboard-protocol.js +0 -605
  30. package/lib/dashboard-server.js +0 -1296
  31. package/lib/dashboard-session.js +0 -136
  32. package/lib/dashboard-status.js +0 -72
  33. package/lib/dashboard-terminal.js +0 -354
  34. package/lib/dashboard-websocket.js +0 -88
  35. package/scripts/dashboard-serve.js +0 -336
  36. package/src/core/commands/serve.md +0 -127
  37. package/tools/cli/commands/serve.js +0 -492
@@ -1,136 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * dashboard-session.js - Dashboard Session Management
5
- *
6
- * Manages individual client sessions with rate limiting,
7
- * message history, and state tracking.
8
- * Extracted from dashboard-server.js for testability.
9
- */
10
-
11
- const { encodeWebSocketFrame } = require('./dashboard-websocket');
12
-
13
- // Session lifecycle
14
- const SESSION_TIMEOUT_MS = 4 * 60 * 60 * 1000; // 4 hours
15
- const SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
16
-
17
- // Rate limiting (token bucket)
18
- const RATE_LIMIT_TOKENS = 100; // max messages per second
19
- const RATE_LIMIT_REFILL_MS = 1000; // refill interval
20
-
21
- // Lazy-loaded protocol
22
- let _protocol;
23
- function getProtocol() {
24
- if (!_protocol) _protocol = require('./dashboard-protocol');
25
- return _protocol;
26
- }
27
-
28
- /**
29
- * Session state for a connected dashboard
30
- */
31
- class DashboardSession {
32
- constructor(id, ws, projectRoot) {
33
- this.id = id;
34
- this.ws = ws;
35
- this.projectRoot = projectRoot;
36
- this.messages = [];
37
- this.state = 'connected';
38
- this.lastActivity = new Date();
39
- this.createdAt = new Date();
40
- this.metadata = {};
41
-
42
- // Token bucket rate limiter
43
- this._rateTokens = RATE_LIMIT_TOKENS;
44
- this._rateLastRefill = Date.now();
45
- }
46
-
47
- /**
48
- * Check if session has expired
49
- * @returns {boolean}
50
- */
51
- isExpired() {
52
- return Date.now() - this.lastActivity.getTime() > SESSION_TIMEOUT_MS;
53
- }
54
-
55
- /**
56
- * Rate-limit incoming messages (token bucket)
57
- * @returns {boolean} true if allowed, false if rate-limited
58
- */
59
- checkRateLimit() {
60
- const now = Date.now();
61
- const elapsed = now - this._rateLastRefill;
62
-
63
- // Refill tokens based on elapsed time
64
- if (elapsed >= RATE_LIMIT_REFILL_MS) {
65
- this._rateTokens = RATE_LIMIT_TOKENS;
66
- this._rateLastRefill = now;
67
- }
68
-
69
- if (this._rateTokens <= 0) {
70
- return false;
71
- }
72
-
73
- this._rateTokens--;
74
- return true;
75
- }
76
-
77
- /**
78
- * Send a message to the dashboard
79
- * @param {Object} message - Message object
80
- */
81
- send(message) {
82
- if (this.ws && this.ws.writable) {
83
- try {
84
- const frame = encodeWebSocketFrame(getProtocol().serializeMessage(message));
85
- this.ws.write(frame);
86
- this.lastActivity = new Date();
87
- } catch (error) {
88
- console.error(`[Session ${this.id}] Send error:`, error.message);
89
- }
90
- }
91
- }
92
-
93
- /**
94
- * Add a message to conversation history
95
- * @param {string} role - 'user' or 'assistant'
96
- * @param {string} content - Message content
97
- */
98
- addMessage(role, content) {
99
- this.messages.push({
100
- role,
101
- content,
102
- timestamp: new Date().toISOString(),
103
- });
104
- this.lastActivity = new Date();
105
- }
106
-
107
- /**
108
- * Get conversation history
109
- * @returns {Array}
110
- */
111
- getHistory() {
112
- return this.messages;
113
- }
114
-
115
- /**
116
- * Update session state
117
- * @param {string} state - New state (connected, thinking, idle, error)
118
- */
119
- setState(state) {
120
- this.state = state;
121
- this.send(
122
- getProtocol().createSessionState(this.id, state, {
123
- messageCount: this.messages.length,
124
- lastActivity: this.lastActivity.toISOString(),
125
- })
126
- );
127
- }
128
- }
129
-
130
- module.exports = {
131
- DashboardSession,
132
- SESSION_TIMEOUT_MS,
133
- SESSION_CLEANUP_INTERVAL_MS,
134
- RATE_LIMIT_TOKENS,
135
- RATE_LIMIT_REFILL_MS,
136
- };
@@ -1,72 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * dashboard-status.js - Status and Team Metrics
5
- *
6
- * Reads project status (stories/epics) and team metrics from disk,
7
- * and formats them for dashboard display.
8
- * Extracted from dashboard-server.js for testability.
9
- */
10
-
11
- const path = require('path');
12
- const fs = require('fs');
13
-
14
- /**
15
- * Build a project status summary from status.json
16
- * @param {string} projectRoot - Project root directory
17
- * @returns {Object|null} - Status summary or null if unavailable
18
- */
19
- function buildStatusSummary(projectRoot) {
20
- const statusPath = path.join(projectRoot, 'docs', '09-agents', 'status.json');
21
- if (!fs.existsSync(statusPath)) return null;
22
-
23
- try {
24
- const data = JSON.parse(fs.readFileSync(statusPath, 'utf8'));
25
- const stories = data.stories || {};
26
- const epics = data.epics || {};
27
-
28
- const storyValues = Object.values(stories);
29
- return {
30
- total: storyValues.length,
31
- done: storyValues.filter(s => s.status === 'done' || s.status === 'completed').length,
32
- inProgress: storyValues.filter(s => s.status === 'in-progress').length,
33
- ready: storyValues.filter(s => s.status === 'ready').length,
34
- blocked: storyValues.filter(s => s.status === 'blocked').length,
35
- epics: Object.entries(epics).map(([id, e]) => ({
36
- id,
37
- title: e.title || id,
38
- status: e.status || 'unknown',
39
- storyCount: (e.stories || []).length,
40
- doneCount: (e.stories || []).filter(
41
- sid =>
42
- stories[sid] && (stories[sid].status === 'done' || stories[sid].status === 'completed')
43
- ).length,
44
- })),
45
- };
46
- } catch (error) {
47
- console.error('[Status Update Error]', error.message);
48
- return null;
49
- }
50
- }
51
-
52
- /**
53
- * Read team metrics traces from session-state.json
54
- * @param {string} projectRoot - Project root directory
55
- * @returns {Object} - Map of traceId -> metrics, or empty object
56
- */
57
- function readTeamMetrics(projectRoot) {
58
- const sessionStatePath = path.join(projectRoot, 'docs', '09-agents', 'session-state.json');
59
- if (!fs.existsSync(sessionStatePath)) return {};
60
-
61
- try {
62
- const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
63
- return (state.team_metrics && state.team_metrics.traces) || {};
64
- } catch {
65
- return {};
66
- }
67
- }
68
-
69
- module.exports = {
70
- buildStatusSummary,
71
- readTeamMetrics,
72
- };
@@ -1,354 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * dashboard-terminal.js - Terminal Management
5
- *
6
- * Handles PTY-based terminal instances and manages multiple
7
- * terminals per session. Supports node-pty with basic spawn fallback.
8
- * Extracted from dashboard-server.js for testability.
9
- */
10
-
11
- // Lazy-loaded dependencies
12
- let _childProcess, _crypto, _protocol;
13
-
14
- function getChildProcess() {
15
- if (!_childProcess) _childProcess = require('child_process');
16
- return _childProcess;
17
- }
18
- function getCrypto() {
19
- if (!_crypto) _crypto = require('crypto');
20
- return _crypto;
21
- }
22
- function getProtocol() {
23
- if (!_protocol) _protocol = require('./dashboard-protocol');
24
- return _protocol;
25
- }
26
-
27
- // Sensitive env var patterns to strip from terminal spawn
28
- const SENSITIVE_ENV_PATTERNS = /SECRET|TOKEN|PASSWORD|CREDENTIAL|API_KEY|PRIVATE_KEY|AUTH/i;
29
-
30
- /**
31
- * Terminal instance for integrated terminal
32
- */
33
- class TerminalInstance {
34
- constructor(id, session, options = {}) {
35
- this.id = id;
36
- this.session = session;
37
- this.cwd = options.cwd || session.projectRoot;
38
- this.shell = options.shell || this.getDefaultShell();
39
- this.cols = options.cols || 80;
40
- this.rows = options.rows || 24;
41
- this.pty = null;
42
- this.closed = false;
43
- }
44
-
45
- /**
46
- * Get the default shell for the current OS
47
- */
48
- getDefaultShell() {
49
- if (process.platform === 'win32') {
50
- return process.env.COMSPEC || 'cmd.exe';
51
- }
52
- return process.env.SHELL || '/bin/bash';
53
- }
54
-
55
- /**
56
- * Get a filtered copy of environment variables with secrets removed
57
- */
58
- _getFilteredEnv() {
59
- const filtered = {};
60
- for (const [key, value] of Object.entries(process.env)) {
61
- if (!SENSITIVE_ENV_PATTERNS.test(key)) {
62
- filtered[key] = value;
63
- }
64
- }
65
- return filtered;
66
- }
67
-
68
- /**
69
- * Start the terminal process
70
- */
71
- start() {
72
- try {
73
- // Try to use node-pty for proper PTY support
74
- const pty = require('node-pty');
75
- const filteredEnv = this._getFilteredEnv();
76
-
77
- this.pty = pty.spawn(this.shell, [], {
78
- name: 'xterm-256color',
79
- cols: this.cols,
80
- rows: this.rows,
81
- cwd: this.cwd,
82
- env: {
83
- ...filteredEnv,
84
- TERM: 'xterm-256color',
85
- COLORTERM: 'truecolor',
86
- },
87
- });
88
-
89
- this.pty.onData(data => {
90
- if (!this.closed) {
91
- this.session.send(getProtocol().createTerminalOutput(this.id, data));
92
- }
93
- });
94
-
95
- this.pty.onExit(({ exitCode }) => {
96
- this.closed = true;
97
- this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
98
- });
99
-
100
- return true;
101
- } catch (error) {
102
- // Fallback to basic spawn if node-pty is not available
103
- console.warn('[Terminal] node-pty not available, using basic spawn:', error.message);
104
- return this.startBasicShell();
105
- }
106
- }
107
-
108
- /**
109
- * Fallback shell using basic spawn (no PTY)
110
- * Note: This provides limited functionality without node-pty
111
- */
112
- startBasicShell() {
113
- try {
114
- const filteredEnv = this._getFilteredEnv();
115
-
116
- // Use bash with interactive flag for better compatibility
117
- this.pty = getChildProcess().spawn(this.shell, ['-i'], {
118
- cwd: this.cwd,
119
- env: {
120
- ...filteredEnv,
121
- TERM: 'dumb',
122
- PS1: '\\w $ ', // Simple prompt
123
- },
124
- shell: false,
125
- stdio: ['pipe', 'pipe', 'pipe'],
126
- });
127
-
128
- // Track input buffer for local echo (since no PTY)
129
- this.inputBuffer = '';
130
-
131
- this.pty.stdout.on('data', data => {
132
- if (!this.closed) {
133
- this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
134
- }
135
- });
136
-
137
- this.pty.stderr.on('data', data => {
138
- if (!this.closed) {
139
- this.session.send(getProtocol().createTerminalOutput(this.id, data.toString()));
140
- }
141
- });
142
-
143
- this.pty.on('close', exitCode => {
144
- this.closed = true;
145
- this.session.send(getProtocol().createTerminalExit(this.id, exitCode));
146
- });
147
-
148
- this.pty.on('error', error => {
149
- console.error('[Terminal] Shell error:', error.message);
150
- if (!this.closed) {
151
- this.session.send(
152
- getProtocol().createTerminalOutput(this.id, `\r\nError: ${error.message}\r\n`)
153
- );
154
- }
155
- });
156
-
157
- // Send welcome message
158
- setTimeout(() => {
159
- if (!this.closed) {
160
- const welcomeMsg = `\x1b[32mAgileFlow Terminal\x1b[0m (basic mode - node-pty not available)\r\n`;
161
- const cwdMsg = `Working directory: ${this.cwd}\r\n\r\n`;
162
- this.session.send(getProtocol().createTerminalOutput(this.id, welcomeMsg + cwdMsg));
163
- }
164
- }, 100);
165
-
166
- return true;
167
- } catch (error) {
168
- console.error('[Terminal] Failed to start basic shell:', error.message);
169
- return false;
170
- }
171
- }
172
-
173
- /**
174
- * Write data to the terminal
175
- * @param {string} data - Data to write (user input)
176
- */
177
- write(data) {
178
- if (this.pty && !this.closed) {
179
- if (this.pty.write) {
180
- // node-pty style - has built-in echo
181
- this.pty.write(data);
182
- } else if (this.pty.stdin) {
183
- // basic spawn style - need manual echo
184
- // Echo the input back to the terminal (since no PTY)
185
- let echoData = data;
186
-
187
- // Handle special characters
188
- if (data === '\r' || data === '\n') {
189
- echoData = '\r\n';
190
- } else if (data === '\x7f' || data === '\b') {
191
- // Backspace - move cursor back and clear
192
- echoData = '\b \b';
193
- } else if (data === '\x03') {
194
- // Ctrl+C
195
- echoData = '^C\r\n';
196
- }
197
-
198
- // Echo to terminal
199
- this.session.send(getProtocol().createTerminalOutput(this.id, echoData));
200
-
201
- // Send to shell stdin
202
- this.pty.stdin.write(data);
203
- }
204
- }
205
- }
206
-
207
- /**
208
- * Resize the terminal
209
- * @param {number} cols - New column count
210
- * @param {number} rows - New row count
211
- */
212
- resize(cols, rows) {
213
- this.cols = cols;
214
- this.rows = rows;
215
- if (this.pty && this.pty.resize && !this.closed) {
216
- this.pty.resize(cols, rows);
217
- }
218
- }
219
-
220
- /**
221
- * Close the terminal
222
- */
223
- close() {
224
- this.closed = true;
225
- if (this.pty) {
226
- if (this.pty.kill) {
227
- this.pty.kill();
228
- } else if (this.pty.destroy) {
229
- this.pty.destroy();
230
- }
231
- }
232
- }
233
- }
234
-
235
- /**
236
- * Terminal manager for handling multiple terminals per session
237
- */
238
- class TerminalManager {
239
- constructor() {
240
- this.terminals = new Map();
241
- }
242
-
243
- /**
244
- * Create a new terminal for a session
245
- * @param {DashboardSession} session - The session
246
- * @param {Object} options - Terminal options
247
- * @returns {string} - Terminal ID
248
- */
249
- createTerminal(session, options = {}) {
250
- const terminalId = options.id || getCrypto().randomBytes(8).toString('hex');
251
- const terminal = new TerminalInstance(terminalId, session, {
252
- cwd: options.cwd || session.projectRoot,
253
- cols: options.cols,
254
- rows: options.rows,
255
- });
256
-
257
- if (terminal.start()) {
258
- this.terminals.set(terminalId, terminal);
259
- console.log(`[Terminal ${terminalId}] Created for session ${session.id}`);
260
- return terminalId;
261
- }
262
-
263
- return null;
264
- }
265
-
266
- /**
267
- * Get a terminal by ID
268
- * @param {string} terminalId - Terminal ID
269
- * @returns {TerminalInstance | undefined}
270
- */
271
- getTerminal(terminalId) {
272
- return this.terminals.get(terminalId);
273
- }
274
-
275
- /**
276
- * Write to a terminal
277
- * @param {string} terminalId - Terminal ID
278
- * @param {string} data - Data to write
279
- */
280
- writeToTerminal(terminalId, data) {
281
- const terminal = this.terminals.get(terminalId);
282
- if (terminal) {
283
- terminal.write(data);
284
- }
285
- }
286
-
287
- /**
288
- * Resize a terminal
289
- * @param {string} terminalId - Terminal ID
290
- * @param {number} cols - New columns
291
- * @param {number} rows - New rows
292
- */
293
- resizeTerminal(terminalId, cols, rows) {
294
- const terminal = this.terminals.get(terminalId);
295
- if (terminal) {
296
- terminal.resize(cols, rows);
297
- }
298
- }
299
-
300
- /**
301
- * Close a terminal
302
- * @param {string} terminalId - Terminal ID
303
- */
304
- closeTerminal(terminalId) {
305
- const terminal = this.terminals.get(terminalId);
306
- if (terminal) {
307
- terminal.close();
308
- this.terminals.delete(terminalId);
309
- console.log(`[Terminal ${terminalId}] Closed`);
310
- }
311
- }
312
-
313
- /**
314
- * Close all terminals for a session
315
- * @param {string} sessionId - Session ID
316
- */
317
- closeSessionTerminals(sessionId) {
318
- // Collect IDs first to avoid mutating Map during iteration
319
- const toClose = [];
320
- for (const [terminalId, terminal] of this.terminals) {
321
- if (terminal.session.id === sessionId) {
322
- toClose.push(terminalId);
323
- }
324
- }
325
- for (const terminalId of toClose) {
326
- const terminal = this.terminals.get(terminalId);
327
- if (terminal) {
328
- terminal.close();
329
- this.terminals.delete(terminalId);
330
- }
331
- }
332
- }
333
-
334
- /**
335
- * Get all terminals for a session
336
- * @param {string} sessionId - Session ID
337
- * @returns {Array<string>} - Terminal IDs
338
- */
339
- getSessionTerminals(sessionId) {
340
- const terminalIds = [];
341
- for (const [terminalId, terminal] of this.terminals) {
342
- if (terminal.session.id === sessionId) {
343
- terminalIds.push(terminalId);
344
- }
345
- }
346
- return terminalIds;
347
- }
348
- }
349
-
350
- module.exports = {
351
- TerminalInstance,
352
- TerminalManager,
353
- SENSITIVE_ENV_PATTERNS,
354
- };
@@ -1,88 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * dashboard-websocket.js - WebSocket Frame Encoding/Decoding
5
- *
6
- * Pure functions for encoding and decoding WebSocket frames.
7
- * Extracted from dashboard-server.js for testability.
8
- */
9
-
10
- /**
11
- * Encode a WebSocket frame
12
- * @param {string|Buffer} data - Data to encode
13
- * @param {number} [opcode=0x1] - Frame opcode (0x1 = text, 0x2 = binary)
14
- * @returns {Buffer}
15
- */
16
- function encodeWebSocketFrame(data, opcode = 0x1) {
17
- const payload = Buffer.isBuffer(data) ? data : Buffer.from(data);
18
- const length = payload.length;
19
-
20
- let header;
21
- if (length < 126) {
22
- header = Buffer.alloc(2);
23
- header[0] = 0x80 | opcode; // FIN + opcode
24
- header[1] = length;
25
- } else if (length < 65536) {
26
- header = Buffer.alloc(4);
27
- header[0] = 0x80 | opcode;
28
- header[1] = 126;
29
- header.writeUInt16BE(length, 2);
30
- } else {
31
- header = Buffer.alloc(10);
32
- header[0] = 0x80 | opcode;
33
- header[1] = 127;
34
- header.writeBigUInt64BE(BigInt(length), 2);
35
- }
36
-
37
- return Buffer.concat([header, payload]);
38
- }
39
-
40
- /**
41
- * Decode a WebSocket frame
42
- * @param {Buffer} buffer - Buffer containing frame data
43
- * @returns {{ opcode: number, payload: Buffer, totalLength: number } | null}
44
- */
45
- function decodeWebSocketFrame(buffer) {
46
- if (buffer.length < 2) return null;
47
-
48
- const firstByte = buffer[0];
49
- const secondByte = buffer[1];
50
-
51
- const opcode = firstByte & 0x0f;
52
- const masked = (secondByte & 0x80) !== 0;
53
- let payloadLength = secondByte & 0x7f;
54
-
55
- let headerLength = 2;
56
- if (payloadLength === 126) {
57
- if (buffer.length < 4) return null;
58
- payloadLength = buffer.readUInt16BE(2);
59
- headerLength = 4;
60
- } else if (payloadLength === 127) {
61
- if (buffer.length < 10) return null;
62
- payloadLength = Number(buffer.readBigUInt64BE(2));
63
- headerLength = 10;
64
- }
65
-
66
- if (masked) headerLength += 4;
67
-
68
- const totalLength = headerLength + payloadLength;
69
- if (buffer.length < totalLength) return null;
70
-
71
- let payload = buffer.slice(headerLength, totalLength);
72
-
73
- // Unmask if needed
74
- if (masked) {
75
- const mask = buffer.slice(headerLength - 4, headerLength);
76
- payload = Buffer.alloc(payloadLength);
77
- for (let i = 0; i < payloadLength; i++) {
78
- payload[i] = buffer[headerLength + i] ^ mask[i % 4];
79
- }
80
- }
81
-
82
- return { opcode, payload, totalLength };
83
- }
84
-
85
- module.exports = {
86
- encodeWebSocketFrame,
87
- decodeWebSocketFrame,
88
- };