agileflow 2.89.2 → 2.90.0

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 (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,316 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Event Stream - Real-time event monitoring from agent bus
5
+ *
6
+ * Watches docs/09-agents/bus/log.jsonl for new events and emits them
7
+ * to subscribers. Handles file rotation and truncation gracefully.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const EventEmitter = require('events');
13
+
14
+ // Get project root
15
+ let getProjectRoot;
16
+ try {
17
+ getProjectRoot = require('../../../lib/paths').getProjectRoot;
18
+ } catch (e) {
19
+ // Fallback
20
+ getProjectRoot = () => process.cwd();
21
+ }
22
+
23
+ /**
24
+ * EventStream class - watches agent bus log and emits events
25
+ */
26
+ class EventStream extends EventEmitter {
27
+ constructor(options = {}) {
28
+ super();
29
+
30
+ this.options = {
31
+ // Path to log file (defaults to agent bus)
32
+ logPath: options.logPath || path.join(
33
+ getProjectRoot(),
34
+ 'docs',
35
+ '09-agents',
36
+ 'bus',
37
+ 'log.jsonl'
38
+ ),
39
+ // Polling interval in ms (fallback if fs.watch fails)
40
+ pollInterval: options.pollInterval || 1000,
41
+ // Maximum events to keep in buffer
42
+ maxBufferSize: options.maxBufferSize || 100,
43
+ // Whether to emit historical events on start
44
+ emitHistory: options.emitHistory || false,
45
+ // How many historical events to emit
46
+ historyLimit: options.historyLimit || 10
47
+ };
48
+
49
+ this.buffer = [];
50
+ this.filePosition = 0;
51
+ this.watcher = null;
52
+ this.pollTimer = null;
53
+ this.isWatching = false;
54
+ }
55
+
56
+ /**
57
+ * Start watching the log file
58
+ */
59
+ start() {
60
+ if (this.isWatching) return;
61
+
62
+ // Check if file exists
63
+ if (!fs.existsSync(this.options.logPath)) {
64
+ this.emit('error', new Error(`Log file not found: ${this.options.logPath}`));
65
+ // Continue anyway - file might be created later
66
+ }
67
+
68
+ // Get initial file size
69
+ this._updateFilePosition();
70
+
71
+ // Emit historical events if requested
72
+ if (this.options.emitHistory) {
73
+ this._emitHistory();
74
+ }
75
+
76
+ // Try to use fs.watch (more efficient)
77
+ try {
78
+ this.watcher = fs.watch(this.options.logPath, (eventType) => {
79
+ if (eventType === 'change') {
80
+ this._processNewLines();
81
+ }
82
+ });
83
+
84
+ this.watcher.on('error', (err) => {
85
+ this.emit('error', err);
86
+ // Fall back to polling
87
+ this._startPolling();
88
+ });
89
+ } catch (e) {
90
+ // Fall back to polling
91
+ this._startPolling();
92
+ }
93
+
94
+ this.isWatching = true;
95
+ this.emit('started');
96
+ }
97
+
98
+ /**
99
+ * Stop watching the log file
100
+ */
101
+ stop() {
102
+ if (!this.isWatching) return;
103
+
104
+ if (this.watcher) {
105
+ this.watcher.close();
106
+ this.watcher = null;
107
+ }
108
+
109
+ if (this.pollTimer) {
110
+ clearInterval(this.pollTimer);
111
+ this.pollTimer = null;
112
+ }
113
+
114
+ this.isWatching = false;
115
+ this.emit('stopped');
116
+ }
117
+
118
+ /**
119
+ * Start polling as fallback
120
+ */
121
+ _startPolling() {
122
+ if (this.pollTimer) return;
123
+
124
+ this.pollTimer = setInterval(() => {
125
+ this._processNewLines();
126
+ }, this.options.pollInterval);
127
+ }
128
+
129
+ /**
130
+ * Update tracked file position
131
+ */
132
+ _updateFilePosition() {
133
+ try {
134
+ if (fs.existsSync(this.options.logPath)) {
135
+ const stats = fs.statSync(this.options.logPath);
136
+ this.filePosition = stats.size;
137
+ }
138
+ } catch (e) {
139
+ // Ignore errors
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Emit historical events from file start
145
+ */
146
+ _emitHistory() {
147
+ try {
148
+ if (!fs.existsSync(this.options.logPath)) return;
149
+
150
+ const content = fs.readFileSync(this.options.logPath, 'utf8');
151
+ const lines = content.trim().split('\n').filter(Boolean);
152
+
153
+ // Get last N lines
154
+ const historyLines = lines.slice(-this.options.historyLimit);
155
+
156
+ for (const line of historyLines) {
157
+ try {
158
+ const event = JSON.parse(line);
159
+ this._addToBuffer(event);
160
+ this.emit('event', event);
161
+ } catch (e) {
162
+ // Skip invalid JSON
163
+ }
164
+ }
165
+ } catch (e) {
166
+ this.emit('error', e);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Process new lines added to the file
172
+ */
173
+ _processNewLines() {
174
+ try {
175
+ if (!fs.existsSync(this.options.logPath)) return;
176
+
177
+ const stats = fs.statSync(this.options.logPath);
178
+
179
+ // Handle file truncation (rotation)
180
+ if (stats.size < this.filePosition) {
181
+ this.filePosition = 0;
182
+ this.emit('truncated');
183
+ }
184
+
185
+ // No new content
186
+ if (stats.size <= this.filePosition) return;
187
+
188
+ // Read new content
189
+ const fd = fs.openSync(this.options.logPath, 'r');
190
+ const bufferSize = stats.size - this.filePosition;
191
+ const buffer = Buffer.alloc(bufferSize);
192
+
193
+ fs.readSync(fd, buffer, 0, bufferSize, this.filePosition);
194
+ fs.closeSync(fd);
195
+
196
+ // Update position
197
+ this.filePosition = stats.size;
198
+
199
+ // Process lines
200
+ const content = buffer.toString('utf8');
201
+ const lines = content.split('\n').filter(Boolean);
202
+
203
+ for (const line of lines) {
204
+ try {
205
+ const event = JSON.parse(line);
206
+ this._addToBuffer(event);
207
+ this.emit('event', event);
208
+ } catch (e) {
209
+ // Skip invalid JSON lines
210
+ }
211
+ }
212
+ } catch (e) {
213
+ this.emit('error', e);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Add event to buffer (with size limit)
219
+ */
220
+ _addToBuffer(event) {
221
+ this.buffer.push(event);
222
+
223
+ // Trim buffer if too large
224
+ while (this.buffer.length > this.options.maxBufferSize) {
225
+ this.buffer.shift();
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get buffered events
231
+ */
232
+ getBuffer() {
233
+ return [...this.buffer];
234
+ }
235
+
236
+ /**
237
+ * Clear buffer
238
+ */
239
+ clearBuffer() {
240
+ this.buffer = [];
241
+ }
242
+
243
+ /**
244
+ * Get events by type
245
+ */
246
+ getEventsByType(type) {
247
+ return this.buffer.filter(e => e.type === type);
248
+ }
249
+
250
+ /**
251
+ * Get events by agent
252
+ */
253
+ getEventsByAgent(agent) {
254
+ return this.buffer.filter(e => e.agent === agent);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Create a singleton event stream instance
260
+ */
261
+ let defaultStream = null;
262
+
263
+ function getDefaultStream() {
264
+ if (!defaultStream) {
265
+ defaultStream = new EventStream();
266
+ }
267
+ return defaultStream;
268
+ }
269
+
270
+ /**
271
+ * Format event for display
272
+ */
273
+ function formatEvent(event) {
274
+ const timestamp = event.timestamp
275
+ ? new Date(event.timestamp).toLocaleTimeString()
276
+ : '';
277
+
278
+ const agent = event.agent || 'unknown';
279
+ const eventType = event.event || event.type || 'unknown';
280
+
281
+ let message = '';
282
+
283
+ switch (event.event) {
284
+ case 'init':
285
+ message = `Loop started: gate=${event.gate}, max=${event.max_iterations}`;
286
+ break;
287
+ case 'iteration':
288
+ message = `Iteration ${event.iter}: value=${event.value}, passed=${event.passed}`;
289
+ break;
290
+ case 'passed':
291
+ message = `Loop passed! final=${event.final_value}`;
292
+ break;
293
+ case 'failed':
294
+ message = `Loop failed: ${event.reason}, final=${event.final_value}`;
295
+ break;
296
+ case 'abort':
297
+ message = `Loop aborted: ${event.reason}`;
298
+ break;
299
+ default:
300
+ message = JSON.stringify(event);
301
+ }
302
+
303
+ return {
304
+ timestamp,
305
+ agent,
306
+ eventType,
307
+ message,
308
+ raw: event
309
+ };
310
+ }
311
+
312
+ module.exports = {
313
+ EventStream,
314
+ getDefaultStream,
315
+ formatEvent
316
+ };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Keyboard Handler - Key bindings for TUI
5
+ *
6
+ * Provides centralized keyboard handling with configurable bindings
7
+ * and event emission for TUI components.
8
+ */
9
+
10
+ const EventEmitter = require('events');
11
+
12
+ /**
13
+ * Default key bindings
14
+ */
15
+ const DEFAULT_BINDINGS = {
16
+ quit: { key: 'q', description: 'Quit TUI' },
17
+ start: { key: 's', description: 'Start loop' },
18
+ pause: { key: 'p', description: 'Pause loop' },
19
+ resume: { key: 'r', description: 'Resume loop' },
20
+ trace: { key: 't', description: 'Toggle trace' },
21
+ help: { key: '?', description: 'Show help' },
22
+ // Session switching (1-9)
23
+ session1: { key: '1', description: 'Session 1' },
24
+ session2: { key: '2', description: 'Session 2' },
25
+ session3: { key: '3', description: 'Session 3' },
26
+ session4: { key: '4', description: 'Session 4' },
27
+ session5: { key: '5', description: 'Session 5' },
28
+ session6: { key: '6', description: 'Session 6' },
29
+ session7: { key: '7', description: 'Session 7' },
30
+ session8: { key: '8', description: 'Session 8' },
31
+ session9: { key: '9', description: 'Session 9' }
32
+ };
33
+
34
+ /**
35
+ * KeyboardHandler class - centralized keyboard handling
36
+ */
37
+ class KeyboardHandler extends EventEmitter {
38
+ constructor(options = {}) {
39
+ super();
40
+
41
+ this.bindings = { ...DEFAULT_BINDINGS, ...options.bindings };
42
+ this.enabled = true;
43
+ this.keyHistory = [];
44
+ this.historyLimit = options.historyLimit || 50;
45
+ }
46
+
47
+ /**
48
+ * Process a key press
49
+ * @param {string} input - The key character
50
+ * @param {object} key - The key object from useInput
51
+ * @returns {object|null} - Action performed or null
52
+ */
53
+ processKey(input, key = {}) {
54
+ if (!this.enabled) return null;
55
+
56
+ // Record key press
57
+ this._recordKey(input, key);
58
+
59
+ // Handle Ctrl+C for quit
60
+ if (key.ctrl && input === 'c') {
61
+ const action = { action: 'quit', key: 'ctrl+c' };
62
+ this.emit('action', action);
63
+ this.emit('quit');
64
+ return action;
65
+ }
66
+
67
+ // Handle escape for quit
68
+ if (key.escape) {
69
+ const action = { action: 'quit', key: 'escape' };
70
+ this.emit('action', action);
71
+ this.emit('quit');
72
+ return action;
73
+ }
74
+
75
+ // Normalize input to lowercase for matching
76
+ const normalizedInput = input.toLowerCase();
77
+
78
+ // Check against bindings
79
+ for (const [actionName, binding] of Object.entries(this.bindings)) {
80
+ if (binding.key === normalizedInput || binding.key === input) {
81
+ const action = { action: actionName, key: input };
82
+ this.emit('action', action);
83
+ this.emit(actionName, action);
84
+
85
+ // Special handling for session switching
86
+ if (actionName.startsWith('session')) {
87
+ const sessionNum = parseInt(actionName.replace('session', ''), 10);
88
+ this.emit('sessionSwitch', { session: sessionNum });
89
+ }
90
+
91
+ return action;
92
+ }
93
+ }
94
+
95
+ // Unknown key
96
+ this.emit('unknownKey', { key: input });
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Record key press to history
102
+ */
103
+ _recordKey(input, key) {
104
+ this.keyHistory.push({
105
+ input,
106
+ key,
107
+ timestamp: Date.now()
108
+ });
109
+
110
+ // Trim history
111
+ while (this.keyHistory.length > this.historyLimit) {
112
+ this.keyHistory.shift();
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get key history
118
+ */
119
+ getHistory() {
120
+ return [...this.keyHistory];
121
+ }
122
+
123
+ /**
124
+ * Clear key history
125
+ */
126
+ clearHistory() {
127
+ this.keyHistory = [];
128
+ }
129
+
130
+ /**
131
+ * Enable keyboard handling
132
+ */
133
+ enable() {
134
+ this.enabled = true;
135
+ }
136
+
137
+ /**
138
+ * Disable keyboard handling
139
+ */
140
+ disable() {
141
+ this.enabled = false;
142
+ }
143
+
144
+ /**
145
+ * Check if a key is bound
146
+ */
147
+ isBound(key) {
148
+ return Object.values(this.bindings).some(b => b.key === key);
149
+ }
150
+
151
+ /**
152
+ * Get binding for action
153
+ */
154
+ getBinding(action) {
155
+ return this.bindings[action] || null;
156
+ }
157
+
158
+ /**
159
+ * Get all bindings
160
+ */
161
+ getBindings() {
162
+ return { ...this.bindings };
163
+ }
164
+
165
+ /**
166
+ * Set custom binding
167
+ */
168
+ setBinding(action, key, description) {
169
+ this.bindings[action] = { key, description };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Format key bindings for footer display
175
+ * @param {object} bindings - Key bindings object
176
+ * @returns {string[]} - Array of formatted binding strings
177
+ */
178
+ function formatBindings(bindings = DEFAULT_BINDINGS) {
179
+ // Primary bindings to show in footer (exclude session numbers)
180
+ const primaryBindings = ['quit', 'start', 'pause', 'resume', 'trace', 'help'];
181
+
182
+ return primaryBindings
183
+ .filter(name => bindings[name])
184
+ .map(name => {
185
+ const binding = bindings[name];
186
+ return `${binding.key.toUpperCase()}:${binding.description}`;
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Format bindings as help text
192
+ * @param {object} bindings - Key bindings object
193
+ * @returns {string} - Multi-line help text
194
+ */
195
+ function formatHelp(bindings = DEFAULT_BINDINGS) {
196
+ const lines = ['Key Bindings:', ''];
197
+
198
+ // Group bindings
199
+ const groups = {
200
+ 'Loop Control': ['start', 'pause', 'resume'],
201
+ 'View': ['trace', 'help'],
202
+ 'Navigation': ['quit'],
203
+ 'Sessions': ['session1', 'session2', 'session3', 'session4', 'session5',
204
+ 'session6', 'session7', 'session8', 'session9']
205
+ };
206
+
207
+ for (const [groupName, actions] of Object.entries(groups)) {
208
+ lines.push(` ${groupName}:`);
209
+ for (const action of actions) {
210
+ if (bindings[action]) {
211
+ const binding = bindings[action];
212
+ lines.push(` ${binding.key.toUpperCase()} - ${binding.description}`);
213
+ }
214
+ }
215
+ lines.push('');
216
+ }
217
+
218
+ return lines.join('\n');
219
+ }
220
+
221
+ /**
222
+ * Create default keyboard handler instance
223
+ */
224
+ let defaultHandler = null;
225
+
226
+ function getDefaultHandler() {
227
+ if (!defaultHandler) {
228
+ defaultHandler = new KeyboardHandler();
229
+ }
230
+ return defaultHandler;
231
+ }
232
+
233
+ /**
234
+ * React hook factory for keyboard handling
235
+ * Returns a function that can be used with useInput
236
+ */
237
+ function createKeyHandler(handler = null) {
238
+ const kbd = handler || getDefaultHandler();
239
+
240
+ return function handleKey(input, key) {
241
+ return kbd.processKey(input, key);
242
+ };
243
+ }
244
+
245
+ module.exports = {
246
+ KeyboardHandler,
247
+ DEFAULT_BINDINGS,
248
+ formatBindings,
249
+ formatHelp,
250
+ getDefaultHandler,
251
+ createKeyHandler
252
+ };